diff --git a/Content.Client/Ensnaring/EnsnareableSystem.cs b/Content.Client/Ensnaring/EnsnareableSystem.cs index b7a5a45ca0..6861bd8f09 100644 --- a/Content.Client/Ensnaring/EnsnareableSystem.cs +++ b/Content.Client/Ensnaring/EnsnareableSystem.cs @@ -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(OnComponentInit); SubscribeLocalEvent(OnAppearanceChange); } - private void OnComponentInit(EntityUid uid, EnsnareableComponent component, ComponentInit args) + protected override void OnEnsnareInit(Entity ent, ref ComponentInit args) { - if(!TryComp(uid, out var sprite)) + base.OnEnsnareInit(ent, ref args); + + if(!TryComp(ent.Owner, out var sprite)) return; // TODO remove this, this should just be in yaml. diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index 132c5ed654..97172f8de8 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -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; } diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs index 700f6b6d26..a249c9251b 100644 --- a/Content.Client/Popups/PopupSystem.cs +++ b/Content.Client/Popups/PopupSystem.cs @@ -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 index 7d6963826a..0000000000 --- a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs +++ /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(OnComponentRemove); - SubscribeLocalEvent(AttemptStepTrigger); - SubscribeLocalEvent(OnStepTrigger); - SubscribeLocalEvent(OnThrowHit); - SubscribeLocalEvent(OnAttemptPacifiedThrow); - SubscribeLocalEvent(OnRemoveEnsnareAlert); - } - - private void OnAttemptPacifiedThrow(Entity ent, ref AttemptPacifiedThrowEvent args) - { - args.Cancel("pacified-cannot-throw-snare"); - } - - private void OnRemoveEnsnareAlert(Entity ent, ref RemoveEnsnareAlertEvent args) - { - if (args.Handled) - return; - - foreach (var ensnare in ent.Comp.Container.ContainedEntities) - { - if (!TryComp(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(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); - } - - /// - /// Used where you want to try to ensnare an entity with the - /// - /// The entity that will be ensnared - /// The entity that is used to ensnare - /// The ensnaring component - public void TryEnsnare(EntityUid target, EntityUid ensnare, EnsnaringComponent component) - { - //Don't do anything if they don't have the ensnareable component. - if (!TryComp(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(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); - } - - /// - /// Used where you want to try to free an entity with the - /// - /// The entity that will be freed - /// The entity that is freeing the target - /// The entity used to ensnare - /// The ensnaring component - 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(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); - } - - /// - /// Used to force free someone for things like if the is removed - /// - public void ForceFree(EntityUid ensnare, EnsnaringComponent component) - { - if (component.Ensnared == null) - return; - - if (!TryComp(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); - } - - /// - /// Update the Ensnared alert for an entity. - /// - /// The entity that has been affected by a snare - public void UpdateAlert(EntityUid target, EnsnareableComponent component) - { - if (!component.IsEnsnared) - _alerts.ClearAlert(target, component.EnsnaredAlert); - else - _alerts.ShowAlert(target, component.EnsnaredAlert); - } -} diff --git a/Content.Server/Ensnaring/EnsnareableSystem.cs b/Content.Server/Ensnaring/EnsnareableSystem.cs index d732c5f3a3..778d7b3580 100644 --- a/Content.Server/Ensnaring/EnsnareableSystem.cs +++ b/Content.Server/Ensnaring/EnsnareableSystem.cs @@ -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(OnEnsnareableInit); - SubscribeLocalEvent(OnDoAfter); - } - - private void OnEnsnareableInit(EntityUid uid, EnsnareableComponent component, ComponentInit args) - { - component.Container = _container.EnsureContainer(uid, "ensnare"); - } - - private void OnDoAfter(EntityUid uid, EnsnareableComponent component, DoAfterEvent args) - { - if (args.Args.Target == null) - return; - - if (args.Handled || !TryComp(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; diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 6d728df9d6..b74e40e1da 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -19,588 +19,9 @@ using Content.Shared.Verbs; using Robust.Shared.Player; using Robust.Shared.Utility; -namespace Content.Server.Strip +namespace Content.Server.Strip; + +public sealed class StrippableSystem : SharedStrippableSystem { - 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>(AddStripVerb); - SubscribeLocalEvent>(AddStripExamineVerb); - - // BUI - SubscribeLocalEvent(OnStripButtonPressed); - SubscribeLocalEvent(OnStripEnsnareMessage); - - // DoAfters - SubscribeLocalEvent>(OnStrippableDoAfterRunning); - SubscribeLocalEvent(OnStrippableDoAfterFinished); - } - - private void AddStripVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args) - { - if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User) - return; - - if (!HasComp(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 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("/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 strippable, ref StrippingSlotButtonPressed args) - { - if (args.Actor is not { Valid: true } user || - !TryComp(user, out var userHands)) - return; - - if (args.IsHand) - { - StripHand((user, userHands), (strippable.Owner, null), 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.Actor 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; - } - } - - /// - /// Checks whether the item is in a user's active hand and whether it can be inserted into the inventory slot. - /// - private bool CanStripInsertInventory( - Entity 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; - } - - /// - /// 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 (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); - } - - /// - /// Inserts the item in the user's active hand into the inventory slot. - /// - 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) - { - 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; - } - - /// - /// 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 (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); - } - - /// - /// Removes the item from the target's inventory and inserts it in the user's active hand. - /// - 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"); - } - - /// - /// 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)) - { - _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; - } - - /// - /// 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, 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); - } - - /// - /// 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; - - 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. - } - - /// - /// 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", Identity.Name(target, EntityManager, user))), 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", Identity.Name(target, EntityManager, user))), 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, 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); - } - - /// - /// 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, - 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 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.SlotOrHandName, ev.Args.Hidden); - } - } - } } diff --git a/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs b/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs index cd7824bb96..307a5e1986 100644 --- a/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs +++ b/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs @@ -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; /// /// Use this on an entity that you would like to be ensnared by anything that has the /// -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] public sealed partial class EnsnareableComponent : Component { /// /// How much should this slow down the entities walk? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("walkSpeed")] + [DataField] public float WalkSpeed = 1.0f; /// /// How much should this slow down the entities sprint? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("sprintSpeed")] + [DataField] public float SprintSpeed = 1.0f; /// /// Is this entity currently ensnared? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("isEnsnared")] + [DataField, AutoNetworkedField] public bool IsEnsnared; /// @@ -37,10 +33,10 @@ public sealed partial class EnsnareableComponent : Component /// 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; diff --git a/Content.Shared/Ensnaring/Components/EnsnaringComponent.cs b/Content.Shared/Ensnaring/Components/EnsnaringComponent.cs index 6e1f3077f3..f900d863c2 100644 --- a/Content.Shared/Ensnaring/Components/EnsnaringComponent.cs +++ b/Content.Shared/Ensnaring/Components/EnsnaringComponent.cs @@ -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 /// /// How long it should take to free someone else. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("freeTime")] + [DataField] public float FreeTime = 3.5f; /// /// How long it should take for an entity to free themselves. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("breakoutTime")] + [DataField] public float BreakoutTime = 30.0f; /// /// How much should this slow down the entities walk? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("walkSpeed")] + [DataField] public float WalkSpeed = 0.9f; /// /// How much should this slow down the entities sprint? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("sprintSpeed")] + [DataField] public float SprintSpeed = 0.9f; /// /// How much stamina does the ensnare sap /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("staminaDamage")] + [DataField] public float StaminaDamage = 55f; /// /// Should this ensnare someone when thrown? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("canThrowTrigger")] + [DataField] public bool CanThrowTrigger; /// /// What is ensnared? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("ensnared")] + [DataField] public EntityUid? Ensnared; /// /// Should breaking out be possible when moving? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("canMoveBreakout")] + [DataField] public bool CanMoveBreakout; + [DataField] + public SoundSpecifier? EnsnareSound = new SoundPathSpecifier("/Audio/Effects/snap.ogg"); } /// @@ -95,29 +89,3 @@ public sealed class EnsnareRemoveEvent : CancellableEntityEventArgs SprintSpeed = sprintSpeed; } } - -/// -/// Used for the do after event to free the entity that owns the -/// -public sealed class FreeEnsnareDoAfterComplete : EntityEventArgs -{ - public readonly EntityUid EnsnaringEntity; - - public FreeEnsnareDoAfterComplete(EntityUid ensnaringEntity) - { - EnsnaringEntity = ensnaringEntity; - } -} - -/// -/// Used for the do after event when it fails to free the entity that owns the -/// -public sealed class FreeEnsnareDoAfterCancel : EntityEventArgs -{ - public readonly EntityUid EnsnaringEntity; - - public FreeEnsnareDoAfterCancel(EntityUid ensnaringEntity) - { - EnsnaringEntity = ensnaringEntity; - } -} diff --git a/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs b/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs index 4f35dc583a..551c76ff8d 100644 --- a/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs +++ b/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs @@ -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(OnEnsnareInit); SubscribeLocalEvent(MovementSpeedModify); SubscribeLocalEvent(OnEnsnare); SubscribeLocalEvent(OnEnsnareRemove); SubscribeLocalEvent(OnEnsnareChange); - SubscribeLocalEvent(OnGetState); - SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnStripEnsnareMessage); + SubscribeLocalEvent(OnRemoveEnsnareAlert); + SubscribeLocalEvent(OnDoAfter); + + SubscribeLocalEvent(OnComponentRemove); + SubscribeLocalEvent(AttemptStepTrigger); + SubscribeLocalEvent(OnStepTrigger); + SubscribeLocalEvent(OnThrowHit); + SubscribeLocalEvent(OnAttemptPacifiedThrow); } - private void OnHandleState(EntityUid uid, EnsnareableComponent component, ref ComponentHandleState args) + protected virtual void OnEnsnareInit(Entity ent, ref ComponentInit args) { - if (args.Current is not EnsnareableComponentState state) - return; + ent.Comp.Container = Container.EnsureContainer(ent.Owner, "ensnare"); + } - if (state.IsEnsnared == component.IsEnsnared) - return; - - 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(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); } + + /// + /// Used where you want to try to free an entity with the + /// + /// The entity that will be freed + /// The entity that is freeing the target + /// The entity used to ensnare + /// The ensnaring component + 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(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(entity, out var ensnaring)) + continue; + + TryFree(uid, args.Actor, entity, ensnaring); + return; + } + } + + private void OnAttemptPacifiedThrow(Entity ent, ref AttemptPacifiedThrowEvent args) + { + args.Cancel("pacified-cannot-throw-snare"); + } + + private void OnRemoveEnsnareAlert(Entity ent, ref RemoveEnsnareAlertEvent args) + { + if (args.Handled) + return; + + foreach (var ensnare in ent.Comp.Container.ContainedEntities) + { + if (!TryComp(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(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); + } + } + + /// + /// Used where you want to try to ensnare an entity with the + /// + /// The entity that will be ensnared + /// The entity that is used to ensnare + /// The ensnaring component + public bool TryEnsnare(EntityUid target, EntityUid ensnare, EnsnaringComponent component) + { + //Don't do anything if they don't have the ensnareable component. + if (!TryComp(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(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; + } + + /// + /// Used to force free someone for things like if the is removed + /// + public void ForceFree(EntityUid ensnare, EnsnaringComponent component) + { + if (component.Ensnared == null) + return; + + if (!TryComp(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); + } + + /// + /// Update the Ensnared alert for an entity. + /// + /// The entity that has been affected by a snare + public void UpdateAlert(EntityUid target, EnsnareableComponent component) + { + if (!component.IsEnsnared) + _alerts.ClearAlert(target, component.EnsnaredAlert); + else + _alerts.ShowAlert(target, component.EnsnaredAlert); + } } diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs index 762561ed32..1d5d91a9e3 100644 --- a/Content.Shared/Inventory/InventorySystem.Equip.cs +++ b/Content.Shared/Inventory/InventorySystem.Equip.cs @@ -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; } diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs index 935dc33540..a68bf755d4 100644 --- a/Content.Shared/Strip/SharedStrippableSystem.cs +++ b/Content.Shared/Strip/SharedStrippableSystem.cs @@ -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>(AddStripVerb); + SubscribeLocalEvent>(AddStripExamineVerb); + + // BUI + SubscribeLocalEvent(OnStripButtonPressed); + + // DoAfters + SubscribeLocalEvent>(OnStrippableDoAfterRunning); + SubscribeLocalEvent(OnStrippableDoAfterFinished); + SubscribeLocalEvent(OnCanDropOn); SubscribeLocalEvent(OnCanDrop); SubscribeLocalEvent(OnDragDrop); SubscribeLocalEvent(OnActivateInWorld); } + private void AddStripVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent 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 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 strippable, ref StrippingSlotButtonPressed args) + { + if (args.Actor is not { Valid: true } user || + !TryComp(user, out var userHands)) + return; + + if (args.IsHand) + { + StripHand((user, userHands), (strippable.Owner, null), 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); + } + + /// + /// Checks whether the item is in a user's active hand and whether it can be inserted into the inventory slot. + /// + private bool CanStripInsertInventory( + Entity 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; + } + + /// + /// 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 (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); + } + + /// + /// Inserts the item in the user's active hand into the inventory slot. + /// + 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) + { + 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; + } + + /// + /// 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 (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); + } + + /// + /// Removes the item from the target's inventory and inserts it in the user's active hand. + /// + 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"); + } + + /// + /// 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)) + { + _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; + } + + /// + /// 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, 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); + } + + /// + /// 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; + + 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. + } + + /// + /// 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", Identity.Name(target, EntityManager, 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", Identity.Name(target, EntityManager, 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, 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); + } + + /// + /// 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, + 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 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.SlotOrHandName, ev.Args.Hidden); + } + } + private void OnActivateInWorld(EntityUid uid, StrippableComponent component, ActivateInWorldEvent args) { if (args.Handled || !args.Complex || args.Target == args.User) diff --git a/Resources/Locale/en-US/ensnare/ensnare-component.ftl b/Resources/Locale/en-US/ensnare/ensnare-component.ftl index 957113ce35..1566b7cffa 100644 --- a/Resources/Locale/en-US/ensnare/ensnare-component.ftl +++ b/Resources/Locale/en-US/ensnare/ensnare-component.ftl @@ -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}!