Stack System Cleanup (#38872)

* 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>
This commit is contained in:
āda
2025-10-25 09:40:48 -05:00
committed by GitHub
parent 39aada2018
commit 8d8af1bab7
39 changed files with 935 additions and 753 deletions

View File

@@ -7,6 +7,9 @@ using Robust.Shared.Timing;
namespace Content.Client.Stack; namespace Content.Client.Stack;
/// <summary>
/// Used by hands in player UI to display the stack count.
/// </summary>
public sealed class StackStatusControl : Control public sealed class StackStatusControl : Control
{ {
private readonly StackComponent _parent; private readonly StackComponent _parent;

View File

@@ -1,4 +1,3 @@
using System.Linq;
using Content.Client.Items; using Content.Client.Items;
using Content.Client.Storage.Systems; using Content.Client.Storage.Systems;
using Content.Shared.Stacks; using Content.Shared.Stacks;
@@ -7,6 +6,7 @@ using Robust.Client.GameObjects;
namespace Content.Client.Stack namespace Content.Client.Stack
{ {
/// <inheritdoc />
[UsedImplicitly] [UsedImplicitly]
public sealed class StackSystem : SharedStackSystem public sealed class StackSystem : SharedStackSystem
{ {
@@ -16,33 +16,21 @@ namespace Content.Client.Stack
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<StackComponent, AppearanceChangeEvent>(OnAppearanceChange); SubscribeLocalEvent<StackComponent, AppearanceChangeEvent>(OnAppearanceChange);
Subs.ItemStatus<StackComponent>(ent => new StackStatusControl(ent)); Subs.ItemStatus<StackComponent>(ent => new StackStatusControl(ent));
} }
public override void SetCount(EntityUid uid, int amount, StackComponent? component = null) #region Appearance
private void OnAppearanceChange(Entity<StackComponent> ent, ref AppearanceChangeEvent args)
{ {
if (!Resolve(uid, ref component)) var (uid, comp) = ent;
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;
}
private void OnAppearanceChange(EntityUid uid, StackComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null || comp.LayerStates.Count < 1) if (args.Sprite == null || comp.LayerStates.Count < 1)
return; 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)) if (!_appearanceSystem.TryGetData<int>(uid, StackVisuals.Actual, out var actual, args.Component))
return; return;
@@ -56,9 +44,24 @@ namespace Content.Client.Stack
ApplyLayerFunction((uid, comp), ref actual, ref maxCount); ApplyLayerFunction((uid, comp), ref actual, ref maxCount);
if (comp.IsComposite) 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 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> /// <summary>
@@ -67,7 +70,7 @@ namespace Content.Client.Stack
/// <param name="ent">The entity considered.</param> /// <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="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> /// <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) private bool ApplyLayerFunction(Entity<StackComponent> ent, ref int actual, ref int maxCount)
{ {
switch (ent.Comp.LayerFunction) switch (ent.Comp.LayerFunction)
@@ -78,8 +81,10 @@ namespace Content.Client.Stack
ApplyThreshold(threshold, ref actual, ref maxCount); ApplyThreshold(threshold, ref actual, ref maxCount);
return true; return true;
} }
break; break;
} }
// No function applied. // No function applied.
return false; return false;
} }
@@ -105,7 +110,10 @@ namespace Content.Client.Stack
else else
break; break;
} }
actual = newActual; actual = newActual;
} }
#endregion
} }
} }

View File

@@ -215,13 +215,10 @@ public sealed class CargoTest
[TestPrototypes] [TestPrototypes]
private const string StackProto = @" private const string StackProto = @"
- type: entity
id: A
- type: stack - type: stack
id: StackProto id: StackProto
name: stack-steel name: stack-steel
spawn: A spawn: StackEnt
- type: entity - type: entity
id: StackEnt id: StackEnt

View File

@@ -95,8 +95,8 @@ public sealed class CraftingTests : InteractionTest
Assert.That(sys.IsEntityInContainer(shard), Is.True); Assert.That(sys.IsEntityInContainer(shard), Is.True);
Assert.That(sys.IsEntityInContainer(rods), Is.False); Assert.That(sys.IsEntityInContainer(rods), Is.False);
Assert.That(sys.IsEntityInContainer(wires), Is.False); Assert.That(sys.IsEntityInContainer(wires), Is.False);
Assert.That(rodStack, Has.Count.EqualTo(8)); Assert.That(rodStack.Count, Is.EqualTo(8));
Assert.That(wireStack, Has.Count.EqualTo(7)); Assert.That(wireStack.Count, Is.EqualTo(7));
await FindEntity(Spear, shouldSucceed: false); await FindEntity(Spear, shouldSucceed: false);

View File

@@ -93,7 +93,7 @@ public abstract partial class InteractionTest
await Server.WaitPost(() => await Server.WaitPost(() =>
{ {
uid = SEntMan.SpawnEntity(stackProto.Spawn, coords); uid = SEntMan.SpawnEntity(stackProto.Spawn, coords);
Stack.SetCount(uid, spec.Quantity); Stack.SetCount((uid, null), spec.Quantity);
}); });
return uid; return uid;
} }

View File

@@ -469,7 +469,8 @@ public sealed class MaterialArbitrageTest
await server.WaitPost(() => await server.WaitPost(() =>
{ {
var ent = entManager.SpawnEntity(id, testMap.GridCoords); 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); priceCache[id] = price = pricing.GetPrice(ent, false);
entManager.DeleteEntity(ent); entManager.DeleteEntity(ent);
}); });

View File

@@ -54,7 +54,7 @@ namespace Content.IntegrationTests.Tests.Materials
$"{proto.ID} material has no stack prototype"); $"{proto.ID} material has no stack prototype");
if (stackProto != null) if (stackProto != null)
Assert.That(proto.StackEntity, Is.EqualTo(stackProto.Spawn)); Assert.That(proto.StackEntity, Is.EqualTo(stackProto.Spawn.Id));
} }
}); });

View File

@@ -413,7 +413,7 @@ public sealed partial class AdminVerbSystem
// Unbounded intentionally. // 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) => _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, Impact = LogImpact.Medium,
@@ -429,7 +429,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill-stack.png")), Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill-stack.png")),
Act = () => Act = () =>
{ {
_stackSystem.SetCount(args.Target, _stackSystem.GetMaxCount(stack), stack); _stackSystem.SetCount((args.Target, stack), _stackSystem.GetMaxCount(stack));
}, },
Impact = LogImpact.Medium, Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-fill-stack-description"), Message = Loc.GetString("admin-trick-fill-stack-description"),

View File

@@ -56,7 +56,7 @@ public sealed partial class CargoSystem
if (args.Account == null) if (args.Account == null)
{ {
var stackPrototype = _protoMan.Index(ent.Comp.CashType); 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)) if (!_emag.CheckFlag(ent, EmagType.Interaction))
{ {

View 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 the clone is a stack as well, adjust the count of the copy
if (TryComp<StackComponent>(args.CloneUid, out var cloneStackComp)) 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) private void OnCloneItemLabel(Entity<LabelComponent> ent, ref CloningItemEvent args)

View File

@@ -27,14 +27,14 @@ public sealed partial class GivePrototype : IGraphAction
if (EntityPrototypeHelpers.HasComponent<StackComponent>(Prototype)) if (EntityPrototypeHelpers.HasComponent<StackComponent>(Prototype))
{ {
var stackSystem = entityManager.EntitySysManager.GetEntitySystem<StackSystem>(); 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)) if (userUid is null || !entityManager.TryGetComponent(userUid, out HandsComponent? handsComp))
return; return;
foreach (var item in stacks) foreach (var item in stacks)
{ {
stackSystem.TryMergeToHands(item, userUid.Value, hands: handsComp); stackSystem.TryMergeToHands(item, (userUid.Value, handsComp));
} }
} }
else else

View File

@@ -12,7 +12,7 @@ namespace Content.Server.Construction.Completions
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
{ {
entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount(uid, Amount); entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount((uid, null), Amount);
} }
} }
} }

View File

@@ -28,7 +28,7 @@ namespace Content.Server.Construction.Completions
{ {
var stackEnt = entityManager.SpawnEntity(Prototype, coordinates); var stackEnt = entityManager.SpawnEntity(Prototype, coordinates);
var stack = entityManager.GetComponent<StackComponent>(stackEnt); var stack = entityManager.GetComponent<StackComponent>(stackEnt);
entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount(stackEnt, Amount, stack); entityManager.EntitySysManager.GetEntitySystem<StackSystem>().SetCount((stackEnt, stack), Amount);
} }
else else
{ {

View File

@@ -187,7 +187,7 @@ namespace Content.Server.Construction
// TODO allow taking from several stacks. // TODO allow taking from several stacks.
// Also update crafting steps to check if it works. // 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) if (splitStack == null)
continue; continue;

View File

@@ -49,7 +49,7 @@ public sealed partial class ConstructionSystem
foreach (var (stackType, amount) in machineBoard.StackRequirements) 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)) 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"}"); throw new Exception($"Couldn't insert machine material of type {stackType} to machine with prototype {Prototype(uid)?.ID ?? "N/A"}");
} }

View File

@@ -182,7 +182,7 @@ public sealed class MachineFrameSystem : EntitySystem
return true; 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) if (splitStack == null)
return false; return false;

View File

@@ -43,7 +43,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.EntityManager.ComponentFactory)) if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.EntityManager.ComponentFactory))
{ {
var spawned = system.EntityManager.SpawnEntity(entityId, xform.Coordinates.Offset(system.Random.NextVector2(-Offset, Offset))); 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(); system.EntityManager.GetComponent<TransformComponent>(spawned).LocalRotation = system.Random.NextAngle();
} }
else else

View File

@@ -58,7 +58,7 @@ namespace Content.Server.Destructible.Thresholds.Behaviors
var spawned = SpawnInContainer var spawned = SpawnInContainer
? system.EntityManager.SpawnNextToOrDrop(entityId, owner) ? system.EntityManager.SpawnNextToOrDrop(entityId, owner)
: system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector())); : system.EntityManager.SpawnEntity(entityId, position.Offset(getRandomVector()));
system.StackSystem.SetCount(spawned, count); system.StackSystem.SetCount((spawned, null), count);
TransferForensics(spawned, system, owner); TransferForensics(spawned, system, owner);
} }

View File

@@ -63,8 +63,8 @@ namespace Content.Server.Engineering.EntitySystems
if (component.Deleted || !IsTileClear()) if (component.Deleted || !IsTileClear())
return; return;
if (TryComp(uid, out StackComponent? stackComp) if (TryComp<StackComponent>(uid, out var stackComp)
&& component.RemoveOnInteract && !_stackSystem.Use(uid, 1, stackComp)) && component.RemoveOnInteract && !_stackSystem.TryUse((uid, stackComp), 1))
{ {
return; return;
} }

View File

@@ -160,7 +160,7 @@ namespace Content.Server.Hands.Systems
if (TryComp(throwEnt, out StackComponent? stack) && stack.Count > 1 && stack.ThrowIndividually) 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}) if (splitStack is not {Valid: true})
return false; return false;

View 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 an entity has a stack component, use the stacktype instead of prototype id
if (TryComp<StackComponent>(item, out var stackComp)) if (TryComp<StackComponent>(item, out var stackComp))
{ {
itemID = _prototype.Index<StackPrototype>(stackComp.StackTypeId).Spawn; itemID = _prototype.Index(stackComp.StackTypeId).Spawn;
} }
else else
{ {
@@ -265,7 +265,7 @@ namespace Content.Server.Kitchen.EntitySystems
{ {
_container.Remove(item, component.Storage); _container.Remove(item, component.Storage);
} }
_stack.Use(item, 1, stackComp); _stack.ReduceCount((item, stackComp), 1);
break; break;
} }
else else

View File

@@ -118,7 +118,7 @@ namespace Content.Server.Kitchen.EntitySystems
scaledSolution.ScaleSolution(fitsCount); scaledSolution.ScaleSolution(fitsCount);
solution = scaledSolution; solution = scaledSolution;
_stackSystem.SetCount(item, stack.Count - fitsCount); // Setting to 0 will QueueDel _stackSystem.ReduceCount((item, stack), fitsCount); // Setting to 0 will QueueDel
} }
else else
{ {

View File

@@ -136,13 +136,13 @@ namespace Content.Server.Light.EntitySystems
component.StateExpiryTime = (float)component.RefuelMaterialTime.TotalSeconds; component.StateExpiryTime = (float)component.RefuelMaterialTime.TotalSeconds;
_nameModifier.RefreshNameModifiers(uid); _nameModifier.RefreshNameModifiers(uid);
_stackSystem.SetCount(args.Used, stack.Count - 1, stack); _stackSystem.ReduceCount((args.Used, stack), 1);
UpdateVisualizer((uid, component)); UpdateVisualizer((uid, component));
return; return;
} }
component.StateExpiryTime += (float)component.RefuelMaterialTime.TotalSeconds; component.StateExpiryTime += (float)component.RefuelMaterialTime.TotalSeconds;
_stackSystem.SetCount(args.Used, stack.Count - 1, stack); _stackSystem.ReduceCount((args.Used, stack), 1);
args.Handled = true; args.Handled = true;
} }

View File

@@ -73,7 +73,7 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
return; return;
var volumePerSheet = composition.MaterialComposition.FirstOrDefault(kvp => kvp.Key == msg.Material).Value; 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; volume = sheetsToExtract * volumePerSheet;
} }
@@ -183,7 +183,7 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
if (amountToSpawn == 0) if (amountToSpawn == 0)
return new List<EntityUid>(); return new List<EntityUid>();
return _stackSystem.SpawnMultiple(materialProto.StackEntity, amountToSpawn, coordinates); return _stackSystem.SpawnMultipleAtPosition(materialProto.StackEntity.Value, amountToSpawn, coordinates);
} }
/// <summary> /// <summary>

View File

@@ -49,7 +49,7 @@ public sealed partial class CableSystem
return; 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; return;
var newCable = Spawn(component.CablePrototypeId, _map.GridTileToLocal(gridUid, grid, snapPos)); var newCable = Spawn(component.CablePrototypeId, _map.GridTileToLocal(gridUid, grid, snapPos));

View File

@@ -1,6 +1,5 @@
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Stacks; using Content.Shared.Stacks;
using Content.Shared.Verbs;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -16,140 +15,238 @@ namespace Content.Server.Stack
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize() #region Spawning
{
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);
}
/// <summary> /// <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> /// </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; return null;
// Try to remove the amount of things we want to split from the original stack... // 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; return null;
// Get a prototype ID to spawn the new entity. Null is also valid, although it should rarely be picked... if (!_prototypeManager.Resolve(ent.Comp.StackTypeId, out var stackType))
var prototype = _prototypeManager.TryIndex<StackPrototype>(stack.StackTypeId, out var stackType) return null;
? stackType.Spawn.ToString()
: Prototype(uid)?.ID;
// Set the output parameter in the event instance to the newly split stack. // 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)) // There should always be a StackComponent
{ var stackComp = Comp<StackComponent>(newEntity);
// Set the split stack's count.
SetCount(entity, amount, stackComp); SetCount((newEntity, stackComp), amount);
// Don't let people dupe unlimited stacks stackComp.Unlimited = false; // Don't let people dupe unlimited stacks
stackComp.Unlimited = false; Dirty(newEntity, stackComp);
var ev = new StackSplitEvent(newEntity);
RaiseLocalEvent(ent, ref ev);
return newEntity;
} }
var ev = new StackSplitEvent(entity); #region SpawnAtPosition
RaiseLocalEvent(uid, ref ev);
/// <summary>
/// Spawns a stack of a certain stack type and sets its count. Won't set the stack over its max.
/// </summary>
/// <param name="count">The amount to set the spawned stack to.</param>
[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; return entity;
} }
/// <summary> /// <inheritdoc cref="SpawnAtPosition(int, StackPrototype, EntityCoordinates)"/>
/// Spawns a stack of a certain stack type. See <see cref="StackPrototype"/>. [PublicAPI]
/// </summary> public EntityUid SpawnAtPosition(int count, ProtoId<StackPrototype> id, EntityCoordinates spawnPosition)
public EntityUid Spawn(int amount, ProtoId<StackPrototype> id, EntityCoordinates spawnPosition)
{ {
var proto = _prototypeManager.Index(id); var proto = _prototypeManager.Index(id);
return Spawn(amount, proto, spawnPosition); return SpawnAtPosition(count, proto, spawnPosition);
}
/// <summary>
/// Spawns a stack of a certain stack type. See <see cref="StackPrototype"/>.
/// </summary>
public EntityUid Spawn(int amount, 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);
// And finally, set the correct amount!
SetCount(entity, amount, stack);
return entity;
} }
/// <summary> /// <summary>
/// Say you want to spawn 97 units of something that has a max stack count of 30. /// 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. /// This would spawn 3 stacks of 30 and 1 stack of 7.
/// </summary> /// </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( 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(); return new();
} }
var spawns = CalculateSpawns(entityPrototype, amount);
var spawnedEnts = new List<EntityUid>(); 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); spawnedEnts.Add(entity);
SetCount(entity, count); if (TryComp<StackComponent>(entity, out var stackComp)) // prevent errors from the Resolve
SetCount((entity, stackComp), count);
} }
return spawnedEnts; return spawnedEnts;
} }
/// <inheritdoc cref="SpawnMultiple(string,int,EntityCoordinates)"/> /// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
public List<EntityUid> SpawnMultiple(string entityPrototype, int amount, EntityUid target) [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( 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(); return new();
} }
var spawns = CalculateSpawns(entityPrototype, amount);
var spawnedEnts = new List<EntityUid>(); 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); spawnedEnts.Add(entity);
SetCount(entity, count); if (TryComp<StackComponent>(entity, out var stackComp)) // prevent errors from the Resolve
SetCount((entity, stackComp), count);
} }
return spawnedEnts; 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> /// <summary>
/// Calculates how many stacks to spawn that total up to <paramref name="amount"/>. /// Calculates how many stacks to spawn that total up to <paramref name="amount"/>.
/// </summary> /// </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> /// <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>(); var amounts = new List<int>();
while (amount > 0) while (amount > 0)
{ {
@@ -161,28 +258,47 @@ namespace Content.Server.Stack
return amounts; return amounts;
} }
protected override void UserSplit(EntityUid uid, EntityUid userUid, int amount, /// <inheritdoc cref="CalculateSpawns(int, int)"/>
StackComponent? stack = null, private List<int> CalculateSpawns(StackPrototype stackProto, int amount)
TransformComponent? userTransform = null)
{ {
if (!Resolve(uid, ref stack)) return CalculateSpawns(GetMaxCount(stackProto), amount);
return; }
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; return;
if (amount <= 0) 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; return;
} }
if (Split(uid, amount, userTransform.Coordinates, stack) is not {} split) if (Split(stack.AsNullable(), amount, user.Comp.Coordinates) is not { } split)
return; 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
} }
} }

View File

@@ -313,7 +313,7 @@ public sealed partial class StoreSystem
{ {
var cashId = proto.Cash[value]; var cashId = proto.Cash[value];
var amountToSpawn = (int) MathF.Floor((float) (amountRemaining / 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) if (ents.FirstOrDefault() is {} ent)
_hands.PickupOrDrop(buyer, ent); _hands.PickupOrDrop(buyer, ent);
amountRemaining -= value * amountToSpawn; amountRemaining -= value * amountToSpawn;

View File

@@ -153,7 +153,7 @@ public sealed partial class StoreSystem : EntitySystem
// same tick // same tick
currency.Comp.Price.Clear(); currency.Comp.Price.Clear();
if (stack != null) if (stack != null)
_stack.SetCount(currency.Owner, 0, stack); _stack.SetCount((currency.Owner, stack), 0);
QueueDel(currency); QueueDel(currency);
return true; return true;

View File

@@ -34,7 +34,7 @@ public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem
if (_whitelistSystem.IsWhitelistPass(crusher.CrushingWhitelist, contained)) if (_whitelistSystem.IsWhitelistPass(crusher.CrushingWhitelist, contained))
{ {
var amount = _random.Next(crusher.MinFragments, crusher.MaxFragments); 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) foreach (var stack in stacks)
{ {
ContainerSystem.Insert((stack, null, null, null), crusher.OutputContainer); ContainerSystem.Insert((stack, null, null, null), crusher.OutputContainer);

View File

@@ -87,9 +87,9 @@ public sealed class HealingSystem : EntitySystem
var dontRepeat = false; var dontRepeat = false;
if (TryComp<StackComponent>(args.Used.Value, out var stackComp)) 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; dontRepeat = true;
} }
else else

View File

@@ -92,7 +92,7 @@ public abstract partial class SharedFultonSystem : EntitySystem
if (args.Cancelled || args.Target == null || !TryComp<FultonComponent>(args.Used, out var fulton)) if (args.Cancelled || args.Target == null || !TryComp<FultonComponent>(args.Used, out var fulton))
return; return;
if (!_stack.Use(args.Used.Value, 1)) if (!_stack.TryUse(args.Used.Value, 1))
{ {
return; return;
} }

View File

@@ -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
}

View File

@@ -1,6 +1,5 @@
using System.Numerics; using System.Numerics;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems; using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Nutrition; using Content.Shared.Nutrition;
@@ -10,16 +9,18 @@ using Content.Shared.Verbs;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing; 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] [UsedImplicitly]
public abstract class SharedStackSystem : EntitySystem public abstract partial class SharedStackSystem : EntitySystem
{ {
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IViewVariablesManager _vvm = default!; [Dependency] private readonly IViewVariablesManager _vvm = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!; [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
@@ -29,18 +30,21 @@ namespace Content.Shared.Stacks
[Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly SharedStorageSystem _storage = 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 static readonly int[] DefaultSplitAmounts = { 1, 5, 10, 20, 30, 50 };
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<StackComponent, InteractUsingEvent>(OnStackInteractUsing);
SubscribeLocalEvent<StackComponent, ComponentGetState>(OnStackGetState); SubscribeLocalEvent<StackComponent, ComponentGetState>(OnStackGetState);
SubscribeLocalEvent<StackComponent, ComponentHandleState>(OnStackHandleState); SubscribeLocalEvent<StackComponent, ComponentHandleState>(OnStackHandleState);
SubscribeLocalEvent<StackComponent, ComponentStartup>(OnStackStarted); SubscribeLocalEvent<StackComponent, ComponentStartup>(OnStackStarted);
SubscribeLocalEvent<StackComponent, ExaminedEvent>(OnStackExamined); SubscribeLocalEvent<StackComponent, ExaminedEvent>(OnStackExamined);
SubscribeLocalEvent<StackComponent, InteractUsingEvent>(OnStackInteractUsing);
SubscribeLocalEvent<StackComponent, BeforeIngestedEvent>(OnBeforeEaten); SubscribeLocalEvent<StackComponent, BeforeIngestedEvent>(OnBeforeEaten);
SubscribeLocalEvent<StackComponent, IngestedEvent>(OnEaten); SubscribeLocalEvent<StackComponent, IngestedEvent>(OnEaten);
SubscribeLocalEvent<StackComponent, GetVerbsEvent<AlternativeVerb>>(OnStackAlternativeInteract); SubscribeLocalEvent<StackComponent, GetVerbsEvent<AlternativeVerb>>(OnStackAlternativeInteract);
@@ -57,26 +61,22 @@ namespace Content.Shared.Stacks
.RemovePath(nameof(StackComponent.Count)); .RemovePath(nameof(StackComponent.Count));
} }
private void OnStackInteractUsing(EntityUid uid, StackComponent stack, InteractUsingEvent args) private void OnStackInteractUsing(Entity<StackComponent> ent, ref InteractUsingEvent args)
{ {
if (args.Handled) if (args.Handled)
return; return;
if (!TryComp(args.Used, out StackComponent? recipientStack)) if (!TryComp<StackComponent>(args.Used, out var recipientStack))
return; return;
var localRotation = Transform(args.Used).LocalRotation; // Transfer stacks from ground to hand
if (!TryMergeStacks((ent.Owner, ent.Comp), (args.Used, recipientStack), out var transferred))
if (!TryMergeStacks(uid, args.Used, out var transfered, stack, recipientStack)) return; // if nothing transferred, leave without a pop-up
return;
args.Handled = true; args.Handled = true;
// interaction is done, the rest is just generating a pop-up // interaction is done, the rest is just generating a pop-up
if (!_gameTiming.IsFirstTimePredicted)
return;
var popupPos = args.ClickLocation; var popupPos = args.ClickLocation;
var userCoords = Transform(args.User).Coordinates; var userCoords = Transform(args.User).Coordinates;
@@ -85,308 +85,63 @@ namespace Content.Shared.Stacks
popupPos = userCoords; popupPos = userCoords;
} }
switch (transfered) switch (transferred)
{ {
case > 0: case > 0:
Popup.PopupCoordinates($"+{transfered}", popupPos, Filter.Local(), false); Popup.PopupClient($"+{transferred}", popupPos, args.User);
if (GetAvailableSpace(recipientStack) == 0) if (GetAvailableSpace(recipientStack) == 0)
{ {
Popup.PopupCoordinates(Loc.GetString("comp-stack-becomes-full"), Popup.PopupClient(Loc.GetString("comp-stack-becomes-full"),
popupPos.Offset(new Vector2(0, -0.5f)), Filter.Local(), false); popupPos.Offset(new Vector2(0, -0.5f)),
args.User);
} }
break; break;
case 0 when GetAvailableSpace(recipientStack) == 0: case 0 when GetAvailableSpace(recipientStack) == 0:
Popup.PopupCoordinates(Loc.GetString("comp-stack-already-full"), popupPos, Filter.Local(), false); Popup.PopupClient(Loc.GetString("comp-stack-already-full"), popupPos, args.User);
break; break;
} }
var localRotation = Transform(args.Used).LocalRotation;
_storage.PlayPickupAnimation(args.Used, popupPos, userCoords, localRotation, args.User); _storage.PlayPickupAnimation(args.Used, popupPos, userCoords, localRotation, args.User);
} }
private bool TryMergeStacks( private void OnStackStarted(Entity<StackComponent> ent, ref ComponentStartup args)
EntityUid donor,
EntityUid recipient,
out int transferred,
StackComponent? donorStack = null,
StackComponent? recipientStack = null)
{ {
transferred = 0; if (!TryComp(ent.Owner, out AppearanceComponent? appearance))
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;
}
/// <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; return;
if (!Resolve(item, ref itemStack, 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);
}
private void OnStackGetState(Entity<StackComponent> ent, ref ComponentGetState args)
{ {
// This isn't even a stack. Just try to pickup as normal. args.State = new StackComponentState(ent.Comp.Count, ent.Comp.MaxCountOverride, ent.Comp.Unlimited);
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. private void OnStackHandleState(Entity<StackComponent> ent, ref ComponentHandleState args)
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));
}
/// <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;
// 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;
}
/// <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;
var map = xform.MapID;
var bounds = _physics.GetWorldAABB(uid);
var intersecting = new HashSet<Entity<StackComponent>>();
_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;
}
/// <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;
}
/// <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);
}
/// <summary>
/// Gets the max count for a given entity
/// </summary>
/// <param name="uid"></param>
/// <returns></returns>
[PublicAPI]
public int GetMaxCount(EntityUid uid)
{
return GetMaxCount(CompOrNull<StackComponent>(uid));
}
/// <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)
{
if (component == null)
return 1;
if (component.MaxCountOverride != null)
return component.MaxCountOverride.Value;
if (string.IsNullOrEmpty(component.StackTypeId))
return 1;
var stackProto = _prototype.Index<StackPrototype>(component.StackTypeId);
return stackProto.MaxCount ?? int.MaxValue;
}
/// <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;
}
/// <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);
}
/// <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;
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) if (args.Current is not StackComponentState cast)
return; return;
component.MaxCountOverride = cast.MaxCount; ent.Comp.MaxCountOverride = cast.MaxCountOverride;
ent.Comp.Unlimited = cast.Unlimited;
// This will change the count and call events. // This will change the count and call events.
SetCount(uid, cast.Count, component); SetCount(ent.AsNullable(), cast.Count);
} }
private void OnStackExamined(EntityUid uid, StackComponent component, ExaminedEvent args) private void OnStackExamined(Entity<StackComponent> ent, ref ExaminedEvent args)
{ {
if (!args.IsInDetailsRange) if (!args.IsInDetailsRange)
return; return;
args.PushMarkup( args.PushMarkup(
Loc.GetString("comp-stack-examine-detail-count", Loc.GetString("comp-stack-examine-detail-count",
("count", component.Count), ("count", ent.Comp.Count),
("markupCountColor", "lightgray") ("markupCountColor", "lightgray")
) )
); );
@@ -423,7 +178,7 @@ namespace Content.Shared.Stacks
private void OnEaten(Entity<StackComponent> eaten, ref IngestedEvent args) private void OnEaten(Entity<StackComponent> eaten, ref IngestedEvent args)
{ {
if (!Use(eaten, 1)) if (!TryUse(eaten.AsNullable(), 1))
return; return;
// We haven't eaten the whole stack yet or are unable to eat it completely. // We haven't eaten the whole stack yet or are unable to eat it completely.
@@ -437,16 +192,18 @@ namespace Content.Shared.Stacks
args.Destroy = true; args.Destroy = true;
} }
private void OnStackAlternativeInteract(EntityUid uid, StackComponent stack, GetVerbsEvent<AlternativeVerb> args) private void OnStackAlternativeInteract(Entity<StackComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{ {
if (!args.CanAccess || !args.CanInteract || args.Hands == null || stack.Count == 1) if (!args.CanAccess || !args.CanInteract || args.Hands == null || ent.Comp.Count == 1)
return; return;
var user = args.User; // Can't pass ref events into verbs
AlternativeVerb halve = new() AlternativeVerb halve = new()
{ {
Text = Loc.GetString("comp-stack-split-halve"), Text = Loc.GetString("comp-stack-split-halve"),
Category = VerbCategory.Split, Category = VerbCategory.Split,
Act = () => UserSplit(uid, args.User, stack.Count / 2, stack), Act = () => UserSplit(ent, user, ent.Comp.Count / 2),
Priority = 1 Priority = 1
}; };
args.Verbs.Add(halve); args.Verbs.Add(halve);
@@ -454,14 +211,14 @@ namespace Content.Shared.Stacks
var priority = 0; var priority = 0;
foreach (var amount in DefaultSplitAmounts) foreach (var amount in DefaultSplitAmounts)
{ {
if (amount >= stack.Count) if (amount >= ent.Comp.Count)
continue; continue;
AlternativeVerb verb = new() AlternativeVerb verb = new()
{ {
Text = amount.ToString(), Text = amount.ToString(),
Category = VerbCategory.Split, Category = VerbCategory.Split,
Act = () => UserSplit(uid, args.User, amount, stack), Act = () => UserSplit(ent, user, amount),
// we want to sort by size, not alphabetically by the verb text. // we want to sort by size, not alphabetically by the verb text.
Priority = priority Priority = priority
}; };
@@ -479,9 +236,7 @@ namespace Content.Shared.Stacks
/// This empty virtual method allows for UserSplit() to be called on the server from the client. /// 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) /// When prediction is improved, those two methods should be moved to shared, in order to predict the splitting itself (not just the verbs)
/// </remarks> /// </remarks>
protected virtual void UserSplit(EntityUid uid, EntityUid userUid, int amount, protected virtual void UserSplit(Entity<StackComponent> stack, Entity<TransformComponent?> user, int amount)
StackComponent? stack = null,
TransformComponent? userTransform = null)
{ {
} }
@@ -508,4 +263,3 @@ namespace Content.Shared.Stacks
NewCount = newCount; NewCount = newCount;
} }
} }
}

View File

@@ -1,50 +1,59 @@
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; 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] [RegisterComponent, NetworkedComponent]
[Access(typeof(SharedStackSystem))]
public sealed partial class StackComponent : Component public sealed partial class StackComponent : Component
{ {
[ViewVariables(VVAccess.ReadWrite)] /// <summary>
[DataField("stackType", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<StackPrototype>))] /// What stack type we are.
public string StackTypeId { get; private set; } = default!; /// </summary>
[DataField("stackType", required: true)]
public ProtoId<StackPrototype> StackTypeId = default!;
/// <summary> /// <summary>
/// Current stack count. /// Current stack count.
/// Do NOT set this directly, use the <see cref="SharedStackSystem.SetCount"/> method instead. /// Do NOT set this directly, use the <see cref="SharedStackSystem.SetCount"/> method instead.
/// </summary> /// </summary>
[DataField("count")] [DataField]
public int Count { get; set; } = 30; public int Count = 30;
/// <summary> /// <summary>
/// Max amount of things that can be in the stack. /// Max amount of things that can be in the stack.
/// Overrides the max defined on the stack prototype. /// Overrides the max defined on the stack prototype.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadOnly)] [DataField]
[DataField("maxCountOverride")] public int? MaxCountOverride;
public int? MaxCountOverride { get; set; }
/// <summary> /// <summary>
/// Set to true to not reduce the count when used. /// 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> /// </summary>
[DataField("unlimited")] [DataField]
[ViewVariables(VVAccess.ReadOnly)] public bool Unlimited;
public bool Unlimited { get; set; }
[DataField("throwIndividually"), ViewVariables(VVAccess.ReadWrite)] /// <summary>
public bool ThrowIndividually { get; set; } = false; /// 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;
/// <summary>
/// Used by StackStatusControl in client to update UI.
/// </summary>
[ViewVariables] [ViewVariables]
[Access(typeof(SharedStackSystem), Other = AccessPermissions.ReadWrite)] // Set by StackStatusControl
public bool UiUpdateNeeded { get; set; } public bool UiUpdateNeeded { get; set; }
/// <summary> /// <summary>
/// Default IconLayer stack. /// Default IconLayer stack.
/// </summary> /// </summary>
[DataField("baseLayer")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public string BaseLayer = ""; public string BaseLayer = "";
/// <summary> /// <summary>
@@ -61,15 +70,13 @@ namespace Content.Shared.Stacks
/// ///
/// </summary> /// </summary>
[DataField("composite")] [DataField("composite")]
[ViewVariables(VVAccess.ReadWrite)]
public bool IsComposite; public bool IsComposite;
/// <summary> /// <summary>
/// Sprite layers used in stack visualizer. Sprites first in layer correspond to lower stack states /// 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>. /// e.g. <code>_spriteLayers[0]</code> is lower stack level than <code>_spriteLayers[1]</code>.
/// </summary> /// </summary>
[DataField("layerStates")] [DataField]
[ViewVariables(VVAccess.ReadWrite)]
public List<string> LayerStates = new(); public List<string> LayerStates = new();
/// <summary> /// <summary>
@@ -84,12 +91,14 @@ namespace Content.Shared.Stacks
public sealed class StackComponentState : ComponentState public sealed class StackComponentState : ComponentState
{ {
public int Count { get; } public int Count { get; }
public int? MaxCount { get; } public int? MaxCountOverride { get; }
public bool Unlimited { get; }
public StackComponentState(int count, int? maxCount) public StackComponentState(int count, int? maxCountOverride, bool unlimited)
{ {
Count = count; Count = count;
MaxCount = maxCount; MaxCountOverride = maxCountOverride;
Unlimited = unlimited;
} }
} }
@@ -107,4 +116,3 @@ namespace Content.Shared.Stacks
// </summary> // </summary>
Threshold Threshold
} }
}

View File

@@ -4,6 +4,9 @@ using Robust.Shared.Utility;
namespace Content.Shared.Stacks; namespace Content.Shared.Stacks;
/// <summary>
/// Prototype used to combine and spawn like-entities for <see cref="SharedStackSystem"/>.
/// </summary>
[Prototype] [Prototype]
public sealed partial class StackPrototype : IPrototype, IInheritingPrototype public sealed partial class StackPrototype : IPrototype, IInheritingPrototype
{ {
@@ -37,12 +40,11 @@ public sealed partial class StackPrototype : IPrototype, IInheritingPrototype
/// 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> /// </summary>
[DataField(required: true)] [DataField(required: true)]
public EntProtoId Spawn { get; private set; } = string.Empty; public EntProtoId<StackComponent> Spawn { get; private set; } = string.Empty;
/// <summary> /// <summary>
/// The maximum amount of things that can be in a stack. /// The maximum amount of things that can be in a stack, can be overriden on <see cref="StackComponent"/>.
/// Can be overriden on <see cref="StackComponent"/> /// If null, simply has unlimited max count.
/// if null, simply has unlimited max count.
/// </summary> /// </summary>
[DataField] [DataField]
public int? MaxCount { get; private set; } public int? MaxCount { get; private set; }

View File

@@ -11,7 +11,7 @@ namespace Content.Shared.Stacks
Actual, Actual,
/// <summary> /// <summary>
/// The total amount of elements in the stack. If unspecified, the visualizer assumes /// The total amount of elements in the stack. If unspecified, the visualizer assumes
/// its /// it's StackComponent.LayerStates.Count
/// </summary> /// </summary>
MaxCount, MaxCount,
Hide Hide

View File

@@ -1219,7 +1219,7 @@ public abstract class SharedStorageSystem : EntitySystem
if (!_stackQuery.TryGetComponent(ent, out var containedStack)) if (!_stackQuery.TryGetComponent(ent, out var containedStack))
continue; continue;
if (!_stack.TryAdd(insertEnt, ent, insertStack, containedStack)) if (!_stack.TryMergeStacks((insertEnt, insertStack), (ent, containedStack), out var _))
continue; continue;
stackedEntity = ent; stackedEntity = ent;
@@ -1773,7 +1773,7 @@ public abstract class SharedStorageSystem : EntitySystem
return GetCumulativeItemAreas(uid) < uid.Comp.Grid.GetArea() || HasSpaceInStacks(uid); 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)) if (!Resolve(uid, ref uid.Comp))
return false; return false;
@@ -1783,7 +1783,7 @@ public abstract class SharedStorageSystem : EntitySystem
if (!_stackQuery.TryGetComponent(contained, out var stack)) if (!_stackQuery.TryGetComponent(contained, out var stack))
continue; continue;
if (stackType != null && !stack.StackTypeId.Equals(stackType)) if (stackType != null && stack.StackTypeId != stackType)
continue; continue;
if (_stack.GetAvailableSpace(stack) == 0) if (_stack.GetAvailableSpace(stack) == 0)

View File

@@ -23,18 +23,18 @@ public sealed partial class CurrencyPrototype : IPrototype
/// doesn't necessarily refer to the full name of the currency, only /// doesn't necessarily refer to the full name of the currency, only
/// that which is displayed to the user. /// that which is displayed to the user.
/// </summary> /// </summary>
[DataField("displayName")] [DataField]
public string DisplayName { get; private set; } = string.Empty; public string DisplayName { get; private set; } = string.Empty;
/// <summary> /// <summary>
/// The physical entity of the currency /// The physical entity of the currency
/// </summary> /// </summary>
[DataField("cash", customTypeSerializer: typeof(PrototypeIdValueDictionarySerializer<FixedPoint2, EntityPrototype>))] [DataField]
public Dictionary<FixedPoint2, string>? Cash { get; private set; } public Dictionary<FixedPoint2, EntProtoId>? Cash { get; private set; }
/// <summary> /// <summary>
/// Whether or not this currency can be withdrawn from a shop by a player. Requires a valid entityId. /// Whether or not this currency can be withdrawn from a shop by a player. Requires a valid entityId.
/// </summary> /// </summary>
[DataField("canWithdraw")] [DataField]
public bool CanWithdraw { get; private set; } = true; public bool CanWithdraw { get; private set; } = true;
} }

View File

@@ -144,7 +144,7 @@ public sealed class FloorTileSystem : EntitySystem
if (HasBaseTurf(currentTileDefinition, baseTurf.ID)) if (HasBaseTurf(currentTileDefinition, baseTurf.ID))
{ {
if (!_stackSystem.Use(uid, 1, stack)) if (!_stackSystem.TryUse((uid, stack), 1))
continue; continue;
PlaceAt(args.User, gridUid, mapGrid, location, currentTileDefinition.TileId, component.PlaceTileSound); 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)) else if (HasBaseTurf(currentTileDefinition, ContentTileDefinition.SpaceID))
{ {
if (!_stackSystem.Use(uid, 1, stack)) if (!_stackSystem.TryUse((uid, stack), 1))
continue; continue;
args.Handled = true; args.Handled = true;