]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict stripping (#32478)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Fri, 27 Sep 2024 07:12:10 +0000 (17:12 +1000)
committerGitHub <noreply@github.com>
Fri, 27 Sep 2024 07:12:10 +0000 (17:12 +1000)
* Predict stripping

Stops mob verbs from getting moved around again.

* Bola

* Fix ftl

12 files changed:
Content.Client/Ensnaring/EnsnareableSystem.cs
Content.Client/Inventory/StrippableBoundUserInterface.cs
Content.Client/Popups/PopupSystem.cs
Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs [deleted file]
Content.Server/Ensnaring/EnsnareableSystem.cs
Content.Server/Strip/StrippableSystem.cs
Content.Shared/Ensnaring/Components/EnsnareableComponent.cs
Content.Shared/Ensnaring/Components/EnsnaringComponent.cs
Content.Shared/Ensnaring/SharedEnsnareableSystem.cs
Content.Shared/Inventory/InventorySystem.Equip.cs
Content.Shared/Strip/SharedStrippableSystem.cs
Resources/Locale/en-US/ensnare/ensnare-component.ftl

index b7a5a45ca0c62a430dbc92cdd8a1ae4362945259..6861bd8f09a81cbf5f12def81266085555b7e997 100644 (file)
@@ -2,7 +2,7 @@ using Content.Shared.Ensnaring;
 using Content.Shared.Ensnaring.Components;
 using Robust.Client.GameObjects;
 
-namespace Content.Client.Ensnaring.Visualizers;
+namespace Content.Client.Ensnaring;
 
 public sealed class EnsnareableSystem : SharedEnsnareableSystem
 {
@@ -12,13 +12,14 @@ public sealed class EnsnareableSystem : SharedEnsnareableSystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<EnsnareableComponent, ComponentInit>(OnComponentInit);
         SubscribeLocalEvent<EnsnareableComponent, AppearanceChangeEvent>(OnAppearanceChange);
     }
 
-    private void OnComponentInit(EntityUid uid, EnsnareableComponent component, ComponentInit args)
+    protected override void OnEnsnareInit(Entity<EnsnareableComponent> ent, ref ComponentInit args)
     {
-        if(!TryComp<SpriteComponent>(uid, out var sprite))
+        base.OnEnsnareInit(ent, ref args);
+
+        if(!TryComp<SpriteComponent>(ent.Owner, out var sprite))
             return;
 
         // TODO remove this, this should just be in yaml.
index 132c5ed654c30aac1e0e5dd46027ef3e81c320c3..97172f8de8c24f40cef28b25e6408368dfbbd5d3 100644 (file)
@@ -136,7 +136,7 @@ namespace Content.Client.Inventory
                     StyleClasses = { StyleBase.ButtonOpenRight }
                 };
 
-                button.OnPressed += (_) => SendMessage(new StrippingEnsnareButtonPressed());
+                button.OnPressed += (_) => SendPredictedMessage(new StrippingEnsnareButtonPressed());
 
                 _strippingMenu.SnareContainer.AddChild(button);
             }
@@ -177,7 +177,7 @@ namespace Content.Client.Inventory
             // So for now: only stripping & examining
             if (ev.Function == EngineKeyFunctions.Use)
             {
-                SendMessage(new StrippingSlotButtonPressed(slot.SlotName, slot is HandButton));
+                SendPredictedMessage(new StrippingSlotButtonPressed(slot.SlotName, slot is HandButton));
                 return;
             }
 
index 700f6b6d26fc6bde45c6d16ab1a5128ebef77b59..a249c9251bf0fa60853f9c13fe7efb90229aa5f1 100644 (file)
@@ -148,7 +148,12 @@ namespace Content.Client.Popups
         }
 
         public override void PopupCursor(string? message, PopupType type = PopupType.Small)
-            => PopupCursorInternal(message, type, true);
+        {
+            if (!_timing.IsFirstTimePredicted)
+                return;
+
+            PopupCursorInternal(message, type, true);
+        }
 
         public override void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small)
         {
diff --git a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs
deleted file mode 100644 (file)
index 7d69638..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-using System.Linq;
-using Content.Server.Body.Systems;
-using Content.Shared.Alert;
-using Content.Shared.Body.Part;
-using Content.Shared.CombatMode.Pacification;
-using Content.Shared.Damage.Components;
-using Content.Shared.Damage.Systems;
-using Content.Shared.DoAfter;
-using Content.Shared.Ensnaring;
-using Content.Shared.Ensnaring.Components;
-using Content.Shared.IdentityManagement;
-using Content.Shared.StepTrigger.Systems;
-using Content.Shared.Throwing;
-
-namespace Content.Server.Ensnaring;
-
-public sealed partial class EnsnareableSystem
-{
-    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
-    [Dependency] private readonly AlertsSystem _alerts = default!;
-    [Dependency] private readonly BodySystem _body = default!;
-    [Dependency] private readonly StaminaSystem _stamina = default!;
-
-    public void InitializeEnsnaring()
-    {
-        SubscribeLocalEvent<EnsnaringComponent, ComponentRemove>(OnComponentRemove);
-        SubscribeLocalEvent<EnsnaringComponent, StepTriggerAttemptEvent>(AttemptStepTrigger);
-        SubscribeLocalEvent<EnsnaringComponent, StepTriggeredOffEvent>(OnStepTrigger);
-        SubscribeLocalEvent<EnsnaringComponent, ThrowDoHitEvent>(OnThrowHit);
-        SubscribeLocalEvent<EnsnaringComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
-        SubscribeLocalEvent<EnsnareableComponent, RemoveEnsnareAlertEvent>(OnRemoveEnsnareAlert);
-    }
-
-    private void OnAttemptPacifiedThrow(Entity<EnsnaringComponent> ent, ref AttemptPacifiedThrowEvent args)
-    {
-        args.Cancel("pacified-cannot-throw-snare");
-    }
-
-    private void OnRemoveEnsnareAlert(Entity<EnsnareableComponent> ent, ref RemoveEnsnareAlertEvent args)
-    {
-        if (args.Handled)
-            return;
-
-        foreach (var ensnare in ent.Comp.Container.ContainedEntities)
-        {
-            if (!TryComp<EnsnaringComponent>(ensnare, out var ensnaringComponent))
-                return;
-
-            TryFree(ent, ent, ensnare, ensnaringComponent);
-
-            args.Handled = true;
-            // Only one snare at a time.
-            break;
-        }
-    }
-
-    private void OnComponentRemove(EntityUid uid, EnsnaringComponent component, ComponentRemove args)
-    {
-        if (!TryComp<EnsnareableComponent>(component.Ensnared, out var ensnared))
-            return;
-
-        if (ensnared.IsEnsnared)
-            ForceFree(uid, component);
-    }
-
-    private void AttemptStepTrigger(EntityUid uid, EnsnaringComponent component, ref StepTriggerAttemptEvent args)
-    {
-        args.Continue = true;
-    }
-
-    private void OnStepTrigger(EntityUid uid, EnsnaringComponent component, ref StepTriggeredOffEvent args)
-    {
-        TryEnsnare(args.Tripper, uid, component);
-    }
-
-    private void OnThrowHit(EntityUid uid, EnsnaringComponent component, ThrowDoHitEvent args)
-    {
-        if (!component.CanThrowTrigger)
-            return;
-
-        TryEnsnare(args.Target, uid, component);
-    }
-
-    /// <summary>
-    /// Used where you want to try to ensnare an entity with the <see cref="EnsnareableComponent"/>
-    /// </summary>
-    /// <param name="target">The entity that will be ensnared</param>
-    /// <paramref name="ensnare"> The entity that is used to ensnare</param>
-    /// <param name="component">The ensnaring component</param>
-    public void TryEnsnare(EntityUid target, EntityUid ensnare, EnsnaringComponent component)
-    {
-        //Don't do anything if they don't have the ensnareable component.
-        if (!TryComp<EnsnareableComponent>(target, out var ensnareable))
-            return;
-
-        var legs = _body.GetBodyChildrenOfType(target, BodyPartType.Leg).Count();
-        var ensnaredLegs = (2 * ensnareable.Container.ContainedEntities.Count);
-        var freeLegs = legs - ensnaredLegs;
-
-        if (freeLegs <= 0)
-            return;
-
-        // Apply stamina damage to target if they weren't ensnared before.
-        if (ensnareable.IsEnsnared != true)
-        {
-            if (TryComp<StaminaComponent>(target, out var stamina))
-            {
-                _stamina.TakeStaminaDamage(target, component.StaminaDamage, with: ensnare);
-            }
-        }
-
-        component.Ensnared = target;
-        _container.Insert(ensnare, ensnareable.Container);
-        ensnareable.IsEnsnared = true;
-        Dirty(target, ensnareable);
-
-        UpdateAlert(target, ensnareable);
-        var ev = new EnsnareEvent(component.WalkSpeed, component.SprintSpeed);
-        RaiseLocalEvent(target, ev);
-    }
-
-    /// <summary>
-    /// Used where you want to try to free an entity with the <see cref="EnsnareableComponent"/>
-    /// </summary>
-    /// <param name="target">The entity that will be freed</param>
-    /// <param name="user">The entity that is freeing the target</param>
-    /// <param name="ensnare">The entity used to ensnare</param>
-    /// <param name="component">The ensnaring component</param>
-    public void TryFree(EntityUid target, EntityUid user, EntityUid ensnare, EnsnaringComponent component)
-    {
-        // Don't do anything if they don't have the ensnareable component.
-        if (!HasComp<EnsnareableComponent>(target))
-            return;
-
-        var freeTime = user == target ? component.BreakoutTime : component.FreeTime;
-        var breakOnMove = !component.CanMoveBreakout;
-
-        var doAfterEventArgs = new DoAfterArgs(EntityManager, user, freeTime, new EnsnareableDoAfterEvent(), target, target: target, used: ensnare)
-        {
-            BreakOnMove = breakOnMove,
-            BreakOnDamage = false,
-            NeedHand = true,
-            BreakOnDropItem = false,
-        };
-
-        if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
-            return;
-
-        if (user == target)
-            _popup.PopupEntity(Loc.GetString("ensnare-component-try-free", ("ensnare", ensnare)), target, target);
-        else
-            _popup.PopupEntity(Loc.GetString("ensnare-component-try-free-other", ("ensnare", ensnare), ("user", Identity.Entity(target, EntityManager))), user, user);
-    }
-
-    /// <summary>
-    /// Used to force free someone for things like if the <see cref="EnsnaringComponent"/> is removed
-    /// </summary>
-    public void ForceFree(EntityUid ensnare, EnsnaringComponent component)
-    {
-        if (component.Ensnared == null)
-            return;
-
-        if (!TryComp<EnsnareableComponent>(component.Ensnared, out var ensnareable))
-            return;
-
-        var target = component.Ensnared.Value;
-
-        _container.Remove(ensnare, ensnareable.Container, force: true);
-        ensnareable.IsEnsnared = ensnareable.Container.ContainedEntities.Count > 0;
-        Dirty(component.Ensnared.Value, ensnareable);
-        component.Ensnared = null;
-
-        UpdateAlert(target, ensnareable);
-        var ev = new EnsnareRemoveEvent(component.WalkSpeed, component.SprintSpeed);
-        RaiseLocalEvent(ensnare, ev);
-    }
-
-    /// <summary>
-    /// Update the Ensnared alert for an entity.
-    /// </summary>
-    /// <param name="target">The entity that has been affected by a snare</param>
-    public void UpdateAlert(EntityUid target, EnsnareableComponent component)
-    {
-        if (!component.IsEnsnared)
-            _alerts.ClearAlert(target, component.EnsnaredAlert);
-        else
-            _alerts.ShowAlert(target, component.EnsnaredAlert);
-    }
-}
index d732c5f3a39d61b7fc588c0a0e5b832f0d2bf4ee..778d7b3580443c1b6e980575ef823d6de78b9cbb 100644 (file)
@@ -1,61 +1,5 @@
-using Content.Server.Popups;
-using Content.Shared.DoAfter;
 using Content.Shared.Ensnaring;
-using Content.Shared.Ensnaring.Components;
-using Content.Shared.Hands.EntitySystems;
-using Content.Shared.Popups;
-using Robust.Server.Containers;
-using Robust.Shared.Containers;
 
 namespace Content.Server.Ensnaring;
 
-public sealed partial class EnsnareableSystem : SharedEnsnareableSystem
-{
-    [Dependency] private readonly ContainerSystem _container = default!;
-    [Dependency] private readonly SharedHandsSystem _hands = default!;
-    [Dependency] private readonly PopupSystem _popup = default!;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        InitializeEnsnaring();
-
-        SubscribeLocalEvent<EnsnareableComponent, ComponentInit>(OnEnsnareableInit);
-        SubscribeLocalEvent<EnsnareableComponent, EnsnareableDoAfterEvent>(OnDoAfter);
-    }
-
-    private void OnEnsnareableInit(EntityUid uid, EnsnareableComponent component, ComponentInit args)
-    {
-        component.Container = _container.EnsureContainer<Container>(uid, "ensnare");
-    }
-
-    private void OnDoAfter(EntityUid uid, EnsnareableComponent component, DoAfterEvent args)
-    {
-        if (args.Args.Target == null)
-            return;
-
-        if (args.Handled || !TryComp<EnsnaringComponent>(args.Args.Used, out var ensnaring))
-            return;
-
-        if (args.Cancelled || !_container.Remove(args.Args.Used.Value, component.Container))
-        {
-            _popup.PopupEntity(Loc.GetString("ensnare-component-try-free-fail", ("ensnare", args.Args.Used)), uid, uid, PopupType.MediumCaution);
-            return;
-        }
-
-        component.IsEnsnared = component.Container.ContainedEntities.Count > 0;
-        Dirty(uid, component);
-        ensnaring.Ensnared = null;
-
-        _hands.PickupOrDrop(args.Args.User, args.Args.Used.Value);
-
-        _popup.PopupEntity(Loc.GetString("ensnare-component-try-free-complete", ("ensnare", args.Args.Used)), uid, uid, PopupType.Medium);
-
-        UpdateAlert(args.Args.Target.Value, component);
-        var ev = new EnsnareRemoveEvent(ensnaring.WalkSpeed, ensnaring.SprintSpeed);
-        RaiseLocalEvent(uid, ev);
-
-        args.Handled = true;
-    }
-}
+public sealed class EnsnareableSystem : SharedEnsnareableSystem;
index 6d728df9d67bfbb5794bf40e1b528479c62a48ad..b74e40e1da916d7e3ce0b9dd9a7ed6c792c1a42e 100644 (file)
@@ -19,588 +19,9 @@ using Content.Shared.Verbs;
 using Robust.Shared.Player;
 using Robust.Shared.Utility;
 
-namespace Content.Server.Strip
-{
-    public sealed class StrippableSystem : SharedStrippableSystem
-    {
-        [Dependency] private readonly InventorySystem _inventorySystem = default!;
-        [Dependency] private readonly EnsnareableSystem _ensnaringSystem = default!;
-
-        [Dependency] private readonly SharedCuffableSystem _cuffableSystem = default!;
-        [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
-        [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
-        [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
-
-        [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-
-        // TODO: ECS popups. Not all of these have ECS equivalents yet.
-
-        public override void Initialize()
-        {
-            base.Initialize();
-
-            SubscribeLocalEvent<StrippableComponent, GetVerbsEvent<Verb>>(AddStripVerb);
-            SubscribeLocalEvent<StrippableComponent, GetVerbsEvent<ExamineVerb>>(AddStripExamineVerb);
-
-            // BUI
-            SubscribeLocalEvent<StrippableComponent, StrippingSlotButtonPressed>(OnStripButtonPressed);
-            SubscribeLocalEvent<EnsnareableComponent, StrippingEnsnareButtonPressed>(OnStripEnsnareMessage);
-
-            // DoAfters
-            SubscribeLocalEvent<HandsComponent, DoAfterAttemptEvent<StrippableDoAfterEvent>>(OnStrippableDoAfterRunning);
-            SubscribeLocalEvent<HandsComponent, StrippableDoAfterEvent>(OnStrippableDoAfterFinished);
-        }
-
-        private void AddStripVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent<Verb> args)
-        {
-            if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User)
-                return;
-
-            if (!HasComp<ActorComponent>(args.User))
-                return;
-
-            Verb verb = new()
-            {
-                Text = Loc.GetString("strip-verb-get-data-text"),
-                Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
-                Act = () => TryOpenStrippingUi(args.User, (uid, component), true),
-            };
-
-            args.Verbs.Add(verb);
-        }
-
-        private void AddStripExamineVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent<ExamineVerb> args)
-        {
-            if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User)
-                return;
-
-            if (!HasComp<ActorComponent>(args.User))
-                return;
-
-            ExamineVerb verb = new()
-            {
-                Text = Loc.GetString("strip-verb-get-data-text"),
-                Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
-                Act = () => TryOpenStrippingUi(args.User, (uid, component), true),
-                Category = VerbCategory.Examine,
-            };
-
-            args.Verbs.Add(verb);
-        }
-
-        private void OnStripButtonPressed(Entity<StrippableComponent> strippable, ref StrippingSlotButtonPressed args)
-        {
-            if (args.Actor is not { Valid: true } user ||
-                !TryComp<HandsComponent>(user, out var userHands))
-                return;
-
-            if (args.IsHand)
-            {
-                StripHand((user, userHands), (strippable.Owner, null), args.Slot, strippable);
-                return;
-            }
-
-            if (!TryComp<InventoryComponent>(strippable, out var inventory))
-                return;
-
-            var hasEnt = _inventorySystem.TryGetSlotEntity(strippable, args.Slot, out var held, inventory);
-
-            if (userHands.ActiveHandEntity != null && !hasEnt)
-                StartStripInsertInventory((user, userHands), strippable.Owner, userHands.ActiveHandEntity.Value, args.Slot);
-            else if (userHands.ActiveHandEntity == null && hasEnt)
-                StartStripRemoveInventory(user, strippable.Owner, held!.Value, args.Slot);
-        }
-
-        private void StripHand(
-            Entity<HandsComponent?> user,
-            Entity<HandsComponent?> target,
-            string handId,
-            StrippableComponent? targetStrippable)
-        {
-            if (!Resolve(user, ref user.Comp) ||
-                !Resolve(target, ref target.Comp) ||
-                !Resolve(target, ref targetStrippable))
-                return;
-
-            if (!_handsSystem.TryGetHand(target.Owner, handId, out var handSlot))
-                return;
-
-            // Is the target a handcuff?
-            if (TryComp<VirtualItemComponent>(handSlot.HeldEntity, out var virtualItem) &&
-                TryComp<CuffableComponent>(target.Owner, out var cuffable) &&
-                _cuffableSystem.GetAllCuffs(cuffable).Contains(virtualItem.BlockingEntity))
-            {
-                _cuffableSystem.TryUncuff(target.Owner, user, virtualItem.BlockingEntity, cuffable);
-                return;
-            }
-
-            if (user.Comp.ActiveHandEntity != null && handSlot.HeldEntity == null)
-                StartStripInsertHand(user, target, user.Comp.ActiveHandEntity.Value, handId, targetStrippable);
-            else if (user.Comp.ActiveHandEntity == null && handSlot.HeldEntity != null)
-                StartStripRemoveHand(user, target, handSlot.HeldEntity.Value, handId, targetStrippable);
-        }
-
-        private void OnStripEnsnareMessage(EntityUid uid, EnsnareableComponent component, StrippingEnsnareButtonPressed args)
-        {
-            if (args.Actor is not { Valid: true } user)
-                return;
-
-            foreach (var entity in component.Container.ContainedEntities)
-            {
-                if (!TryComp<EnsnaringComponent>(entity, out var ensnaring))
-                    continue;
-
-                _ensnaringSystem.TryFree(uid, user, entity, ensnaring);
-                return;
-            }
-        }
-
-        /// <summary>
-        ///     Checks whether the item is in a user's active hand and whether it can be inserted into the inventory slot.
-        /// </summary>
-        private bool CanStripInsertInventory(
-            Entity<HandsComponent?> user,
-            EntityUid target,
-            EntityUid held,
-            string slot)
-        {
-            if (!Resolve(user, ref user.Comp))
-                return false;
-
-            if (user.Comp.ActiveHand == null)
-                return false;
-
-            if (user.Comp.ActiveHandEntity == null)
-                return false;
-
-            if (user.Comp.ActiveHandEntity != held)
-                return false;
-
-            if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHand))
-            {
-                _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user);
-                return false;
-            }
-
-            if (_inventorySystem.TryGetSlotEntity(target, slot, out _))
-            {
-                _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-occupied", ("owner", target)), user);
-                return false;
-            }
-
-            if (!_inventorySystem.CanEquip(user, target, held, slot, out _))
-            {
-                _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-equip-message", ("owner", target)), user);
-                return false;
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        ///     Begins a DoAfter to insert the item in the user's active hand into the inventory slot.
-        /// </summary>
-        private void StartStripInsertInventory(
-            Entity<HandsComponent?> user,
-            EntityUid target,
-            EntityUid held,
-            string slot)
-        {
-            if (!Resolve(user, ref user.Comp))
-                return;
-
-            if (!CanStripInsertInventory(user, target, held, slot))
-                return;
-
-            if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef))
-            {
-                Log.Error($"{ToPrettyString(user)} attempted to place an item in a non-existent inventory slot ({slot}) on {ToPrettyString(target)}");
-                return;
-            }
-
-            var (time, stealth) = GetStripTimeModifiers(user, target, held, slotDef.StripTime);
-
-            if (!stealth)
-                _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large);
-
-            var prefix = stealth ? "stealthily " : "";
-            _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot");
-
-            var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, true, slot), user, target, held)
-            {
-                Hidden = stealth,
-                AttemptFrequency = AttemptFrequency.EveryTick,
-                BreakOnDamage = true,
-                BreakOnMove = true,
-                NeedHand = true,
-                DuplicateCondition = DuplicateConditions.SameTool
-            };
-
-            _doAfterSystem.TryStartDoAfter(doAfterArgs);
-        }
-
-        /// <summary>
-        ///     Inserts the item in the user's active hand into the inventory slot.
-        /// </summary>
-        private void StripInsertInventory(
-            Entity<HandsComponent?> user,
-            EntityUid target,
-            EntityUid held,
-            string slot)
-        {
-            if (!Resolve(user, ref user.Comp))
-                return;
-
-            if (!CanStripInsertInventory(user, target, held, slot))
-                return;
-
-            if (!_handsSystem.TryDrop(user, handsComp: user.Comp))
-                return;
-
-            _inventorySystem.TryEquip(user, target, held, slot);
-            _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot");
-        }
-
-        /// <summary>
-        ///     Checks whether the item can be removed from the target's inventory.
-        /// </summary>
-        private bool CanStripRemoveInventory(
-            EntityUid user,
-            EntityUid target,
-            EntityUid item,
-            string slot)
-        {
-            if (!_inventorySystem.TryGetSlotEntity(target, slot, out var slotItem))
-            {
-                _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", target)), user);
-                return false;
-            }
-
-            if (slotItem != item)
-                return false;
-
-            if (!_inventorySystem.CanUnequip(user, target, slot, out var reason))
-            {
-                _popupSystem.PopupCursor(Loc.GetString(reason), user);
-                return false;
-            }
-
-            return true;
-        }
+namespace Content.Server.Strip;
 
-        /// <summary>
-        ///     Begins a DoAfter to remove the item from the target's inventory and insert it in the user's active hand.
-        /// </summary>
-        private void StartStripRemoveInventory(
-            EntityUid user,
-            EntityUid target,
-            EntityUid item,
-            string slot)
-        {
-            if (!CanStripRemoveInventory(user, target, item, slot))
-                return;
-
-            if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef))
-            {
-                Log.Error($"{ToPrettyString(user)} attempted to take an item from a non-existent inventory slot ({slot}) on {ToPrettyString(target)}");
-                return;
-            }
-
-            var (time, stealth) = GetStripTimeModifiers(user, target, item, slotDef.StripTime);
-
-            if (!stealth)
-            {
-                if (slotDef.StripHidden)
-                    _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, target, PopupType.Large);
-                else
-                    _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target, PopupType.Large);
-            }
-
-            var prefix = stealth ? "stealthily " : "";
-            _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot");
-
-            var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, true, slot), user, target, item)
-            {
-                Hidden = stealth,
-                AttemptFrequency = AttemptFrequency.EveryTick,
-                BreakOnDamage = true,
-                BreakOnMove = true,
-                NeedHand = true,
-                BreakOnHandChange = false, // Allow simultaneously removing multiple items.
-                DuplicateCondition = DuplicateConditions.SameTool
-            };
-
-            _doAfterSystem.TryStartDoAfter(doAfterArgs);
-        }
-
-        /// <summary>
-        ///     Removes the item from the target's inventory and inserts it in the user's active hand.
-        /// </summary>
-        private void StripRemoveInventory(
-            EntityUid user,
-            EntityUid target,
-            EntityUid item,
-            string slot,
-            bool stealth)
-        {
-            if (!CanStripRemoveInventory(user, target, item, slot))
-                return;
-
-            if (!_inventorySystem.TryUnequip(user, target, slot))
-                return;
-
-            RaiseLocalEvent(item, new DroppedEvent(user), true); // Gas tank internals etc.
-
-            _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: !stealth);
-            _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot");
-        }
-
-        /// <summary>
-        ///     Checks whether the item in the user's active hand can be inserted into one of the target's hands.
-        /// </summary>
-        private bool CanStripInsertHand(
-            Entity<HandsComponent?> user,
-            Entity<HandsComponent?> target,
-            EntityUid held,
-            string handName)
-        {
-            if (!Resolve(user, ref user.Comp) ||
-                !Resolve(target, ref target.Comp))
-                return false;
-
-            if (user.Comp.ActiveHand == null)
-                return false;
-
-            if (user.Comp.ActiveHandEntity == null)
-                return false;
-
-            if (user.Comp.ActiveHandEntity != held)
-                return false;
-
-            if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHand))
-            {
-                _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user);
-                return false;
-            }
-
-            if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp) ||
-                !_handsSystem.CanPickupToHand(target, user.Comp.ActiveHandEntity.Value, handSlot, checkActionBlocker: false, target.Comp))
-            {
-                _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-put-message", ("owner", target)), user);
-                return false;
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        ///     Begins a DoAfter to insert the item in the user's active hand into one of the target's hands.
-        /// </summary>
-        private void StartStripInsertHand(
-            Entity<HandsComponent?> user,
-            Entity<HandsComponent?> target,
-            EntityUid held,
-            string handName,
-            StrippableComponent? targetStrippable = null)
-        {
-            if (!Resolve(user, ref user.Comp) ||
-                !Resolve(target, ref target.Comp) ||
-                !Resolve(target, ref targetStrippable))
-                return;
-
-            if (!CanStripInsertHand(user, target, held, handName))
-                return;
-
-            var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay);
-
-            if (!stealth)
-                _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert-hand", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large);
-
-            var prefix = stealth ? "stealthily " : "";
-            _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
-
-            var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, false, handName), user, target, held)
-            {
-                Hidden = stealth,
-                AttemptFrequency = AttemptFrequency.EveryTick,
-                BreakOnDamage = true,
-                BreakOnMove = true,
-                NeedHand = true,
-                DuplicateCondition = DuplicateConditions.SameTool
-            };
-
-            _doAfterSystem.TryStartDoAfter(doAfterArgs);
-        }
-
-        /// <summary>
-        ///     Places the item in the user's active hand into one of the target's hands.
-        /// </summary>
-        private void StripInsertHand(
-            Entity<HandsComponent?> user,
-            Entity<HandsComponent?> target,
-            EntityUid held,
-            string handName,
-            bool stealth)
-        {
-            if (!Resolve(user, ref user.Comp) ||
-                !Resolve(target, ref target.Comp))
-                return;
-
-            if (!CanStripInsertHand(user, target, held, handName))
-                return;
-
-            _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: user.Comp);
-            _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: stealth, animate: !stealth, handsComp: target.Comp);
-            _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
-
-            // Hand update will trigger strippable update.
-        }
-
-        /// <summary>
-        ///     Checks whether the item is in the target's hand and whether it can be dropped.
-        /// </summary>
-        private bool CanStripRemoveHand(
-            EntityUid user,
-            Entity<HandsComponent?> target,
-            EntityUid item,
-            string handName)
-        {
-            if (!Resolve(target, ref target.Comp))
-                return false;
-
-            if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp))
-            {
-                _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", Identity.Name(target, EntityManager, user))), user);
-                return false;
-            }
-
-            if (HasComp<VirtualItemComponent>(handSlot.HeldEntity))
-                return false;
-
-            if (handSlot.HeldEntity == null)
-                return false;
-
-            if (handSlot.HeldEntity != item)
-                return false;
-
-            if (!_handsSystem.CanDropHeld(target, handSlot, false))
-            {
-                _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop-message", ("owner", Identity.Name(target, EntityManager, user))), user);
-                return false;
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        ///     Begins a DoAfter to remove the item from the target's hand and insert it in the user's active hand.
-        /// </summary>
-        private void StartStripRemoveHand(
-            Entity<HandsComponent?> user,
-            Entity<HandsComponent?> target,
-            EntityUid item,
-            string handName,
-            StrippableComponent? targetStrippable = null)
-        {
-            if (!Resolve(user, ref user.Comp) ||
-                !Resolve(target, ref target.Comp) ||
-                !Resolve(target, ref targetStrippable))
-                return;
-
-            if (!CanStripRemoveHand(user, target, item, handName))
-                return;
-
-            var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay);
-
-            if (!stealth)
-                _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target);
-
-            var prefix = stealth ? "stealthily " : "";
-            _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
-
-            var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, false, handName), user, target, item)
-            {
-                Hidden = stealth,
-                AttemptFrequency = AttemptFrequency.EveryTick,
-                BreakOnDamage = true,
-                BreakOnMove = true,
-                NeedHand = true,
-                BreakOnHandChange = false, // Allow simultaneously removing multiple items.
-                DuplicateCondition = DuplicateConditions.SameTool
-            };
-
-            _doAfterSystem.TryStartDoAfter(doAfterArgs);
-        }
-
-        /// <summary>
-        ///     Takes the item from the target's hand and inserts it in the user's active hand.
-        /// </summary>
-        private void StripRemoveHand(
-            Entity<HandsComponent?> user,
-            Entity<HandsComponent?> target,
-            EntityUid item,
-            string handName,
-            bool stealth)
-        {
-            if (!Resolve(user, ref user.Comp) ||
-                !Resolve(target, ref target.Comp))
-                return;
-
-            if (!CanStripRemoveHand(user, target, item, handName))
-                return;
-
-            _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: target.Comp);
-            _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: !stealth, handsComp: user.Comp);
-            _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
-
-            // Hand update will trigger strippable update.
-        }
-
-        private void OnStrippableDoAfterRunning(Entity<HandsComponent> entity, ref DoAfterAttemptEvent<StrippableDoAfterEvent> ev)
-        {
-            var args = ev.DoAfter.Args;
-
-            DebugTools.Assert(entity.Owner == args.User);
-            DebugTools.Assert(args.Target != null);
-            DebugTools.Assert(args.Used != null);
-            DebugTools.Assert(ev.Event.SlotOrHandName != null);
-
-            if (ev.Event.InventoryOrHand)
-            {
-                if ( ev.Event.InsertOrRemove && !CanStripInsertInventory((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
-                    !ev.Event.InsertOrRemove && !CanStripRemoveInventory(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName))
-                        ev.Cancel();
-            }
-            else
-            {
-                if ( ev.Event.InsertOrRemove && !CanStripInsertHand((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
-                    !ev.Event.InsertOrRemove && !CanStripRemoveHand(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName))
-                        ev.Cancel();
-            }
-        }
-
-        private void OnStrippableDoAfterFinished(Entity<HandsComponent> entity, ref StrippableDoAfterEvent ev)
-        {
-            if (ev.Cancelled)
-                return;
-
-            DebugTools.Assert(entity.Owner == ev.User);
-            DebugTools.Assert(ev.Target != null);
-            DebugTools.Assert(ev.Used != null);
-            DebugTools.Assert(ev.SlotOrHandName != null);
+public sealed class StrippableSystem : SharedStrippableSystem
+{
 
-            if (ev.InventoryOrHand)
-            {
-                if (ev.InsertOrRemove)
-                        StripInsertInventory((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName);
-                else    StripRemoveInventory(entity.Owner, ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
-            }
-            else
-            {
-                if (ev.InsertOrRemove)
-                        StripInsertHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
-                else    StripRemoveHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
-            }
-        }
-    }
 }
index cd7824bb960b7c1a612d362f5151965919f6b2f3..307a5e19863306b637bac3e9204ce0d207e18784 100644 (file)
@@ -2,34 +2,30 @@ using Content.Shared.Alert;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
 
 namespace Content.Shared.Ensnaring.Components;
 /// <summary>
 /// Use this on an entity that you would like to be ensnared by anything that has the <see cref="EnsnaringComponent"/>
 /// </summary>
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
 public sealed partial class EnsnareableComponent : Component
 {
     /// <summary>
     /// How much should this slow down the entities walk?
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("walkSpeed")]
+    [DataField]
     public float WalkSpeed = 1.0f;
 
     /// <summary>
     /// How much should this slow down the entities sprint?
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("sprintSpeed")]
+    [DataField]
     public float SprintSpeed = 1.0f;
 
     /// <summary>
     /// Is this entity currently ensnared?
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("isEnsnared")]
+    [DataField, AutoNetworkedField]
     public bool IsEnsnared;
 
     /// <summary>
@@ -37,10 +33,10 @@ public sealed partial class EnsnareableComponent : Component
     /// </summary>
     public Container Container = default!;
 
-    [DataField("sprite")]
+    [DataField]
     public string? Sprite;
 
-    [DataField("state")]
+    [DataField]
     public string? State;
 
     [DataField]
@@ -49,17 +45,6 @@ public sealed partial class EnsnareableComponent : Component
 
 public sealed partial class RemoveEnsnareAlertEvent : BaseAlertEvent;
 
-[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;
index 6e1f3077f300073a1ec6fb50ae2591250df633aa..f900d863c2a9aaf59b82fbfeb1780f24e08896f0 100644 (file)
@@ -1,4 +1,4 @@
-using System.Threading;
+using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
 
 namespace Content.Shared.Ensnaring.Components;
@@ -11,59 +11,53 @@ public sealed partial class EnsnaringComponent : Component
     /// <summary>
     /// How long it should take to free someone else.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("freeTime")]
+    [DataField]
     public float FreeTime = 3.5f;
 
     /// <summary>
     /// How long it should take for an entity to free themselves.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("breakoutTime")]
+    [DataField]
     public float BreakoutTime = 30.0f;
 
     /// <summary>
     /// How much should this slow down the entities walk?
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("walkSpeed")]
+    [DataField]
     public float WalkSpeed = 0.9f;
 
     /// <summary>
     /// How much should this slow down the entities sprint?
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("sprintSpeed")]
+    [DataField]
     public float SprintSpeed = 0.9f;
 
     /// <summary>
     /// How much stamina does the ensnare sap
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("staminaDamage")]
+    [DataField]
     public float StaminaDamage = 55f;
 
     /// <summary>
     /// Should this ensnare someone when thrown?
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("canThrowTrigger")]
+    [DataField]
     public bool CanThrowTrigger;
 
     /// <summary>
     /// What is ensnared?
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("ensnared")]
+    [DataField]
     public EntityUid? Ensnared;
 
     /// <summary>
     /// Should breaking out be possible when moving?
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("canMoveBreakout")]
+    [DataField]
     public bool CanMoveBreakout;
 
+    [DataField]
+    public SoundSpecifier? EnsnareSound = new SoundPathSpecifier("/Audio/Effects/snap.ogg");
 }
 
 /// <summary>
@@ -95,29 +89,3 @@ public sealed class EnsnareRemoveEvent : CancellableEntityEventArgs
         SprintSpeed = sprintSpeed;
     }
 }
-
-/// <summary>
-/// Used for the do after event to free the entity that owns the <see cref="EnsnareableComponent"/>
-/// </summary>
-public sealed class FreeEnsnareDoAfterComplete : EntityEventArgs
-{
-    public readonly EntityUid EnsnaringEntity;
-
-    public FreeEnsnareDoAfterComplete(EntityUid ensnaringEntity)
-    {
-        EnsnaringEntity = ensnaringEntity;
-    }
-}
-
-/// <summary>
-/// Used for the do after event when it fails to free the entity that owns the <see cref="EnsnareableComponent"/>
-/// </summary>
-public sealed class FreeEnsnareDoAfterCancel : EntityEventArgs
-{
-    public readonly EntityUid EnsnaringEntity;
-
-    public FreeEnsnareDoAfterCancel(EntityUid ensnaringEntity)
-    {
-        EnsnaringEntity = ensnaringEntity;
-    }
-}
index 4f35dc583a1ad21061abcfec34fe151f2d0a0f54..551c76ff8d5f53d4137a99750d4cc0ab5e93e83a 100644 (file)
@@ -1,7 +1,21 @@
+using System.Linq;
+using Content.Shared.Alert;
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Systems;
+using Content.Shared.CombatMode.Pacification;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
 using Content.Shared.DoAfter;
 using Content.Shared.Ensnaring.Components;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.IdentityManagement;
 using Content.Shared.Movement.Systems;
-using Robust.Shared.GameStates;
+using Content.Shared.Popups;
+using Content.Shared.StepTrigger.Systems;
+using Content.Shared.Strip.Components;
+using Content.Shared.Throwing;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Ensnaring;
@@ -13,36 +27,82 @@ public sealed partial class EnsnareableDoAfterEvent : SimpleDoAfterEvent
 
 public abstract class SharedEnsnareableSystem : EntitySystem
 {
-    [Dependency] private readonly MovementSpeedModifierSystem _speedModifier = default!;
+    [Dependency] private   readonly AlertsSystem _alerts = default!;
+    [Dependency] private   readonly MovementSpeedModifierSystem _speedModifier = default!;
     [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
+    [Dependency] private   readonly SharedAudioSystem _audio = default!;
+    [Dependency] private   readonly SharedBodySystem _body = default!;
+    [Dependency] protected readonly SharedContainerSystem Container = default!;
+    [Dependency] private   readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private   readonly SharedHandsSystem _hands = default!;
+    [Dependency] protected readonly SharedPopupSystem Popup = default!;
+    [Dependency] private   readonly StaminaSystem _stamina = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
+        SubscribeLocalEvent<EnsnareableComponent, ComponentInit>(OnEnsnareInit);
         SubscribeLocalEvent<EnsnareableComponent, RefreshMovementSpeedModifiersEvent>(MovementSpeedModify);
         SubscribeLocalEvent<EnsnareableComponent, EnsnareEvent>(OnEnsnare);
         SubscribeLocalEvent<EnsnareableComponent, EnsnareRemoveEvent>(OnEnsnareRemove);
         SubscribeLocalEvent<EnsnareableComponent, EnsnaredChangedEvent>(OnEnsnareChange);
-        SubscribeLocalEvent<EnsnareableComponent, ComponentGetState>(OnGetState);
-        SubscribeLocalEvent<EnsnareableComponent, ComponentHandleState>(OnHandleState);
+        SubscribeLocalEvent<EnsnareableComponent, AfterAutoHandleStateEvent>(OnHandleState);
+        SubscribeLocalEvent<EnsnareableComponent, StrippingEnsnareButtonPressed>(OnStripEnsnareMessage);
+        SubscribeLocalEvent<EnsnareableComponent, RemoveEnsnareAlertEvent>(OnRemoveEnsnareAlert);
+        SubscribeLocalEvent<EnsnareableComponent, EnsnareableDoAfterEvent>(OnDoAfter);
+
+        SubscribeLocalEvent<EnsnaringComponent, ComponentRemove>(OnComponentRemove);
+        SubscribeLocalEvent<EnsnaringComponent, StepTriggerAttemptEvent>(AttemptStepTrigger);
+        SubscribeLocalEvent<EnsnaringComponent, StepTriggeredOffEvent>(OnStepTrigger);
+        SubscribeLocalEvent<EnsnaringComponent, ThrowDoHitEvent>(OnThrowHit);
+        SubscribeLocalEvent<EnsnaringComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
     }
 
-    private void OnHandleState(EntityUid uid, EnsnareableComponent component, ref ComponentHandleState args)
+    protected virtual void OnEnsnareInit(Entity<EnsnareableComponent> ent, ref ComponentInit args)
     {
-        if (args.Current is not EnsnareableComponentState state)
-            return;
-
-        if (state.IsEnsnared == component.IsEnsnared)
-            return;
+        ent.Comp.Container = Container.EnsureContainer<Container>(ent.Owner, "ensnare");
+    }
 
-        component.IsEnsnared = state.IsEnsnared;
+    private void OnHandleState(EntityUid uid, EnsnareableComponent component, ref AfterAutoHandleStateEvent args)
+    {
         RaiseLocalEvent(uid, new EnsnaredChangedEvent(component.IsEnsnared));
     }
 
-    private void OnGetState(EntityUid uid, EnsnareableComponent component, ref ComponentGetState args)
+    private void OnDoAfter(EntityUid uid, EnsnareableComponent component, DoAfterEvent args)
     {
-        args.State = new EnsnareableComponentState(component.IsEnsnared);
+        if (args.Args.Target == null)
+            return;
+
+        if (args.Handled || !TryComp<EnsnaringComponent>(args.Args.Used, out var ensnaring))
+            return;
+
+        if (args.Cancelled || !Container.Remove(args.Args.Used.Value, component.Container))
+        {
+            if (args.User == args.Target)
+                Popup.PopupPredicted(Loc.GetString("ensnare-component-try-free-fail", ("ensnare", args.Args.Used)), uid, args.User, PopupType.MediumCaution);
+            else if (args.Target != null)
+                Popup.PopupPredicted(Loc.GetString("ensnare-component-try-free-fail-other", ("ensnare", args.Args.Used), ("user", args.Target)), uid, args.User, PopupType.MediumCaution);
+
+            return;
+        }
+
+        component.IsEnsnared = component.Container.ContainedEntities.Count > 0;
+        Dirty(uid, component);
+        ensnaring.Ensnared = null;
+
+        _hands.PickupOrDrop(args.Args.User, args.Args.Used.Value);
+
+        if (args.User == args.Target)
+            Popup.PopupPredicted(Loc.GetString("ensnare-component-try-free-complete", ("ensnare", args.Args.Used)), uid, args.User, PopupType.Medium);
+        else if (args.Target != null)
+            Popup.PopupPredicted(Loc.GetString("ensnare-component-try-free-complete-other", ("ensnare", args.Args.Used), ("user", args.Target)), uid, args.User, PopupType.Medium);
+
+        UpdateAlert(args.Args.Target.Value, component);
+        var ev = new EnsnareRemoveEvent(ensnaring.WalkSpeed, ensnaring.SprintSpeed);
+        RaiseLocalEvent(uid, ev);
+
+        args.Handled = true;
     }
 
     private void OnEnsnare(EntityUid uid, EnsnareableComponent component, EnsnareEvent args)
@@ -85,4 +145,178 @@ public abstract class SharedEnsnareableSystem : EntitySystem
 
         args.ModifySpeed(component.WalkSpeed, component.SprintSpeed);
     }
+
+    /// <summary>
+    /// Used where you want to try to free an entity with the <see cref="EnsnareableComponent"/>
+    /// </summary>
+    /// <param name="target">The entity that will be freed</param>
+    /// <param name="user">The entity that is freeing the target</param>
+    /// <param name="ensnare">The entity used to ensnare</param>
+    /// <param name="component">The ensnaring component</param>
+    public void TryFree(EntityUid target, EntityUid user, EntityUid ensnare, EnsnaringComponent component)
+    {
+        // Don't do anything if they don't have the ensnareable component.
+        if (!HasComp<EnsnareableComponent>(target))
+            return;
+
+        var freeTime = user == target ? component.BreakoutTime : component.FreeTime;
+        var breakOnMove = !component.CanMoveBreakout;
+
+        var doAfterEventArgs = new DoAfterArgs(EntityManager, user, freeTime, new EnsnareableDoAfterEvent(), target, target: target, used: ensnare)
+        {
+            BreakOnMove = breakOnMove,
+            BreakOnDamage = false,
+            NeedHand = true,
+            BreakOnDropItem = false,
+        };
+
+        if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
+            return;
+
+        if (user == target)
+            Popup.PopupPredicted(Loc.GetString("ensnare-component-try-free", ("ensnare", ensnare)), target, target);
+        else
+            Popup.PopupPredicted(Loc.GetString("ensnare-component-try-free-other", ("ensnare", ensnare), ("user", Identity.Entity(target, EntityManager))), user, user);
+    }
+
+    private void OnStripEnsnareMessage(EntityUid uid, EnsnareableComponent component, StrippingEnsnareButtonPressed args)
+    {
+        foreach (var entity in component.Container.ContainedEntities)
+        {
+            if (!TryComp<EnsnaringComponent>(entity, out var ensnaring))
+                continue;
+
+            TryFree(uid, args.Actor, entity, ensnaring);
+            return;
+        }
+    }
+
+    private void OnAttemptPacifiedThrow(Entity<EnsnaringComponent> ent, ref AttemptPacifiedThrowEvent args)
+    {
+        args.Cancel("pacified-cannot-throw-snare");
+    }
+
+    private void OnRemoveEnsnareAlert(Entity<EnsnareableComponent> ent, ref RemoveEnsnareAlertEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        foreach (var ensnare in ent.Comp.Container.ContainedEntities)
+        {
+            if (!TryComp<EnsnaringComponent>(ensnare, out var ensnaringComponent))
+                continue;
+
+            TryFree(ent, ent, ensnare, ensnaringComponent);
+
+            args.Handled = true;
+            // Only one snare at a time.
+            break;
+        }
+    }
+
+    private void OnComponentRemove(EntityUid uid, EnsnaringComponent component, ComponentRemove args)
+    {
+        if (!TryComp<EnsnareableComponent>(component.Ensnared, out var ensnared))
+            return;
+
+        if (ensnared.IsEnsnared)
+            ForceFree(uid, component);
+    }
+
+    private void AttemptStepTrigger(EntityUid uid, EnsnaringComponent component, ref StepTriggerAttemptEvent args)
+    {
+        args.Continue = true;
+    }
+
+    private void OnStepTrigger(EntityUid uid, EnsnaringComponent component, ref StepTriggeredOffEvent args)
+    {
+        TryEnsnare(args.Tripper, uid, component);
+    }
+
+    private void OnThrowHit(EntityUid uid, EnsnaringComponent component, ThrowDoHitEvent args)
+    {
+        if (!component.CanThrowTrigger)
+            return;
+
+        if (TryEnsnare(args.Target, uid, component))
+        {
+            _audio.PlayPvs(component.EnsnareSound, uid);
+        }
+    }
+
+    /// <summary>
+    /// Used where you want to try to ensnare an entity with the <see cref="EnsnareableComponent"/>
+    /// </summary>
+    /// <param name="target">The entity that will be ensnared</param>
+    /// <paramref name="ensnare"> The entity that is used to ensnare</param>
+    /// <param name="component">The ensnaring component</param>
+    public bool TryEnsnare(EntityUid target, EntityUid ensnare, EnsnaringComponent component)
+    {
+        //Don't do anything if they don't have the ensnareable component.
+        if (!TryComp<EnsnareableComponent>(target, out var ensnareable))
+            return false;
+
+        // Need to insert before free legs check.
+        Container.Insert(ensnare, ensnareable.Container);
+
+        var legs = _body.GetBodyChildrenOfType(target, BodyPartType.Leg).Count();
+        var ensnaredLegs = (2 * ensnareable.Container.ContainedEntities.Count);
+        var freeLegs = legs - ensnaredLegs;
+
+        if (freeLegs > 0)
+            return false;
+
+        // Apply stamina damage to target if they weren't ensnared before.
+        if (ensnareable.IsEnsnared != true)
+        {
+            if (TryComp<StaminaComponent>(target, out var stamina))
+            {
+                _stamina.TakeStaminaDamage(target, component.StaminaDamage, with: ensnare, component: stamina);
+            }
+        }
+
+        component.Ensnared = target;
+        ensnareable.IsEnsnared = true;
+        Dirty(target, ensnareable);
+
+        UpdateAlert(target, ensnareable);
+        var ev = new EnsnareEvent(component.WalkSpeed, component.SprintSpeed);
+        RaiseLocalEvent(target, ev);
+        return true;
+    }
+
+    /// <summary>
+    /// Used to force free someone for things like if the <see cref="EnsnaringComponent"/> is removed
+    /// </summary>
+    public void ForceFree(EntityUid ensnare, EnsnaringComponent component)
+    {
+        if (component.Ensnared == null)
+            return;
+
+        if (!TryComp<EnsnareableComponent>(component.Ensnared, out var ensnareable))
+            return;
+
+        var target = component.Ensnared.Value;
+
+        Container.Remove(ensnare, ensnareable.Container, force: true);
+        ensnareable.IsEnsnared = ensnareable.Container.ContainedEntities.Count > 0;
+        Dirty(component.Ensnared.Value, ensnareable);
+        component.Ensnared = null;
+
+        UpdateAlert(target, ensnareable);
+        var ev = new EnsnareRemoveEvent(component.WalkSpeed, component.SprintSpeed);
+        RaiseLocalEvent(ensnare, ev);
+    }
+
+    /// <summary>
+    /// Update the Ensnared alert for an entity.
+    /// </summary>
+    /// <param name="target">The entity that has been affected by a snare</param>
+    public void UpdateAlert(EntityUid target, EnsnareableComponent component)
+    {
+        if (!component.IsEnsnared)
+            _alerts.ClearAlert(target, component.EnsnaredAlert);
+        else
+            _alerts.ShowAlert(target, component.EnsnaredAlert);
+    }
 }
index 762561ed32233fa252147bffae4717d1338b9df4..1d5d91a9e36c355427aaf8a9ea696c44d1e99459 100644 (file)
@@ -109,8 +109,7 @@ public abstract partial class InventorySystem
         // before we drop the item, check that it can be equipped in the first place.
         if (!CanEquip(actor, held.Value, ev.Slot, out var reason))
         {
-            if (_gameTiming.IsFirstTimePredicted)
-                _popup.PopupCursor(Loc.GetString(reason));
+            _popup.PopupCursor(Loc.GetString(reason));
             return;
         }
 
@@ -131,7 +130,7 @@ public abstract partial class InventorySystem
     {
         if (!Resolve(target, ref inventory, false))
         {
-            if(!silent && _gameTiming.IsFirstTimePredicted)
+            if(!silent)
                 _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot"));
             return false;
         }
@@ -142,14 +141,14 @@ public abstract partial class InventorySystem
 
         if (!TryGetSlotContainer(target, slot, out var slotContainer, out var slotDefinition, inventory))
         {
-            if(!silent && _gameTiming.IsFirstTimePredicted)
+            if(!silent)
                 _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot"));
             return false;
         }
 
         if (!force && !CanEquip(actor, target, itemUid, slot, out var reason, slotDefinition, inventory, clothing))
         {
-            if(!silent && _gameTiming.IsFirstTimePredicted)
+            if(!silent)
                 _popup.PopupCursor(Loc.GetString(reason));
             return false;
         }
@@ -179,7 +178,7 @@ public abstract partial class InventorySystem
 
         if (!_containerSystem.Insert(itemUid, slotContainer))
         {
-            if(!silent && _gameTiming.IsFirstTimePredicted)
+            if(!silent)
                 _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"));
             return false;
         }
@@ -374,14 +373,14 @@ public abstract partial class InventorySystem
 
         if (!Resolve(target, ref inventory, false))
         {
-            if(!silent && _gameTiming.IsFirstTimePredicted)
+            if(!silent)
                 _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"));
             return false;
         }
 
         if (!TryGetSlotContainer(target, slot, out var slotContainer, out var slotDefinition, inventory))
         {
-            if(!silent && _gameTiming.IsFirstTimePredicted)
+            if(!silent)
                 _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"));
             return false;
         }
@@ -393,7 +392,7 @@ public abstract partial class InventorySystem
 
         if (!force && !CanUnequip(actor, target, slot, out var reason, slotContainer, slotDefinition, inventory))
         {
-            if(!silent && _gameTiming.IsFirstTimePredicted)
+            if(!silent)
                 _popup.PopupCursor(Loc.GetString(reason));
             return false;
         }
index 935dc33540eef34d933043f38f9550b8f0e28f4e..a68bf755d429e9da443ae6e9088c2a485ec2ec45 100644 (file)
@@ -1,8 +1,22 @@
+using System.Linq;
+using Content.Shared.Administration.Logs;
 using Content.Shared.CombatMode;
+using Content.Shared.Cuffs;
+using Content.Shared.Cuffs.Components;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
 using Content.Shared.DragDrop;
 using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Inventory;
+using Content.Shared.Inventory.VirtualItem;
+using Content.Shared.Popups;
 using Content.Shared.Strip.Components;
+using Content.Shared.Verbs;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Strip;
 
@@ -10,15 +24,568 @@ public abstract class SharedStrippableSystem : EntitySystem
 {
     [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
 
+    [Dependency] private readonly InventorySystem _inventorySystem = default!;
+
+    [Dependency] private readonly SharedCuffableSystem _cuffableSystem = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+    [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+    [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+
     public override void Initialize()
     {
         base.Initialize();
+
+        SubscribeLocalEvent<StrippableComponent, GetVerbsEvent<Verb>>(AddStripVerb);
+        SubscribeLocalEvent<StrippableComponent, GetVerbsEvent<ExamineVerb>>(AddStripExamineVerb);
+
+        // BUI
+        SubscribeLocalEvent<StrippableComponent, StrippingSlotButtonPressed>(OnStripButtonPressed);
+
+        // DoAfters
+        SubscribeLocalEvent<HandsComponent, DoAfterAttemptEvent<StrippableDoAfterEvent>>(OnStrippableDoAfterRunning);
+        SubscribeLocalEvent<HandsComponent, StrippableDoAfterEvent>(OnStrippableDoAfterFinished);
+
         SubscribeLocalEvent<StrippingComponent, CanDropTargetEvent>(OnCanDropOn);
         SubscribeLocalEvent<StrippableComponent, CanDropDraggedEvent>(OnCanDrop);
         SubscribeLocalEvent<StrippableComponent, DragDropDraggedEvent>(OnDragDrop);
         SubscribeLocalEvent<StrippableComponent, ActivateInWorldEvent>(OnActivateInWorld);
     }
 
+    private void AddStripVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent<Verb> args)
+    {
+        if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User)
+            return;
+
+        Verb verb = new()
+        {
+            Text = Loc.GetString("strip-verb-get-data-text"),
+            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
+            Act = () => TryOpenStrippingUi(args.User, (uid, component), true),
+        };
+
+        args.Verbs.Add(verb);
+    }
+
+    private void AddStripExamineVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent<ExamineVerb> args)
+    {
+        if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User)
+            return;
+
+        ExamineVerb verb = new()
+        {
+            Text = Loc.GetString("strip-verb-get-data-text"),
+            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
+            Act = () => TryOpenStrippingUi(args.User, (uid, component), true),
+            Category = VerbCategory.Examine,
+        };
+
+        args.Verbs.Add(verb);
+    }
+
+    private void OnStripButtonPressed(Entity<StrippableComponent> strippable, ref StrippingSlotButtonPressed args)
+    {
+        if (args.Actor is not { Valid: true } user ||
+            !TryComp<HandsComponent>(user, out var userHands))
+            return;
+
+        if (args.IsHand)
+        {
+            StripHand((user, userHands), (strippable.Owner, null), args.Slot, strippable);
+            return;
+        }
+
+        if (!TryComp<InventoryComponent>(strippable, out var inventory))
+            return;
+
+        var hasEnt = _inventorySystem.TryGetSlotEntity(strippable, args.Slot, out var held, inventory);
+
+        if (userHands.ActiveHandEntity != null && !hasEnt)
+            StartStripInsertInventory((user, userHands), strippable.Owner, userHands.ActiveHandEntity.Value, args.Slot);
+        else if (userHands.ActiveHandEntity == null && hasEnt)
+            StartStripRemoveInventory(user, strippable.Owner, held!.Value, args.Slot);
+    }
+
+    private void StripHand(
+        Entity<HandsComponent?> user,
+        Entity<HandsComponent?> target,
+        string handId,
+        StrippableComponent? targetStrippable)
+    {
+        if (!Resolve(user, ref user.Comp) ||
+            !Resolve(target, ref target.Comp) ||
+            !Resolve(target, ref targetStrippable))
+            return;
+
+        if (!_handsSystem.TryGetHand(target.Owner, handId, out var handSlot))
+            return;
+
+        // Is the target a handcuff?
+        if (TryComp<VirtualItemComponent>(handSlot.HeldEntity, out var virtualItem) &&
+            TryComp<CuffableComponent>(target.Owner, out var cuffable) &&
+            _cuffableSystem.GetAllCuffs(cuffable).Contains(virtualItem.BlockingEntity))
+        {
+            _cuffableSystem.TryUncuff(target.Owner, user, virtualItem.BlockingEntity, cuffable);
+            return;
+        }
+
+        if (user.Comp.ActiveHandEntity != null && handSlot.HeldEntity == null)
+            StartStripInsertHand(user, target, user.Comp.ActiveHandEntity.Value, handId, targetStrippable);
+        else if (user.Comp.ActiveHandEntity == null && handSlot.HeldEntity != null)
+            StartStripRemoveHand(user, target, handSlot.HeldEntity.Value, handId, targetStrippable);
+    }
+
+    /// <summary>
+    ///     Checks whether the item is in a user's active hand and whether it can be inserted into the inventory slot.
+    /// </summary>
+    private bool CanStripInsertInventory(
+        Entity<HandsComponent?> user,
+        EntityUid target,
+        EntityUid held,
+        string slot)
+    {
+        if (!Resolve(user, ref user.Comp))
+            return false;
+
+        if (user.Comp.ActiveHand == null)
+            return false;
+
+        if (user.Comp.ActiveHandEntity == null)
+            return false;
+
+        if (user.Comp.ActiveHandEntity != held)
+            return false;
+
+        if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHand))
+        {
+            _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"));
+            return false;
+        }
+
+        if (_inventorySystem.TryGetSlotEntity(target, slot, out _))
+        {
+            _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-occupied", ("owner", target)));
+            return false;
+        }
+
+        if (!_inventorySystem.CanEquip(user, target, held, slot, out _))
+        {
+            _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-equip-message", ("owner", target)));
+            return false;
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    ///     Begins a DoAfter to insert the item in the user's active hand into the inventory slot.
+    /// </summary>
+    private void StartStripInsertInventory(
+        Entity<HandsComponent?> user,
+        EntityUid target,
+        EntityUid held,
+        string slot)
+    {
+        if (!Resolve(user, ref user.Comp))
+            return;
+
+        if (!CanStripInsertInventory(user, target, held, slot))
+            return;
+
+        if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef))
+        {
+            Log.Error($"{ToPrettyString(user)} attempted to place an item in a non-existent inventory slot ({slot}) on {ToPrettyString(target)}");
+            return;
+        }
+
+        var (time, stealth) = GetStripTimeModifiers(user, target, held, slotDef.StripTime);
+
+        if (!stealth)
+            _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large);
+
+        var prefix = stealth ? "stealthily " : "";
+        _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot");
+
+        var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, true, slot), user, target, held)
+        {
+            Hidden = stealth,
+            AttemptFrequency = AttemptFrequency.EveryTick,
+            BreakOnDamage = true,
+            BreakOnMove = true,
+            NeedHand = true,
+            DuplicateCondition = DuplicateConditions.SameTool
+        };
+
+        _doAfterSystem.TryStartDoAfter(doAfterArgs);
+    }
+
+    /// <summary>
+    ///     Inserts the item in the user's active hand into the inventory slot.
+    /// </summary>
+    private void StripInsertInventory(
+        Entity<HandsComponent?> user,
+        EntityUid target,
+        EntityUid held,
+        string slot)
+    {
+        if (!Resolve(user, ref user.Comp))
+            return;
+
+        if (!CanStripInsertInventory(user, target, held, slot))
+            return;
+
+        if (!_handsSystem.TryDrop(user, handsComp: user.Comp))
+            return;
+
+        _inventorySystem.TryEquip(user, target, held, slot);
+        _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot");
+    }
+
+    /// <summary>
+    ///     Checks whether the item can be removed from the target's inventory.
+    /// </summary>
+    private bool CanStripRemoveInventory(
+        EntityUid user,
+        EntityUid target,
+        EntityUid item,
+        string slot)
+    {
+        if (!_inventorySystem.TryGetSlotEntity(target, slot, out var slotItem))
+        {
+            _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", target)));
+            return false;
+        }
+
+        if (slotItem != item)
+            return false;
+
+        if (!_inventorySystem.CanUnequip(user, target, slot, out var reason))
+        {
+            _popupSystem.PopupCursor(Loc.GetString(reason));
+            return false;
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    ///     Begins a DoAfter to remove the item from the target's inventory and insert it in the user's active hand.
+    /// </summary>
+    private void StartStripRemoveInventory(
+        EntityUid user,
+        EntityUid target,
+        EntityUid item,
+        string slot)
+    {
+        if (!CanStripRemoveInventory(user, target, item, slot))
+            return;
+
+        if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef))
+        {
+            Log.Error($"{ToPrettyString(user)} attempted to take an item from a non-existent inventory slot ({slot}) on {ToPrettyString(target)}");
+            return;
+        }
+
+        var (time, stealth) = GetStripTimeModifiers(user, target, item, slotDef.StripTime);
+
+        if (!stealth)
+        {
+            if (slotDef.StripHidden)
+                _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, target, PopupType.Large);
+            else
+                _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target, PopupType.Large);
+        }
+
+        var prefix = stealth ? "stealthily " : "";
+        _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot");
+
+        var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, true, slot), user, target, item)
+        {
+            Hidden = stealth,
+            AttemptFrequency = AttemptFrequency.EveryTick,
+            BreakOnDamage = true,
+            BreakOnMove = true,
+            NeedHand = true,
+            BreakOnHandChange = false, // Allow simultaneously removing multiple items.
+            DuplicateCondition = DuplicateConditions.SameTool
+        };
+
+        _doAfterSystem.TryStartDoAfter(doAfterArgs);
+    }
+
+    /// <summary>
+    ///     Removes the item from the target's inventory and inserts it in the user's active hand.
+    /// </summary>
+    private void StripRemoveInventory(
+        EntityUid user,
+        EntityUid target,
+        EntityUid item,
+        string slot,
+        bool stealth)
+    {
+        if (!CanStripRemoveInventory(user, target, item, slot))
+            return;
+
+        if (!_inventorySystem.TryUnequip(user, target, slot))
+            return;
+
+        RaiseLocalEvent(item, new DroppedEvent(user), true); // Gas tank internals etc.
+
+        _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: !stealth);
+        _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot");
+    }
+
+    /// <summary>
+    ///     Checks whether the item in the user's active hand can be inserted into one of the target's hands.
+    /// </summary>
+    private bool CanStripInsertHand(
+        Entity<HandsComponent?> user,
+        Entity<HandsComponent?> target,
+        EntityUid held,
+        string handName)
+    {
+        if (!Resolve(user, ref user.Comp) ||
+            !Resolve(target, ref target.Comp))
+            return false;
+
+        if (user.Comp.ActiveHand == null)
+            return false;
+
+        if (user.Comp.ActiveHandEntity == null)
+            return false;
+
+        if (user.Comp.ActiveHandEntity != held)
+            return false;
+
+        if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHand))
+        {
+            _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"));
+            return false;
+        }
+
+        if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp) ||
+            !_handsSystem.CanPickupToHand(target, user.Comp.ActiveHandEntity.Value, handSlot, checkActionBlocker: false, target.Comp))
+        {
+            _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-put-message", ("owner", target)));
+            return false;
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    ///     Begins a DoAfter to insert the item in the user's active hand into one of the target's hands.
+    /// </summary>
+    private void StartStripInsertHand(
+        Entity<HandsComponent?> user,
+        Entity<HandsComponent?> target,
+        EntityUid held,
+        string handName,
+        StrippableComponent? targetStrippable = null)
+    {
+        if (!Resolve(user, ref user.Comp) ||
+            !Resolve(target, ref target.Comp) ||
+            !Resolve(target, ref targetStrippable))
+            return;
+
+        if (!CanStripInsertHand(user, target, held, handName))
+            return;
+
+        var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay);
+
+        if (!stealth)
+            _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert-hand", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large);
+
+        var prefix = stealth ? "stealthily " : "";
+        _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
+
+        var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, false, handName), user, target, held)
+        {
+            Hidden = stealth,
+            AttemptFrequency = AttemptFrequency.EveryTick,
+            BreakOnDamage = true,
+            BreakOnMove = true,
+            NeedHand = true,
+            DuplicateCondition = DuplicateConditions.SameTool
+        };
+
+        _doAfterSystem.TryStartDoAfter(doAfterArgs);
+    }
+
+    /// <summary>
+    ///     Places the item in the user's active hand into one of the target's hands.
+    /// </summary>
+    private void StripInsertHand(
+        Entity<HandsComponent?> user,
+        Entity<HandsComponent?> target,
+        EntityUid held,
+        string handName,
+        bool stealth)
+    {
+        if (!Resolve(user, ref user.Comp) ||
+            !Resolve(target, ref target.Comp))
+            return;
+
+        if (!CanStripInsertHand(user, target, held, handName))
+            return;
+
+        _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: user.Comp);
+        _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: stealth, animate: !stealth, handsComp: target.Comp);
+        _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
+
+        // Hand update will trigger strippable update.
+    }
+
+    /// <summary>
+    ///     Checks whether the item is in the target's hand and whether it can be dropped.
+    /// </summary>
+    private bool CanStripRemoveHand(
+        EntityUid user,
+        Entity<HandsComponent?> target,
+        EntityUid item,
+        string handName)
+    {
+        if (!Resolve(target, ref target.Comp))
+            return false;
+
+        if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp))
+        {
+            _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", Identity.Name(target, EntityManager, user))));
+            return false;
+        }
+
+        if (HasComp<VirtualItemComponent>(handSlot.HeldEntity))
+            return false;
+
+        if (handSlot.HeldEntity == null)
+            return false;
+
+        if (handSlot.HeldEntity != item)
+            return false;
+
+        if (!_handsSystem.CanDropHeld(target, handSlot, false))
+        {
+            _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop-message", ("owner", Identity.Name(target, EntityManager, user))));
+            return false;
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    ///     Begins a DoAfter to remove the item from the target's hand and insert it in the user's active hand.
+    /// </summary>
+    private void StartStripRemoveHand(
+        Entity<HandsComponent?> user,
+        Entity<HandsComponent?> target,
+        EntityUid item,
+        string handName,
+        StrippableComponent? targetStrippable = null)
+    {
+        if (!Resolve(user, ref user.Comp) ||
+            !Resolve(target, ref target.Comp) ||
+            !Resolve(target, ref targetStrippable))
+            return;
+
+        if (!CanStripRemoveHand(user, target, item, handName))
+            return;
+
+        var (time, stealth) = GetStripTimeModifiers(user, target, null, targetStrippable.HandStripDelay);
+
+        if (!stealth)
+            _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target);
+
+        var prefix = stealth ? "stealthily " : "";
+        _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
+
+        var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, false, handName), user, target, item)
+        {
+            Hidden = stealth,
+            AttemptFrequency = AttemptFrequency.EveryTick,
+            BreakOnDamage = true,
+            BreakOnMove = true,
+            NeedHand = true,
+            BreakOnHandChange = false, // Allow simultaneously removing multiple items.
+            DuplicateCondition = DuplicateConditions.SameTool
+        };
+
+        _doAfterSystem.TryStartDoAfter(doAfterArgs);
+    }
+
+    /// <summary>
+    ///     Takes the item from the target's hand and inserts it in the user's active hand.
+    /// </summary>
+    private void StripRemoveHand(
+        Entity<HandsComponent?> user,
+        Entity<HandsComponent?> target,
+        EntityUid item,
+        string handName,
+        bool stealth)
+    {
+        if (!Resolve(user, ref user.Comp) ||
+            !Resolve(target, ref target.Comp))
+            return;
+
+        if (!CanStripRemoveHand(user, target, item, handName))
+            return;
+
+        _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: target.Comp);
+        _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: !stealth, handsComp: user.Comp);
+        _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
+
+        // Hand update will trigger strippable update.
+    }
+
+    private void OnStrippableDoAfterRunning(Entity<HandsComponent> entity, ref DoAfterAttemptEvent<StrippableDoAfterEvent> ev)
+    {
+        var args = ev.DoAfter.Args;
+
+        DebugTools.Assert(entity.Owner == args.User);
+        DebugTools.Assert(args.Target != null);
+        DebugTools.Assert(args.Used != null);
+        DebugTools.Assert(ev.Event.SlotOrHandName != null);
+
+        if (ev.Event.InventoryOrHand)
+        {
+            if ( ev.Event.InsertOrRemove && !CanStripInsertInventory((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
+                !ev.Event.InsertOrRemove && !CanStripRemoveInventory(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName))
+                    ev.Cancel();
+        }
+        else
+        {
+            if ( ev.Event.InsertOrRemove && !CanStripInsertHand((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
+                !ev.Event.InsertOrRemove && !CanStripRemoveHand(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName))
+                    ev.Cancel();
+        }
+    }
+
+    private void OnStrippableDoAfterFinished(Entity<HandsComponent> entity, ref StrippableDoAfterEvent ev)
+    {
+        if (ev.Cancelled)
+            return;
+
+        DebugTools.Assert(entity.Owner == ev.User);
+        DebugTools.Assert(ev.Target != null);
+        DebugTools.Assert(ev.Used != null);
+        DebugTools.Assert(ev.SlotOrHandName != null);
+
+        if (ev.InventoryOrHand)
+        {
+            if (ev.InsertOrRemove)
+                StripInsertInventory((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName);
+            else
+                StripRemoveInventory(entity.Owner, ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
+        }
+        else
+        {
+            if (ev.InsertOrRemove)
+                StripInsertHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
+            else
+                StripRemoveHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
+        }
+    }
+
     private void OnActivateInWorld(EntityUid uid, StrippableComponent component, ActivateInWorldEvent args)
     {
         if (args.Handled || !args.Complex || args.Target == args.User)
index 957113ce35ac65e8efbce0d5023c8e4478c01291..1566b7cffa33b861ba0720a367fda8f6a9f8cb06 100644 (file)
@@ -2,4 +2,6 @@
 ensnare-component-try-free-complete = You successfully free yourself from the {$ensnare}!
 ensnare-component-try-free-fail = You fail to free yourself from the {$ensnare}!
 
+ensnare-component-try-free-complete-other = You successfully free {$user} from the {$ensnare}!
+ensnare-component-try-free-fail-other = You fail to free {$user} from the {$ensnare}!
 ensnare-component-try-free-other = You start removing the {$ensnare} caught on {$user}!