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;
/// <summary>
/// Used by hands in player UI to display the stack count.
/// </summary>
public sealed class StackStatusControl : Control
{
private readonly StackComponent _parent;

View File

@@ -1,4 +1,3 @@
using System.Linq;
using Content.Client.Items;
using Content.Client.Storage.Systems;
using Content.Shared.Stacks;
@@ -7,6 +6,7 @@ using Robust.Client.GameObjects;
namespace Content.Client.Stack
{
/// <inheritdoc />
[UsedImplicitly]
public sealed class StackSystem : SharedStackSystem
{
@@ -16,33 +16,21 @@ namespace Content.Client.Stack
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StackComponent, AppearanceChangeEvent>(OnAppearanceChange);
Subs.ItemStatus<StackComponent>(ent => new StackStatusControl(ent));
}
public override void SetCount(EntityUid uid, int amount, StackComponent? component = null)
#region Appearance
private void OnAppearanceChange(Entity<StackComponent> ent, ref AppearanceChangeEvent args)
{
if (!Resolve(uid, ref component))
return;
var (uid, comp) = ent;
base.SetCount(uid, amount, component);
// TODO PREDICT ENTITY DELETION: This should really just be a normal entity deletion call.
if (component.Count <= 0)
{
Xform.DetachEntity(uid, Transform(uid));
return;
}
component.UiUpdateNeeded = true;
}
private void OnAppearanceChange(EntityUid uid, StackComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null || comp.LayerStates.Count < 1)
return;
// Skip processing if no actual
// Skip processing if no elements in the stack
if (!_appearanceSystem.TryGetData<int>(uid, StackVisuals.Actual, out var actual, args.Component))
return;
@@ -56,9 +44,24 @@ namespace Content.Client.Stack
ApplyLayerFunction((uid, comp), ref actual, ref maxCount);
if (comp.IsComposite)
_counterSystem.ProcessCompositeSprite(uid, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite);
{
_counterSystem.ProcessCompositeSprite(uid,
actual,
maxCount,
comp.LayerStates,
hidden,
sprite: args.Sprite);
}
else
_counterSystem.ProcessOpaqueSprite(uid, comp.BaseLayer, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite);
{
_counterSystem.ProcessOpaqueSprite(uid,
comp.BaseLayer,
actual,
maxCount,
comp.LayerStates,
hidden,
sprite: args.Sprite);
}
}
/// <summary>
@@ -67,7 +70,7 @@ namespace Content.Client.Stack
/// <param name="ent">The entity considered.</param>
/// <param name="actual">The actual number of items in the stack. Altered depending on the function to run.</param>
/// <param name="maxCount">The maximum number of items in the stack. Altered depending on the function to run.</param>
/// <returns>Whether or not a function was applied.</returns>
/// <returns>True if a function was applied.</returns>
private bool ApplyLayerFunction(Entity<StackComponent> ent, ref int actual, ref int maxCount)
{
switch (ent.Comp.LayerFunction)
@@ -78,8 +81,10 @@ namespace Content.Client.Stack
ApplyThreshold(threshold, ref actual, ref maxCount);
return true;
}
break;
}
// No function applied.
return false;
}
@@ -105,7 +110,10 @@ namespace Content.Client.Stack
else
break;
}
actual = newActual;
}
#endregion
}
}

View File

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

View File

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

View File

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

View File

@@ -469,7 +469,8 @@ public sealed class MaterialArbitrageTest
await server.WaitPost(() =>
{
var ent = entManager.SpawnEntity(id, testMap.GridCoords);
stackSys.SetCount(ent, 1);
if (entManager.TryGetComponent<StackComponent>(ent, out var stackComp))
stackSys.SetCount((ent, stackComp), 1);
priceCache[id] = price = pricing.GetPrice(ent, false);
entManager.DeleteEntity(ent);
});

View File

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

View File

@@ -413,7 +413,7 @@ public sealed partial class AdminVerbSystem
// Unbounded intentionally.
_quickDialog.OpenDialog(player, Loc.GetString("admin-verbs-adjust-stack"), Loc.GetString("admin-verbs-dialog-adjust-stack-amount", ("max", _stackSystem.GetMaxCount(stack))), (int newAmount) =>
{
_stackSystem.SetCount(args.Target, newAmount, stack);
_stackSystem.SetCount((args.Target, stack), newAmount);
});
},
Impact = LogImpact.Medium,
@@ -429,7 +429,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill-stack.png")),
Act = () =>
{
_stackSystem.SetCount(args.Target, _stackSystem.GetMaxCount(stack), stack);
_stackSystem.SetCount((args.Target, stack), _stackSystem.GetMaxCount(stack));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-fill-stack-description"),

View File

@@ -56,7 +56,7 @@ public sealed partial class CargoSystem
if (args.Account == null)
{
var stackPrototype = _protoMan.Index(ent.Comp.CashType);
_stack.Spawn(args.Amount, stackPrototype, Transform(ent).Coordinates);
_stack.SpawnAtPosition(args.Amount, stackPrototype, Transform(ent).Coordinates);
if (!_emag.CheckFlag(ent, EmagType.Interaction))
{

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 (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)

View File

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

View File

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

View File

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

View File

@@ -187,7 +187,7 @@ namespace Content.Server.Construction
// TODO allow taking from several stacks.
// Also update crafting steps to check if it works.
var splitStack = _stackSystem.Split(entity, materialStep.Amount, user.ToCoordinates(0, 0), stack);
var splitStack = _stackSystem.Split((entity, stack), materialStep.Amount, user.ToCoordinates(0, 0));
if (splitStack == null)
continue;

View File

@@ -49,7 +49,7 @@ public sealed partial class ConstructionSystem
foreach (var (stackType, amount) in machineBoard.StackRequirements)
{
var stack = _stackSystem.Spawn(amount, stackType, xform.Coordinates);
var stack = _stackSystem.SpawnAtPosition(amount, stackType, xform.Coordinates);
if (!_container.Insert(stack, partContainer))
throw new Exception($"Couldn't insert machine material of type {stackType} to machine with prototype {Prototype(uid)?.ID ?? "N/A"}");
}

View File

@@ -182,7 +182,7 @@ public sealed class MachineFrameSystem : EntitySystem
return true;
}
var splitStack = _stack.Split(used, needed, Transform(uid).Coordinates, stack);
var splitStack = _stack.Split((used, stack), needed, Transform(uid).Coordinates);
if (splitStack == null)
return false;

View File

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

View File

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

View File

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

View File

@@ -160,7 +160,7 @@ namespace Content.Server.Hands.Systems
if (TryComp(throwEnt, out StackComponent? stack) && stack.Count > 1 && stack.ThrowIndividually)
{
var splitStack = _stackSystem.Split(throwEnt.Value, 1, Comp<TransformComponent>(player).Coordinates, stack);
var splitStack = _stackSystem.Split((throwEnt.Value, stack), 1, Comp<TransformComponent>(player).Coordinates);
if (splitStack is not {Valid: true})
return false;

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 (TryComp<StackComponent>(item, out var stackComp))
{
itemID = _prototype.Index<StackPrototype>(stackComp.StackTypeId).Spawn;
itemID = _prototype.Index(stackComp.StackTypeId).Spawn;
}
else
{
@@ -265,7 +265,7 @@ namespace Content.Server.Kitchen.EntitySystems
{
_container.Remove(item, component.Storage);
}
_stack.Use(item, 1, stackComp);
_stack.ReduceCount((item, stackComp), 1);
break;
}
else

View File

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

View File

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

View File

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

View File

@@ -49,7 +49,7 @@ public sealed partial class CableSystem
return;
}
if (TryComp<StackComponent>(placer, out var stack) && !_stack.Use(placer, 1, stack))
if (TryComp<StackComponent>(placer, out var stack) && !_stack.TryUse((placer.Owner, stack), 1))
return;
var newCable = Spawn(component.CablePrototypeId, _map.GridTileToLocal(gridUid, grid, snapPos));

View File

@@ -1,6 +1,5 @@
using Content.Shared.Popups;
using Content.Shared.Stacks;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
@@ -16,140 +15,238 @@ namespace Content.Server.Stack
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
}
public override void SetCount(EntityUid uid, int amount, StackComponent? component = null)
{
if (!Resolve(uid, ref component, false))
return;
base.SetCount(uid, amount, component);
// Queue delete stack if count reaches zero.
if (component.Count <= 0)
QueueDel(uid);
}
#region Spawning
/// <summary>
/// Try to split this stack into two. Returns a non-null <see cref="Robust.Shared.GameObjects.EntityUid"/> if successful.
/// Spawns a new entity and moves an amount to it from the stack.
/// Moves nothing if amount is greater than ent's stack count.
/// </summary>
public EntityUid? Split(EntityUid uid, int amount, EntityCoordinates spawnPosition, StackComponent? stack = null)
/// <param name="amount"> How much to move to the new entity. </param>
/// <returns>Null if StackComponent doesn't resolve, or amount to move is greater than ent has available.</returns>
[PublicAPI]
public EntityUid? Split(Entity<StackComponent?> ent, int amount, EntityCoordinates spawnPosition)
{
if (!Resolve(uid, ref stack))
if (!Resolve(ent.Owner, ref ent.Comp))
return null;
// Try to remove the amount of things we want to split from the original stack...
if (!Use(uid, amount, stack))
if (!TryUse(ent, amount))
return null;
// Get a prototype ID to spawn the new entity. Null is also valid, although it should rarely be picked...
var prototype = _prototypeManager.TryIndex<StackPrototype>(stack.StackTypeId, out var stackType)
? stackType.Spawn.ToString()
: Prototype(uid)?.ID;
if (!_prototypeManager.Resolve(ent.Comp.StackTypeId, out var stackType))
return null;
// Set the output parameter in the event instance to the newly split stack.
var entity = Spawn(prototype, spawnPosition);
var newEntity = SpawnAtPosition(stackType.Spawn, spawnPosition);
if (TryComp(entity, out StackComponent? stackComp))
{
// Set the split stack's count.
SetCount(entity, amount, stackComp);
// Don't let people dupe unlimited stacks
stackComp.Unlimited = false;
// There should always be a StackComponent
var stackComp = Comp<StackComponent>(newEntity);
SetCount((newEntity, stackComp), amount);
stackComp.Unlimited = false; // Don't let people dupe unlimited stacks
Dirty(newEntity, stackComp);
var ev = new StackSplitEvent(newEntity);
RaiseLocalEvent(ent, ref ev);
return newEntity;
}
var ev = new StackSplitEvent(entity);
RaiseLocalEvent(uid, ref ev);
#region SpawnAtPosition
/// <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;
}
/// <summary>
/// Spawns a stack of a certain stack type. See <see cref="StackPrototype"/>.
/// </summary>
public EntityUid Spawn(int amount, ProtoId<StackPrototype> id, EntityCoordinates spawnPosition)
/// <inheritdoc cref="SpawnAtPosition(int, StackPrototype, EntityCoordinates)"/>
[PublicAPI]
public EntityUid SpawnAtPosition(int count, ProtoId<StackPrototype> id, EntityCoordinates spawnPosition)
{
var proto = _prototypeManager.Index(id);
return Spawn(amount, 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;
return SpawnAtPosition(count, proto, spawnPosition);
}
/// <summary>
/// Say you want to spawn 97 units of something that has a max stack count of 30.
/// This would spawn 3 stacks of 30 and 1 stack of 7.
/// </summary>
public List<EntityUid> SpawnMultiple(string entityPrototype, int amount, EntityCoordinates spawnPosition)
/// <returns>The entities spawned.</returns>
/// <remarks> If the entity to spawn doesn't have stack component this will spawn a bunch of single items. </remarks>
private List<EntityUid> SpawnMultipleAtPosition(EntProtoId entityPrototype,
List<int> amounts,
EntityCoordinates spawnPosition)
{
if (amount <= 0)
if (amounts.Count <= 0)
{
Log.Error(
$"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}");
$"Attempted to spawn stacks of nothing: {entityPrototype}, {amounts}. Trace: {Environment.StackTrace}");
return new();
}
var spawns = CalculateSpawns(entityPrototype, amount);
var spawnedEnts = new List<EntityUid>();
foreach (var count in spawns)
foreach (var count in amounts)
{
var entity = SpawnAtPosition(entityPrototype, spawnPosition);
var entity = SpawnAtPosition(entityPrototype, spawnPosition); // The real SpawnAtPosition
spawnedEnts.Add(entity);
SetCount(entity, count);
if (TryComp<StackComponent>(entity, out var stackComp)) // prevent errors from the Resolve
SetCount((entity, stackComp), count);
}
return spawnedEnts;
}
/// <inheritdoc cref="SpawnMultiple(string,int,EntityCoordinates)"/>
public List<EntityUid> SpawnMultiple(string entityPrototype, int amount, EntityUid target)
/// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
[PublicAPI]
public List<EntityUid> SpawnMultipleAtPosition(EntProtoId entityPrototypeId,
int amount,
EntityCoordinates spawnPosition)
{
if (amount <= 0)
return SpawnMultipleAtPosition(entityPrototypeId,
CalculateSpawns(entityPrototypeId, amount),
spawnPosition);
}
/// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
[PublicAPI]
public List<EntityUid> SpawnMultipleAtPosition(EntityPrototype entityProto,
int amount,
EntityCoordinates spawnPosition)
{
return SpawnMultipleAtPosition(entityProto.ID,
CalculateSpawns(entityProto, amount),
spawnPosition);
}
/// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
[PublicAPI]
public List<EntityUid> SpawnMultipleAtPosition(StackPrototype stack,
int amount,
EntityCoordinates spawnPosition)
{
return SpawnMultipleAtPosition(stack.Spawn,
CalculateSpawns(stack, amount),
spawnPosition);
}
/// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
[PublicAPI]
public List<EntityUid> SpawnMultipleAtPosition(ProtoId<StackPrototype> stackId,
int amount,
EntityCoordinates spawnPosition)
{
var stackProto = _prototypeManager.Index(stackId);
return SpawnMultipleAtPosition(stackProto.Spawn,
CalculateSpawns(stackProto, amount),
spawnPosition);
}
#endregion
#region SpawnNextToOrDrop
/// <inheritdoc cref="SpawnAtPosition(int, StackPrototype, EntityCoordinates)"/>
[PublicAPI]
public EntityUid SpawnNextToOrDrop(int amount, StackPrototype prototype, EntityUid source)
{
var entity = SpawnNextToOrDrop(prototype.Spawn, source); // The real SpawnNextToOrDrop
SetCount((entity, null), amount);
return entity;
}
/// <inheritdoc cref="SpawnNextToOrDrop(int, StackPrototype, EntityUid)"/>
[PublicAPI]
public EntityUid SpawnNextToOrDrop(int amount, ProtoId<StackPrototype> id, EntityUid source)
{
var proto = _prototypeManager.Index(id);
return SpawnNextToOrDrop(amount, proto, source);
}
/// <inheritdoc cref="SpawnMultipleAtPosition(EntProtoId, List{int}, EntityCoordinates)"/>
private List<EntityUid> SpawnMultipleNextToOrDrop(EntProtoId entityPrototype,
List<int> amounts,
EntityUid target)
{
if (amounts.Count <= 0)
{
Log.Error(
$"Attempted to spawn an invalid stack: {entityPrototype}, {amount}. Trace: {Environment.StackTrace}");
$"Attempted to spawn stacks of nothing: {entityPrototype}, {amounts}. Trace: {Environment.StackTrace}");
return new();
}
var spawns = CalculateSpawns(entityPrototype, amount);
var spawnedEnts = new List<EntityUid>();
foreach (var count in spawns)
foreach (var count in amounts)
{
var entity = SpawnNextToOrDrop(entityPrototype, target);
var entity = SpawnNextToOrDrop(entityPrototype, target); // The real SpawnNextToOrDrop
spawnedEnts.Add(entity);
SetCount(entity, count);
if (TryComp<StackComponent>(entity, out var stackComp)) // prevent errors from the Resolve
SetCount((entity, stackComp), count);
}
return spawnedEnts;
}
/// <inheritdoc cref="SpawnMultipleNextToOrDrop(EntProtoId, List{int}, EntityUid)"/>
[PublicAPI]
public List<EntityUid> SpawnMultipleNextToOrDrop(EntProtoId stack,
int amount,
EntityUid target)
{
return SpawnMultipleNextToOrDrop(stack,
CalculateSpawns(stack, amount),
target);
}
/// <inheritdoc cref="SpawnMultipleNextToOrDrop(EntProtoId, List{int}, EntityUid)"/>
[PublicAPI]
public List<EntityUid> SpawnMultipleNextToOrDrop(EntityPrototype stack,
int amount,
EntityUid target)
{
return SpawnMultipleNextToOrDrop(stack.ID,
CalculateSpawns(stack, amount),
target);
}
/// <inheritdoc cref="SpawnMultipleNextToOrDrop(EntProtoId, List{int}, EntityUid)"/>
[PublicAPI]
public List<EntityUid> SpawnMultipleNextToOrDrop(StackPrototype stack,
int amount,
EntityUid target)
{
return SpawnMultipleNextToOrDrop(stack.Spawn,
CalculateSpawns(stack, amount),
target);
}
/// <inheritdoc cref="SpawnMultipleNextToOrDrop(EntProtoId, List{int}, EntityUid)"/>
[PublicAPI]
public List<EntityUid> SpawnMultipleNextToOrDrop(ProtoId<StackPrototype> stackId,
int amount,
EntityUid target)
{
var stackProto = _prototypeManager.Index(stackId);
return SpawnMultipleNextToOrDrop(stackProto.Spawn,
CalculateSpawns(stackProto, amount),
target);
}
#endregion
#region Calculate
/// <summary>
/// Calculates how many stacks to spawn that total up to <paramref name="amount"/>.
/// </summary>
/// <param name="entityPrototype">The stack to spawn.</param>
/// <param name="amount">The amount of pieces across all stacks.</param>
/// <returns>The list of stack counts per entity.</returns>
private List<int> CalculateSpawns(string entityPrototype, int amount)
private List<int> CalculateSpawns(int maxCountPerStack, int amount)
{
var proto = _prototypeManager.Index<EntityPrototype>(entityPrototype);
proto.TryGetComponent<StackComponent>(out var stack, EntityManager.ComponentFactory);
var maxCountPerStack = GetMaxCount(stack);
var amounts = new List<int>();
while (amount > 0)
{
@@ -161,28 +258,47 @@ namespace Content.Server.Stack
return amounts;
}
protected override void UserSplit(EntityUid uid, EntityUid userUid, int amount,
StackComponent? stack = null,
TransformComponent? userTransform = null)
/// <inheritdoc cref="CalculateSpawns(int, int)"/>
private List<int> CalculateSpawns(StackPrototype stackProto, int amount)
{
if (!Resolve(uid, ref stack))
return;
return CalculateSpawns(GetMaxCount(stackProto), amount);
}
if (!Resolve(userUid, ref userTransform))
/// <inheritdoc cref="CalculateSpawns(int, int)"/>
private List<int> CalculateSpawns(EntityPrototype entityPrototype, int amount)
{
return CalculateSpawns(GetMaxCount(entityPrototype), amount);
}
/// <inheritdoc cref="CalculateSpawns(int, int)"/>
private List<int> CalculateSpawns(EntProtoId entityId, int amount)
{
return CalculateSpawns(GetMaxCount(entityId), amount);
}
#endregion
#endregion
#region Event Handlers
/// <inheritdoc />
protected override void UserSplit(Entity<StackComponent> stack, Entity<TransformComponent?> user, int amount)
{
if (!Resolve(user.Owner, ref user.Comp, false))
return;
if (amount <= 0)
{
Popup.PopupCursor(Loc.GetString("comp-stack-split-too-small"), userUid, PopupType.Medium);
Popup.PopupCursor(Loc.GetString("comp-stack-split-too-small"), user.Owner, PopupType.Medium);
return;
}
if (Split(uid, amount, userTransform.Coordinates, stack) is not {} split)
if (Split(stack.AsNullable(), amount, user.Comp.Coordinates) is not { } split)
return;
Hands.PickupOrDrop(userUid, split);
Hands.PickupOrDrop(user.Owner, split);
Popup.PopupCursor(Loc.GetString("comp-stack-split"), userUid);
}
Popup.PopupCursor(Loc.GetString("comp-stack-split"), user.Owner);
}
#endregion
}
}

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem
if (_whitelistSystem.IsWhitelistPass(crusher.CrushingWhitelist, contained))
{
var amount = _random.Next(crusher.MinFragments, crusher.MaxFragments);
var stacks = _stack.SpawnMultiple(crusher.FragmentStackProtoId, amount, coords);
var stacks = _stack.SpawnMultipleAtPosition(crusher.FragmentStackProtoId, amount, coords);
foreach (var stack in stacks)
{
ContainerSystem.Insert((stack, null, null, null), crusher.OutputContainer);

View File

@@ -87,9 +87,9 @@ public sealed class HealingSystem : EntitySystem
var dontRepeat = false;
if (TryComp<StackComponent>(args.Used.Value, out var stackComp))
{
_stacks.Use(args.Used.Value, 1, stackComp);
_stacks.ReduceCount((args.Used.Value, stackComp), 1);
if (_stacks.GetCount(args.Used.Value, stackComp) <= 0)
if (_stacks.GetCount((args.Used.Value, stackComp)) <= 0)
dontRepeat = true;
}
else

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))
return;
if (!_stack.Use(args.Used.Value, 1))
if (!_stack.TryUse(args.Used.Value, 1))
{
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 Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Nutrition;
@@ -10,16 +9,18 @@ using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Shared.GameStates;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Stacks
{
namespace Content.Shared.Stacks;
// Partial for general system code and event handlers.
/// <summary>
/// System for handling entities which represent a stack of identical items, usually materials.
/// </summary>
[UsedImplicitly]
public abstract class SharedStackSystem : EntitySystem
public abstract partial class SharedStackSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IViewVariablesManager _vvm = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
@@ -29,18 +30,21 @@ namespace Content.Shared.Stacks
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] private readonly IGameTiming _timing = default!;
// TODO: These should be in the prototype.
public static readonly int[] DefaultSplitAmounts = { 1, 5, 10, 20, 30, 50 };
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StackComponent, InteractUsingEvent>(OnStackInteractUsing);
SubscribeLocalEvent<StackComponent, ComponentGetState>(OnStackGetState);
SubscribeLocalEvent<StackComponent, ComponentHandleState>(OnStackHandleState);
SubscribeLocalEvent<StackComponent, ComponentStartup>(OnStackStarted);
SubscribeLocalEvent<StackComponent, ExaminedEvent>(OnStackExamined);
SubscribeLocalEvent<StackComponent, InteractUsingEvent>(OnStackInteractUsing);
SubscribeLocalEvent<StackComponent, BeforeIngestedEvent>(OnBeforeEaten);
SubscribeLocalEvent<StackComponent, IngestedEvent>(OnEaten);
SubscribeLocalEvent<StackComponent, GetVerbsEvent<AlternativeVerb>>(OnStackAlternativeInteract);
@@ -57,26 +61,22 @@ namespace Content.Shared.Stacks
.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)
return;
if (!TryComp(args.Used, out StackComponent? recipientStack))
if (!TryComp<StackComponent>(args.Used, out var recipientStack))
return;
var localRotation = Transform(args.Used).LocalRotation;
if (!TryMergeStacks(uid, args.Used, out var transfered, stack, recipientStack))
return;
// Transfer stacks from ground to hand
if (!TryMergeStacks((ent.Owner, ent.Comp), (args.Used, recipientStack), out var transferred))
return; // if nothing transferred, leave without a pop-up
args.Handled = true;
// interaction is done, the rest is just generating a pop-up
if (!_gameTiming.IsFirstTimePredicted)
return;
var popupPos = args.ClickLocation;
var userCoords = Transform(args.User).Coordinates;
@@ -85,308 +85,63 @@ namespace Content.Shared.Stacks
popupPos = userCoords;
}
switch (transfered)
switch (transferred)
{
case > 0:
Popup.PopupCoordinates($"+{transfered}", popupPos, Filter.Local(), false);
Popup.PopupClient($"+{transferred}", popupPos, args.User);
if (GetAvailableSpace(recipientStack) == 0)
{
Popup.PopupCoordinates(Loc.GetString("comp-stack-becomes-full"),
popupPos.Offset(new Vector2(0, -0.5f)), Filter.Local(), false);
Popup.PopupClient(Loc.GetString("comp-stack-becomes-full"),
popupPos.Offset(new Vector2(0, -0.5f)),
args.User);
}
break;
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;
}
var localRotation = Transform(args.Used).LocalRotation;
_storage.PlayPickupAnimation(args.Used, popupPos, userCoords, localRotation, args.User);
}
private bool TryMergeStacks(
EntityUid donor,
EntityUid recipient,
out int transferred,
StackComponent? donorStack = null,
StackComponent? recipientStack = null)
private void OnStackStarted(Entity<StackComponent> ent, ref ComponentStartup args)
{
transferred = 0;
if (donor == recipient)
return false;
if (!Resolve(recipient, ref recipientStack, false) || !Resolve(donor, ref donorStack, false))
return false;
if (string.IsNullOrEmpty(recipientStack.StackTypeId) || !recipientStack.StackTypeId.Equals(donorStack.StackTypeId))
return false;
transferred = Math.Min(donorStack.Count, GetAvailableSpace(recipientStack));
SetCount(donor, donorStack.Count - transferred, donorStack);
SetCount(recipient, recipientStack.Count + transferred, recipientStack);
return transferred > 0;
}
/// <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))
if (!TryComp(ent.Owner, out AppearanceComponent? appearance))
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.
Hands.PickupOrDrop(user, item, handsComp: hands);
return;
args.State = new StackComponentState(ent.Comp.Count, ent.Comp.MaxCountOverride, ent.Comp.Unlimited);
}
// This is shit code until hands get fixed and give an easy way to enumerate over items, starting with the currently active item.
foreach (var held in Hands.EnumerateHeld((user, hands)))
{
TryMergeStacks(item, held, out _, donorStack: itemStack);
if (itemStack.Count == 0)
return;
}
Hands.PickupOrDrop(user, item, handsComp: hands);
}
public virtual void SetCount(EntityUid uid, int amount, StackComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
// Do nothing if amount is already the same.
if (amount == component.Count)
return;
// Store old value for event-raising purposes...
var old = component.Count;
// Clamp the value.
amount = Math.Min(amount, GetMaxCount(component));
amount = Math.Max(amount, 0);
// Server-side override deletes the entity if count == 0
component.Count = amount;
Dirty(uid, component);
Appearance.SetData(uid, StackVisuals.Actual, component.Count);
RaiseLocalEvent(uid, new StackCountChangedEvent(old, component.Count));
}
/// <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)
private void OnStackHandleState(Entity<StackComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not StackComponentState cast)
return;
component.MaxCountOverride = cast.MaxCount;
ent.Comp.MaxCountOverride = cast.MaxCountOverride;
ent.Comp.Unlimited = cast.Unlimited;
// 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)
return;
args.PushMarkup(
Loc.GetString("comp-stack-examine-detail-count",
("count", component.Count),
("count", ent.Comp.Count),
("markupCountColor", "lightgray")
)
);
@@ -423,7 +178,7 @@ namespace Content.Shared.Stacks
private void OnEaten(Entity<StackComponent> eaten, ref IngestedEvent args)
{
if (!Use(eaten, 1))
if (!TryUse(eaten.AsNullable(), 1))
return;
// 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;
}
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;
var user = args.User; // Can't pass ref events into verbs
AlternativeVerb halve = new()
{
Text = Loc.GetString("comp-stack-split-halve"),
Category = VerbCategory.Split,
Act = () => UserSplit(uid, args.User, stack.Count / 2, stack),
Act = () => UserSplit(ent, user, ent.Comp.Count / 2),
Priority = 1
};
args.Verbs.Add(halve);
@@ -454,14 +211,14 @@ namespace Content.Shared.Stacks
var priority = 0;
foreach (var amount in DefaultSplitAmounts)
{
if (amount >= stack.Count)
if (amount >= ent.Comp.Count)
continue;
AlternativeVerb verb = new()
{
Text = amount.ToString(),
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.
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.
/// When prediction is improved, those two methods should be moved to shared, in order to predict the splitting itself (not just the verbs)
/// </remarks>
protected virtual void UserSplit(EntityUid uid, EntityUid userUid, int amount,
StackComponent? stack = null,
TransformComponent? userTransform = null)
protected virtual void UserSplit(Entity<StackComponent> stack, Entity<TransformComponent?> user, int amount)
{
}
@@ -508,4 +263,3 @@ namespace Content.Shared.Stacks
NewCount = newCount;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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