]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
full sticky prediction (#30230)
authordeltanedas <39013340+deltanedas@users.noreply.github.com>
Thu, 8 Aug 2024 23:36:15 +0000 (23:36 +0000)
committerGitHub <noreply@github.com>
Thu, 8 Aug 2024 23:36:15 +0000 (09:36 +1000)
* move all sticky stuff to shared and cleanup/grammar fix

* update imports and ref

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
Content.Client/Sticky/Visualizers/StickyVisualizerSystem.cs
Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs
Content.Server/Ninja/Systems/SpiderChargeSystem.cs
Content.Server/Sticky/Components/StickyComponent.cs [deleted file]
Content.Server/Sticky/Events/EntityStuckEvent.cs [deleted file]
Content.Server/Sticky/Systems/StickySystem.cs [deleted file]
Content.Shared/Sticky/Components/StickyComponent.cs [new file with mode: 0644]
Content.Shared/Sticky/Components/StickyVisualizerComponent.cs
Content.Shared/Sticky/EntityStuckEvent.cs [new file with mode: 0644]
Content.Shared/Sticky/Systems/StickySystem.cs [new file with mode: 0644]

index 8bf8ac56eab5142d12fdec83aa50591d204ffecc..f7764479c01838efeb2d022b426213e57003689a 100644 (file)
@@ -5,21 +5,26 @@ namespace Content.Client.Sticky.Visualizers;
 
 public sealed class StickyVisualizerSystem : VisualizerSystem<StickyVisualizerComponent>
 {
+    private EntityQuery<SpriteComponent> _spriteQuery;
+
     public override void Initialize()
     {
         base.Initialize();
+
+        _spriteQuery = GetEntityQuery<SpriteComponent>();
+
         SubscribeLocalEvent<StickyVisualizerComponent, ComponentInit>(OnInit);
     }
 
-    private void OnInit(EntityUid uid, StickyVisualizerComponent component, ComponentInit args)
+    private void OnInit(Entity<StickyVisualizerComponent> 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<StickyVisualizerCo
         if (!AppearanceSystem.TryGetData<bool>(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;
-
     }
 }
index dcd11062bb78df73da88757a93ed09365e96ba1b..d06e9fa1c2da4c5aba5c6974767a6808c520ba74 100644 (file)
@@ -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<RandomTimerTriggerComponent, MapInitEvent>(OnRandomTimerTriggerMapInit);
     }
 
-    private void OnStuck(EntityUid uid, OnUseTimerTriggerComponent component, EntityStuckEvent args)
+    private void OnStuck(EntityUid uid, OnUseTimerTriggerComponent component, ref EntityStuckEvent args)
     {
         if (!component.StartOnStick)
             return;
index c262651f27ae780a258f1c8ee6ff58f134102ec0..c916d568d5f1df4212d3e9d0cbb2ce5dc7907fff 100644 (file)
@@ -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
     /// <summary>
     /// Require that the planter is a ninja and the charge is near the target warp point.
     /// </summary>
-    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
     /// <summary>
     /// Allows greentext to occur after exploding.
     /// </summary>
-    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 (file)
index 3209380..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-using Content.Shared.Whitelist;
-
-namespace Content.Server.Sticky.Components;
-
-/// <summary>
-///     Items that can be stick to other structures or entities.
-///     For example paper stickers or C4 charges.
-/// </summary>
-[RegisterComponent]
-public sealed partial class StickyComponent : Component
-{
-    /// <summary>
-    ///     What target entities are valid to be surface for sticky entity.
-    /// </summary>
-    [DataField("whitelist")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public EntityWhitelist? Whitelist;
-
-    /// <summary>
-    ///     What target entities can't be used as surface for sticky entity.
-    /// </summary>
-    [DataField("blacklist")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public EntityWhitelist? Blacklist;
-
-    /// <summary>
-    ///     How much time does it take to stick entity to target.
-    ///     If zero will stick entity immediately.
-    /// </summary>
-    [DataField("stickDelay")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public TimeSpan StickDelay = TimeSpan.Zero;
-
-    /// <summary>
-    ///     Whether users can unstick item when it was stuck to surface.
-    /// </summary>
-    [DataField("canUnstick")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public bool CanUnstick = true;
-
-    /// <summary>
-    ///     How much time does it take to unstick entity.
-    ///     If zero will unstick entity immediately.
-    /// </summary>
-    [DataField("unstickDelay")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public TimeSpan UnstickDelay = TimeSpan.Zero;
-
-    /// <summary>
-    ///     Popup message shown when player started sticking entity to another entity.
-    /// </summary>
-    [DataField("stickPopupStart")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public string? StickPopupStart;
-
-    /// <summary>
-    ///     Popup message shown when player successfully stuck entity.
-    /// </summary>
-    [DataField("stickPopupSuccess")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public string? StickPopupSuccess;
-
-    /// <summary>
-    ///     Popup message shown when player started unsticking entity from another entity.
-    /// </summary>
-    [DataField("unstickPopupStart")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public string? UnstickPopupStart;
-
-    /// <summary>
-    ///     Popup message shown when player successfully unstuck entity.
-    /// </summary>
-    [DataField("unstickPopupSuccess")]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public string? UnstickPopupSuccess;
-
-    /// <summary>
-    ///     Entity that is used as surface for sticky entity.
-    ///     Null if entity doesn't stuck to anything.
-    /// </summary>
-    [ViewVariables(VVAccess.ReadOnly)]
-    public EntityUid? StuckTo;
-
-    /// <summary>
-    /// For the DoAfter event to tell if it should stick or unstick
-    /// </summary>
-    public bool Stick;
-}
diff --git a/Content.Server/Sticky/Events/EntityStuckEvent.cs b/Content.Server/Sticky/Events/EntityStuckEvent.cs
deleted file mode 100644 (file)
index 7857fad..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-namespace Content.Server.Sticky.Events;
-
-/// <summary>
-///     Risen on sticky entity to see if it can stick to another entity.
-/// </summary>
-[ByRefEvent]
-public record struct AttemptEntityStickEvent(EntityUid Target, EntityUid User)
-{
-    public readonly EntityUid Target = Target;
-    public readonly EntityUid User = User;
-    public bool Cancelled = false;
-}
-
-/// <summary>
-///     Risen on sticky entity to see if it can unstick from another entity.
-/// </summary>
-[ByRefEvent]
-public record struct AttemptEntityUnstickEvent(EntityUid Target, EntityUid User)
-{
-    public readonly EntityUid Target = Target;
-    public readonly EntityUid User = User;
-    public bool Cancelled = false;
-}
-
-
-/// <summary>
-///     Risen on sticky entity when it was stuck to other entity.
-/// </summary>
-public sealed class EntityStuckEvent : EntityEventArgs
-{
-    /// <summary>
-    ///     Entity that was used as a surface for sticky object.
-    /// </summary>
-    public readonly EntityUid Target;
-
-    /// <summary>
-    ///     Entity that stuck sticky object on target.
-    /// </summary>
-    public readonly EntityUid User;
-
-    public EntityStuckEvent(EntityUid target, EntityUid user)
-    {
-        Target = target;
-        User = user;
-    }
-}
-
-/// <summary>
-///     Risen on sticky entity when it was unstuck from other entity.
-/// </summary>
-public sealed class EntityUnstuckEvent : EntityEventArgs
-{
-    /// <summary>
-    ///     Entity that was used as a surface for sticky object.
-    /// </summary>
-    public readonly EntityUid Target;
-
-    /// <summary>
-    ///     Entity that unstuck sticky object on target.
-    /// </summary>
-    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 (file)
index 23064a9..0000000
+++ /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<StickyComponent, StickyDoAfterEvent>(OnStickFinished);
-        SubscribeLocalEvent<StickyComponent, AfterInteractEvent>(OnAfterInteract);
-        SubscribeLocalEvent<StickyComponent, GetVerbsEvent<Verb>>(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<Verb> 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<Container>(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 (file)
index 0000000..4513091
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Items that can be stuck to other structures or entities.
+/// For example, paper stickers or C4 charges.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(StickySystem))]
+[AutoGenerateComponentState]
+public sealed partial class StickyComponent : Component
+{
+    /// <summary>
+    /// What target entities are valid to be surface for sticky entity.
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Whitelist;
+
+    /// <summary>
+    /// What target entities can't be used as surface for sticky entity.
+    /// </summary>
+    [DataField]
+    public EntityWhitelist? Blacklist;
+
+    /// <summary>
+    /// How much time it takes to stick the entity to a target.
+    /// If zero, it will immediately be stuck.
+    /// </summary>
+    [DataField]
+    public TimeSpan StickDelay = TimeSpan.Zero;
+
+    /// <summary>
+    /// Whether users can unstick the entity after it has been stuck.
+    /// </summary>
+    [DataField]
+    public bool CanUnstick = true;
+
+    /// <summary>
+    /// How much time it takes to unstick the entity.
+    /// If zero, it will immediately be unstuck.
+    /// </summary>
+    [DataField]
+    public TimeSpan UnstickDelay = TimeSpan.Zero;
+
+    /// <summary>
+    /// Popup message shown when player starts sticking the entity to another entity.
+    /// </summary>
+    [DataField]
+    public LocId? StickPopupStart;
+
+    /// <summary>
+    /// Popup message shown when a player successfully sticks the entity.
+    /// </summary>
+    [DataField]
+    public LocId? StickPopupSuccess;
+
+    /// <summary>
+    /// Popup message shown when a player starts unsticking the entity from another entity.
+    /// </summary>
+    [DataField]
+    public LocId? UnstickPopupStart;
+
+    /// <summary>
+    /// Popup message shown when a player successfully unsticks the entity.
+    /// </summary>
+    [DataField]
+    public LocId? UnstickPopupSuccess;
+
+    /// <summary>
+    /// Entity that is used as a surface for the sticky entity.
+    /// Null if entity isn't stuck to anything.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid? StuckTo;
+
+    /// <summary>
+    /// Text to use for the unstick verb.
+    /// </summary>
+    [DataField]
+    public LocId VerbText = "comp-sticky-unstick-verb-text";
+
+    /// <summary>
+    /// Icon to use for the unstick verb.
+    /// </summary>
+    [DataField]
+    public SpriteSpecifier VerbIcon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png"));
+}
index c32be646fc3465facb6a53653de622f8dc7b9500..fd37836dcf6203d1bf9dc344d6b460a7c6a1b47c 100644 (file)
@@ -1,23 +1,26 @@
-using Robust.Shared.Serialization;
+using Robust.Shared.Serialization;
 
 namespace Content.Shared.Sticky.Components;
-using DrawDepth;
 
+using DrawDepth;
+
+/// <summary>
+/// Sets the sprite's draw depth depending on whether it's stuck.
+/// </summary>
 [RegisterComponent]
 public sealed partial class StickyVisualizerComponent : Component
 {
     /// <summary>
-    ///     What sprite draw depth set when entity stuck.
+    /// What sprite draw depth gets set to when stuck to something.
     /// </summary>
-    [DataField("stuckDrawDepth")]
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public int StuckDrawDepth = (int) DrawDepth.Overdoors;
 
     /// <summary>
-    ///     What sprite draw depth set when entity unstuck.
+    /// The sprite's original draw depth before being stuck.
     /// </summary>
-    [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 (file)
index 0000000..ae7a65c
--- /dev/null
@@ -0,0 +1,26 @@
+namespace Content.Shared.Sticky;
+
+/// <summary>
+///     Risen on sticky entity to see if it can stick to another entity.
+/// </summary>
+[ByRefEvent]
+public record struct AttemptEntityStickEvent(EntityUid Target, EntityUid User, bool Cancelled = false);
+
+/// <summary>
+///     Risen on sticky entity to see if it can unstick from another entity.
+/// </summary>
+[ByRefEvent]
+public record struct AttemptEntityUnstickEvent(EntityUid Target, EntityUid User, bool Cancelled = false);
+
+
+/// <summary>
+///     Risen on sticky entity when it was stuck to other entity.
+/// </summary>
+[ByRefEvent]
+public record struct EntityStuckEvent(EntityUid Target, EntityUid User);
+
+/// <summary>
+///     Risen on sticky entity when it was unstuck from other entity.
+/// </summary>
+[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 (file)
index 0000000..ea768fe
--- /dev/null
@@ -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<StickyComponent, AfterInteractEvent>(OnAfterInteract);
+        SubscribeLocalEvent<StickyComponent, StickyDoAfterEvent>(OnStickyDoAfter);
+        SubscribeLocalEvent<StickyComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
+    }
+
+    private void OnAfterInteract(Entity<StickyComponent> 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<StickyComponent> ent, ref GetVerbsEvent<Verb> 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<StickyComponent> 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<StickyComponent> 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<StickyComponent> 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<StickyComponent> 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<Container>(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<StickyComponent> 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);
+    }
+}