diff --git a/Content.Client/Stack/StackStatusControl.cs b/Content.Client/Stack/StackStatusControl.cs index 52941089af..1031b9ec9d 100644 --- a/Content.Client/Stack/StackStatusControl.cs +++ b/Content.Client/Stack/StackStatusControl.cs @@ -7,6 +7,9 @@ using Robust.Shared.Timing; namespace Content.Client.Stack; +/// +/// Used by hands in player UI to display the stack count. +/// public sealed class StackStatusControl : Control { private readonly StackComponent _parent; diff --git a/Content.Client/Stack/StackSystem.cs b/Content.Client/Stack/StackSystem.cs index 182daa73a5..9396c76df5 100644 --- a/Content.Client/Stack/StackSystem.cs +++ b/Content.Client/Stack/StackSystem.cs @@ -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 { + /// [UsedImplicitly] public sealed class StackSystem : SharedStackSystem { @@ -16,33 +16,21 @@ namespace Content.Client.Stack public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnAppearanceChange); Subs.ItemStatus(ent => new StackStatusControl(ent)); } - public override void SetCount(EntityUid uid, int amount, StackComponent? component = null) + #region Appearance + + private void OnAppearanceChange(Entity ent, ref AppearanceChangeEvent args) { - if (!Resolve(uid, ref component)) - return; + var (uid, comp) = ent; - 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; - } - - private void OnAppearanceChange(EntityUid uid, StackComponent comp, ref AppearanceChangeEvent args) - { 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(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); + } } /// @@ -67,7 +70,7 @@ namespace Content.Client.Stack /// The entity considered. /// The actual number of items in the stack. Altered depending on the function to run. /// The maximum number of items in the stack. Altered depending on the function to run. - /// Whether or not a function was applied. + /// True if a function was applied. private bool ApplyLayerFunction(Entity 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 } } diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs index aad87b711a..df85e61550 100644 --- a/Content.IntegrationTests/Tests/CargoTest.cs +++ b/Content.IntegrationTests/Tests/CargoTest.cs @@ -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 diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs index 05e8197c8d..a7b96bdd2f 100644 --- a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs +++ b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs @@ -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); diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs index ca7445c359..37526f39a7 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs @@ -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; } diff --git a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs index 5cf9831077..dcb47fb81c 100644 --- a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs +++ b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs @@ -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(ent, out var stackComp)) + stackSys.SetCount((ent, stackComp), 1); priceCache[id] = price = pricing.GetPrice(ent, false); entManager.DeleteEntity(ent); }); diff --git a/Content.IntegrationTests/Tests/Materials/MaterialTests.cs b/Content.IntegrationTests/Tests/Materials/MaterialTests.cs index 30800f358e..a177869e7f 100644 --- a/Content.IntegrationTests/Tests/Materials/MaterialTests.cs +++ b/Content.IntegrationTests/Tests/Materials/MaterialTests.cs @@ -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)); } }); diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index 6563d7b452..41228b5ac8 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -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"), diff --git a/Content.Server/Cargo/Systems/CargoSystem.Funds.cs b/Content.Server/Cargo/Systems/CargoSystem.Funds.cs index 4a3fa5330e..bc6571c0cc 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Funds.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Funds.cs @@ -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)) { diff --git a/Content.Server/Cloning/CloningSystem.Subscriptions.cs b/Content.Server/Cloning/CloningSystem.Subscriptions.cs index 84ef050305..a05c7069f0 100644 --- a/Content.Server/Cloning/CloningSystem.Subscriptions.cs +++ b/Content.Server/Cloning/CloningSystem.Subscriptions.cs @@ -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(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 ent, ref CloningItemEvent args) diff --git a/Content.Server/Construction/Completions/GivePrototype.cs b/Content.Server/Construction/Completions/GivePrototype.cs index f05feb70c0..22c5473c8d 100644 --- a/Content.Server/Construction/Completions/GivePrototype.cs +++ b/Content.Server/Construction/Completions/GivePrototype.cs @@ -27,14 +27,14 @@ public sealed partial class GivePrototype : IGraphAction if (EntityPrototypeHelpers.HasComponent(Prototype)) { var stackSystem = entityManager.EntitySysManager.GetEntitySystem(); - 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 diff --git a/Content.Server/Construction/Completions/SetStackCount.cs b/Content.Server/Construction/Completions/SetStackCount.cs index f1e3f9fb9e..409d6abf95 100644 --- a/Content.Server/Construction/Completions/SetStackCount.cs +++ b/Content.Server/Construction/Completions/SetStackCount.cs @@ -12,7 +12,7 @@ namespace Content.Server.Construction.Completions public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) { - entityManager.EntitySysManager.GetEntitySystem().SetCount(uid, Amount); + entityManager.EntitySysManager.GetEntitySystem().SetCount((uid, null), Amount); } } } diff --git a/Content.Server/Construction/Completions/SpawnPrototype.cs b/Content.Server/Construction/Completions/SpawnPrototype.cs index c42ecb611f..5aca29edf4 100644 --- a/Content.Server/Construction/Completions/SpawnPrototype.cs +++ b/Content.Server/Construction/Completions/SpawnPrototype.cs @@ -28,7 +28,7 @@ namespace Content.Server.Construction.Completions { var stackEnt = entityManager.SpawnEntity(Prototype, coordinates); var stack = entityManager.GetComponent(stackEnt); - entityManager.EntitySysManager.GetEntitySystem().SetCount(stackEnt, Amount, stack); + entityManager.EntitySysManager.GetEntitySystem().SetCount((stackEnt, stack), Amount); } else { diff --git a/Content.Server/Construction/ConstructionSystem.Initial.cs b/Content.Server/Construction/ConstructionSystem.Initial.cs index 3739951a6f..98b1da034e 100644 --- a/Content.Server/Construction/ConstructionSystem.Initial.cs +++ b/Content.Server/Construction/ConstructionSystem.Initial.cs @@ -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; diff --git a/Content.Server/Construction/ConstructionSystem.Machine.cs b/Content.Server/Construction/ConstructionSystem.Machine.cs index eb922f198c..ce7f17f9b9 100644 --- a/Content.Server/Construction/ConstructionSystem.Machine.cs +++ b/Content.Server/Construction/ConstructionSystem.Machine.cs @@ -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"}"); } diff --git a/Content.Server/Construction/MachineFrameSystem.cs b/Content.Server/Construction/MachineFrameSystem.cs index b8624aeef2..3af1c6ab5f 100644 --- a/Content.Server/Construction/MachineFrameSystem.cs +++ b/Content.Server/Construction/MachineFrameSystem.cs @@ -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; diff --git a/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs b/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs index a8448a1b7f..5646ce6d88 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/DumpRestockInventory.cs @@ -43,7 +43,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors if (EntityPrototypeHelpers.HasComponent(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(spawned).LocalRotation = system.Random.NextAngle(); } else diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs index 413991515b..13027a31fc 100644 --- a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs +++ b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs @@ -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); } diff --git a/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs b/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs index 743646c92b..82e2d9c30d 100644 --- a/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs +++ b/Content.Server/Engineering/EntitySystems/SpawnAfterInteractSystem.cs @@ -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(uid, out var stackComp) + && component.RemoveOnInteract && !_stackSystem.TryUse((uid, stackComp), 1)) { return; } diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index 4d47ea4a78..7688d14ada 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -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(player).Coordinates, stack); + var splitStack = _stackSystem.Split((throwEnt.Value, stack), 1, Comp(player).Coordinates); if (splitStack is not {Valid: true}) return false; diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs index 1430f53cdd..e23bea7bb4 100644 --- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs @@ -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(item, out var stackComp)) { - itemID = _prototype.Index(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 diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs index cd0ce8f3a6..b850bc87fa 100644 --- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs @@ -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 { diff --git a/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs b/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs index f643bec73f..0436ea7d3c 100644 --- a/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs +++ b/Content.Server/Light/EntitySystems/ExpendableLightSystem.cs @@ -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; } diff --git a/Content.Server/Materials/MaterialStorageSystem.cs b/Content.Server/Materials/MaterialStorageSystem.cs index 3a462dd4d5..f6a1b6c4d8 100644 --- a/Content.Server/Materials/MaterialStorageSystem.cs +++ b/Content.Server/Materials/MaterialStorageSystem.cs @@ -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(); - return _stackSystem.SpawnMultiple(materialProto.StackEntity, amountToSpawn, coordinates); + return _stackSystem.SpawnMultipleAtPosition(materialProto.StackEntity.Value, amountToSpawn, coordinates); } /// diff --git a/Content.Server/Power/EntitySystems/CableSystem.Placer.cs b/Content.Server/Power/EntitySystems/CableSystem.Placer.cs index 79ea6b5285..d6fe1a8f85 100644 --- a/Content.Server/Power/EntitySystems/CableSystem.Placer.cs +++ b/Content.Server/Power/EntitySystems/CableSystem.Placer.cs @@ -49,7 +49,7 @@ public sealed partial class CableSystem return; } - if (TryComp(placer, out var stack) && !_stack.Use(placer, 1, stack)) + if (TryComp(placer, out var stack) && !_stack.TryUse((placer.Owner, stack), 1)) return; var newCable = Spawn(component.CablePrototypeId, _map.GridTileToLocal(gridUid, grid, snapPos)); diff --git a/Content.Server/Stack/StackSystem.cs b/Content.Server/Stack/StackSystem.cs index aac5a23902..a0d923dd1e 100644 --- a/Content.Server/Stack/StackSystem.cs +++ b/Content.Server/Stack/StackSystem.cs @@ -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 { /// - /// 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. /// [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 /// - /// Try to split this stack into two. Returns a non-null 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. /// - public EntityUid? Split(EntityUid uid, int amount, EntityCoordinates spawnPosition, StackComponent? stack = null) + /// How much to move to the new entity. + /// Null if StackComponent doesn't resolve, or amount to move is greater than ent has available. + [PublicAPI] + public EntityUid? Split(Entity 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(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(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); + var ev = new StackSplitEvent(newEntity); + RaiseLocalEvent(ent, ref ev); + + return newEntity; + } + + #region SpawnAtPosition + + /// + /// Spawns a stack of a certain stack type and sets its count. Won't set the stack over its max. + /// + /// The amount to set the spawned stack to. + [PublicAPI] + public EntityUid SpawnAtPosition(int count, StackPrototype prototype, EntityCoordinates spawnPosition) + { + var entity = SpawnAtPosition(prototype.Spawn, spawnPosition); // The real SpawnAtPosition + + SetCount((entity, null), count); return entity; } - /// - /// Spawns a stack of a certain stack type. See . - /// - public EntityUid Spawn(int amount, ProtoId id, EntityCoordinates spawnPosition) + /// + [PublicAPI] + public EntityUid SpawnAtPosition(int count, ProtoId id, EntityCoordinates spawnPosition) { var proto = _prototypeManager.Index(id); - return Spawn(amount, proto, spawnPosition); + return SpawnAtPosition(count, proto, spawnPosition); } /// - /// Spawns a stack of a certain stack type. See . + /// 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. /// - public EntityUid Spawn(int amount, StackPrototype prototype, EntityCoordinates spawnPosition) + /// The entities spawned. + /// If the entity to spawn doesn't have stack component this will spawn a bunch of single items. + private List SpawnMultipleAtPosition(EntProtoId entityPrototype, + List amounts, + EntityCoordinates spawnPosition) { - // Set the output result parameter to the new stack entity... - var entity = SpawnAtPosition(prototype.Spawn, spawnPosition); - var stack = Comp(entity); + if (amounts.Count <= 0) + { + Log.Error( + $"Attempted to spawn stacks of nothing: {entityPrototype}, {amounts}. Trace: {Environment.StackTrace}"); + return new(); + } - // And finally, set the correct amount! - SetCount(entity, amount, stack); + var spawnedEnts = new List(); + foreach (var count in amounts) + { + var entity = SpawnAtPosition(entityPrototype, spawnPosition); // The real SpawnAtPosition + spawnedEnts.Add(entity); + if (TryComp(entity, out var stackComp)) // prevent errors from the Resolve + SetCount((entity, stackComp), count); + } + + return spawnedEnts; + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(EntProtoId entityPrototypeId, + int amount, + EntityCoordinates spawnPosition) + { + return SpawnMultipleAtPosition(entityPrototypeId, + CalculateSpawns(entityPrototypeId, amount), + spawnPosition); + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(EntityPrototype entityProto, + int amount, + EntityCoordinates spawnPosition) + { + return SpawnMultipleAtPosition(entityProto.ID, + CalculateSpawns(entityProto, amount), + spawnPosition); + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(StackPrototype stack, + int amount, + EntityCoordinates spawnPosition) + { + return SpawnMultipleAtPosition(stack.Spawn, + CalculateSpawns(stack, amount), + spawnPosition); + } + + /// + [PublicAPI] + public List SpawnMultipleAtPosition(ProtoId stackId, + int amount, + EntityCoordinates spawnPosition) + { + var stackProto = _prototypeManager.Index(stackId); + return SpawnMultipleAtPosition(stackProto.Spawn, + CalculateSpawns(stackProto, amount), + spawnPosition); + } + + #endregion + #region SpawnNextToOrDrop + + /// + [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; } - /// - /// 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. - /// - public List SpawnMultiple(string entityPrototype, int amount, EntityCoordinates spawnPosition) + /// + [PublicAPI] + public EntityUid SpawnNextToOrDrop(int amount, ProtoId id, EntityUid source) { - if (amount <= 0) + var proto = _prototypeManager.Index(id); + return SpawnNextToOrDrop(amount, proto, source); + } + + /// + private List SpawnMultipleNextToOrDrop(EntProtoId entityPrototype, + List 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(); - foreach (var count in spawns) + foreach (var count in amounts) { - var entity = SpawnAtPosition(entityPrototype, spawnPosition); + var entity = SpawnNextToOrDrop(entityPrototype, target); // The real SpawnNextToOrDrop spawnedEnts.Add(entity); - SetCount(entity, count); + if (TryComp(entity, out var stackComp)) // prevent errors from the Resolve + SetCount((entity, stackComp), count); } return spawnedEnts; } - /// - public List SpawnMultiple(string entityPrototype, int amount, EntityUid target) + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(EntProtoId stack, + int amount, + EntityUid target) { - if (amount <= 0) - { - Log.Error( - $"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}"); - return new(); - } - - var spawns = CalculateSpawns(entityPrototype, amount); - - var spawnedEnts = new List(); - foreach (var count in spawns) - { - var entity = SpawnNextToOrDrop(entityPrototype, target); - spawnedEnts.Add(entity); - SetCount(entity, count); - } - - return spawnedEnts; + return SpawnMultipleNextToOrDrop(stack, + CalculateSpawns(stack, amount), + target); } + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(EntityPrototype stack, + int amount, + EntityUid target) + { + return SpawnMultipleNextToOrDrop(stack.ID, + CalculateSpawns(stack, amount), + target); + } + + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(StackPrototype stack, + int amount, + EntityUid target) + { + return SpawnMultipleNextToOrDrop(stack.Spawn, + CalculateSpawns(stack, amount), + target); + } + + /// + [PublicAPI] + public List SpawnMultipleNextToOrDrop(ProtoId stackId, + int amount, + EntityUid target) + { + var stackProto = _prototypeManager.Index(stackId); + return SpawnMultipleNextToOrDrop(stackProto.Spawn, + CalculateSpawns(stackProto, amount), + target); + } + + #endregion + #region Calculate + /// /// Calculates how many stacks to spawn that total up to . /// - /// The stack to spawn. - /// The amount of pieces across all stacks. /// The list of stack counts per entity. - private List CalculateSpawns(string entityPrototype, int amount) + private List CalculateSpawns(int maxCountPerStack, int amount) { - var proto = _prototypeManager.Index(entityPrototype); - proto.TryGetComponent(out var stack, EntityManager.ComponentFactory); - var maxCountPerStack = GetMaxCount(stack); var amounts = new List(); 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) + /// + private List CalculateSpawns(StackPrototype stackProto, int amount) { - if (!Resolve(uid, ref stack)) - return; + return CalculateSpawns(GetMaxCount(stackProto), amount); + } - if (!Resolve(userUid, ref userTransform)) + /// + private List CalculateSpawns(EntityPrototype entityPrototype, int amount) + { + return CalculateSpawns(GetMaxCount(entityPrototype), amount); + } + + /// + private List CalculateSpawns(EntProtoId entityId, int amount) + { + return CalculateSpawns(GetMaxCount(entityId), amount); + } + + #endregion + #endregion + #region Event Handlers + + /// + protected override void UserSplit(Entity stack, Entity 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 } } diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 742434ff23..c3d37a74b8 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -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; diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 10060dc7d3..279026c873 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -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; diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs index 05bb2327e6..a2cd1eb715 100644 --- a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs +++ b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs @@ -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); diff --git a/Content.Shared/Medical/Healing/HealingSystem.cs b/Content.Shared/Medical/Healing/HealingSystem.cs index b737914dcb..2ac2c50871 100644 --- a/Content.Shared/Medical/Healing/HealingSystem.cs +++ b/Content.Shared/Medical/Healing/HealingSystem.cs @@ -87,9 +87,9 @@ public sealed class HealingSystem : EntitySystem var dontRepeat = false; if (TryComp(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 diff --git a/Content.Shared/Salvage/Fulton/SharedFultonSystem.cs b/Content.Shared/Salvage/Fulton/SharedFultonSystem.cs index 0b091d3a61..fec1890928 100644 --- a/Content.Shared/Salvage/Fulton/SharedFultonSystem.cs +++ b/Content.Shared/Salvage/Fulton/SharedFultonSystem.cs @@ -92,7 +92,7 @@ public abstract partial class SharedFultonSystem : EntitySystem if (args.Cancelled || args.Target == null || !TryComp(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 index 0000000000..1356c8ecda --- /dev/null +++ b/Content.Shared/Stacks/SharedStackSystem.API.cs @@ -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 + + /// + /// Moves as much stack count as we can from the donor to the recipient. + /// Deletes the donor if count goes to 0. + /// + /// How much stack count was moved. + /// Optional. Limits amount of stack count to move from the donor. + /// True if transferred is greater than 0. + [PublicAPI] + public bool TryMergeStacks(Entity donor, + Entity 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; + } + + /// + /// If the given item is a stack, this attempts to find a matching stack in the users hand and merge with that. + /// + /// + /// 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. + /// + [PublicAPI] + public void TryMergeToHands(Entity item, Entity 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); + } + + /// + /// Donor entity merges stack count into contacting entities. + /// Deletes the donor if count goes to 0. + /// + /// True if donor moved any count to contacts. + [PublicAPI] + public bool TryMergeToContacts(Entity 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>(); // 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 + + /// + /// Sets a stack count to an amount. Server will delete ent if count is 0. + /// Clamps between zero and the stack's max size. + /// + /// All setter functions should end up here. + public void SetCount(Entity 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); + } + + /// + [Obsolete("Use Entity method instead")] + public void SetCount(EntityUid uid, int amount, StackComponent? component = null) + { + SetCount((uid, component), amount); + } + + // TODO + /// + /// Increase a stack count by an amount, and spawn new entities if above the max. + /// + // public List RaiseCountAndSpawn(Entity ent, int amount); + + /// + /// Reduce a stack count by an amount, even if it would go below 0. + /// If it reaches 0 the stack will despawn. + /// + /// + [PublicAPI] + public void ReduceCount(Entity 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); + } + + /// + /// Try to reduce a stack count by a whole amount. + /// Won't reduce the stack count if the amount is larger than the stack. + /// + /// True if the count was lowered. Always true if the stack is unlimited. + [PublicAPI] + public bool TryUse(Entity 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 + + /// + /// Gets the count in a stack. If it cannot be stacked, returns 1. + /// + [PublicAPI] + public int GetCount(Entity ent) + { + return Resolve(ent.Owner, ref ent.Comp, false) ? ent.Comp.Count : 1; + } + + /// + /// Gets the maximum amount that can be fit on a stack. + /// + /// + ///

+ /// 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). + ///

+ ///
+ [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; + } + + /// + [PublicAPI] + public int GetMaxCount(EntProtoId entityId) + { + var entProto = _prototype.Index(entityId); + entProto.TryGetComponent(out var stackComp, EntityManager.ComponentFactory); + return GetMaxCount(stackComp); + } + + /// + [PublicAPI] + public int GetMaxCount(EntityPrototype entityId) + { + entityId.TryGetComponent(out var stackComp, EntityManager.ComponentFactory); + return GetMaxCount(stackComp); + } + + /// + [PublicAPI] + public int GetMaxCount(EntityUid uid) + { + return GetMaxCount(CompOrNull(uid)); + } + + /// + /// Gets the maximum amount that can be fit on a stack, or int.MaxValue if no max value exists. + /// + [PublicAPI] + public static int GetMaxCount(StackPrototype stack) + { + return stack.MaxCount ?? int.MaxValue; + } + + /// + [PublicAPI] + public int GetMaxCount(ProtoId stackId) + { + return GetMaxCount(_prototype.Index(stackId)); + } + + /// + /// Gets the remaining space in a stack. + /// + [PublicAPI] + public int GetAvailableSpace(StackComponent component) + { + return GetMaxCount(component) - component.Count; + } + + #endregion +} diff --git a/Content.Shared/Stacks/SharedStackSystem.cs b/Content.Shared/Stacks/SharedStackSystem.cs index b3de1870fe..83c55e08ea 100644 --- a/Content.Shared/Stacks/SharedStackSystem.cs +++ b/Content.Shared/Stacks/SharedStackSystem.cs @@ -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. +/// +/// System for handling entities which represent a stack of identical items, usually materials. +/// +[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!; + base.Initialize(); - public static readonly int[] DefaultSplitAmounts = { 1, 5, 10, 20, 30, 50 }; + SubscribeLocalEvent(OnStackInteractUsing); + SubscribeLocalEvent(OnStackGetState); + SubscribeLocalEvent(OnStackHandleState); + SubscribeLocalEvent(OnStackStarted); + SubscribeLocalEvent(OnStackExamined); - public override void Initialize() + SubscribeLocalEvent(OnBeforeEaten); + SubscribeLocalEvent(OnEaten); + SubscribeLocalEvent>(OnStackAlternativeInteract); + + _vvm.GetTypeHandler() + .AddPath(nameof(StackComponent.Count), (_, comp) => comp.Count, SetCount); + } + + public override void Shutdown() + { + base.Shutdown(); + + _vvm.GetTypeHandler() + .RemovePath(nameof(StackComponent.Count)); + } + + private void OnStackInteractUsing(Entity ent, ref InteractUsingEvent args) + { + if (args.Handled) + return; + + if (!TryComp(args.Used, out var recipientStack)) + return; + + // 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 + + args.Handled = true; + + // interaction is done, the rest is just generating a pop-up + + var popupPos = args.ClickLocation; + var userCoords = Transform(args.User).Coordinates; + + if (!popupPos.IsValid(EntityManager)) { - base.Initialize(); - - SubscribeLocalEvent(OnStackGetState); - SubscribeLocalEvent(OnStackHandleState); - SubscribeLocalEvent(OnStackStarted); - SubscribeLocalEvent(OnStackExamined); - SubscribeLocalEvent(OnStackInteractUsing); - SubscribeLocalEvent(OnBeforeEaten); - SubscribeLocalEvent(OnEaten); - SubscribeLocalEvent>(OnStackAlternativeInteract); - - _vvm.GetTypeHandler() - .AddPath(nameof(StackComponent.Count), (_, comp) => comp.Count, SetCount); + popupPos = userCoords; } - public override void Shutdown() + switch (transferred) { - base.Shutdown(); + case > 0: + Popup.PopupClient($"+{transferred}", popupPos, args.User); - _vvm.GetTypeHandler() - .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; - - transferred = Math.Min(donorStack.Count, GetAvailableSpace(recipientStack)); - SetCount(donor, donorStack.Count - transferred, donorStack); - SetCount(recipient, recipientStack.Count + transferred, recipientStack); - return transferred > 0; - } - - /// - /// If the given item is a stack, this attempts to find a matching stack in the users hand, and merge with that. - /// - /// - /// 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. - /// - 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); - - // Server-side override deletes the entity if count == 0 - component.Count = amount; - Dirty(uid, component); - - Appearance.SetData(uid, StackVisuals.Actual, component.Count); - RaiseLocalEvent(uid, new StackCountChangedEvent(old, component.Count)); - } - - /// - /// Try to use an amount of items on this stack. Returns whether this succeeded. - /// - public bool Use(EntityUid uid, int amount, StackComponent? stack = null) - { - if (!Resolve(uid, ref stack)) - return false; - - // 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; - } - - // We do have enough things in the stack, so remove them and change. - if (!stack.Unlimited) - { - SetCount(uid, stack.Count - amount, stack); - } - - return true; - } - - /// - /// Tries to merge a stack into any of the stacks it is touching. - /// - /// Whether or not it was successfully merged into another stack - public bool TryMergeToContacts(EntityUid uid, StackComponent? stack = null, TransformComponent? xform = null) - { - if (!Resolve(uid, ref stack, ref xform, false)) - return false; - - var map = xform.MapID; - var bounds = _physics.GetWorldAABB(uid); - var intersecting = new HashSet>(); - _entityLookup.GetEntitiesIntersecting(map, bounds, intersecting, LookupFlags.Dynamic | LookupFlags.Sundries); - - 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; - } - - /// - /// Gets the amount of items in a stack. If it cannot be stacked, returns 1. - /// - /// - /// - /// - public int GetCount(EntityUid uid, StackComponent? component = null) - { - return Resolve(uid, ref component, false) ? component.Count : 1; - } - - /// - /// Gets the max count for a given entity prototype - /// - /// - /// - [PublicAPI] - public int GetMaxCount(string entityId) - { - var entProto = _prototype.Index(entityId); - entProto.TryGetComponent(out var stackComp, EntityManager.ComponentFactory); - return GetMaxCount(stackComp); - } - - /// - /// Gets the max count for a given entity - /// - /// - /// - [PublicAPI] - public int GetMaxCount(EntityUid uid) - { - return GetMaxCount(CompOrNull(uid)); - } - - /// - /// Gets the maximum amount that can be fit on a stack. - /// - /// - ///

- /// 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). - ///

- ///
- /// - /// - public int GetMaxCount(StackComponent? component) - { - if (component == null) - return 1; - - if (component.MaxCountOverride != null) - return component.MaxCountOverride.Value; - - if (string.IsNullOrEmpty(component.StackTypeId)) - return 1; - - var stackProto = _prototype.Index(component.StackTypeId); - - return stackProto.MaxCount ?? int.MaxValue; - } - - /// - /// Gets the remaining space in a stack. - /// - /// - /// - [PublicAPI] - public int GetAvailableSpace(StackComponent component) - { - return GetMaxCount(component) - component.Count; - } - - /// - /// Tries to add one stack to another. May have some leftover count in the inserted entity. - /// - 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); - } - - /// - /// Tries to add one stack to another. May have some leftover count in the inserted entity. - /// - 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; - - if (insertStack.StackTypeId != targetStack.StackTypeId) - return false; - - var available = GetAvailableSpace(targetStack); - - if (available <= 0) - return false; - - var change = Math.Min(available, count); - - SetCount(targetEnt, targetStack.Count + change, targetStack); - SetCount(insertEnt, insertStack.Count - change, insertStack); - return true; - } - - private void OnStackStarted(EntityUid uid, StackComponent component, ComponentStartup args) - { - if (!TryComp(uid, out AppearanceComponent? appearance)) - return; - - Appearance.SetData(uid, StackVisuals.Actual, component.Count, appearance); - Appearance.SetData(uid, StackVisuals.MaxCount, GetMaxCount(component), appearance); - Appearance.SetData(uid, StackVisuals.Hide, false, appearance); - } - - private void OnStackGetState(EntityUid uid, StackComponent component, ref ComponentGetState args) - { - args.State = new StackComponentState(component.Count, component.MaxCountOverride); - } - - private void OnStackHandleState(EntityUid uid, StackComponent component, ref ComponentHandleState args) - { - if (args.Current is not StackComponentState cast) - return; - - component.MaxCountOverride = cast.MaxCount; - // This will change the count and call events. - SetCount(uid, cast.Count, component); - } - - private void OnStackExamined(EntityUid uid, StackComponent component, ExaminedEvent args) - { - if (!args.IsInDetailsRange) - return; - - args.PushMarkup( - Loc.GetString("comp-stack-examine-detail-count", - ("count", component.Count), - ("markupCountColor", "lightgray") - ) - ); - } - - private void OnBeforeEaten(Entity eaten, ref BeforeIngestedEvent args) - { - if (args.Cancelled) - return; - - if (args.Solution is not { } sol) - return; - - // 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; - - args.Cancelled = true; - } - - private void OnEaten(Entity eaten, ref IngestedEvent args) - { - 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 args) - { - if (!args.CanAccess || !args.CanInteract || args.Hands == null || stack.Count == 1) - return; - - AlternativeVerb halve = new() - { - Text = Loc.GetString("comp-stack-split-halve"), - Category = VerbCategory.Split, - Act = () => UserSplit(uid, args.User, stack.Count / 2, stack), - Priority = 1 - }; - args.Verbs.Add(halve); - - var priority = 0; - foreach (var amount in DefaultSplitAmounts) - { - if (amount >= stack.Count) - continue; - - AlternativeVerb verb = new() + if (GetAvailableSpace(recipientStack) == 0) { - 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 - }; + Popup.PopupClient(Loc.GetString("comp-stack-becomes-full"), + popupPos.Offset(new Vector2(0, -0.5f)), + args.User); + } - priority--; + break; - args.Verbs.Add(verb); - } + case 0 when GetAvailableSpace(recipientStack) == 0: + Popup.PopupClient(Loc.GetString("comp-stack-already-full"), popupPos, args.User); + break; } - /// - /// 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) - /// - protected virtual void UserSplit(EntityUid uid, EntityUid userUid, int amount, - StackComponent? stack = null, - TransformComponent? userTransform = null) - { + var localRotation = Transform(args.Used).LocalRotation; + _storage.PlayPickupAnimation(args.Used, popupPos, userCoords, localRotation, args.User); + } + private void OnStackStarted(Entity ent, ref ComponentStartup args) + { + if (!TryComp(ent.Owner, out AppearanceComponent? appearance)) + return; + + 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); + } + + private void OnStackGetState(Entity ent, ref ComponentGetState args) + { + args.State = new StackComponentState(ent.Comp.Count, ent.Comp.MaxCountOverride, ent.Comp.Unlimited); + } + + private void OnStackHandleState(Entity ent, ref ComponentHandleState args) + { + if (args.Current is not StackComponentState cast) + return; + + ent.Comp.MaxCountOverride = cast.MaxCountOverride; + ent.Comp.Unlimited = cast.Unlimited; + // This will change the count and call events. + SetCount(ent.AsNullable(), cast.Count); + } + + private void OnStackExamined(Entity 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 OnBeforeEaten(Entity eaten, ref BeforeIngestedEvent args) + { + if (args.Cancelled) + return; + + if (args.Solution is not { } sol) + return; + + // 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; + + args.Cancelled = true; + } + + private void OnEaten(Entity eaten, ref IngestedEvent args) + { + if (!TryUse(eaten.AsNullable(), 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(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands == null || ent.Comp.Count == 1) + return; + + var user = args.User; // Can't pass ref events into verbs + + AlternativeVerb halve = new() + { + 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 (amount >= ent.Comp.Count) + continue; + + AlternativeVerb verb = new() + { + Text = amount.ToString(), + Category = VerbCategory.Split, + Act = () => UserSplit(ent, user, amount), + // we want to sort by size, not alphabetically by the verb text. + Priority = priority + }; + + priority--; + + args.Verbs.Add(verb); } } - /// - /// Event raised when a stack's count has changed. - /// - public sealed class StackCountChangedEvent : EntityEventArgs + /// + /// 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) + /// + protected virtual void UserSplit(Entity stack, Entity user, int amount) { - /// - /// The old stack count. - /// - public int OldCount; - /// - /// The new stack count. - /// - public int NewCount; - - public StackCountChangedEvent(int oldCount, int newCount) - { - OldCount = oldCount; - NewCount = newCount; - } + } +} + +/// +/// Event raised when a stack's count has changed. +/// +public sealed class StackCountChangedEvent : EntityEventArgs +{ + /// + /// The old stack count. + /// + public int OldCount; + + /// + /// The new stack count. + /// + public int NewCount; + + public StackCountChangedEvent(int oldCount, int newCount) + { + OldCount = oldCount; + NewCount = newCount; } } diff --git a/Content.Shared/Stacks/StackComponent.cs b/Content.Shared/Stacks/StackComponent.cs index 453c8a737d..f604f99654 100644 --- a/Content.Shared/Stacks/StackComponent.cs +++ b/Content.Shared/Stacks/StackComponent.cs @@ -1,110 +1,118 @@ 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; + +/// +/// Component on an entity that represents a stack of identical things, usually materials. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedStackSystem))] +public sealed partial class StackComponent : Component { - [RegisterComponent, NetworkedComponent] - public sealed partial class StackComponent : Component + /// + /// What stack type we are. + /// + [DataField("stackType", required: true)] + public ProtoId StackTypeId = default!; + + /// + /// Current stack count. + /// Do NOT set this directly, use the method instead. + /// + [DataField] + public int Count = 30; + + /// + /// Max amount of things that can be in the stack. + /// Overrides the max defined on the stack prototype. + /// + [DataField] + public int? MaxCountOverride; + + /// + /// Set to true to not reduce the count when used. + /// + [DataField] + public bool Unlimited; + + /// + /// When throwing this item, do we want to only throw one part of the stack or the whole stack at once? + /// + [DataField] + public bool ThrowIndividually; + + /// + /// Used by StackStatusControl in client to update UI. + /// + [ViewVariables] + [Access(typeof(SharedStackSystem), Other = AccessPermissions.ReadWrite)] // Set by StackStatusControl + public bool UiUpdateNeeded { get; set; } + + /// + /// Default IconLayer stack. + /// + [DataField] + public string BaseLayer = ""; + + /// + /// Determines if the visualizer uses composite or non-composite layers for icons. Defaults to false. + /// + /// + /// + /// false: they are opaque and mutually exclusive (e.g. sprites in a cable coil). Default value + /// + /// + /// true: they are transparent and thus layered one over another in ascending order first + /// + /// + /// + /// + [DataField("composite")] + public bool IsComposite; + + /// + /// Sprite layers used in stack visualizer. Sprites first in layer correspond to lower stack states + /// e.g. _spriteLayers[0] is lower stack level than _spriteLayers[1]. + /// + [DataField] + public List LayerStates = new(); + + /// + /// An optional function to convert the amounts used to adjust a stack's appearance. + /// Useful for different denominations of cash, for example. + /// + [DataField] + public StackLayerFunction LayerFunction = StackLayerFunction.None; +} + +[Serializable, NetSerializable] +public sealed class StackComponentState : ComponentState +{ + public int Count { get; } + public int? MaxCountOverride { get; } + public bool Unlimited { get; } + + public StackComponentState(int count, int? maxCountOverride, bool unlimited) { - [ViewVariables(VVAccess.ReadWrite)] - [DataField("stackType", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] - public string StackTypeId { get; private set; } = default!; - - /// - /// Current stack count. - /// Do NOT set this directly, use the method instead. - /// - [DataField("count")] - public int Count { get; set; } = 30; - - /// - /// Max amount of things that can be in the stack. - /// Overrides the max defined on the stack prototype. - /// - [ViewVariables(VVAccess.ReadOnly)] - [DataField("maxCountOverride")] - public int? MaxCountOverride { get; set; } - - /// - /// Set to true to not reduce the count when used. - /// Note that still limits the amount that can be used at any one time. - /// - [DataField("unlimited")] - [ViewVariables(VVAccess.ReadOnly)] - public bool Unlimited { get; set; } - - [DataField("throwIndividually"), ViewVariables(VVAccess.ReadWrite)] - public bool ThrowIndividually { get; set; } = false; - - [ViewVariables] - public bool UiUpdateNeeded { get; set; } - - /// - /// Default IconLayer stack. - /// - [DataField("baseLayer")] - [ViewVariables(VVAccess.ReadWrite)] - public string BaseLayer = ""; - - /// - /// Determines if the visualizer uses composite or non-composite layers for icons. Defaults to false. - /// - /// - /// - /// false: they are opaque and mutually exclusive (e.g. sprites in a cable coil). Default value - /// - /// - /// true: they are transparent and thus layered one over another in ascending order first - /// - /// - /// - /// - [DataField("composite")] - [ViewVariables(VVAccess.ReadWrite)] - public bool IsComposite; - - /// - /// Sprite layers used in stack visualizer. Sprites first in layer correspond to lower stack states - /// e.g. _spriteLayers[0] is lower stack level than _spriteLayers[1]. - /// - [DataField("layerStates")] - [ViewVariables(VVAccess.ReadWrite)] - public List LayerStates = new(); - - /// - /// An optional function to convert the amounts used to adjust a stack's appearance. - /// Useful for different denominations of cash, for example. - /// - [DataField] - public StackLayerFunction LayerFunction = StackLayerFunction.None; - } - - [Serializable, NetSerializable] - public sealed class StackComponentState : ComponentState - { - public int Count { get; } - public int? MaxCount { get; } - - public StackComponentState(int count, int? maxCount) - { - Count = count; - MaxCount = maxCount; - } - } - - [Serializable, NetSerializable] - public enum StackLayerFunction : byte - { - // - // No operation performed. - // - None, - - // - // Arbitrarily thresholds the stack amount for each layer. - // Expects entity to have StackLayerThresholdComponent. - // - Threshold + Count = count; + MaxCountOverride = maxCountOverride; + Unlimited = unlimited; } } + +[Serializable, NetSerializable] +public enum StackLayerFunction : byte +{ + // + // No operation performed. + // + None, + + // + // Arbitrarily thresholds the stack amount for each layer. + // Expects entity to have StackLayerThresholdComponent. + // + Threshold +} diff --git a/Content.Shared/Stacks/StackPrototype.cs b/Content.Shared/Stacks/StackPrototype.cs index 5b95935ec4..dfc9d4a997 100644 --- a/Content.Shared/Stacks/StackPrototype.cs +++ b/Content.Shared/Stacks/StackPrototype.cs @@ -4,6 +4,9 @@ using Robust.Shared.Utility; namespace Content.Shared.Stacks; +/// +/// Prototype used to combine and spawn like-entities for . +/// [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; } /// - /// Human-readable name for this stack type e.g. "Steel" + /// Human-readable name for this stack type e.g. "Steel" /// /// This is a localization string ID. [DataField] public LocId Name { get; private set; } = string.Empty; /// - /// An icon that will be used to represent this stack type. + /// An icon that will be used to represent this stack type. /// [DataField] public SpriteSpecifier? Icon { get; private set; } /// - /// The entity id that will be spawned by default from this stack. + /// The entity id that will be spawned by default from this stack. /// [DataField(required: true)] - public EntProtoId Spawn { get; private set; } = string.Empty; + public EntProtoId Spawn { get; private set; } = string.Empty; /// - /// The maximum amount of things that can be in a stack. - /// Can be overriden on - /// if null, simply has unlimited max count. + /// The maximum amount of things that can be in a stack, can be overriden on . + /// If null, simply has unlimited max count. /// [DataField] public int? MaxCount { get; private set; } diff --git a/Content.Shared/Stacks/StackVisuals.cs b/Content.Shared/Stacks/StackVisuals.cs index 2f29b303fb..49f3e830d8 100644 --- a/Content.Shared/Stacks/StackVisuals.cs +++ b/Content.Shared/Stacks/StackVisuals.cs @@ -11,7 +11,7 @@ namespace Content.Shared.Stacks Actual, /// /// The total amount of elements in the stack. If unspecified, the visualizer assumes - /// its + /// it's StackComponent.LayerStates.Count /// MaxCount, Hide diff --git a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs index fcda7eb0ac..cda5eb5263 100644 --- a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs @@ -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 uid, string? stackType = null) + private bool HasSpaceInStacks(Entity uid, ProtoId? 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) diff --git a/Content.Shared/Store/CurrencyPrototype.cs b/Content.Shared/Store/CurrencyPrototype.cs index fdd113d3a2..2b7e3d807b 100644 --- a/Content.Shared/Store/CurrencyPrototype.cs +++ b/Content.Shared/Store/CurrencyPrototype.cs @@ -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. ///
- [DataField("displayName")] + [DataField] public string DisplayName { get; private set; } = string.Empty; /// /// The physical entity of the currency /// - [DataField("cash", customTypeSerializer: typeof(PrototypeIdValueDictionarySerializer))] - public Dictionary? Cash { get; private set; } + [DataField] + public Dictionary? Cash { get; private set; } /// /// Whether or not this currency can be withdrawn from a shop by a player. Requires a valid entityId. /// - [DataField("canWithdraw")] + [DataField] public bool CanWithdraw { get; private set; } = true; } diff --git a/Content.Shared/Tiles/FloorTileSystem.cs b/Content.Shared/Tiles/FloorTileSystem.cs index 298c0390ba..2c6df5ce89 100644 --- a/Content.Shared/Tiles/FloorTileSystem.cs +++ b/Content.Shared/Tiles/FloorTileSystem.cs @@ -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;