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]
public abstract partial class SharedStackSystem : EntitySystem
{ {
[UsedImplicitly]
public abstract 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")
) )
); );
@@ -415,7 +170,7 @@ namespace Content.Shared.Stacks
The easiest and safest option is and always will be Option 1 otherwise we risk reagent deletion or duplication. 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. That is why we cancel if we cannot set the minimum to the entire volume of the solution.
*/ */
if(args.TryNewMinimum(sol.Volume)) if (args.TryNewMinimum(sol.Volume))
return; return;
args.Cancelled = true; args.Cancelled = true;
@@ -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,19 +236,17 @@ 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)
{ {
} }
} }
/// <summary> /// <summary>
/// Event raised when a stack's count has changed. /// Event raised when a stack's count has changed.
/// </summary> /// </summary>
public sealed class StackCountChangedEvent : EntityEventArgs public sealed class StackCountChangedEvent : EntityEventArgs
{ {
/// <summary> /// <summary>
/// The old stack count. /// The old stack count.
/// </summary> /// </summary>
@@ -507,5 +262,4 @@ namespace Content.Shared.Stacks
OldCount = oldCount; OldCount = oldCount;
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]
[Access(typeof(SharedStackSystem))]
public sealed partial class StackComponent : Component
{ {
[RegisterComponent, NetworkedComponent] /// <summary>
public sealed partial class StackComponent : Component /// What stack type we are.
{ /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("stackType", required: true)]
[DataField("stackType", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<StackPrototype>))] public ProtoId<StackPrototype> StackTypeId = default!;
public string StackTypeId { get; private set; } = 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>
@@ -78,24 +85,26 @@ namespace Content.Shared.Stacks
/// </summary> /// </summary>
[DataField] [DataField]
public StackLayerFunction LayerFunction = StackLayerFunction.None; public StackLayerFunction LayerFunction = StackLayerFunction.None;
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
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;
} }
}
[Serializable, NetSerializable] [Serializable, NetSerializable]
public enum StackLayerFunction : byte public enum StackLayerFunction : byte
{ {
// <summary> // <summary>
// No operation performed. // No operation performed.
// </summary> // </summary>
@@ -106,5 +115,4 @@ namespace Content.Shared.Stacks
// Expects entity to have StackLayerThresholdComponent. // Expects entity to have StackLayerThresholdComponent.
// </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;