diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 96b2ecc00c..950411a8e2 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -1,4 +1,3 @@ -using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Ensnaring; using Content.Shared.CombatMode; @@ -21,18 +20,21 @@ using Content.Shared.Verbs; using Robust.Server.GameObjects; using Robust.Shared.Player; using Robust.Shared.Utility; +using System.Linq; namespace Content.Server.Strip { public sealed class StrippableSystem : SharedStrippableSystem { - [Dependency] private readonly SharedCuffableSystem _cuffable = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly EnsnareableSystem _ensnaring = default!; + [Dependency] private readonly EnsnareableSystem _ensnaringSystem = default!; [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = 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. @@ -48,79 +50,10 @@ namespace Content.Server.Strip // BUI SubscribeLocalEvent(OnStripButtonPressed); SubscribeLocalEvent(OnStripEnsnareMessage); - } - private void OnStripEnsnareMessage(EntityUid uid, EnsnareableComponent component, StrippingEnsnareButtonPressed args) - { - if (args.Session.AttachedEntity is not {Valid: true} user) - return; - - foreach (var entity in component.Container.ContainedEntities) - { - if (!TryComp(entity, out var ensnaring)) - continue; - - _ensnaring.TryFree(uid, user, entity, ensnaring); - return; - } - } - - private void OnStripButtonPressed(Entity strippable, ref StrippingSlotButtonPressed args) - { - if (args.Session.AttachedEntity is not {Valid: true} user || - !TryComp(user, out var userHands)) - return; - - if (args.IsHand) - { - StripHand(user, args.Slot, strippable, userHands); - return; - } - - if (!TryComp(strippable, out var inventory)) - return; - - var hasEnt = _inventorySystem.TryGetSlotEntity(strippable, args.Slot, out var held, inventory); - - if (userHands.ActiveHandEntity != null && !hasEnt) - PlaceActiveHandItemInInventory(user, strippable, userHands.ActiveHandEntity.Value, args.Slot, strippable); - else if (userHands.ActiveHandEntity == null && hasEnt) - TakeItemFromInventory(user, strippable, held!.Value, args.Slot, strippable); - } - - private void StripHand(EntityUid user, string handId, Entity target, HandsComponent userHands) - { - if (!_handsSystem.TryGetHand(target, handId, out var hand)) - return; - - // is the target a handcuff? - if (TryComp(hand.HeldEntity, out VirtualItemComponent? virt) - && TryComp(target, out CuffableComponent? cuff) - && _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity)) - { - _cuffable.TryUncuff(target, user, virt.BlockingEntity, cuffable: cuff); - return; - } - - if (userHands.ActiveHandEntity != null && hand.HeldEntity == null) - PlaceActiveHandItemInHands(user, target, userHands.ActiveHandEntity.Value, handId, target); - else if (userHands.ActiveHandEntity == null && hand.HeldEntity != null) - TakeItemFromHands(user, target, hand.HeldEntity.Value, handId, target); - } - - public override void StartOpeningStripper(EntityUid user, Entity strippable, bool openInCombat = false) - { - base.StartOpeningStripper(user, strippable, openInCombat); - - if (TryComp(user, out var mode) && mode.IsInCombatMode && !openInCombat) - return; - - if (TryComp(user, out var actor)) - { - if (_userInterfaceSystem.SessionHasOpenUi(strippable, StrippingUiKey.Key, actor.PlayerSession)) - return; - _userInterfaceSystem.TryOpen(strippable, StrippingUiKey.Key, actor.PlayerSession); - } + // DoAfters + SubscribeLocalEvent>(OnStrippableDoAfterRunning); + SubscribeLocalEvent(OnStrippableDoAfterFinished); } private void AddStripVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args) @@ -134,9 +67,10 @@ namespace Content.Server.Strip Verb verb = new() { Text = Loc.GetString("strip-verb-get-data-text"), - Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), Act = () => StartOpeningStripper(args.User, (uid, component), true), }; + args.Verbs.Add(verb); } @@ -151,7 +85,7 @@ namespace Content.Server.Strip ExamineVerb verb = new() { Text = Loc.GetString("strip-verb-get-data-text"), - Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), Act = () => StartOpeningStripper(args.User, (uid, component), true), Category = VerbCategory.Examine, }; @@ -170,134 +104,163 @@ namespace Content.Server.Strip StartOpeningStripper(args.User, (uid, component)); } + public override void StartOpeningStripper(EntityUid user, Entity strippable, bool openInCombat = false) + { + base.StartOpeningStripper(user, strippable, openInCombat); + + if (TryComp(user, out var mode) && mode.IsInCombatMode && !openInCombat) + return; + + if (TryComp(user, out var actor)) + { + if (_userInterfaceSystem.SessionHasOpenUi(strippable, StrippingUiKey.Key, actor.PlayerSession)) + return; + _userInterfaceSystem.TryOpen(strippable, StrippingUiKey.Key, actor.PlayerSession); + } + } + + private void OnStripButtonPressed(Entity strippable, ref StrippingSlotButtonPressed args) + { + if (args.Session.AttachedEntity is not { Valid: true } user || + !TryComp(user, out var userHands) || + !TryComp(strippable.Owner, out var targetHands)) + return; + + if (args.IsHand) + { + StripHand((user, userHands), (strippable.Owner, targetHands), args.Slot, strippable); + return; + } + + if (!TryComp(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 user, + Entity 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(handSlot.HeldEntity, out var virtualItem) && + TryComp(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.Session.AttachedEntity is not { Valid: true } user) + return; + + foreach (var entity in component.Container.ContainedEntities) + { + if (!TryComp(entity, out var ensnaring)) + continue; + + _ensnaringSystem.TryFree(uid, user, entity, ensnaring); + return; + } + } + /// - /// Places item in user's active hand to an inventory slot. + /// Checks whether the item is in a user's active hand and whether it can be inserted into the inventory slot. /// - private async void PlaceActiveHandItemInInventory( - EntityUid user, + private bool CanStripInsertInventory( + Entity user, EntityUid target, EntityUid held, - string slot, - StrippableComponent component) + string slot) { - var userHands = Comp(user); + if (!Resolve(user, ref user.Comp)) + return false; - bool Check() + 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)) { - if (userHands.ActiveHandEntity != held) - return false; - - if (!_handsSystem.CanDropHeld(user, userHands.ActiveHand!)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user); - return false; - } - - if (_inventorySystem.TryGetSlotEntity(target, slot, out _)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-item-slot-occupied",("owner", target)), user); - return false; - } - - if (!_inventorySystem.CanEquip(user, target, held, slot, out _)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-cannot-equip-message",("owner", target)), user); - return false; - } - - return true; + _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; + } + + /// + /// Begins a DoAfter to insert the item in the user's active hand into the inventory slot. + /// + private void StartStripInsertInventory( + Entity 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 userEv = new BeforeStripEvent(slotDef.StripTime); - RaiseLocalEvent(user, userEv); - var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ev); + var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); - var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: held) - { - ExtraCheck = Check, - Hidden = ev.Stealth, - AttemptFrequency = AttemptFrequency.EveryTick, - BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, - NeedHand = true, - DuplicateCondition = DuplicateConditions.SameTool // Block any other DoAfters featuring this same entity. - }; + 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); - if (!ev.Stealth && Check() && userHands.ActiveHandEntity != null) - { - var message = Loc.GetString("strippable-component-alert-owner-insert", - ("user", Identity.Entity(user, EntityManager)), ("item", userHands.ActiveHandEntity)); - _popup.PopupEntity(message, target, target, PopupType.Large); - } - - var prefix = ev.Stealth ? "stealthily " : ""; + 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 result = await _doAfter.WaitDoAfter(doAfterArgs); - if (result != DoAfterStatus.Finished) - return; - - DebugTools.Assert(userHands.ActiveHand?.HeldEntity == held); - - if (_handsSystem.TryDrop(user, handsComp: userHands)) + var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, true, slot), user, target, held) { - _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"); - } - } - - /// - /// Places item in user's active hand in one of the entity's hands. - /// - private async void PlaceActiveHandItemInHands( - EntityUid user, - EntityUid target, - EntityUid held, - string handName, - StrippableComponent component) - { - var hands = Comp(target); - var userHands = Comp(user); - - bool Check() - { - if (userHands.ActiveHandEntity != held) - return false; - - if (!_handsSystem.CanDropHeld(user, userHands.ActiveHand!)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user); - return false; - } - - if (!_handsSystem.TryGetHand(target, handName, out var hand, hands) - || !_handsSystem.CanPickupToHand(target, userHands.ActiveHandEntity.Value, hand, checkActionBlocker: false, hands)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-cannot-put-message",("owner", target)), user); - return false; - } - - return true; - } - - var userEv = new BeforeStripEvent(component.HandStripDelay); - RaiseLocalEvent(user, userEv); - var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ev); - - var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: held) - { - ExtraCheck = Check, - Hidden = ev.Stealth, + Hidden = stealth, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, @@ -306,169 +269,364 @@ namespace Content.Server.Strip DuplicateCondition = DuplicateConditions.SameTool }; - var prefix = ev.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 result = await _doAfter.WaitDoAfter(doAfterArgs); - if (result != DoAfterStatus.Finished) return; - - _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: userHands); - _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: !ev.Stealth, animate: !ev.Stealth, handsComp: hands); - _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 + _doAfterSystem.TryStartDoAfter(doAfterArgs); } /// - /// Takes an item from the inventory and places it in the user's active hand. + /// Inserts the item in the user's active hand into the inventory slot. /// - private async void TakeItemFromInventory( + private void StripInsertInventory( + Entity 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"); + } + + /// + /// Checks whether the item can be removed from the target's inventory. + /// + private bool CanStripRemoveInventory( EntityUid user, EntityUid target, EntityUid item, - string slot, - Entity strippable) + string slot) { - bool Check() + if (!_inventorySystem.TryGetSlotEntity(target, slot, out var slotItem)) { - if (!_inventorySystem.TryGetSlotEntity(target, slot, out var ent) && ent == item) - { - _popup.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", target)), user); - return false; - } - - if (!_inventorySystem.CanUnequip(user, target, slot, out var reason)) - { - _popup.PopupCursor(Loc.GetString(reason), user); - return false; - } - - return true; + _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; + } + + /// + /// Begins a DoAfter to remove the item from the target's inventory and insert it in the user's active hand. + /// + 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 userEv = new BeforeStripEvent(slotDef.StripTime); - RaiseLocalEvent(user, userEv); - var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ev); + var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime); - var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: item) + if (!stealth) { - ExtraCheck = Check, - Hidden = ev.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, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, - BreakOnHandChange = false, // allow simultaneously removing multiple items. + BreakOnHandChange = false, // Allow simultaneously removing multiple items. DuplicateCondition = DuplicateConditions.SameTool }; - if (!ev.Stealth && Check()) - { - if (slotDef.StripHidden) - { - _popup.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, - target, PopupType.Large); - } - else if (_inventorySystem.TryGetSlotEntity(strippable, slot, out var slotItem)) - { - _popup.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", slotItem)), target, - target, PopupType.Large); - } - } - - var prefix = ev.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 result = await _doAfter.WaitDoAfter(doAfterArgs); - if (result != DoAfterStatus.Finished) - return; - - if (!_inventorySystem.TryUnequip(user, strippable, slot)) - return; - - // Raise a dropped event, so that things like gas tank internals properly deactivate when stripping - RaiseLocalEvent(item, new DroppedEvent(user), true); - - _handsSystem.PickupOrDrop(user, item, animateUser: !ev.Stealth, animate: !ev.Stealth); - _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot"); - + _doAfterSystem.TryStartDoAfter(doAfterArgs); } /// - /// Takes an item from a hand and places it in the user's active hand. + /// Removes the item from the target's inventory and inserts it in the user's active hand. /// - private async void TakeItemFromHands(EntityUid user, EntityUid target, EntityUid item, string handName, Entity strippable) + private void StripRemoveInventory( + EntityUid user, + EntityUid target, + EntityUid item, + string slot, + bool stealth) { - var hands = Comp(target); - var userHands = Comp(user); + if (!CanStripRemoveInventory(user, target, item, slot)) + return; - bool Check() + 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"); + } + + /// + /// Checks whether the item in the user's active hand can be inserted into one of the target's hands. + /// + private bool CanStripInsertHand( + Entity user, + Entity 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)) { - if (!_handsSystem.TryGetHand(target, handName, out var hand, hands) || hand.HeldEntity != item) - { - _popup.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message",("owner", target)), user); - return false; - } - - if (HasComp(hand.HeldEntity)) - return false; - - if (!_handsSystem.CanDropHeld(target, hand, false)) - { - _popup.PopupCursor(Loc.GetString("strippable-component-cannot-drop-message",("owner", target)), user); - return false; - } - - return true; + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user); + return false; } - var userEv = new BeforeStripEvent(strippable.Comp.HandStripDelay); - RaiseLocalEvent(user, userEv); - var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ev); - - var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: item) + if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp) || + !_handsSystem.CanPickupToHand(target, user.Comp.ActiveHandEntity.Value, handSlot, checkActionBlocker: false, target.Comp)) { - ExtraCheck = Check, - Hidden = ev.Stealth, + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-put-message", ("owner", target)), user); + return false; + } + + return true; + } + + /// + /// Begins a DoAfter to insert the item in the user's active hand into one of the target's hands. + /// + private void StartStripInsertHand( + Entity user, + Entity 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, targetStrippable.HandStripDelay); + + 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, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, - BreakOnHandChange = false, // allow simultaneously removing multiple items. DuplicateCondition = DuplicateConditions.SameTool }; - if (!ev.Stealth && Check() && _handsSystem.TryGetHand(target, handName, out var handSlot, hands) && handSlot.HeldEntity != null) - { - _popup.PopupEntity( - Loc.GetString("strippable-component-alert-owner", - ("user", Identity.Entity(user, EntityManager)), ("item", item)), - strippable.Owner, - strippable.Owner); - } + _doAfterSystem.TryStartDoAfter(doAfterArgs); + } - var prefix = ev.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 result = await _doAfter.WaitDoAfter(doAfterArgs); - if (result != DoAfterStatus.Finished) + /// + /// Places the item in the user's active hand into one of the target's hands. + /// + private void StripInsertHand( + Entity user, + Entity target, + EntityUid held, + string handName, + bool stealth) + { + if (!Resolve(user, ref user.Comp) || + !Resolve(target, ref target.Comp)) return; - _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: hands); - _handsSystem.PickupOrDrop(user, item, animateUser: !ev.Stealth, animate: !ev.Stealth, handsComp: userHands); - // hand update will trigger strippable update - _adminLogger.Add(LogType.Stripping, LogImpact.Medium, - $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands"); + _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. + } + + /// + /// Checks whether the item is in the target's hand and whether it can be dropped. + /// + private bool CanStripRemoveHand( + EntityUid user, + Entity 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", target)), user); + return false; + } + + if (HasComp(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", target)), user); + return false; + } + + return true; + } + + /// + /// Begins a DoAfter to remove the item from the target's hand and insert it in the user's active hand. + /// + private void StartStripRemoveHand( + Entity user, + Entity 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, 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, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = true, + BreakOnHandChange = false, // Allow simultaneously removing multiple items. + DuplicateCondition = DuplicateConditions.SameTool + }; + + _doAfterSystem.TryStartDoAfter(doAfterArgs); + } + + /// + /// Takes the item from the target's hand and inserts it in the user's active hand. + /// + private void StripRemoveHand( + Entity user, + Entity target, + EntityUid item, + bool stealth) + { + if (!Resolve(user, ref user.Comp) || + !Resolve(target, ref target.Comp)) + 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 entity, ref DoAfterAttemptEvent 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 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.Args.Hidden); + } } } } diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs index 0138de7a98..22a1d1a8f5 100644 --- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs @@ -95,7 +95,7 @@ public sealed class ToggleableClothingSystem : EntitySystem if (component.StripDelay == null) return; - var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, (float) component.StripDelay.Value.TotalSeconds); + var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, component.StripDelay.Value); var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item) { diff --git a/Content.Shared/Inventory/InventoryTemplatePrototype.cs b/Content.Shared/Inventory/InventoryTemplatePrototype.cs index a477969962..585f80d4ce 100644 --- a/Content.Shared/Inventory/InventoryTemplatePrototype.cs +++ b/Content.Shared/Inventory/InventoryTemplatePrototype.cs @@ -20,7 +20,7 @@ public sealed partial class SlotDefinition [DataField("slotFlags")] public SlotFlags SlotFlags { get; private set; } = SlotFlags.PREVENTEQUIP; [DataField("showInWindow")] public bool ShowInWindow { get; private set; } = true; [DataField("slotGroup")] public string SlotGroup { get; private set; } = "Default"; - [DataField("stripTime")] public float StripTime { get; private set; } = 4f; + [DataField("stripTime")] public TimeSpan StripTime { get; private set; } = TimeSpan.FromSeconds(4f); [DataField("uiWindowPos", required: true)] public Vector2i UIWindowPosition { get; private set; } diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs index fbf99992e3..8bf09c3f4c 100644 --- a/Content.Shared/Strip/Components/StrippableComponent.cs +++ b/Content.Shared/Strip/Components/StrippableComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.DoAfter; using Content.Shared.Inventory; using Robust.Shared.GameStates; using Robust.Shared.Serialization; @@ -8,10 +9,10 @@ namespace Content.Shared.Strip.Components public sealed partial class StrippableComponent : Component { /// - /// The strip delay for hands. + /// The strip delay for hands. /// [ViewVariables(VVAccess.ReadWrite), DataField("handDelay")] - public float HandStripDelay = 4f; + public TimeSpan HandStripDelay = TimeSpan.FromSeconds(4f); } [NetSerializable, Serializable] @@ -21,63 +22,63 @@ namespace Content.Shared.Strip.Components } [NetSerializable, Serializable] - public sealed class StrippingSlotButtonPressed : BoundUserInterfaceMessage + public sealed class StrippingSlotButtonPressed(string slot, bool isHand) : BoundUserInterfaceMessage { - public readonly string Slot; - - public readonly bool IsHand; - - public StrippingSlotButtonPressed(string slot, bool isHand) - { - Slot = slot; - IsHand = isHand; - } + public readonly string Slot = slot; + public readonly bool IsHand = isHand; } [NetSerializable, Serializable] - public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage - { - public StrippingEnsnareButtonPressed() - { - } - } + public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage; - public abstract class BaseBeforeStripEvent : EntityEventArgs, IInventoryRelayEvent + [ByRefEvent] + public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = false) : EntityEventArgs, IInventoryRelayEvent { - public readonly float InitialTime; - public float Time => MathF.Max(InitialTime * Multiplier + Additive, 0f); - public float Additive = 0; - public float Multiplier = 1f; - public bool Stealth; + public readonly TimeSpan InitialTime = initialTime; + public TimeSpan Multiplier = TimeSpan.FromSeconds(1f); + public TimeSpan Additive = TimeSpan.Zero; + public bool Stealth = stealth; + + public TimeSpan Time => TimeSpan.FromSeconds(MathF.Max(InitialTime.Seconds * Multiplier.Seconds + Additive.Seconds, 0f)); public SlotFlags TargetSlots { get; } = SlotFlags.GLOVES; + } - public BaseBeforeStripEvent(float initialTime, bool stealth = false) + /// + /// Used to modify strip times. Raised directed at the user. + /// + /// + /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. + /// + [ByRefEvent] + public sealed class BeforeStripEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); + + /// + /// Used to modify strip times. Raised directed at the target. + /// + /// + /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. + /// + [ByRefEvent] + public sealed class BeforeGettingStrippedEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth); + + /// + /// Organizes the behavior of DoAfters for . + /// + [Serializable, NetSerializable] + public sealed partial class StrippableDoAfterEvent : DoAfterEvent + { + public readonly bool InsertOrRemove; + public readonly bool InventoryOrHand; + public readonly string SlotOrHandName; + + public StrippableDoAfterEvent(bool insertOrRemove, bool inventoryOrHand, string slotOrHandName) { - InitialTime = initialTime; - Stealth = stealth; + InsertOrRemove = insertOrRemove; + InventoryOrHand = inventoryOrHand; + SlotOrHandName = slotOrHandName; } - } - /// - /// Used to modify strip times. Raised directed at the user. - /// - /// - /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. - /// - public sealed class BeforeStripEvent : BaseBeforeStripEvent - { - public BeforeStripEvent(float initialTime, bool stealth = false) : base(initialTime, stealth) { } - } - - /// - /// Used to modify strip times. Raised directed at the target. - /// - /// - /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player. - /// - public sealed class BeforeGettingStrippedEvent : BaseBeforeStripEvent - { - public BeforeGettingStrippedEvent(float initialTime, bool stealth = false) : base(initialTime, stealth) { } + public override DoAfterEvent Clone() => this; } } diff --git a/Content.Shared/Strip/Components/ThievingComponent.cs b/Content.Shared/Strip/Components/ThievingComponent.cs index 83679f132c..a851dd5ef6 100644 --- a/Content.Shared/Strip/Components/ThievingComponent.cs +++ b/Content.Shared/Strip/Components/ThievingComponent.cs @@ -11,7 +11,7 @@ public sealed partial class ThievingComponent : Component /// [ViewVariables(VVAccess.ReadWrite)] [DataField("stripTimeReduction")] - public float StripTimeReduction = 0.5f; + public TimeSpan StripTimeReduction = TimeSpan.FromSeconds(0.5f); /// /// Should it notify the user if they're stripping a pocket? diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs index a698ae5035..7afd4f245a 100644 --- a/Content.Shared/Strip/SharedStrippableSystem.cs +++ b/Content.Shared/Strip/SharedStrippableSystem.cs @@ -14,12 +14,12 @@ public abstract class SharedStrippableSystem : EntitySystem SubscribeLocalEvent(OnDragDrop); } - public (float Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, float initialTime) + public (TimeSpan Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime) { var userEv = new BeforeStripEvent(initialTime); - RaiseLocalEvent(user, userEv); + RaiseLocalEvent(user, ref userEv); var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); - RaiseLocalEvent(target, ev); + RaiseLocalEvent(target, ref ev); return (ev.Time, ev.Stealth); } diff --git a/Content.Shared/Strip/ThievingSystem.cs b/Content.Shared/Strip/ThievingSystem.cs index 0ef4b66571..2b3d3b38a0 100644 --- a/Content.Shared/Strip/ThievingSystem.cs +++ b/Content.Shared/Strip/ThievingSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.Inventory; +using Content.Shared.Strip; using Content.Shared.Strip.Components; namespace Content.Shared.Strip;