diff --git a/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs b/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs index 8bf8ac56ea..f7764479c0 100644 --- a/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs +++ b/Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs @@ -5,21 +5,26 @@ namespace Content.Client.Sticky.Visualizers; public sealed class StickyVisualizerSystem : VisualizerSystem { + private EntityQuery _spriteQuery; + public override void Initialize() { base.Initialize(); + + _spriteQuery = GetEntityQuery(); + SubscribeLocalEvent(OnInit); } - private void OnInit(EntityUid uid, StickyVisualizerComponent component, ComponentInit args) + private void OnInit(Entity ent, ref ComponentInit args) { - if (!TryComp(uid, out SpriteComponent? sprite)) + if (!_spriteQuery.TryComp(ent, out var sprite)) return; - component.DefaultDrawDepth = sprite.DrawDepth; + ent.Comp.OriginalDrawDepth = sprite.DrawDepth; } - protected override void OnAppearanceChange(EntityUid uid, StickyVisualizerComponent component, ref AppearanceChangeEvent args) + protected override void OnAppearanceChange(EntityUid uid, StickyVisualizerComponent comp, ref AppearanceChangeEvent args) { if (args.Sprite == null) return; @@ -27,8 +32,7 @@ public sealed class StickyVisualizerSystem : VisualizerSystem(uid, StickyVisuals.IsStuck, out var isStuck, args.Component)) return; - var drawDepth = isStuck ? component.StuckDrawDepth : component.DefaultDrawDepth; + var drawDepth = isStuck ? comp.StuckDrawDepth : comp.OriginalDrawDepth; args.Sprite.DrawDepth = drawDepth; - } } diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs index dcd11062bb..d06e9fa1c2 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs @@ -1,9 +1,9 @@ using Content.Server.Explosion.Components; -using Content.Server.Sticky.Events; using Content.Shared.Examine; using Content.Shared.Explosion.Components; using Content.Shared.Interaction.Events; using Content.Shared.Popups; +using Content.Shared.Sticky; using Content.Shared.Verbs; namespace Content.Server.Explosion.EntitySystems; @@ -21,7 +21,7 @@ public sealed partial class TriggerSystem SubscribeLocalEvent(OnRandomTimerTriggerMapInit); } - private void OnStuck(EntityUid uid, OnUseTimerTriggerComponent component, EntityStuckEvent args) + private void OnStuck(EntityUid uid, OnUseTimerTriggerComponent component, ref EntityStuckEvent args) { if (!component.StartOnStick) return; diff --git a/Content.Server/Ninja/Systems/SpiderChargeSystem.cs b/Content.Server/Ninja/Systems/SpiderChargeSystem.cs index c262651f27..c916d568d5 100644 --- a/Content.Server/Ninja/Systems/SpiderChargeSystem.cs +++ b/Content.Server/Ninja/Systems/SpiderChargeSystem.cs @@ -4,10 +4,10 @@ using Content.Server.Mind; using Content.Server.Objectives.Components; using Content.Server.Popups; using Content.Server.Roles; -using Content.Server.Sticky.Events; using Content.Shared.Interaction; using Content.Shared.Ninja.Components; using Content.Shared.Ninja.Systems; +using Content.Shared.Sticky; using Robust.Shared.GameObjects; namespace Content.Server.Ninja.Systems; @@ -34,7 +34,7 @@ public sealed class SpiderChargeSystem : SharedSpiderChargeSystem /// /// Require that the planter is a ninja and the charge is near the target warp point. /// - private void OnAttemptStick(EntityUid uid, SpiderChargeComponent comp, AttemptEntityStickEvent args) + private void OnAttemptStick(EntityUid uid, SpiderChargeComponent comp, ref AttemptEntityStickEvent args) { if (args.Cancelled) return; @@ -67,7 +67,7 @@ public sealed class SpiderChargeSystem : SharedSpiderChargeSystem /// /// Allows greentext to occur after exploding. /// - private void OnStuck(EntityUid uid, SpiderChargeComponent comp, EntityStuckEvent args) + private void OnStuck(EntityUid uid, SpiderChargeComponent comp, ref EntityStuckEvent args) { comp.Planter = args.User; } diff --git a/Content.Server/Sticky/Components/StickyComponent.cs b/Content.Server/Sticky/Components/StickyComponent.cs deleted file mode 100644 index 320938000f..0000000000 --- a/Content.Server/Sticky/Components/StickyComponent.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Content.Shared.Whitelist; - -namespace Content.Server.Sticky.Components; - -/// -/// Items that can be stick to other structures or entities. -/// For example paper stickers or C4 charges. -/// -[RegisterComponent] -public sealed partial class StickyComponent : Component -{ - /// - /// What target entities are valid to be surface for sticky entity. - /// - [DataField("whitelist")] - [ViewVariables(VVAccess.ReadWrite)] - public EntityWhitelist? Whitelist; - - /// - /// What target entities can't be used as surface for sticky entity. - /// - [DataField("blacklist")] - [ViewVariables(VVAccess.ReadWrite)] - public EntityWhitelist? Blacklist; - - /// - /// How much time does it take to stick entity to target. - /// If zero will stick entity immediately. - /// - [DataField("stickDelay")] - [ViewVariables(VVAccess.ReadWrite)] - public TimeSpan StickDelay = TimeSpan.Zero; - - /// - /// Whether users can unstick item when it was stuck to surface. - /// - [DataField("canUnstick")] - [ViewVariables(VVAccess.ReadWrite)] - public bool CanUnstick = true; - - /// - /// How much time does it take to unstick entity. - /// If zero will unstick entity immediately. - /// - [DataField("unstickDelay")] - [ViewVariables(VVAccess.ReadWrite)] - public TimeSpan UnstickDelay = TimeSpan.Zero; - - /// - /// Popup message shown when player started sticking entity to another entity. - /// - [DataField("stickPopupStart")] - [ViewVariables(VVAccess.ReadWrite)] - public string? StickPopupStart; - - /// - /// Popup message shown when player successfully stuck entity. - /// - [DataField("stickPopupSuccess")] - [ViewVariables(VVAccess.ReadWrite)] - public string? StickPopupSuccess; - - /// - /// Popup message shown when player started unsticking entity from another entity. - /// - [DataField("unstickPopupStart")] - [ViewVariables(VVAccess.ReadWrite)] - public string? UnstickPopupStart; - - /// - /// Popup message shown when player successfully unstuck entity. - /// - [DataField("unstickPopupSuccess")] - [ViewVariables(VVAccess.ReadWrite)] - public string? UnstickPopupSuccess; - - /// - /// Entity that is used as surface for sticky entity. - /// Null if entity doesn't stuck to anything. - /// - [ViewVariables(VVAccess.ReadOnly)] - public EntityUid? StuckTo; - - /// - /// For the DoAfter event to tell if it should stick or unstick - /// - public bool Stick; -} diff --git a/Content.Server/Sticky/Events/EntityStuckEvent.cs b/Content.Server/Sticky/Events/EntityStuckEvent.cs deleted file mode 100644 index 7857fad7d5..0000000000 --- a/Content.Server/Sticky/Events/EntityStuckEvent.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace Content.Server.Sticky.Events; - -/// -/// Risen on sticky entity to see if it can stick to another entity. -/// -[ByRefEvent] -public record struct AttemptEntityStickEvent(EntityUid Target, EntityUid User) -{ - public readonly EntityUid Target = Target; - public readonly EntityUid User = User; - public bool Cancelled = false; -} - -/// -/// Risen on sticky entity to see if it can unstick from another entity. -/// -[ByRefEvent] -public record struct AttemptEntityUnstickEvent(EntityUid Target, EntityUid User) -{ - public readonly EntityUid Target = Target; - public readonly EntityUid User = User; - public bool Cancelled = false; -} - - -/// -/// Risen on sticky entity when it was stuck to other entity. -/// -public sealed class EntityStuckEvent : EntityEventArgs -{ - /// - /// Entity that was used as a surface for sticky object. - /// - public readonly EntityUid Target; - - /// - /// Entity that stuck sticky object on target. - /// - public readonly EntityUid User; - - public EntityStuckEvent(EntityUid target, EntityUid user) - { - Target = target; - User = user; - } -} - -/// -/// Risen on sticky entity when it was unstuck from other entity. -/// -public sealed class EntityUnstuckEvent : EntityEventArgs -{ - /// - /// Entity that was used as a surface for sticky object. - /// - public readonly EntityUid Target; - - /// - /// Entity that unstuck sticky object on target. - /// - public readonly EntityUid User; - - public EntityUnstuckEvent(EntityUid target, EntityUid user) - { - Target = target; - User = user; - } -} diff --git a/Content.Server/Sticky/Systems/StickySystem.cs b/Content.Server/Sticky/Systems/StickySystem.cs deleted file mode 100644 index 23064a93cb..0000000000 --- a/Content.Server/Sticky/Systems/StickySystem.cs +++ /dev/null @@ -1,234 +0,0 @@ -using Content.Server.Popups; -using Content.Server.Sticky.Components; -using Content.Server.Sticky.Events; -using Content.Shared.DoAfter; -using Content.Shared.Hands.EntitySystems; -using Content.Shared.Interaction; -using Content.Shared.Sticky; -using Content.Shared.Sticky.Components; -using Content.Shared.Verbs; -using Content.Shared.Whitelist; -using Robust.Shared.Containers; -using Robust.Shared.Utility; - -namespace Content.Server.Sticky.Systems; - -public sealed class StickySystem : EntitySystem -{ - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly SharedContainerSystem _containerSystem = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - - private const string StickerSlotId = "stickers_container"; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnStickFinished); - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent>(AddUnstickVerb); - } - - private void OnAfterInteract(EntityUid uid, StickyComponent component, AfterInteractEvent args) - { - if (args.Handled || !args.CanReach || args.Target == null) - return; - - // try stick object to a clicked target entity - args.Handled = StartSticking(uid, args.User, args.Target.Value, component); - } - - private void AddUnstickVerb(EntityUid uid, StickyComponent component, GetVerbsEvent args) - { - if (component.StuckTo == null || !component.CanUnstick || !args.CanInteract || args.Hands == null) - return; - - // we can't use args.CanAccess, because it stuck in another container - // we also need to ignore entity that it stuck to - var inRange = _interactionSystem.InRangeUnobstructed(uid, args.User, - predicate: entity => component.StuckTo == entity); - if (!inRange) - return; - - args.Verbs.Add(new Verb - { - DoContactInteraction = true, - Text = Loc.GetString("comp-sticky-unstick-verb-text"), - Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")), - Act = () => StartUnsticking(uid, args.User, component) - }); - } - - private bool StartSticking(EntityUid uid, EntityUid user, EntityUid target, StickyComponent? component = null) - { - if (!Resolve(uid, ref component)) - return false; - - // check whitelist and blacklist - if (_whitelistSystem.IsWhitelistFail(component.Whitelist, target) || - _whitelistSystem.IsBlacklistPass(component.Blacklist, target)) - return false; - - var attemptEv = new AttemptEntityStickEvent(target, user); - RaiseLocalEvent(uid, ref attemptEv); - if (attemptEv.Cancelled) - return false; - - // check if delay is not zero to start do after - var delay = (float) component.StickDelay.TotalSeconds; - if (delay > 0) - { - // show message to user - if (component.StickPopupStart != null) - { - var msg = Loc.GetString(component.StickPopupStart); - _popupSystem.PopupEntity(msg, user, user); - } - - component.Stick = true; - - // start sticking object to target - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new StickyDoAfterEvent(), uid, target: target, used: uid) - { - BreakOnMove = true, - NeedHand = true, - }); - } - else - { - // if delay is zero - stick entity immediately - StickToEntity(uid, target, user, component); - } - - return true; - } - - private void OnStickFinished(EntityUid uid, StickyComponent component, DoAfterEvent args) - { - if (args.Handled || args.Cancelled || args.Args.Target == null) - return; - - if (component.Stick) - StickToEntity(uid, args.Args.Target.Value, args.Args.User, component); - else - UnstickFromEntity(uid, args.Args.User, component); - - args.Handled = true; - } - - private void StartUnsticking(EntityUid uid, EntityUid user, StickyComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (component.StuckTo is not { } stuckTo) - return; - - var attemptEv = new AttemptEntityUnstickEvent(stuckTo, user); - RaiseLocalEvent(uid, ref attemptEv); - if (attemptEv.Cancelled) - return; - - var delay = (float) component.UnstickDelay.TotalSeconds; - if (delay > 0) - { - // show message to user - if (component.UnstickPopupStart != null) - { - var msg = Loc.GetString(component.UnstickPopupStart); - _popupSystem.PopupEntity(msg, user, user); - } - - component.Stick = false; - - // start unsticking object - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new StickyDoAfterEvent(), uid, target: uid) - { - BreakOnMove = true, - NeedHand = true, - }); - } - else - { - // if delay is zero - unstick entity immediately - UnstickFromEntity(uid, user, component); - } - } - - public void StickToEntity(EntityUid uid, EntityUid target, EntityUid user, StickyComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var attemptEv = new AttemptEntityStickEvent(target, user); - RaiseLocalEvent(uid, ref attemptEv); - if (attemptEv.Cancelled) - return; - - // add container to entity and insert sticker into it - var container = _containerSystem.EnsureContainer(target, StickerSlotId); - container.ShowContents = true; - if (!_containerSystem.Insert(uid, container)) - return; - - // show message to user - if (component.StickPopupSuccess != null) - { - var msg = Loc.GetString(component.StickPopupSuccess); - _popupSystem.PopupEntity(msg, user, user); - } - - // send information to appearance that entity is stuck - if (TryComp(uid, out AppearanceComponent? appearance)) - { - _appearance.SetData(uid, StickyVisuals.IsStuck, true, appearance); - } - - component.StuckTo = target; - RaiseLocalEvent(uid, new EntityStuckEvent(target, user), true); - } - - public void UnstickFromEntity(EntityUid uid, EntityUid user, StickyComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (component.StuckTo is not { } stuckTo) - return; - - var attemptEv = new AttemptEntityUnstickEvent(stuckTo, user); - RaiseLocalEvent(uid, ref attemptEv); - if (attemptEv.Cancelled) - return; - - // try to remove sticky item from target container - if (!_containerSystem.TryGetContainer(stuckTo, StickerSlotId, out var container) || !_containerSystem.Remove(uid, container)) - return; - // delete container if it's now empty - if (container.ContainedEntities.Count == 0) - _containerSystem.ShutdownContainer(container); - - // try place dropped entity into user hands - _handsSystem.PickupOrDrop(user, uid); - - // send information to appearance that entity isn't stuck - if (TryComp(uid, out AppearanceComponent? appearance)) - { - _appearance.SetData(uid, StickyVisuals.IsStuck, false, appearance); - } - - // show message to user - if (component.UnstickPopupSuccess != null) - { - var msg = Loc.GetString(component.UnstickPopupSuccess); - _popupSystem.PopupEntity(msg, user, user); - } - - component.StuckTo = null; - RaiseLocalEvent(uid, new EntityUnstuckEvent(stuckTo, user), true); - } -} diff --git a/Content.Shared/Sticky/Components/StickyComponent.cs b/Content.Shared/Sticky/Components/StickyComponent.cs new file mode 100644 index 0000000000..4513091754 --- /dev/null +++ b/Content.Shared/Sticky/Components/StickyComponent.cs @@ -0,0 +1,90 @@ +using Content.Shared.Sticky.Systems; +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.Sticky.Components; + +/// +/// Items that can be stuck to other structures or entities. +/// For example, paper stickers or C4 charges. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(StickySystem))] +[AutoGenerateComponentState] +public sealed partial class StickyComponent : Component +{ + /// + /// What target entities are valid to be surface for sticky entity. + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// What target entities can't be used as surface for sticky entity. + /// + [DataField] + public EntityWhitelist? Blacklist; + + /// + /// How much time it takes to stick the entity to a target. + /// If zero, it will immediately be stuck. + /// + [DataField] + public TimeSpan StickDelay = TimeSpan.Zero; + + /// + /// Whether users can unstick the entity after it has been stuck. + /// + [DataField] + public bool CanUnstick = true; + + /// + /// How much time it takes to unstick the entity. + /// If zero, it will immediately be unstuck. + /// + [DataField] + public TimeSpan UnstickDelay = TimeSpan.Zero; + + /// + /// Popup message shown when player starts sticking the entity to another entity. + /// + [DataField] + public LocId? StickPopupStart; + + /// + /// Popup message shown when a player successfully sticks the entity. + /// + [DataField] + public LocId? StickPopupSuccess; + + /// + /// Popup message shown when a player starts unsticking the entity from another entity. + /// + [DataField] + public LocId? UnstickPopupStart; + + /// + /// Popup message shown when a player successfully unsticks the entity. + /// + [DataField] + public LocId? UnstickPopupSuccess; + + /// + /// Entity that is used as a surface for the sticky entity. + /// Null if entity isn't stuck to anything. + /// + [DataField, AutoNetworkedField] + public EntityUid? StuckTo; + + /// + /// Text to use for the unstick verb. + /// + [DataField] + public LocId VerbText = "comp-sticky-unstick-verb-text"; + + /// + /// Icon to use for the unstick verb. + /// + [DataField] + public SpriteSpecifier VerbIcon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")); +} diff --git a/Content.Shared/Sticky/Components/StickyVisualizerComponent.cs b/Content.Shared/Sticky/Components/StickyVisualizerComponent.cs index c32be646fc..fd37836dcf 100644 --- a/Content.Shared/Sticky/Components/StickyVisualizerComponent.cs +++ b/Content.Shared/Sticky/Components/StickyVisualizerComponent.cs @@ -1,23 +1,26 @@ -using Robust.Shared.Serialization; +using Robust.Shared.Serialization; namespace Content.Shared.Sticky.Components; -using DrawDepth; +using DrawDepth; + +/// +/// Sets the sprite's draw depth depending on whether it's stuck. +/// [RegisterComponent] public sealed partial class StickyVisualizerComponent : Component { /// - /// What sprite draw depth set when entity stuck. + /// What sprite draw depth gets set to when stuck to something. /// - [DataField("stuckDrawDepth")] - [ViewVariables(VVAccess.ReadWrite)] + [DataField] public int StuckDrawDepth = (int) DrawDepth.Overdoors; /// - /// What sprite draw depth set when entity unstuck. + /// The sprite's original draw depth before being stuck. /// - [ViewVariables(VVAccess.ReadWrite)] - public int DefaultDrawDepth; + [DataField] + public int OriginalDrawDepth; } [Serializable, NetSerializable] diff --git a/Content.Shared/Sticky/EntityStuckEvent.cs b/Content.Shared/Sticky/EntityStuckEvent.cs new file mode 100644 index 0000000000..ae7a65c0ad --- /dev/null +++ b/Content.Shared/Sticky/EntityStuckEvent.cs @@ -0,0 +1,26 @@ +namespace Content.Shared.Sticky; + +/// +/// Risen on sticky entity to see if it can stick to another entity. +/// +[ByRefEvent] +public record struct AttemptEntityStickEvent(EntityUid Target, EntityUid User, bool Cancelled = false); + +/// +/// Risen on sticky entity to see if it can unstick from another entity. +/// +[ByRefEvent] +public record struct AttemptEntityUnstickEvent(EntityUid Target, EntityUid User, bool Cancelled = false); + + +/// +/// Risen on sticky entity when it was stuck to other entity. +/// +[ByRefEvent] +public record struct EntityStuckEvent(EntityUid Target, EntityUid User); + +/// +/// Risen on sticky entity when it was unstuck from other entity. +/// +[ByRefEvent] +public record struct EntityUnstuckEvent(EntityUid Target, EntityUid User); diff --git a/Content.Shared/Sticky/Systems/StickySystem.cs b/Content.Shared/Sticky/Systems/StickySystem.cs new file mode 100644 index 0000000000..ea768fea93 --- /dev/null +++ b/Content.Shared/Sticky/Systems/StickySystem.cs @@ -0,0 +1,220 @@ +using Content.Shared.DoAfter; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Sticky.Components; +using Content.Shared.Verbs; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; + +namespace Content.Shared.Sticky.Systems; + +public sealed class StickySystem : EntitySystem +{ + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + private const string StickerSlotId = "stickers_container"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnStickyDoAfter); + SubscribeLocalEvent>(OnGetVerbs); + } + + private void OnAfterInteract(Entity ent, ref AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target is not {} target) + return; + + // try stick object to a clicked target entity + args.Handled = StartSticking(ent, target, args.User); + } + + private void OnGetVerbs(Entity ent, ref GetVerbsEvent args) + { + var (uid, comp) = ent; + if (comp.StuckTo == null || !comp.CanUnstick || !args.CanInteract || args.Hands == null) + return; + + // we can't use args.CanAccess, because it stuck in another container + // we also need to ignore entity that it stuck to + var user = args.User; + var inRange = _interaction.InRangeUnobstructed(uid, user, + predicate: entity => comp.StuckTo == entity); + if (!inRange) + return; + + args.Verbs.Add(new Verb + { + DoContactInteraction = true, + Text = Loc.GetString(comp.VerbText), + Icon = comp.VerbIcon, + Act = () => StartUnsticking(ent, user) + }); + } + + private bool StartSticking(Entity ent, EntityUid target, EntityUid user) + { + var (uid, comp) = ent; + + // check whitelist and blacklist + if (_whitelist.IsWhitelistFail(comp.Whitelist, target) || + _whitelist.IsBlacklistPass(comp.Blacklist, target)) + return false; + + var attemptEv = new AttemptEntityStickEvent(target, user); + RaiseLocalEvent(uid, ref attemptEv); + if (attemptEv.Cancelled) + return false; + + // skip doafter and popup if it's instant + if (comp.StickDelay <= TimeSpan.Zero) + { + StickToEntity(ent, target, user); + return true; + } + + // show message to user + if (comp.StickPopupStart != null) + { + var msg = Loc.GetString(comp.StickPopupStart); + _popup.PopupClient(msg, user, user); + } + + // start sticking object to target + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, comp.StickDelay, new StickyDoAfterEvent(), uid, target: target, used: uid) + { + BreakOnMove = true, + NeedHand = true, + }); + + return true; + } + + private void OnStickyDoAfter(Entity ent, ref StickyDoAfterEvent args) + { + // target is the sticky item when unsticking and the surface when sticking, it will never be null + if (args.Handled || args.Cancelled || args.Args.Target is not {} target) + return; + + var user = args.User; + if (ent.Comp.StuckTo == null) + StickToEntity(ent, target, user); + else + UnstickFromEntity(ent, user); + + args.Handled = true; + } + + private void StartUnsticking(Entity ent, EntityUid user) + { + var (uid, comp) = ent; + if (comp.StuckTo is not {} stuckTo) + return; + + var attemptEv = new AttemptEntityUnstickEvent(stuckTo, user); + RaiseLocalEvent(uid, ref attemptEv); + if (attemptEv.Cancelled) + return; + + // skip doafter and popup if it's instant + if (comp.UnstickDelay <= TimeSpan.Zero) + { + UnstickFromEntity(ent, user); + return; + } + + // show message to user + if (comp.UnstickPopupStart != null) + { + var msg = Loc.GetString(comp.UnstickPopupStart); + _popup.PopupClient(msg, user, user); + } + + // start unsticking object + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, comp.UnstickDelay, new StickyDoAfterEvent(), uid, target: uid) + { + BreakOnMove = true, + NeedHand = true, + }); + } + + public void StickToEntity(Entity ent, EntityUid target, EntityUid user) + { + var (uid, comp) = ent; + var attemptEv = new AttemptEntityStickEvent(target, user); + RaiseLocalEvent(uid, ref attemptEv); + if (attemptEv.Cancelled) + return; + + // add container to entity and insert sticker into it + var container = _container.EnsureContainer(target, StickerSlotId); + container.ShowContents = true; + if (!_container.Insert(uid, container)) + return; + + // show message to user + if (comp.StickPopupSuccess != null) + { + var msg = Loc.GetString(comp.StickPopupSuccess); + _popup.PopupClient(msg, user, user); + } + + // send information to appearance that entity is stuck + _appearance.SetData(uid, StickyVisuals.IsStuck, true); + + comp.StuckTo = target; + Dirty(uid, comp); + + var ev = new EntityStuckEvent(target, user); + RaiseLocalEvent(uid, ref ev); + } + + public void UnstickFromEntity(Entity ent, EntityUid user) + { + var (uid, comp) = ent; + if (comp.StuckTo is not {} stuckTo) + return; + + var attemptEv = new AttemptEntityUnstickEvent(stuckTo, user); + RaiseLocalEvent(uid, ref attemptEv); + if (attemptEv.Cancelled) + return; + + // try to remove sticky item from target container + if (!_container.TryGetContainer(stuckTo, StickerSlotId, out var container) || !_container.Remove(uid, container)) + return; + + // delete container if it's now empty + if (container.ContainedEntities.Count == 0) + _container.ShutdownContainer(container); + + // try place dropped entity into user hands + _hands.PickupOrDrop(user, uid); + + // send information to appearance that entity isn't stuck + _appearance.SetData(uid, StickyVisuals.IsStuck, false); + + // show message to user + if (comp.UnstickPopupSuccess != null) + { + var msg = Loc.GetString(comp.UnstickPopupSuccess); + _popup.PopupClient(msg, user, user); + } + + comp.StuckTo = null; + Dirty(uid, comp); + + var ev = new EntityUnstuckEvent(stuckTo, user); + RaiseLocalEvent(uid, ref ev); + } +}