]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Stack System Cleanup (#38872)
authorāda <ss.adasts@gmail.com>
Sat, 25 Oct 2025 14:40:48 +0000 (09:40 -0500)
committerGitHub <noreply@github.com>
Sat, 25 Oct 2025 14:40:48 +0000 (14:40 +0000)
* eye on the prize

* OnStackInteractUsing, TryMergeStacks, TryMergeToHands, TryMergeToContacts

* namespace

* Use, get count, getMaxCount

* component access

* add regions, mark TODO

* obsolete TryAdd, public TryMergeStacks

* GetMaxCount

* event handlers

* event handlers

* SetCount

* client server event handlers

* move to shared

* Revert "move to shared"

This reverts commit 45540a2d6b8e1e6d2a8f83a584267776c7edcd73.

* misc changes to shared

* split

* spawn and SpawnNextToOrDrop

* SpawnMultipleAtPosition, SpawnMultipleNextToOrDrop, CalculateSpawns, general server cleanup

* Rename Use to TryUse.

* Small misc changes

* Remove obsolete functions

* Remove some SetCount calls

* Partialize

* small misc change

* don't nuke the git dif with the namespace block

* Comments and reordering

* touchup to UpdateLingering

* Summary comment for StackStatusControl

* Last pass

* Actual last pass (for now)

* I know myself too well

* fixup

* goodbye lingering

* fixes

* review

* fix test

* second look

* fix test

* forgot

* remove early comp getting

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
39 files changed:
Content.Client/Stack/StackStatusControl.cs
Content.Client/Stack/StackSystem.cs
Content.IntegrationTests/Tests/CargoTest.cs
Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs
Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
Content.IntegrationTests/Tests/Materials/MaterialTests.cs
Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
Content.Server/Cargo/Systems/CargoSystem.Funds.cs
Content.Server/Cloning/CloningSystem.Subscriptions.cs
Content.Server/Construction/Completions/GivePrototype.cs
Content.Server/Construction/Completions/SetStackCount.cs
Content.Server/Construction/Completions/SpawnPrototype.cs
Content.Server/Construction/ConstructionSystem.Initial.cs
Content.Server/Construction/ConstructionSystem.Machine.cs
Content.Server/Construction/MachineFrameSystem.cs
Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs
Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs
Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs
Content.Server/Hands/Systems/HandsSystem.cs
Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs
Content.Server/Light/EntitySystems/ExpendableLightSystem.cs
Content.Server/Materials/MaterialStorageSystem.cs
Content.Server/Power/EntitySystems/CableSystem.Placer.cs
Content.Server/Stack/StackSystem.cs
Content.Server/Store/Systems/StoreSystem.Ui.cs
Content.Server/Store/Systems/StoreSystem.cs
Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs
Content.Shared/Medical/Healing/HealingSystem.cs
Content.Shared/Salvage/Fulton/SharedFultonSystem.cs
Content.Shared/Stacks/SharedStackSystem.API.cs [new file with mode: 0644]
Content.Shared/Stacks/SharedStackSystem.cs
Content.Shared/Stacks/StackComponent.cs
Content.Shared/Stacks/StackPrototype.cs
Content.Shared/Stacks/StackVisuals.cs
Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs
Content.Shared/Store/CurrencyPrototype.cs
Content.Shared/Tiles/FloorTileSystem.cs

index 52941089af601f10761ca41f19f6f0f0215d5968..1031b9ec9de3bf522b029b6fd441addb871862d0 100644 (file)
@@ -7,6 +7,9 @@ using Robust.Shared.Timing;
 
 namespace Content.Client.Stack;
 
+/// <summary>
+/// Used by hands in player UI to display the stack count.
+/// </summary>
 public sealed class StackStatusControl : Control
 {
     private readonly StackComponent _parent;
index 182daa73a50639d7940077a93baaeed141b026d3..9396c76df59c63bf12ceecdbd8f2491653c2e176 100644 (file)
@@ -1,4 +1,3 @@
-using System.Linq;
 using Content.Client.Items;
 using Content.Client.Storage.Systems;
 using Content.Shared.Stacks;
@@ -7,6 +6,7 @@ using Robust.Client.GameObjects;
 
 namespace Content.Client.Stack
 {
+    /// <inheritdoc />
     [UsedImplicitly]
     public sealed class StackSystem : SharedStackSystem
     {
@@ -16,33 +16,21 @@ namespace Content.Client.Stack
         public override void Initialize()
         {
             base.Initialize();
+
             SubscribeLocalEvent<StackComponent, AppearanceChangeEvent>(OnAppearanceChange);
             Subs.ItemStatus<StackComponent>(ent => new StackStatusControl(ent));
         }
 
-        public override void SetCount(EntityUid uid, int amount, StackComponent? component = null)
-        {
-            if (!Resolve(uid, ref component))
-                return;
-
-            base.SetCount(uid, amount, component);
-
-            // TODO PREDICT ENTITY DELETION: This should really just be a normal entity deletion call.
-            if (component.Count <= 0)
-            {
-                Xform.DetachEntity(uid, Transform(uid));
-                return;
-            }
-
-            component.UiUpdateNeeded = true;
-        }
+        #region Appearance
 
-        private void OnAppearanceChange(EntityUid uid, StackComponent comp, ref AppearanceChangeEvent args)
+        private void OnAppearanceChange(Entity<StackComponent> ent, ref AppearanceChangeEvent args)
         {
+            var (uid, comp) = ent;
+
             if (args.Sprite == null || comp.LayerStates.Count < 1)
                 return;
 
-            // Skip processing if no actual
+            // Skip processing if no elements in the stack
             if (!_appearanceSystem.TryGetData<int>(uid, StackVisuals.Actual, out var actual, args.Component))
                 return;
 
@@ -56,9 +44,24 @@ namespace Content.Client.Stack
                 ApplyLayerFunction((uid, comp), ref actual, ref maxCount);
 
             if (comp.IsComposite)
-                _counterSystem.ProcessCompositeSprite(uid, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite);
+            {
+                _counterSystem.ProcessCompositeSprite(uid,
+                                                    actual,
+                                                    maxCount,
+                                                    comp.LayerStates,
+                                                    hidden,
+                                                    sprite: args.Sprite);
+            }
             else
-                _counterSystem.ProcessOpaqueSprite(uid, comp.BaseLayer, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite);
+            {
+                _counterSystem.ProcessOpaqueSprite(uid,
+                                                comp.BaseLayer,
+                                                actual,
+                                                maxCount,
+                                                comp.LayerStates,
+                                                hidden,
+                                                sprite: args.Sprite);
+            }
         }
 
         /// <summary>
@@ -67,7 +70,7 @@ namespace Content.Client.Stack
         /// <param name="ent">The entity considered.</param>
         /// <param name="actual">The actual number of items in the stack. Altered depending on the function to run.</param>
         /// <param name="maxCount">The maximum number of items in the stack. Altered depending on the function to run.</param>
-        /// <returns>Whether or not a function was applied.</returns>
+        /// <returns>True if a function was applied.</returns>
         private bool ApplyLayerFunction(Entity<StackComponent> ent, ref int actual, ref int maxCount)
         {
             switch (ent.Comp.LayerFunction)
@@ -78,8 +81,10 @@ namespace Content.Client.Stack
                         ApplyThreshold(threshold, ref actual, ref maxCount);
                         return true;
                     }
+
                     break;
             }
+
             // No function applied.
             return false;
         }
@@ -105,7 +110,10 @@ namespace Content.Client.Stack
                 else
                     break;
             }
+
             actual = newActual;
         }
+
+        #endregion
     }
 }
index aad87b711a4ee7ce4b6b7c815a7c382ef04d0d86..df85e61550ad84c5e5ee9105e4f4c11db41f8955 100644 (file)
@@ -215,13 +215,10 @@ public sealed class CargoTest
 
     [TestPrototypes]
     private const string StackProto = @"
-- type: entity
-  id: A
-
 - type: stack
   id: StackProto
   name: stack-steel
-  spawn: A
+  spawn: StackEnt
 
 - type: entity
   id: StackEnt
index 05e8197c8da5fa3617b47823b12fc00c762706f3..a7b96bdd2f9d79b8d3d642651d8c99ecd116fc5a 100644 (file)
@@ -95,8 +95,8 @@ public sealed class CraftingTests : InteractionTest
         Assert.That(sys.IsEntityInContainer(shard), Is.True);
         Assert.That(sys.IsEntityInContainer(rods), Is.False);
         Assert.That(sys.IsEntityInContainer(wires), Is.False);
-        Assert.That(rodStack, Has.Count.EqualTo(8));
-        Assert.That(wireStack, Has.Count.EqualTo(7));
+        Assert.That(rodStack.Count, Is.EqualTo(8));
+        Assert.That(wireStack.Count, Is.EqualTo(7));
 
         await FindEntity(Spear, shouldSucceed: false);
 
index ca7445c359a649cbc45d4166d52aa98a04d2309f..37526f39a778f600901d13b5dd65b095555e81e0 100644 (file)
@@ -93,7 +93,7 @@ public abstract partial class InteractionTest
             await Server.WaitPost(() =>
             {
                 uid = SEntMan.SpawnEntity(stackProto.Spawn, coords);
-                Stack.SetCount(uid, spec.Quantity);
+                Stack.SetCount((uid, null), spec.Quantity);
             });
             return uid;
         }
index 5cf9831077086f27178765ac5ca47a2569108359..dcb47fb81cca1c43eea4875f64504a6d36e09646 100644 (file)
@@ -469,7 +469,8 @@ public sealed class MaterialArbitrageTest
                 await server.WaitPost(() =>
                 {
                     var ent = entManager.SpawnEntity(id, testMap.GridCoords);
-                    stackSys.SetCount(ent, 1);
+                    if (entManager.TryGetComponent<StackComponent>(ent, out var stackComp))
+                        stackSys.SetCount((ent, stackComp), 1);
                     priceCache[id] = price = pricing.GetPrice(ent, false);
                     entManager.DeleteEntity(ent);
                 });
index 30800f358e2cad736dd7351c1596f0a085910d59..a177869e7f3c602c45afc2a0a5db0e95fa1be02a 100644 (file)
@@ -54,7 +54,7 @@ namespace Content.IntegrationTests.Tests.Materials
                             $"{proto.ID} material has no stack prototype");
 
                         if (stackProto != null)
-                            Assert.That(proto.StackEntity, Is.EqualTo(stackProto.Spawn));
+                            Assert.That(proto.StackEntity, Is.EqualTo(stackProto.Spawn.Id));
                     }
                 });
 
index 6563d7b452785b2a2d3806a8e89adbe507276704..41228b5ac8f5f90103d38d3f6b04b7a90afe19bd 100644 (file)
@@ -413,7 +413,7 @@ public sealed partial class AdminVerbSystem
                     // Unbounded intentionally.
                     _quickDialog.OpenDialog(player, Loc.GetString("admin-verbs-adjust-stack"), Loc.GetString("admin-verbs-dialog-adjust-stack-amount", ("max", _stackSystem.GetMaxCount(stack))), (int newAmount) =>
                     {
-                        _stackSystem.SetCount(args.Target, newAmount, stack);
+                        _stackSystem.SetCount((args.Target, stack), newAmount);
                     });
                 },
                 Impact = LogImpact.Medium,
@@ -429,7 +429,7 @@ public sealed partial class AdminVerbSystem
                 Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill-stack.png")),
                 Act = () =>
                 {
-                    _stackSystem.SetCount(args.Target, _stackSystem.GetMaxCount(stack), stack);
+                    _stackSystem.SetCount((args.Target, stack), _stackSystem.GetMaxCount(stack));
                 },
                 Impact = LogImpact.Medium,
                 Message = Loc.GetString("admin-trick-fill-stack-description"),
index 4a3fa5330e5ebf2054397b380b0a71d7ea44ab5e..bc6571c0cc54adf6e91d84ad87772eb10c4596f6 100644 (file)
@@ -56,7 +56,7 @@ public sealed partial class CargoSystem
         if (args.Account == null)
         {
             var stackPrototype = _protoMan.Index(ent.Comp.CashType);
-            _stack.Spawn(args.Amount, stackPrototype, Transform(ent).Coordinates);
+            _stack.SpawnAtPosition(args.Amount, stackPrototype, Transform(ent).Coordinates);
 
             if (!_emag.CheckFlag(ent, EmagType.Interaction))
             {
index 84ef0503051ccdbae249b3c813fa5adb1c03a018..a05c7069f0c839484db7e43d3a23632e388e934d 100644 (file)
@@ -60,7 +60,7 @@ public sealed partial class CloningSystem
     {
         // if the clone is a stack as well, adjust the count of the copy
         if (TryComp<StackComponent>(args.CloneUid, out var cloneStackComp))
-            _stack.SetCount(args.CloneUid, ent.Comp.Count, cloneStackComp);
+            _stack.SetCount((args.CloneUid, cloneStackComp), ent.Comp.Count);
     }
 
     private void OnCloneItemLabel(Entity<LabelComponent> ent, ref CloningItemEvent args)
index f05feb70c0346d74ac0060acb90a2704e13da8a4..22c5473c8de4253ae2c1ed25e49ebc7db93df754 100644 (file)
@@ -27,14 +27,14 @@ public sealed partial class GivePrototype : IGraphAction
         if (EntityPrototypeHelpers.HasComponent<StackComponent>(Prototype))
         {
             var stackSystem = entityManager.EntitySysManager.GetEntitySystem<StackSystem>();
-            var stacks = stackSystem.SpawnMultiple(Prototype, Amount, userUid ?? uid);
+            var stacks = stackSystem.SpawnMultipleNextToOrDrop(Prototype, Amount, userUid ?? uid);
 
             if (userUid is null || !entityManager.TryGetComponent(userUid, out HandsComponent? handsComp))
                 return;
 
             foreach (var item in stacks)
             {
-                stackSystem.TryMergeToHands(item, userUid.Value, hands: handsComp);
+                stackSystem.TryMergeToHands(item, (userUid.Value, handsComp));
             }
         }
         else
index f1e3f9fb9ea067165598e52b10fc7ce0e19239af..409d6abf952b89962ccf99f28b29bac019f5477a 100644 (file)
@@ -12,7 +12,7 @@ namespace Content.Server.Construction.Completions
 
         public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
         {
-            entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount(uid, Amount);
+            entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount((uid, null), Amount);
         }
     }
 }
index c42ecb611f3e8439205610686d6c256be7693385..5aca29edf470122822c7ec4ec94335197363f97f 100644 (file)
@@ -28,7 +28,7 @@ namespace Content.Server.Construction.Completions
             {
                 var stackEnt = entityManager.SpawnEntity(Prototype, coordinates);
                 var stack = entityManager.GetComponent<StackComponent>(stackEnt);
-                entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount(stackEnt, Amount, stack);
+                entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount((stackEnt, stack), Amount);
             }
             else
             {
index 3739951a6f251508d0ba516567d245a7ff5eec23..98b1da034eb57823beef0930d3e3a28b3ec3eefa 100644 (file)
@@ -187,7 +187,7 @@ namespace Content.Server.Construction
 
                             // TODO allow taking from several stacks.
                             // Also update crafting steps to check if it works.
-                            var splitStack = _stackSystem.Split(entity, materialStep.Amount, user.ToCoordinates(0, 0), stack);
+                            var splitStack = _stackSystem.Split((entity, stack), materialStep.Amount, user.ToCoordinates(0, 0));
 
                             if (splitStack == null)
                                 continue;
index eb922f198c7b69085e9e7ca9479959de694fe263..ce7f17f9b94d8d9a005f2ddf1b1285b5afd4ead5 100644 (file)
@@ -49,7 +49,7 @@ public sealed partial class ConstructionSystem
 
         foreach (var (stackType, amount) in machineBoard.StackRequirements)
         {
-            var stack = _stackSystem.Spawn(amount, stackType, xform.Coordinates);
+            var stack = _stackSystem.SpawnAtPosition(amount, stackType, xform.Coordinates);
             if (!_container.Insert(stack, partContainer))
                 throw new Exception($"Couldn't insert machine material of type {stackType} to machine with prototype {Prototype(uid)?.ID ?? "N/A"}");
         }
index b8624aeef28cb51f965f7079ce1c0561185a9305..3af1c6ab5f76d84f4d8ed319cdc499d35bf2e05b 100644 (file)
@@ -182,7 +182,7 @@ public sealed class MachineFrameSystem : EntitySystem
             return true;
         }
 
-        var splitStack = _stack.Split(used, needed, Transform(uid).Coordinates, stack);
+        var splitStack = _stack.Split((used, stack), needed, Transform(uid).Coordinates);
 
         if (splitStack == null)
             return false;
index a8448a1b7f27b8248c3d9fda1ae7ef3302cfd4e7..5646ce6d88bc4ad54bb4ec41570481cb97a5ef25 100644 (file)
@@ -43,7 +43,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
                 if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.EntityManager.ComponentFactory))
                 {
                     var spawned = system.EntityManager.SpawnEntity(entityId, xform.Coordinates.Offset(system.Random.NextVector2(-Offset, Offset)));
-                    system.StackSystem.SetCount(spawned, toSpawn);
+                    system.StackSystem.SetCount((spawned, null), toSpawn);
                     system.EntityManager.GetComponent<TransformComponent>(spawned).LocalRotation = system.Random.NextAngle();
                 }
                 else
index 413991515b9475828d2a3346270b32bee7708ede..13027a31fcfd6d9617b6a3ef0791e85be7915d05 100644 (file)
@@ -58,7 +58,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
                         var spawned = SpawnInContainer
                             ? system.EntityManager.SpawnNextToOrDrop(entityId, owner)
                             : system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector()));
-                        system.StackSystem.SetCount(spawned, count);
+                        system.StackSystem.SetCount((spawned, null), count);
 
                         TransferForensics(spawned, system, owner);
                     }
index 743646c92b649a92058c8b9ebd42719e379cb5fc..82e2d9c30d903686648b49afd9f2087516e7afae 100644 (file)
@@ -63,8 +63,8 @@ namespace Content.Server.Engineering.EntitySystems
             if (component.Deleted || !IsTileClear())
                 return;
 
-            if (TryComp(uid, out StackComponent? stackComp)
-                && component.RemoveOnInteract && !_stackSystem.Use(uid, 1, stackComp))
+            if (TryComp<StackComponent>(uid, out var stackComp)
+                && component.RemoveOnInteract && !_stackSystem.TryUse((uid, stackComp), 1))
             {
                 return;
             }
index 4d47ea4a78039266cd3522d4a9e6661b969cb84d..7688d14ada40b8538714fbf313209e3e0f49dcfe 100644 (file)
@@ -160,7 +160,7 @@ namespace Content.Server.Hands.Systems
 
             if (TryComp(throwEnt, out StackComponent? stack) && stack.Count > 1 && stack.ThrowIndividually)
             {
-                var splitStack = _stackSystem.Split(throwEnt.Value, 1, Comp<TransformComponent>(player).Coordinates, stack);
+                var splitStack = _stackSystem.Split((throwEnt.Value, stack), 1, Comp<TransformComponent>(player).Coordinates);
 
                 if (splitStack is not {Valid: true})
                     return false;
index 1430f53cddbe1bef4b69f8786cc34e3d2c4b858a..e23bea7bb42d5064c8f8fbad82e88d416413105c 100644 (file)
@@ -242,7 +242,7 @@ namespace Content.Server.Kitchen.EntitySystems
                         // If an entity has a stack component, use the stacktype instead of prototype id
                         if (TryComp<StackComponent>(item, out var stackComp))
                         {
-                            itemID = _prototype.Index<StackPrototype>(stackComp.StackTypeId).Spawn;
+                            itemID = _prototype.Index(stackComp.StackTypeId).Spawn;
                         }
                         else
                         {
@@ -265,7 +265,7 @@ namespace Content.Server.Kitchen.EntitySystems
                             {
                                 _container.Remove(item, component.Storage);
                             }
-                            _stack.Use(item, 1, stackComp);
+                            _stack.ReduceCount((item, stackComp), 1);
                             break;
                         }
                         else
index cd0ce8f3a60e6f08302df62ef429a28527ab23bb..b850bc87fa4f0ed47740657ac0e715e511e124c4 100644 (file)
@@ -118,7 +118,7 @@ namespace Content.Server.Kitchen.EntitySystems
                         scaledSolution.ScaleSolution(fitsCount);
                         solution = scaledSolution;
 
-                        _stackSystem.SetCount(item, stack.Count - fitsCount); // Setting to 0 will QueueDel
+                        _stackSystem.ReduceCount((item, stack), fitsCount); // Setting to 0 will QueueDel
                     }
                     else
                     {
index f643bec73f1dec039543c5b77685b76b0fcc0011..0436ea7d3c3756d7be6958d2d6596d051d2aa5f0 100644 (file)
@@ -136,13 +136,13 @@ namespace Content.Server.Light.EntitySystems
                 component.StateExpiryTime = (float)component.RefuelMaterialTime.TotalSeconds;
 
                 _nameModifier.RefreshNameModifiers(uid);
-                _stackSystem.SetCount(args.Used, stack.Count - 1, stack);
+                _stackSystem.ReduceCount((args.Used, stack), 1);
                 UpdateVisualizer((uid, component));
                 return;
             }
 
             component.StateExpiryTime += (float)component.RefuelMaterialTime.TotalSeconds;
-            _stackSystem.SetCount(args.Used, stack.Count - 1, stack);
+            _stackSystem.ReduceCount((args.Used, stack), 1);
             args.Handled = true;
         }
 
index 3a462dd4d5e64ee0e8b85e8045671bda6e3d3b79..f6a1b6c4d8d2e36687249861db64d57077db8abd 100644 (file)
@@ -73,7 +73,7 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
                 return;
 
             var volumePerSheet = composition.MaterialComposition.FirstOrDefault(kvp => kvp.Key == msg.Material).Value;
-            var sheetsToExtract = Math.Min(msg.SheetsToExtract, _stackSystem.GetMaxCount(material.StackEntity));
+            var sheetsToExtract = Math.Min(msg.SheetsToExtract, _stackSystem.GetMaxCount(material.StackEntity.Value));
 
             volume = sheetsToExtract * volumePerSheet;
         }
@@ -183,7 +183,7 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
         if (amountToSpawn == 0)
             return new List<EntityUid>();
 
-        return _stackSystem.SpawnMultiple(materialProto.StackEntity, amountToSpawn, coordinates);
+        return _stackSystem.SpawnMultipleAtPosition(materialProto.StackEntity.Value, amountToSpawn, coordinates);
     }
 
     /// <summary>
index 79ea6b5285c189546dc552954d2f52b1237422b7..d6fe1a8f85ff6896375a2521c596db03e1a10be4 100644 (file)
@@ -49,7 +49,7 @@ public sealed partial class CableSystem
                 return;
         }
 
-        if (TryComp<StackComponent>(placer, out var stack) && !_stack.Use(placer, 1, stack))
+        if (TryComp<StackComponent>(placer, out var stack) && !_stack.TryUse((placer.Owner, stack), 1))
             return;
 
         var newCable = Spawn(component.CablePrototypeId, _map.GridTileToLocal(gridUid, grid, snapPos));
index aac5a23902543ce9866d58594a80d3f1fece9608..a0d923dd1ed688726cd3d606df0aee886bb080ca 100644 (file)
@@ -1,6 +1,5 @@
 using Content.Shared.Popups;
 using Content.Shared.Stacks;
-using Content.Shared.Verbs;
 using JetBrains.Annotations;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
@@ -8,148 +7,246 @@ using Robust.Shared.Prototypes;
 namespace Content.Server.Stack
 {
     /// <summary>
-    ///     Entity system that handles everything relating to stacks.
-    ///     This is a good example for learning how to code in an ECS manner.
+    /// Entity system that handles everything relating to stacks.
+    /// This is a good example for learning how to code in an ECS manner.
     /// </summary>
     [UsedImplicitly]
     public sealed class StackSystem : SharedStackSystem
     {
         [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
 
-        public override void Initialize()
-        {
-            base.Initialize();
-        }
-
-        public override void SetCount(EntityUid uid, int amount, StackComponent? component = null)
-        {
-            if (!Resolve(uid, ref component, false))
-                return;
-
-            base.SetCount(uid, amount, component);
-
-            // Queue delete stack if count reaches zero.
-            if (component.Count <= 0)
-                QueueDel(uid);
-        }
+        #region Spawning
 
         /// <summary>
-        ///     Try to split this stack into two. Returns a non-null <see cref="Robust.Shared.GameObjects.EntityUid"/> if successful.
+        /// Spawns a new entity and moves an amount to it from the stack.
+        /// Moves nothing if amount is greater than ent's stack count.
         /// </summary>
-        public EntityUid? Split(EntityUid uid, int amount, EntityCoordinates spawnPosition, StackComponent? stack = null)
+        /// <param name="amount"> How much to move to the new entity. </param>
+        /// <returns>Null if StackComponent doesn't resolve, or amount to move is greater than ent has available.</returns>
+        [PublicAPI]
+        public EntityUid? Split(Entity<StackComponent?> ent, int amount, EntityCoordinates spawnPosition)
         {
-            if (!Resolve(uid, ref stack))
+            if (!Resolve(ent.Owner, ref ent.Comp))
                 return null;
 
             // Try to remove the amount of things we want to split from the original stack...
-            if (!Use(uid, amount, stack))
+            if (!TryUse(ent, amount))
                 return null;
 
-            // Get a prototype ID to spawn the new entity. Null is also valid, although it should rarely be picked...
-            var prototype = _prototypeManager.TryIndex<StackPrototype>(stack.StackTypeId, out var stackType)
-                ? stackType.Spawn.ToString()
-                : Prototype(uid)?.ID;
+            if (!_prototypeManager.Resolve(ent.Comp.StackTypeId, out var stackType))
+                return null;
 
             // Set the output parameter in the event instance to the newly split stack.
-            var entity = Spawn(prototype, spawnPosition);
+            var newEntity = SpawnAtPosition(stackType.Spawn, spawnPosition);
 
-            if (TryComp(entity, out StackComponent? stackComp))
-            {
-                // Set the split stack's count.
-                SetCount(entity, amount, stackComp);
-                // Don't let people dupe unlimited stacks
-                stackComp.Unlimited = false;
-            }
+            // There should always be a StackComponent
+            var stackComp = Comp<StackComponent>(newEntity);
 
-            var ev = new StackSplitEvent(entity);
-            RaiseLocalEvent(uid, ref ev);
+            SetCount((newEntity, stackComp), amount);
+            stackComp.Unlimited = false; // Don't let people dupe unlimited stacks
+            Dirty(newEntity, stackComp);
 
-            return entity;
-        }
+            var ev = new StackSplitEvent(newEntity);
+            RaiseLocalEvent(ent, ref ev);
 
-        /// <summary>
-        ///     Spawns a stack of a certain stack type. See <see cref="StackPrototype"/>.
-        /// </summary>
-        public EntityUid Spawn(int amount, ProtoId<StackPrototype> id, EntityCoordinates spawnPosition)
-        {
-            var proto = _prototypeManager.Index(id);
-            return Spawn(amount, proto, spawnPosition);
+            return newEntity;
         }
 
+        #region SpawnAtPosition
+
         /// <summary>
-        ///     Spawns a stack of a certain stack type. See <see cref="StackPrototype"/>.
+        /// Spawns a stack of a certain stack type and sets its count. Won't set the stack over its max.
         /// </summary>
-        public EntityUid Spawn(int amount, StackPrototype prototype, EntityCoordinates spawnPosition)
+        /// <param name="count">The amount to set the spawned stack to.</param>
+        [PublicAPI]
+        public EntityUid SpawnAtPosition(int count, StackPrototype prototype, EntityCoordinates spawnPosition)
         {
-            // Set the output result parameter to the new stack entity...
-            var entity = SpawnAtPosition(prototype.Spawn, spawnPosition);
-            var stack = Comp<StackComponent>(entity);
+            var entity = SpawnAtPosition(prototype.Spawn, spawnPosition); // The real SpawnAtPosition
 
-            // And finally, set the correct amount!
-            SetCount(entity, amount, stack);
+            SetCount((entity, null), count);
             return entity;
         }
 
+        /// <inheritdoc cref="SpawnAtPosition(int, StackPrototype, EntityCoordinates)"/>
+        [PublicAPI]
+        public EntityUid SpawnAtPosition(int count, ProtoId<StackPrototype> id, EntityCoordinates spawnPosition)
+        {
+            var proto = _prototypeManager.Index(id);
+            return SpawnAtPosition(count, proto, spawnPosition);
+        }
+
         /// <summary>
-        ///     Say you want to spawn 97 units of something that has a max stack count of 30.
-        ///     This would spawn 3 stacks of 30 and 1 stack of 7.
+        /// Say you want to spawn 97 units of something that has a max stack count of 30.
+        /// This would spawn 3 stacks of 30 and 1 stack of 7.
         /// </summary>
-        public List<EntityUid> SpawnMultiple(string entityPrototype, int amount, EntityCoordinates spawnPosition)
+        /// <returns>The entities spawned.</returns>
+        /// <remarks> If the entity to spawn doesn't have stack component this will spawn a bunch of single items. </remarks>
+        private List<EntityUid> SpawnMultipleAtPosition(EntProtoId entityPrototype,
+                                                        List<int> amounts,
+                                                        EntityCoordinates spawnPosition)
         {
-            if (amount <= 0)
+            if (amounts.Count <= 0)
             {
                 Log.Error(
-                    $"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}");
+                    $"Attempted to spawn stacks of nothing: {entityPrototype}, {amounts}. Trace: {Environment.StackTrace}");
                 return new();
             }
 
-            var spawns = CalculateSpawns(entityPrototype, amount);
-
             var spawnedEnts = new List<EntityUid>();
-            foreach (var count in spawns)
+            foreach (var count in amounts)
             {
-                var entity = SpawnAtPosition(entityPrototype, spawnPosition);
+                var entity = SpawnAtPosition(entityPrototype, spawnPosition); // The real SpawnAtPosition
                 spawnedEnts.Add(entity);
-                SetCount(entity, count);
+                if (TryComp<StackComponent>(entity, out var stackComp)) // prevent errors from the Resolve
+                    SetCount((entity, stackComp), count);
             }
 
             return spawnedEnts;
         }
 
-        /// <inheritdoc cref="SpawnMultiple(string,int,EntityCoordinates)"/>
-        public List<EntityUid> SpawnMultiple(string entityPrototype, int amount, EntityUid target)
+        /// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
+        [PublicAPI]
+        public List<EntityUid> SpawnMultipleAtPosition(EntProtoId entityPrototypeId,
+                                                       int amount,
+                                                       EntityCoordinates spawnPosition)
         {
-            if (amount <= 0)
+            return SpawnMultipleAtPosition(entityPrototypeId,
+                                            CalculateSpawns(entityPrototypeId, amount),
+                                            spawnPosition);
+        }
+
+        /// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
+        [PublicAPI]
+        public List<EntityUid> SpawnMultipleAtPosition(EntityPrototype entityProto,
+                                                       int amount,
+                                                       EntityCoordinates spawnPosition)
+        {
+            return SpawnMultipleAtPosition(entityProto.ID,
+                                            CalculateSpawns(entityProto, amount),
+                                            spawnPosition);
+        }
+
+        /// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
+        [PublicAPI]
+        public List<EntityUid> SpawnMultipleAtPosition(StackPrototype stack,
+                                                       int amount,
+                                                       EntityCoordinates spawnPosition)
+        {
+            return SpawnMultipleAtPosition(stack.Spawn,
+                                            CalculateSpawns(stack, amount),
+                                            spawnPosition);
+        }
+
+        /// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
+        [PublicAPI]
+        public List<EntityUid> SpawnMultipleAtPosition(ProtoId<StackPrototype> stackId,
+                                                       int amount,
+                                                       EntityCoordinates spawnPosition)
+        {
+            var stackProto = _prototypeManager.Index(stackId);
+            return SpawnMultipleAtPosition(stackProto.Spawn,
+                                            CalculateSpawns(stackProto, amount),
+                                            spawnPosition);
+        }
+
+        #endregion
+        #region SpawnNextToOrDrop
+
+        /// <inheritdoc cref="SpawnAtPosition(int, StackPrototype, EntityCoordinates)"/>
+        [PublicAPI]
+        public EntityUid SpawnNextToOrDrop(int amount, StackPrototype prototype, EntityUid source)
+        {
+            var entity = SpawnNextToOrDrop(prototype.Spawn, source); // The real SpawnNextToOrDrop
+            SetCount((entity, null), amount);
+            return entity;
+        }
+
+        /// <inheritdoc cref="SpawnNextToOrDrop(int, StackPrototype, EntityUid)"/>
+        [PublicAPI]
+        public EntityUid SpawnNextToOrDrop(int amount, ProtoId<StackPrototype> id, EntityUid source)
+        {
+            var proto = _prototypeManager.Index(id);
+            return SpawnNextToOrDrop(amount, proto, source);
+        }
+
+        /// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
+        private List<EntityUid> SpawnMultipleNextToOrDrop(EntProtoId entityPrototype,
+                                                          List<int> amounts,
+                                                          EntityUid target)
+        {
+            if (amounts.Count <= 0)
             {
                 Log.Error(
-                    $"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}");
+                    $"Attempted to spawn stacks of nothing: {entityPrototype}, {amounts}. Trace: {Environment.StackTrace}");
                 return new();
             }
 
-            var spawns = CalculateSpawns(entityPrototype, amount);
-
             var spawnedEnts = new List<EntityUid>();
-            foreach (var count in spawns)
+            foreach (var count in amounts)
             {
-                var entity = SpawnNextToOrDrop(entityPrototype, target);
+                var entity = SpawnNextToOrDrop(entityPrototype, target); // The real SpawnNextToOrDrop
                 spawnedEnts.Add(entity);
-                SetCount(entity, count);
+                if (TryComp<StackComponent>(entity, out var stackComp)) // prevent errors from the Resolve
+                    SetCount((entity, stackComp), count);
             }
 
             return spawnedEnts;
         }
 
+        /// <inheritdoc cref="SpawnMultipleNextToOrDrop(EntProtoId, List{int}, EntityUid)"/>
+        [PublicAPI]
+        public List<EntityUid> SpawnMultipleNextToOrDrop(EntProtoId stack,
+                                                         int amount,
+                                                         EntityUid target)
+        {
+            return SpawnMultipleNextToOrDrop(stack,
+                                             CalculateSpawns(stack, amount),
+                                             target);
+        }
+
+        /// <inheritdoc cref="SpawnMultipleNextToOrDrop(EntProtoId, List{int}, EntityUid)"/>
+        [PublicAPI]
+        public List<EntityUid> SpawnMultipleNextToOrDrop(EntityPrototype stack,
+                                                         int amount,
+                                                         EntityUid target)
+        {
+            return SpawnMultipleNextToOrDrop(stack.ID,
+                                             CalculateSpawns(stack, amount),
+                                             target);
+        }
+
+        /// <inheritdoc cref="SpawnMultipleNextToOrDrop(EntProtoId, List{int}, EntityUid)"/>
+        [PublicAPI]
+        public List<EntityUid> SpawnMultipleNextToOrDrop(StackPrototype stack,
+                                                         int amount,
+                                                         EntityUid target)
+        {
+            return SpawnMultipleNextToOrDrop(stack.Spawn,
+                                             CalculateSpawns(stack, amount),
+                                             target);
+        }
+
+        /// <inheritdoc cref="SpawnMultipleNextToOrDrop(EntProtoId, List{int}, EntityUid)"/>
+        [PublicAPI]
+        public List<EntityUid> SpawnMultipleNextToOrDrop(ProtoId<StackPrototype> stackId,
+                                                         int amount,
+                                                         EntityUid target)
+        {
+            var stackProto = _prototypeManager.Index(stackId);
+            return SpawnMultipleNextToOrDrop(stackProto.Spawn,
+                                             CalculateSpawns(stackProto, amount),
+                                             target);
+        }
+
+        #endregion
+        #region Calculate
+
         /// <summary>
         /// Calculates how many stacks to spawn that total up to <paramref name="amount"/>.
         /// </summary>
-        /// <param name="entityPrototype">The stack to spawn.</param>
-        /// <param name="amount">The amount of pieces across all stacks.</param>
         /// <returns>The list of stack counts per entity.</returns>
-        private List<int> CalculateSpawns(string entityPrototype, int amount)
+        private List<int> CalculateSpawns(int maxCountPerStack, int amount)
         {
-            var proto = _prototypeManager.Index<EntityPrototype>(entityPrototype);
-            proto.TryGetComponent<StackComponent>(out var stack, EntityManager.ComponentFactory);
-            var maxCountPerStack = GetMaxCount(stack);
             var amounts = new List<int>();
             while (amount > 0)
             {
@@ -161,28 +258,47 @@ namespace Content.Server.Stack
             return amounts;
         }
 
-        protected override void UserSplit(EntityUid uid, EntityUid userUid, int amount,
-            StackComponent? stack = null,
-            TransformComponent? userTransform = null)
+        /// <inheritdoc cref="CalculateSpawns(int, int)"/>
+        private List<int> CalculateSpawns(StackPrototype stackProto, int amount)
         {
-            if (!Resolve(uid, ref stack))
-                return;
+            return CalculateSpawns(GetMaxCount(stackProto), amount);
+        }
 
-            if (!Resolve(userUid, ref userTransform))
+        /// <inheritdoc cref="CalculateSpawns(int, int)"/>
+        private List<int> CalculateSpawns(EntityPrototype entityPrototype, int amount)
+        {
+            return CalculateSpawns(GetMaxCount(entityPrototype), amount);
+        }
+
+        /// <inheritdoc cref="CalculateSpawns(int, int)"/>
+        private List<int> CalculateSpawns(EntProtoId entityId, int amount)
+        {
+            return CalculateSpawns(GetMaxCount(entityId), amount);
+        }
+
+        #endregion
+        #endregion
+        #region Event Handlers
+
+        /// <inheritdoc />
+        protected override void UserSplit(Entity<StackComponent> stack, Entity<TransformComponent?> user, int amount)
+        {
+            if (!Resolve(user.Owner, ref user.Comp, false))
                 return;
 
             if (amount <= 0)
             {
-                Popup.PopupCursor(Loc.GetString("comp-stack-split-too-small"), userUid, PopupType.Medium);
+                Popup.PopupCursor(Loc.GetString("comp-stack-split-too-small"), user.Owner, PopupType.Medium);
                 return;
             }
 
-            if (Split(uid, amount, userTransform.Coordinates, stack) is not {} split)
+            if (Split(stack.AsNullable(), amount, user.Comp.Coordinates) is not { } split)
                 return;
 
-            Hands.PickupOrDrop(userUid, split);
+            Hands.PickupOrDrop(user.Owner, split);
 
-            Popup.PopupCursor(Loc.GetString("comp-stack-split"), userUid);
+            Popup.PopupCursor(Loc.GetString("comp-stack-split"), user.Owner);
         }
+        #endregion
     }
 }
index 742434ff23a9eb3b310e937f6de06a7250f3d470..c3d37a74b87ee45cf1ed1d34cbcdaf1ac860ebbd 100644 (file)
@@ -313,7 +313,7 @@ public sealed partial class StoreSystem
         {
             var cashId = proto.Cash[value];
             var amountToSpawn = (int) MathF.Floor((float) (amountRemaining / value));
-            var ents = _stack.SpawnMultiple(cashId, amountToSpawn, coordinates);
+            var ents = _stack.SpawnMultipleAtPosition(cashId, amountToSpawn, coordinates);
             if (ents.FirstOrDefault() is {} ent)
                 _hands.PickupOrDrop(buyer, ent);
             amountRemaining -= value * amountToSpawn;
index 10060dc7d3844e45af366a9ad5d095193c005ea6..279026c8738bf4bd0a5d578a8fb33618206a2e1a 100644 (file)
@@ -153,7 +153,7 @@ public sealed partial class StoreSystem : EntitySystem
         // same tick
         currency.Comp.Price.Clear();
         if (stack != null)
-            _stack.SetCount(currency.Owner, 0, stack);
+            _stack.SetCount((currency.Owner, stack), 0);
 
         QueueDel(currency);
         return true;
index 05bb2327e6d593f819ff6a69250172c0b90f2acb..a2cd1eb7155ee0269b626276c6a2bcb8b10d5e8f 100644 (file)
@@ -34,7 +34,7 @@ public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem
             if (_whitelistSystem.IsWhitelistPass(crusher.CrushingWhitelist, contained))
             {
                 var amount = _random.Next(crusher.MinFragments, crusher.MaxFragments);
-                var stacks = _stack.SpawnMultiple(crusher.FragmentStackProtoId, amount, coords);
+                var stacks = _stack.SpawnMultipleAtPosition(crusher.FragmentStackProtoId, amount, coords);
                 foreach (var stack in stacks)
                 {
                     ContainerSystem.Insert((stack, null, null, null), crusher.OutputContainer);
index b737914dcba9be941b2465df728ceabd104f2a91..2ac2c50871f670de449981b4a3e34fc4a459b083 100644 (file)
@@ -87,9 +87,9 @@ public sealed class HealingSystem : EntitySystem
         var dontRepeat = false;
         if (TryComp<StackComponent>(args.Used.Value, out var stackComp))
         {
-            _stacks.Use(args.Used.Value, 1, stackComp);
+            _stacks.ReduceCount((args.Used.Value, stackComp), 1);
 
-            if (_stacks.GetCount(args.Used.Value, stackComp) <= 0)
+            if (_stacks.GetCount((args.Used.Value, stackComp)) <= 0)
                 dontRepeat = true;
         }
         else
index 0b091d3a61262de4fd7d92740e44b1410624e29c..fec1890928665dfc69b02ee121c385ac337a68c4 100644 (file)
@@ -92,7 +92,7 @@ public abstract partial class SharedFultonSystem : EntitySystem
         if (args.Cancelled || args.Target == null || !TryComp<FultonComponent>(args.Used, out var fulton))
             return;
 
-        if (!_stack.Use(args.Used.Value, 1))
+        if (!_stack.TryUse(args.Used.Value, 1))
         {
             return;
         }
diff --git a/Content.Shared/Stacks/SharedStackSystem.API.cs b/Content.Shared/Stacks/SharedStackSystem.API.cs
new file mode 100644 (file)
index 0000000..1356c8e
--- /dev/null
@@ -0,0 +1,293 @@
+using Content.Shared.Hands.Components;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Stacks;
+
+// Partial for public API functions.
+public abstract partial class SharedStackSystem
+{
+    #region Merge Stacks
+
+    /// <summary>
+    /// Moves as much stack count as we can from the donor to the recipient.
+    /// Deletes the donor if count goes to 0.
+    /// </summary>
+    /// <param name="transferred">How much stack count was moved.</param>
+    /// <param name="amount">Optional. Limits amount of stack count to move from the donor.</param>
+    /// <returns> True if transferred is greater than 0. </returns>
+    [PublicAPI]
+    public bool TryMergeStacks(Entity<StackComponent?> donor,
+                                Entity<StackComponent?> recipient,
+                                out int transferred,
+                                int? amount = null)
+    {
+        transferred = 0;
+
+        if (donor == recipient)
+            return false;
+
+        if (!Resolve(recipient, ref recipient.Comp, false) || !Resolve(donor, ref donor.Comp, false))
+            return false;
+
+        if (recipient.Comp.StackTypeId != donor.Comp.StackTypeId)
+            return false;
+
+        // The most we can transfer
+        transferred = Math.Min(donor.Comp.Count, GetAvailableSpace(recipient.Comp));
+        if (transferred <= 0)
+            return false;
+
+        // transfer only as much as we want
+        if (amount > 0)
+            transferred = Math.Min(transferred, amount.Value);
+
+        SetCount(donor, donor.Comp.Count - transferred);
+        SetCount(recipient, recipient.Comp.Count + transferred);
+        return true;
+    }
+
+    /// <summary>
+    /// If the given item is a stack, this attempts to find a matching stack in the users hand and merge with that.
+    /// </summary>
+    /// <remarks>
+    /// If the interaction fails to fully merge the stack, or if this is just not a stack, it will instead try
+    /// to place it in the user's hand normally.
+    /// </remarks>
+    [PublicAPI]
+    public void TryMergeToHands(Entity<StackComponent?> item, Entity<HandsComponent?> user)
+    {
+        if (!Resolve(user.Owner, ref user.Comp, false))
+            return;
+
+        if (!Resolve(item.Owner, ref item.Comp, false))
+        {
+            // This isn't even a stack. Just try to pickup as normal.
+            Hands.PickupOrDrop(user.Owner, item.Owner, handsComp: user.Comp);
+            return;
+        }
+
+        foreach (var held in Hands.EnumerateHeld(user))
+        {
+            TryMergeStacks(item, held, out _);
+
+            if (item.Comp.Count == 0)
+                return;
+        }
+
+        Hands.PickupOrDrop(user.Owner, item.Owner, handsComp: user.Comp);
+    }
+
+    /// <summary>
+    /// Donor entity merges stack count into contacting entities.
+    /// Deletes the donor if count goes to 0.
+    /// </summary>
+    /// <returns> True if donor moved any count to contacts. </returns>
+    [PublicAPI]
+    public bool TryMergeToContacts(Entity<StackComponent?, TransformComponent?> donor)
+    {
+        var (uid, stack, xform) = donor; // sue me
+        if (!Resolve(uid, ref stack, ref xform, false))
+            return false;
+
+        var map = xform.MapID;
+        var bounds = _physics.GetWorldAABB(uid);
+        var intersecting = new HashSet<Entity<StackComponent>>(); // Should we reuse a HashSet instead of making a new one?
+        _entityLookup.GetEntitiesIntersecting(map, bounds, intersecting, LookupFlags.Dynamic | LookupFlags.Sundries);
+
+        var merged = false;
+        foreach (var recipientStack in intersecting)
+        {
+            var otherEnt = recipientStack.Owner;
+            // if you merge a ton of stacks together, you will end up deleting a few by accident.
+            if (TerminatingOrDeleted(otherEnt) || EntityManager.IsQueuedForDeletion(otherEnt))
+                continue;
+
+            if (!TryMergeStacks((uid, stack), recipientStack.AsNullable(), out _))
+                continue;
+            merged = true;
+
+            if (stack.Count <= 0)
+                break;
+        }
+        return merged;
+    }
+
+    #endregion
+    #region Setters
+
+    /// <summary>
+    /// Sets a stack count to an amount. Server will delete ent if count is 0.
+    /// Clamps between zero and the stack's max size.
+    /// </summary>
+    /// <remarks> All setter functions should end up here. </remarks>
+    public void SetCount(Entity<StackComponent?> ent, int amount)
+    {
+        if (!Resolve(ent.Owner, ref ent.Comp))
+            return;
+
+        // Do nothing if amount is already the same.
+        if (amount == ent.Comp.Count)
+            return;
+
+        // Store old value for event-raising purposes...
+        var old = ent.Comp.Count;
+
+        // Clamp the value.
+        amount = Math.Min(amount, GetMaxCount(ent.Comp));
+        amount = Math.Max(amount, 0);
+
+        ent.Comp.Count = amount;
+        ent.Comp.UiUpdateNeeded = true;
+        Dirty(ent);
+
+        Appearance.SetData(ent.Owner, StackVisuals.Actual, ent.Comp.Count);
+        RaiseLocalEvent(ent.Owner, new StackCountChangedEvent(old, ent.Comp.Count));
+
+        // Queue delete stack if count reaches zero.
+        if (ent.Comp.Count <= 0)
+            PredictedQueueDel(ent.Owner);
+    }
+
+    /// <inheritdoc cref="SetCount(Entity{StackComponent?}, int)"/>
+    [Obsolete("Use Entity<T> method instead")]
+    public void SetCount(EntityUid uid, int amount, StackComponent? component = null)
+    {
+        SetCount((uid, component), amount);
+    }
+
+    // TODO
+    /// <summary>
+    /// Increase a stack count by an amount, and spawn new entities if above the max.
+    /// </summary>
+    // public List<EntityUid> RaiseCountAndSpawn(Entity<StackComponent?> ent, int amount);
+
+    /// <summary>
+    /// Reduce a stack count by an amount, even if it would go below 0.
+    /// If it reaches 0 the stack will despawn.
+    /// </summary>
+    /// <seealso cref="TryUse"/>
+    [PublicAPI]
+    public void ReduceCount(Entity<StackComponent?> ent, int amount)
+    {
+        if (!Resolve(ent.Owner, ref ent.Comp))
+            return;
+
+        // Don't reduce unlimited stacks
+        if (ent.Comp.Unlimited)
+            return;
+
+        SetCount(ent, ent.Comp.Count - amount);
+    }
+
+    /// <summary>
+    /// Try to reduce a stack count by a whole amount.
+    /// Won't reduce the stack count if the amount is larger than the stack.
+    /// </summary>
+    /// <returns> True if the count was lowered. Always true if the stack is unlimited. </returns>
+    [PublicAPI]
+    public bool TryUse(Entity<StackComponent?> ent, int amount)
+    {
+        if (!Resolve(ent.Owner, ref ent.Comp))
+            return false;
+
+        // We're unlimited and always greater than amount
+        if (ent.Comp.Unlimited)
+            return true;
+
+        // Check if we have enough things in the stack for this...
+        if (amount > ent.Comp.Count)
+            return false;
+
+        // We do have enough things in the stack, so remove them and change.
+        SetCount(ent, ent.Comp.Count - amount);
+        return true;
+    }
+
+    #endregion
+    #region Getters
+
+    /// <summary>
+    /// Gets the count in a stack. If it cannot be stacked, returns 1.
+    /// </summary>
+    [PublicAPI]
+    public int GetCount(Entity<StackComponent?> ent)
+    {
+        return Resolve(ent.Owner, ref ent.Comp, false) ? ent.Comp.Count : 1;
+    }
+
+    /// <summary>
+    /// Gets the maximum amount that can be fit on a stack.
+    /// </summary>
+    /// <remarks>
+    /// <p>
+    /// if there's no StackComponent, this equals 1. Otherwise, if there's a max
+    /// count override, it equals that. It then checks for a max count value
+    /// on the stack prototype. If there isn't one, it defaults to the max integer
+    /// value (unlimited).
+    /// </p>
+    /// </remarks>
+    [PublicAPI]
+    public int GetMaxCount(StackComponent? component)
+    {
+        if (component == null)
+            return 1;
+
+        if (component.MaxCountOverride != null)
+            return component.MaxCountOverride.Value;
+
+        var stackProto = _prototype.Index(component.StackTypeId);
+        return stackProto.MaxCount ?? int.MaxValue;
+    }
+
+    /// <inheritdoc cref="GetMaxCount(StackComponent?)"/>
+    [PublicAPI]
+    public int GetMaxCount(EntProtoId entityId)
+    {
+        var entProto = _prototype.Index<EntityPrototype>(entityId);
+        entProto.TryGetComponent<StackComponent>(out var stackComp, EntityManager.ComponentFactory);
+        return GetMaxCount(stackComp);
+    }
+
+    /// <inheritdoc cref="GetMaxCount(StackComponent?)"/>
+    [PublicAPI]
+    public int GetMaxCount(EntityPrototype entityId)
+    {
+        entityId.TryGetComponent<StackComponent>(out var stackComp, EntityManager.ComponentFactory);
+        return GetMaxCount(stackComp);
+    }
+
+    /// <inheritdoc cref="GetMaxCount(StackComponent?)"/>
+    [PublicAPI]
+    public int GetMaxCount(EntityUid uid)
+    {
+        return GetMaxCount(CompOrNull<StackComponent>(uid));
+    }
+
+    /// <summary>
+    /// Gets the maximum amount that can be fit on a stack, or int.MaxValue if no max value exists.
+    /// </summary>
+    [PublicAPI]
+    public static int GetMaxCount(StackPrototype stack)
+    {
+        return stack.MaxCount ?? int.MaxValue;
+    }
+
+    /// <inheritdoc cref="GetMaxCount(StackPrototype)"/>
+    [PublicAPI]
+    public int GetMaxCount(ProtoId<StackPrototype> stackId)
+    {
+        return GetMaxCount(_prototype.Index(stackId));
+    }
+
+    /// <summary>
+    /// Gets the remaining space in a stack.
+    /// </summary>
+    [PublicAPI]
+    public int GetAvailableSpace(StackComponent component)
+    {
+        return GetMaxCount(component) - component.Count;
+    }
+
+    #endregion
+}
index b3de1870febbd90a18567aef9fc072cc4d017aaa..83c55e08ea27b82ae346aeab8813db24caf2fd3a 100644 (file)
@@ -1,6 +1,5 @@
 using System.Numerics;
 using Content.Shared.Examine;
-using Content.Shared.Hands.Components;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
 using Content.Shared.Nutrition;
@@ -10,502 +9,257 @@ using Content.Shared.Verbs;
 using JetBrains.Annotations;
 using Robust.Shared.GameStates;
 using Robust.Shared.Physics.Systems;
-using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 
-namespace Content.Shared.Stacks
+namespace Content.Shared.Stacks;
+
+// Partial for general system code and event handlers.
+/// <summary>
+/// System for handling entities which represent a stack of identical items, usually materials.
+/// </summary>
+[UsedImplicitly]
+public abstract partial class SharedStackSystem : EntitySystem
 {
-    [UsedImplicitly]
-    public abstract class SharedStackSystem : EntitySystem
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+    [Dependency] private readonly IViewVariablesManager _vvm = default!;
+    [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
+    [Dependency] protected readonly SharedHandsSystem Hands = default!;
+    [Dependency] protected readonly SharedTransformSystem Xform = default!;
+    [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+    [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] protected readonly SharedPopupSystem Popup = default!;
+    [Dependency] private readonly SharedStorageSystem _storage = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+
+    // TODO: These should be in the prototype.
+    public static readonly int[] DefaultSplitAmounts = { 1, 5, 10, 20, 30, 50 };
+
+    public override void Initialize()
     {
-        [Dependency] private readonly IGameTiming _gameTiming = default!;
-        [Dependency] private readonly IPrototypeManager _prototype = default!;
-        [Dependency] private readonly IViewVariablesManager _vvm = default!;
-        [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
-        [Dependency] protected readonly SharedHandsSystem Hands = default!;
-        [Dependency] protected readonly SharedTransformSystem Xform = default!;
-        [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
-        [Dependency] private readonly SharedPhysicsSystem _physics = default!;
-        [Dependency] protected readonly SharedPopupSystem Popup = default!;
-        [Dependency] private readonly SharedStorageSystem _storage = default!;
-
-        public static readonly int[] DefaultSplitAmounts = { 1, 5, 10, 20, 30, 50 };
-
-        public override void Initialize()
-        {
-            base.Initialize();
-
-            SubscribeLocalEvent<StackComponent, ComponentGetState>(OnStackGetState);
-            SubscribeLocalEvent<StackComponent, ComponentHandleState>(OnStackHandleState);
-            SubscribeLocalEvent<StackComponent, ComponentStartup>(OnStackStarted);
-            SubscribeLocalEvent<StackComponent, ExaminedEvent>(OnStackExamined);
-            SubscribeLocalEvent<StackComponent, InteractUsingEvent>(OnStackInteractUsing);
-            SubscribeLocalEvent<StackComponent, BeforeIngestedEvent>(OnBeforeEaten);
-            SubscribeLocalEvent<StackComponent, IngestedEvent>(OnEaten);
-            SubscribeLocalEvent<StackComponent, GetVerbsEvent<AlternativeVerb>>(OnStackAlternativeInteract);
-
-            _vvm.GetTypeHandler<StackComponent>()
-                .AddPath(nameof(StackComponent.Count), (_, comp) => comp.Count, SetCount);
-        }
-
-        public override void Shutdown()
-        {
-            base.Shutdown();
-
-            _vvm.GetTypeHandler<StackComponent>()
-                .RemovePath(nameof(StackComponent.Count));
-        }
-
-        private void OnStackInteractUsing(EntityUid uid, StackComponent stack, InteractUsingEvent args)
-        {
-            if (args.Handled)
-                return;
-
-            if (!TryComp(args.Used, out StackComponent? recipientStack))
-                return;
-
-            var localRotation = Transform(args.Used).LocalRotation;
-
-            if (!TryMergeStacks(uid, args.Used, out var transfered, stack, recipientStack))
-                return;
-
-            args.Handled = true;
-
-            // interaction is done, the rest is just generating a pop-up
-
-            if (!_gameTiming.IsFirstTimePredicted)
-                return;
-
-            var popupPos = args.ClickLocation;
-            var userCoords = Transform(args.User).Coordinates;
-
-            if (!popupPos.IsValid(EntityManager))
-            {
-                popupPos = userCoords;
-            }
-
-            switch (transfered)
-            {
-                case > 0:
-                    Popup.PopupCoordinates($"+{transfered}", popupPos, Filter.Local(), false);
-
-                    if (GetAvailableSpace(recipientStack) == 0)
-                    {
-                        Popup.PopupCoordinates(Loc.GetString("comp-stack-becomes-full"),
-                            popupPos.Offset(new Vector2(0, -0.5f)), Filter.Local(), false);
-                    }
-
-                    break;
-
-                case 0 when GetAvailableSpace(recipientStack) == 0:
-                    Popup.PopupCoordinates(Loc.GetString("comp-stack-already-full"), popupPos, Filter.Local(), false);
-                    break;
-            }
-
-            _storage.PlayPickupAnimation(args.Used, popupPos, userCoords, localRotation, args.User);
-        }
-
-        private bool TryMergeStacks(
-            EntityUid donor,
-            EntityUid recipient,
-            out int transferred,
-            StackComponent? donorStack = null,
-            StackComponent? recipientStack = null)
-        {
-            transferred = 0;
-            if (donor == recipient)
-                return false;
-
-            if (!Resolve(recipient, ref recipientStack, false) || !Resolve(donor, ref donorStack, false))
-                return false;
-
-            if (string.IsNullOrEmpty(recipientStack.StackTypeId) || !recipientStack.StackTypeId.Equals(donorStack.StackTypeId))
-                return false;
+        base.Initialize();
 
-            transferred = Math.Min(donorStack.Count, GetAvailableSpace(recipientStack));
-            SetCount(donor, donorStack.Count - transferred, donorStack);
-            SetCount(recipient, recipientStack.Count + transferred, recipientStack);
-            return transferred > 0;
-        }
-
-        /// <summary>
-        ///     If the given item is a stack, this attempts to find a matching stack in the users hand, and merge with that.
-        /// </summary>
-        /// <remarks>
-        ///     If the interaction fails to fully merge the stack, or if this is just not a stack, it will instead try
-        ///     to place it in the user's hand normally.
-        /// </remarks>
-        public void TryMergeToHands(
-            EntityUid item,
-            EntityUid user,
-            StackComponent? itemStack = null,
-            HandsComponent? hands = null)
-        {
-            if (!Resolve(user, ref hands, false))
-                return;
-
-            if (!Resolve(item, ref itemStack, false))
-            {
-                // This isn't even a stack. Just try to pickup as normal.
-                Hands.PickupOrDrop(user, item, handsComp: hands);
-                return;
-            }
-
-            // This is shit code until hands get fixed and give an easy way to enumerate over items, starting with the currently active item.
-            foreach (var held in Hands.EnumerateHeld((user, hands)))
-            {
-                TryMergeStacks(item, held, out _, donorStack: itemStack);
-
-                if (itemStack.Count == 0)
-                    return;
-            }
-
-            Hands.PickupOrDrop(user, item, handsComp: hands);
-        }
-
-        public virtual void SetCount(EntityUid uid, int amount, StackComponent? component = null)
-        {
-            if (!Resolve(uid, ref component))
-                return;
-
-            // Do nothing if amount is already the same.
-            if (amount == component.Count)
-                return;
-
-            // Store old value for event-raising purposes...
-            var old = component.Count;
-
-            // Clamp the value.
-            amount = Math.Min(amount, GetMaxCount(component));
-            amount = Math.Max(amount, 0);
+        SubscribeLocalEvent<StackComponent, InteractUsingEvent>(OnStackInteractUsing);
+        SubscribeLocalEvent<StackComponent, ComponentGetState>(OnStackGetState);
+        SubscribeLocalEvent<StackComponent, ComponentHandleState>(OnStackHandleState);
+        SubscribeLocalEvent<StackComponent, ComponentStartup>(OnStackStarted);
+        SubscribeLocalEvent<StackComponent, ExaminedEvent>(OnStackExamined);
 
-            // Server-side override deletes the entity if count == 0
-            component.Count = amount;
-            Dirty(uid, component);
+        SubscribeLocalEvent<StackComponent, BeforeIngestedEvent>(OnBeforeEaten);
+        SubscribeLocalEvent<StackComponent, IngestedEvent>(OnEaten);
+        SubscribeLocalEvent<StackComponent, GetVerbsEvent<AlternativeVerb>>(OnStackAlternativeInteract);
 
-            Appearance.SetData(uid, StackVisuals.Actual, component.Count);
-            RaiseLocalEvent(uid, new StackCountChangedEvent(old, component.Count));
-        }
-
-        /// <summary>
-        ///     Try to use an amount of items on this stack. Returns whether this succeeded.
-        /// </summary>
-        public bool Use(EntityUid uid, int amount, StackComponent? stack = null)
-        {
-            if (!Resolve(uid, ref stack))
-                return false;
+        _vvm.GetTypeHandler<StackComponent>()
+            .AddPath(nameof(StackComponent.Count), (_, comp) => comp.Count, SetCount);
+    }
 
-            // Check if we have enough things in the stack for this...
-            if (stack.Count < amount)
-            {
-                // Not enough things in the stack, return false.
-                return false;
-            }
+    public override void Shutdown()
+    {
+        base.Shutdown();
 
-            // We do have enough things in the stack, so remove them and change.
-            if (!stack.Unlimited)
-            {
-                SetCount(uid, stack.Count - amount, stack);
-            }
+        _vvm.GetTypeHandler<StackComponent>()
+            .RemovePath(nameof(StackComponent.Count));
+    }
 
-            return true;
-        }
+    private void OnStackInteractUsing(Entity<StackComponent> ent, ref InteractUsingEvent args)
+    {
+        if (args.Handled)
+            return;
 
-        /// <summary>
-        /// Tries to merge a stack into any of the stacks it is touching.
-        /// </summary>
-        /// <returns>Whether or not it was successfully merged into another stack</returns>
-        public bool TryMergeToContacts(EntityUid uid, StackComponent? stack = null, TransformComponent? xform = null)
-        {
-            if (!Resolve(uid, ref stack, ref xform, false))
-                return false;
+        if (!TryComp<StackComponent>(args.Used, out var recipientStack))
+            return;
 
-            var map = xform.MapID;
-            var bounds = _physics.GetWorldAABB(uid);
-            var intersecting = new HashSet<Entity<StackComponent>>();
-            _entityLookup.GetEntitiesIntersecting(map, bounds, intersecting, LookupFlags.Dynamic | LookupFlags.Sundries);
+        // Transfer stacks from ground to hand
+        if (!TryMergeStacks((ent.Owner, ent.Comp), (args.Used, recipientStack), out var transferred))
+            return; // if nothing transferred, leave without a pop-up
 
-            var merged = false;
-            foreach (var otherStack in intersecting)
-            {
-                var otherEnt = otherStack.Owner;
-                // if you merge a ton of stacks together, you will end up deleting a few by accident.
-                if (TerminatingOrDeleted(otherEnt) || EntityManager.IsQueuedForDeletion(otherEnt))
-                    continue;
-
-                if (!TryMergeStacks(uid, otherEnt, out _, stack, otherStack))
-                    continue;
-                merged = true;
-
-                if (stack.Count <= 0)
-                    break;
-            }
-            return merged;
-        }
+        args.Handled = true;
 
-        /// <summary>
-        /// Gets the amount of items in a stack. If it cannot be stacked, returns 1.
-        /// </summary>
-        /// <param name="uid"></param>
-        /// <param name="component"></param>
-        /// <returns></returns>
-        public int GetCount(EntityUid uid, StackComponent? component = null)
-        {
-            return Resolve(uid, ref component, false) ? component.Count : 1;
-        }
+        // interaction is done, the rest is just generating a pop-up
 
-        /// <summary>
-        /// Gets the max count for a given entity prototype
-        /// </summary>
-        /// <param name="entityId"></param>
-        /// <returns></returns>
-        [PublicAPI]
-        public int GetMaxCount(string entityId)
-        {
-            var entProto = _prototype.Index<EntityPrototype>(entityId);
-            entProto.TryGetComponent<StackComponent>(out var stackComp, EntityManager.ComponentFactory);
-            return GetMaxCount(stackComp);
-        }
+        var popupPos = args.ClickLocation;
+        var userCoords = Transform(args.User).Coordinates;
 
-        /// <summary>
-        /// Gets the max count for a given entity
-        /// </summary>
-        /// <param name="uid"></param>
-        /// <returns></returns>
-        [PublicAPI]
-        public int GetMaxCount(EntityUid uid)
+        if (!popupPos.IsValid(EntityManager))
         {
-            return GetMaxCount(CompOrNull<StackComponent>(uid));
+            popupPos = userCoords;
         }
 
-        /// <summary>
-        /// Gets the maximum amount that can be fit on a stack.
-        /// </summary>
-        /// <remarks>
-        /// <p>
-        /// if there's no stackcomp, this equals 1. Otherwise, if there's a max
-        /// count override, it equals that. It then checks for a max count value
-        /// on the prototype. If there isn't one, it defaults to the max integer
-        /// value (unlimimted).
-        /// </p>
-        /// </remarks>
-        /// <param name="component"></param>
-        /// <returns></returns>
-        public int GetMaxCount(StackComponent? component)
+        switch (transferred)
         {
-            if (component == null)
-                return 1;
-
-            if (component.MaxCountOverride != null)
-                return component.MaxCountOverride.Value;
+            case > 0:
+                Popup.PopupClient($"+{transferred}", popupPos, args.User);
 
-            if (string.IsNullOrEmpty(component.StackTypeId))
-                return 1;
-
-            var stackProto = _prototype.Index<StackPrototype>(component.StackTypeId);
+                if (GetAvailableSpace(recipientStack) == 0)
+                {
+                    Popup.PopupClient(Loc.GetString("comp-stack-becomes-full"),
+                        popupPos.Offset(new Vector2(0, -0.5f)),
+                        args.User);
+                }
 
-            return stackProto.MaxCount ?? int.MaxValue;
-        }
+                break;
 
-        /// <summary>
-        /// Gets the remaining space in a stack.
-        /// </summary>
-        /// <param name="component"></param>
-        /// <returns></returns>
-        [PublicAPI]
-        public int GetAvailableSpace(StackComponent component)
-        {
-            return GetMaxCount(component) - component.Count;
+            case 0 when GetAvailableSpace(recipientStack) == 0:
+                Popup.PopupClient(Loc.GetString("comp-stack-already-full"), popupPos, args.User);
+                break;
         }
 
-        /// <summary>
-        /// Tries to add one stack to another. May have some leftover count in the inserted entity.
-        /// </summary>
-        public bool TryAdd(EntityUid insertEnt, EntityUid targetEnt, StackComponent? insertStack = null, StackComponent? targetStack = null)
-        {
-            if (!Resolve(insertEnt, ref insertStack) || !Resolve(targetEnt, ref targetStack))
-                return false;
-
-            var count = insertStack.Count;
-            return TryAdd(insertEnt, targetEnt, count, insertStack, targetStack);
-        }
+        var localRotation = Transform(args.Used).LocalRotation;
+        _storage.PlayPickupAnimation(args.Used, popupPos, userCoords, localRotation, args.User);
+    }
 
-        /// <summary>
-        /// Tries to add one stack to another. May have some leftover count in the inserted entity.
-        /// </summary>
-        public bool TryAdd(EntityUid insertEnt, EntityUid targetEnt, int count, StackComponent? insertStack = null, StackComponent? targetStack = null)
-        {
-            if (!Resolve(insertEnt, ref insertStack) || !Resolve(targetEnt, ref targetStack))
-                return false;
+    private void OnStackStarted(Entity<StackComponent> ent, ref ComponentStartup args)
+    {
+        if (!TryComp(ent.Owner, out AppearanceComponent? appearance))
+            return;
 
-            if (insertStack.StackTypeId != targetStack.StackTypeId)
-                return false;
+        Appearance.SetData(ent.Owner, StackVisuals.Actual, ent.Comp.Count, appearance);
+        Appearance.SetData(ent.Owner, StackVisuals.MaxCount, GetMaxCount(ent.Comp), appearance);
+        Appearance.SetData(ent.Owner, StackVisuals.Hide, false, appearance);
+    }
 
-            var available = GetAvailableSpace(targetStack);
+    private void OnStackGetState(Entity<StackComponent> ent, ref ComponentGetState args)
+    {
+        args.State = new StackComponentState(ent.Comp.Count, ent.Comp.MaxCountOverride, ent.Comp.Unlimited);
+    }
 
-            if (available <= 0)
-                return false;
+    private void OnStackHandleState(Entity<StackComponent> ent, ref ComponentHandleState args)
+    {
+        if (args.Current is not StackComponentState cast)
+            return;
 
-            var change = Math.Min(available, count);
+        ent.Comp.MaxCountOverride = cast.MaxCountOverride;
+        ent.Comp.Unlimited = cast.Unlimited;
+        // This will change the count and call events.
+        SetCount(ent.AsNullable(), cast.Count);
+    }
 
-            SetCount(targetEnt, targetStack.Count + change, targetStack);
-            SetCount(insertEnt, insertStack.Count - change, insertStack);
-            return true;
-        }
+    private void OnStackExamined(Entity<StackComponent> ent, ref ExaminedEvent args)
+    {
+        if (!args.IsInDetailsRange)
+            return;
+
+        args.PushMarkup(
+            Loc.GetString("comp-stack-examine-detail-count",
+                ("count", ent.Comp.Count),
+                ("markupCountColor", "lightgray")
+            )
+        );
+    }
 
-        private void OnStackStarted(EntityUid uid, StackComponent component, ComponentStartup args)
-        {
-            if (!TryComp(uid, out AppearanceComponent? appearance))
-                return;
+    private void OnBeforeEaten(Entity<StackComponent> eaten, ref BeforeIngestedEvent args)
+    {
+        if (args.Cancelled)
+            return;
 
-            Appearance.SetData(uid, StackVisuals.Actual, component.Count, appearance);
-            Appearance.SetData(uid, StackVisuals.MaxCount, GetMaxCount(component), appearance);
-            Appearance.SetData(uid, StackVisuals.Hide, false, appearance);
-        }
+        if (args.Solution is not { } sol)
+            return;
 
-        private void OnStackGetState(EntityUid uid, StackComponent component, ref ComponentGetState args)
+        // If the entity is empty and is a lingering entity we can't eat from it.
+        if (eaten.Comp.Count <= 0)
         {
-            args.State = new StackComponentState(component.Count, component.MaxCountOverride);
+            args.Cancelled = true;
+            return;
         }
 
-        private void OnStackHandleState(EntityUid uid, StackComponent component, ref ComponentHandleState args)
-        {
-            if (args.Current is not StackComponentState cast)
-                return;
+        /*
+        Edible stacked items is near completely evil so we must choose one of the following:
+        - Option 1: Eat the entire solution each bite and reduce the stack by 1.
+        - Option 2: Multiply the solution eaten by the stack size.
+        - Option 3: Divide the solution consumed by stack size.
+        The easiest and safest option is and always will be Option 1 otherwise we risk reagent deletion or duplication.
+        That is why we cancel if we cannot set the minimum to the entire volume of the solution.
+        */
+        if (args.TryNewMinimum(sol.Volume))
+            return;
+
+        args.Cancelled = true;
+    }
 
-            component.MaxCountOverride = cast.MaxCount;
-            // This will change the count and call events.
-            SetCount(uid, cast.Count, component);
-        }
+    private void OnEaten(Entity<StackComponent> eaten, ref IngestedEvent args)
+    {
+        if (!TryUse(eaten.AsNullable(), 1))
+            return;
 
-        private void OnStackExamined(EntityUid uid, StackComponent component, ExaminedEvent args)
+        // We haven't eaten the whole stack yet or are unable to eat it completely.
+        if (eaten.Comp.Count > 0)
         {
-            if (!args.IsInDetailsRange)
-                return;
-
-            args.PushMarkup(
-                Loc.GetString("comp-stack-examine-detail-count",
-                    ("count", component.Count),
-                    ("markupCountColor", "lightgray")
-                )
-            );
+            args.Refresh = true;
+            return;
         }
 
-        private void OnBeforeEaten(Entity<StackComponent> eaten, ref BeforeIngestedEvent args)
-        {
-            if (args.Cancelled)
-                return;
-
-            if (args.Solution is not { } sol)
-                return;
+        // Here to tell the food system to do destroy stuff.
+        args.Destroy = true;
+    }
 
-            // If the entity is empty and is a lingering entity we can't eat from it.
-            if (eaten.Comp.Count <= 0)
-            {
-                args.Cancelled = true;
-                return;
-            }
-
-            /*
-            Edible stacked items is near completely evil so we must choose one of the following:
-            - Option 1: Eat the entire solution each bite and reduce the stack by 1.
-            - Option 2: Multiply the solution eaten by the stack size.
-            - Option 3: Divide the solution consumed by stack size.
-            The easiest and safest option is and always will be Option 1 otherwise we risk reagent deletion or duplication.
-            That is why we cancel if we cannot set the minimum to the entire volume of the solution.
-            */
-            if(args.TryNewMinimum(sol.Volume))
-                return;
+    private void OnStackAlternativeInteract(Entity<StackComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
+    {
+        if (!args.CanAccess || !args.CanInteract || args.Hands == null || ent.Comp.Count == 1)
+            return;
 
-            args.Cancelled = true;
-        }
+        var user = args.User; // Can't pass ref events into verbs
 
-        private void OnEaten(Entity<StackComponent> eaten, ref IngestedEvent args)
+        AlternativeVerb halve = new()
         {
-            if (!Use(eaten, 1))
-                return;
-
-            // We haven't eaten the whole stack yet or are unable to eat it completely.
-            if (eaten.Comp.Count > 0)
-            {
-                args.Refresh = true;
-                return;
-            }
-
-            // Here to tell the food system to do destroy stuff.
-            args.Destroy = true;
-        }
-
-        private void OnStackAlternativeInteract(EntityUid uid, StackComponent stack, GetVerbsEvent<AlternativeVerb> args)
+            Text = Loc.GetString("comp-stack-split-halve"),
+            Category = VerbCategory.Split,
+            Act = () => UserSplit(ent, user, ent.Comp.Count / 2),
+            Priority = 1
+        };
+        args.Verbs.Add(halve);
+
+        var priority = 0;
+        foreach (var amount in DefaultSplitAmounts)
         {
-            if (!args.CanAccess || !args.CanInteract || args.Hands == null || stack.Count == 1)
-                return;
+            if (amount >= ent.Comp.Count)
+                continue;
 
-            AlternativeVerb halve = new()
+            AlternativeVerb verb = new()
             {
-                Text = Loc.GetString("comp-stack-split-halve"),
+                Text = amount.ToString(),
                 Category = VerbCategory.Split,
-                Act = () => UserSplit(uid, args.User, stack.Count / 2, stack),
-                Priority = 1
+                Act = () => UserSplit(ent, user, amount),
+                // we want to sort by size, not alphabetically by the verb text.
+                Priority = priority
             };
-            args.Verbs.Add(halve);
 
-            var priority = 0;
-            foreach (var amount in DefaultSplitAmounts)
-            {
-                if (amount >= stack.Count)
-                    continue;
+            priority--;
 
-                AlternativeVerb verb = new()
-                {
-                    Text = amount.ToString(),
-                    Category = VerbCategory.Split,
-                    Act = () => UserSplit(uid, args.User, amount, stack),
-                    // we want to sort by size, not alphabetically by the verb text.
-                    Priority = priority
-                };
-
-                priority--;
-
-                args.Verbs.Add(verb);
-            }
+            args.Verbs.Add(verb);
         }
+    }
 
-        /// <remarks>
-        ///     OnStackAlternativeInteract() was moved to shared in order to faciliate prediction of stack splitting verbs.
-        ///     However, prediction of interacitons with spawned entities is non-functional (or so i'm told)
-        ///     So, UserSplit() and Split() should remain on the server for the time being.
-        ///     This empty virtual method allows for UserSplit() to be called on the server from the client.
-        ///     When prediction is improved, those two methods should be moved to shared, in order to predict the splitting itself (not just the verbs)
-        /// </remarks>
-        protected virtual void UserSplit(EntityUid uid, EntityUid userUid, int amount,
-            StackComponent? stack = null,
-            TransformComponent? userTransform = null)
-        {
+    /// <remarks>
+    ///     OnStackAlternativeInteract() was moved to shared in order to faciliate prediction of stack splitting verbs.
+    ///     However, prediction of interacitons with spawned entities is non-functional (or so i'm told)
+    ///     So, UserSplit() and Split() should remain on the server for the time being.
+    ///     This empty virtual method allows for UserSplit() to be called on the server from the client.
+    ///     When prediction is improved, those two methods should be moved to shared, in order to predict the splitting itself (not just the verbs)
+    /// </remarks>
+    protected virtual void UserSplit(Entity<StackComponent> stack, Entity<TransformComponent?> user, int amount)
+    {
 
-        }
     }
+}
 
+/// <summary>
+/// Event raised when a stack's count has changed.
+/// </summary>
+public sealed class StackCountChangedEvent : EntityEventArgs
+{
     /// <summary>
-    ///     Event raised when a stack's count has changed.
+    /// The old stack count.
     /// </summary>
-    public sealed class StackCountChangedEvent : EntityEventArgs
-    {
-        /// <summary>
-        ///     The old stack count.
-        /// </summary>
-        public int OldCount;
+    public int OldCount;
 
-        /// <summary>
-        ///     The new stack count.
-        /// </summary>
-        public int NewCount;
+    /// <summary>
+    /// The new stack count.
+    /// </summary>
+    public int NewCount;
 
-        public StackCountChangedEvent(int oldCount, int newCount)
-        {
-            OldCount = oldCount;
-            NewCount = newCount;
-        }
+    public StackCountChangedEvent(int oldCount, int newCount)
+    {
+        OldCount = oldCount;
+        NewCount = newCount;
     }
 }
index 453c8a737d63c0759aff54c210a99f3990f4f942..f604f99654ce46c43129cec0781685a8716f6339 100644 (file)
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
-namespace Content.Shared.Stacks
+namespace Content.Shared.Stacks;
+
+/// <summary>
+/// Component on an entity that represents a stack of identical things, usually materials.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedStackSystem))]
+public sealed partial class StackComponent : Component
 {
-    [RegisterComponent, NetworkedComponent]
-    public sealed partial class StackComponent : Component
-    {
-        [ViewVariables(VVAccess.ReadWrite)]
-        [DataField("stackType", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<StackPrototype>))]
-        public string StackTypeId { get; private set; } = default!;
+    /// <summary>
+    /// What stack type we are.
+    /// </summary>
+    [DataField("stackType", required: true)]
+    public ProtoId<StackPrototype> StackTypeId = default!;
 
-        /// <summary>
-        ///     Current stack count.
-        ///     Do NOT set this directly, use the <see cref="SharedStackSystem.SetCount"/> method instead.
-        /// </summary>
-        [DataField("count")]
-        public int Count { get; set; } = 30;
+    /// <summary>
+    /// Current stack count.
+    /// Do NOT set this directly, use the <see cref="SharedStackSystem.SetCount"/> method instead.
+    /// </summary>
+    [DataField]
+    public int Count = 30;
 
-        /// <summary>
-        ///     Max amount of things that can be in the stack.
-        ///     Overrides the max defined on the stack prototype.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadOnly)]
-        [DataField("maxCountOverride")]
-        public int? MaxCountOverride  { get; set; }
+    /// <summary>
+    /// Max amount of things that can be in the stack.
+    /// Overrides the max defined on the stack prototype.
+    /// </summary>
+    [DataField]
+    public int? MaxCountOverride;
 
-        /// <summary>
-        ///     Set to true to not reduce the count when used.
-        ///     Note that <see cref="Count"/> still limits the amount that can be used at any one time.
-        /// </summary>
-        [DataField("unlimited")]
-        [ViewVariables(VVAccess.ReadOnly)]
-        public bool Unlimited { get; set; }
+    /// <summary>
+    /// Set to true to not reduce the count when used.
+    /// </summary>
+    [DataField]
+    public bool Unlimited;
 
-        [DataField("throwIndividually"), ViewVariables(VVAccess.ReadWrite)]
-        public bool ThrowIndividually { get; set; } = false;
+    /// <summary>
+    /// When throwing this item, do we want to only throw one part of the stack or the whole stack at once?
+    /// </summary>
+    [DataField]
+    public bool ThrowIndividually;
 
-        [ViewVariables]
-        public bool UiUpdateNeeded { get; set; }
+    /// <summary>
+    /// Used by StackStatusControl in client to update UI.
+    /// </summary>
+    [ViewVariables]
+    [Access(typeof(SharedStackSystem), Other = AccessPermissions.ReadWrite)] // Set by StackStatusControl
+    public bool UiUpdateNeeded { get; set; }
 
-        /// <summary>
-        /// Default IconLayer stack.
-        /// </summary>
-        [DataField("baseLayer")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public string BaseLayer = "";
+    /// <summary>
+    ///     Default IconLayer stack.
+    /// </summary>
+    [DataField]
+    public string BaseLayer = "";
 
-        /// <summary>
-        /// Determines if the visualizer uses composite or non-composite layers for icons. Defaults to false.
-        ///
-        /// <list type="bullet">
-        /// <item>
-        /// <description>false: they are opaque and mutually exclusive (e.g. sprites in a cable coil). <b>Default value</b></description>
-        /// </item>
-        /// <item>
-        /// <description>true: they are transparent and thus layered one over another in ascending order first</description>
-        /// </item>
-        /// </list>
-        ///
-        /// </summary>
-        [DataField("composite")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public bool IsComposite;
+    /// <summary>
+    /// Determines if the visualizer uses composite or non-composite layers for icons. Defaults to false.
+    ///
+    /// <list type="bullet">
+    /// <item>
+    /// <description>false: they are opaque and mutually exclusive (e.g. sprites in a cable coil). <b>Default value</b></description>
+    /// </item>
+    /// <item>
+    /// <description>true: they are transparent and thus layered one over another in ascending order first</description>
+    /// </item>
+    /// </list>
+    ///
+    /// </summary>
+    [DataField("composite")]
+    public bool IsComposite;
 
-        /// <summary>
-        /// Sprite layers used in stack visualizer. Sprites first in layer correspond to lower stack states
-        /// e.g. <code>_spriteLayers[0]</code> is lower stack level than <code>_spriteLayers[1]</code>.
-        /// </summary>
-        [DataField("layerStates")]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public List<string> LayerStates = new();
+    /// <summary>
+    /// Sprite layers used in stack visualizer. Sprites first in layer correspond to lower stack states
+    /// e.g. <code>_spriteLayers[0]</code> is lower stack level than <code>_spriteLayers[1]</code>.
+    /// </summary>
+    [DataField]
+    public List<string> LayerStates = new();
 
-        /// <summary>
-        /// An optional function to convert the amounts used to adjust a stack's appearance.
-        /// Useful for different denominations of cash, for example.
-        /// </summary>
-        [DataField]
-        public StackLayerFunction LayerFunction = StackLayerFunction.None;
-    }
+    /// <summary>
+    /// An optional function to convert the amounts used to adjust a stack's appearance.
+    /// Useful for different denominations of cash, for example.
+    /// </summary>
+    [DataField]
+    public StackLayerFunction LayerFunction = StackLayerFunction.None;
+}
 
-    [Serializable, NetSerializable]
-    public sealed class StackComponentState : ComponentState
-    {
-        public int Count { get; }
-        public int? MaxCount { get; }
+[Serializable, NetSerializable]
+public sealed class StackComponentState : ComponentState
+{
+    public int Count { get; }
+    public int? MaxCountOverride { get; }
+    public bool Unlimited { get; }
 
-        public StackComponentState(int count, int? maxCount)
-        {
-            Count = count;
-            MaxCount = maxCount;
-        }
+    public StackComponentState(int count, int? maxCountOverride, bool unlimited)
+    {
+        Count = count;
+        MaxCountOverride = maxCountOverride;
+        Unlimited = unlimited;
     }
+}
 
-    [Serializable, NetSerializable]
-    public enum StackLayerFunction : byte
-    {
-        // <summary>
-        // No operation performed.
-        // </summary>
-        None,
+[Serializable, NetSerializable]
+public enum StackLayerFunction : byte
+{
+    // <summary>
+    // No operation performed.
+    // </summary>
+    None,
 
-        // <summary>
-        // Arbitrarily thresholds the stack amount for each layer.
-        // Expects entity to have StackLayerThresholdComponent.
-        // </summary>
-        Threshold
-    }
+    // <summary>
+    // Arbitrarily thresholds the stack amount for each layer.
+    // Expects entity to have StackLayerThresholdComponent.
+    // </summary>
+    Threshold
 }
index 5b95935ec47ccc5f02af3bca869ae6d3bcb62da4..dfc9d4a997bc03847a8035c2e88d5906ed0b6d40 100644 (file)
@@ -4,6 +4,9 @@ using Robust.Shared.Utility;
 
 namespace Content.Shared.Stacks;
 
+/// <summary>
+/// Prototype used to combine and spawn like-entities for <see cref="SharedStackSystem"/>.
+/// </summary>
 [Prototype]
 public sealed partial class StackPrototype : IPrototype, IInheritingPrototype
 {
@@ -21,28 +24,27 @@ public sealed partial class StackPrototype : IPrototype, IInheritingPrototype
     public bool Abstract { get; private set; }
 
     /// <summary>
-    ///     Human-readable name for this stack type e.g. "Steel"
+    /// Human-readable name for this stack type e.g. "Steel"
     /// </summary>
     /// <remarks>This is a localization string ID.</remarks>
     [DataField]
     public LocId Name { get; private set; } = string.Empty;
 
     /// <summary>
-    ///     An icon that will be used to represent this stack type.
+    /// An icon that will be used to represent this stack type.
     /// </summary>
     [DataField]
     public SpriteSpecifier? Icon { get; private set; }
 
     /// <summary>
-    ///     The entity id that will be spawned by default from this stack.
+    /// The entity id that will be spawned by default from this stack.
     /// </summary>
     [DataField(required: true)]
-    public EntProtoId Spawn { get; private set; } = string.Empty;
+    public EntProtoId<StackComponent> Spawn { get; private set; } = string.Empty;
 
     /// <summary>
-    ///     The maximum amount of things that can be in a stack.
-    ///     Can be overriden on <see cref="StackComponent"/>
-    ///     if null, simply has unlimited max count.
+    /// The maximum amount of things that can be in a stack, can be overriden on <see cref="StackComponent"/>.
+    /// If null, simply has unlimited max count.
     /// </summary>
     [DataField]
     public int? MaxCount { get; private set; }
index 2f29b303fb2e046e4a26d5c7dda61eb47a887603..49f3e830d885640fe2e6a1bddb67b08c3b0fd0ef 100644 (file)
@@ -11,7 +11,7 @@ namespace Content.Shared.Stacks
         Actual,
         /// <summary>
         /// The total amount of elements in the stack. If unspecified, the visualizer assumes
-        /// its
+        /// it's StackComponent.LayerStates.Count
         /// </summary>
         MaxCount,
         Hide
index fcda7eb0acb2c657c71ded3785a54725b258e89f..cda5eb5263ca2789be08767946dc055f6178179d 100644 (file)
@@ -1219,7 +1219,7 @@ public abstract class SharedStorageSystem : EntitySystem
             if (!_stackQuery.TryGetComponent(ent, out var containedStack))
                 continue;
 
-            if (!_stack.TryAdd(insertEnt, ent, insertStack, containedStack))
+            if (!_stack.TryMergeStacks((insertEnt, insertStack), (ent, containedStack), out var _))
                 continue;
 
             stackedEntity = ent;
@@ -1773,7 +1773,7 @@ public abstract class SharedStorageSystem : EntitySystem
         return GetCumulativeItemAreas(uid) < uid.Comp.Grid.GetArea() || HasSpaceInStacks(uid);
     }
 
-    private bool HasSpaceInStacks(Entity<StorageComponent?> uid, string? stackType = null)
+    private bool HasSpaceInStacks(Entity<StorageComponent?> uid, ProtoId<StackPrototype>? stackType = null)
     {
         if (!Resolve(uid, ref uid.Comp))
             return false;
@@ -1783,7 +1783,7 @@ public abstract class SharedStorageSystem : EntitySystem
             if (!_stackQuery.TryGetComponent(contained, out var stack))
                 continue;
 
-            if (stackType != null && !stack.StackTypeId.Equals(stackType))
+            if (stackType != null && stack.StackTypeId != stackType)
                 continue;
 
             if (_stack.GetAvailableSpace(stack) == 0)
index fdd113d3a2867d4becdd742d420edcf873a2f5f3..2b7e3d807ba301bcac1f62be1699beea1837046e 100644 (file)
@@ -23,18 +23,18 @@ public sealed partial class CurrencyPrototype : IPrototype
     /// doesn't necessarily refer to the full name of the currency, only
     /// that which is displayed to the user.
     /// </summary>
-    [DataField("displayName")]
+    [DataField]
     public string DisplayName { get; private set; } = string.Empty;
 
     /// <summary>
     /// The physical entity of the currency
     /// </summary>
-    [DataField("cash", customTypeSerializer: typeof(PrototypeIdValueDictionarySerializer<FixedPoint2, EntityPrototype>))]
-    public Dictionary<FixedPoint2, string>? Cash { get; private set; }
+    [DataField]
+    public Dictionary<FixedPoint2, EntProtoId>? Cash { get; private set; }
 
     /// <summary>
     /// Whether or not this currency can be withdrawn from a shop by a player. Requires a valid entityId.
     /// </summary>
-    [DataField("canWithdraw")]
+    [DataField]
     public bool CanWithdraw { get; private set; } = true;
 }
index 298c0390babccb9c90dbd4aec49ac43dc087b0cd..2c6df5ce89b16ef8117fa79134197787a4091c6c 100644 (file)
@@ -144,7 +144,7 @@ public sealed class FloorTileSystem : EntitySystem
 
                 if (HasBaseTurf(currentTileDefinition, baseTurf.ID))
                 {
-                    if (!_stackSystem.Use(uid, 1, stack))
+                    if (!_stackSystem.TryUse((uid, stack), 1))
                         continue;
 
                     PlaceAt(args.User, gridUid, mapGrid, location, currentTileDefinition.TileId, component.PlaceTileSound);
@@ -154,7 +154,7 @@ public sealed class FloorTileSystem : EntitySystem
             }
             else if (HasBaseTurf(currentTileDefinition, ContentTileDefinition.SpaceID))
             {
-                if (!_stackSystem.Use(uid, 1, stack))
+                if (!_stackSystem.TryUse((uid, stack), 1))
                     continue;
 
                 args.Handled = true;