using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Ensnaring; using Content.Shared.CombatMode; using Content.Shared.Cuffs; using Content.Shared.Cuffs.Components; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.Ensnaring.Components; 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.Popups; using Content.Shared.Strip; using Content.Shared.Strip.Components; using Content.Shared.Verbs; using Robust.Server.GameObjects; using Robust.Shared.Utility; 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 UserInterfaceSystem _userInterfaceSystem = 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>(AddStripVerb); SubscribeLocalEvent>(AddStripExamineVerb); SubscribeLocalEvent(OnActivateInWorld); // 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(EntityUid target, StrippableComponent component, StrippingSlotButtonPressed args) { if (args.Session.AttachedEntity is not {Valid: true} user || !TryComp(user, out var userHands)) return; if (args.IsHand) { StripHand(target, user, args.Slot, component, userHands); return; } if (!TryComp(target, out var inventory)) return; var hasEnt = _inventorySystem.TryGetSlotEntity(target, args.Slot, out var held, inventory); if (userHands.ActiveHandEntity != null && !hasEnt) PlaceActiveHandItemInInventory(user, target, userHands.ActiveHandEntity.Value, args.Slot, component); else if (userHands.ActiveHandEntity == null && hasEnt) TakeItemFromInventory(user, target, held!.Value, args.Slot, component); } private void StripHand(EntityUid target, EntityUid user, string handId, StrippableComponent component, HandsComponent userHands) { if (!_handsSystem.TryGetHand(target, handId, out var hand)) return; // is the target a handcuff? if (TryComp(hand.HeldEntity, out HandVirtualItemComponent? 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, component); else if (userHands.ActiveHandEntity == null && hand.HeldEntity != null) TakeItemFromHands(user,target, hand.HeldEntity.Value, handId, component); } public override void StartOpeningStripper(EntityUid user, StrippableComponent component, bool openInCombat = false) { base.StartOpeningStripper(user, component, openInCombat); if (TryComp(user, out var mode) && mode.IsInCombatMode && !openInCombat) return; if (TryComp(user, out var actor)) { if (_userInterfaceSystem.SessionHasOpenUi(component.Owner, StrippingUiKey.Key, actor.PlayerSession)) return; _userInterfaceSystem.TryOpen(component.Owner, StrippingUiKey.Key, actor.PlayerSession); } } private void AddStripVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args) { if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User) return; if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor)) return; Verb verb = new() { Text = Loc.GetString("strip-verb-get-data-text"), Icon = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), Act = () => StartOpeningStripper(args.User, component, true), }; args.Verbs.Add(verb); } private void AddStripExamineVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args) { if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User) return; if (!HasComp(args.User)) return; ExamineVerb verb = new() { Text = Loc.GetString("strip-verb-get-data-text"), Icon = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")), Act = () => StartOpeningStripper(args.User, component, true), Category = VerbCategory.Examine, }; args.Verbs.Add(verb); } private void OnActivateInWorld(EntityUid uid, StrippableComponent component, ActivateInWorldEvent args) { if (args.Target == args.User) return; if (!TryComp(args.User, out var actor)) return; StartOpeningStripper(args.User, component); } /// /// Places item in user's active hand to an inventory slot. /// private async void PlaceActiveHandItemInInventory( EntityUid user, EntityUid target, EntityUid held, string slot, StrippableComponent component) { 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 (_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; } if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef)) { Logger.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 doAfterArgs = new DoAfterArgs(user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: held) { ExtraCheck = Check, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, DuplicateCondition = DuplicateConditions.SameTool // Block any other DoAfters featuring this same entity. }; 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 result = await _doAfter.WaitDoAfter(doAfterArgs); if (result != DoAfterStatus.Finished) return; DebugTools.Assert(userHands.ActiveHand?.HeldEntity == held); if (_handsSystem.TryDrop(user, handsComp: userHands)) { _inventorySystem.TryEquip(user, target, held, slot); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} 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(user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: held) { ExtraCheck = Check, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, DuplicateCondition = DuplicateConditions.SameTool }; 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: true, handsComp: hands); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands"); // hand update will trigger strippable update } /// /// Takes an item from the inventory and places it in the user's active hand. /// private async void TakeItemFromInventory( EntityUid user, EntityUid target, EntityUid item, string slot, StrippableComponent component) { bool Check() { 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(reason, user); return false; } return true; } if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef)) { Logger.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 doAfterArgs = new DoAfterArgs(user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: item) { ExtraCheck = Check, 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()) { if (slotDef.StripHidden) { _popup.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, target, PopupType.Large); } else if (_inventorySystem.TryGetSlotEntity(component.Owner, slot, out var slotItem)) { _popup.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", slotItem)), target, target, PopupType.Large); } } var result = await _doAfter.WaitDoAfter(doAfterArgs); if (result != DoAfterStatus.Finished) return; if (!_inventorySystem.TryUnequip(user, component.Owner, 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); _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}"); } /// /// Takes an item from a hand and places it in the user's active hand. /// private async void TakeItemFromHands(EntityUid user, EntityUid target, EntityUid item, string handName, StrippableComponent component) { var hands = Comp(target); var userHands = Comp(user); bool Check() { 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; } var userEv = new BeforeStripEvent(component.HandStripDelay); RaiseLocalEvent(user, userEv); var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth); RaiseLocalEvent(target, ev); var doAfterArgs = new DoAfterArgs(user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: item) { ExtraCheck = Check, AttemptFrequency = AttemptFrequency.EveryTick, BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, BreakOnHandChange = false, // allow simultaneously removing multiple items. DuplicateCondition = DuplicateConditions.SameTool }; if (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)), component.Owner, component.Owner); } var result = await _doAfter.WaitDoAfter(doAfterArgs); if (result != DoAfterStatus.Finished) return; _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: hands); _handsSystem.PickupOrDrop(user, item, handsComp: userHands); // hand update will trigger strippable update _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):user} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}"); } } }