using Content.Shared.Hands.Components; using JetBrains.Annotations; using Robust.Shared.Map; using Robust.Shared.Prototypes; namespace Content.Shared.Stacks; // Partial for public API functions. public abstract partial class SharedStackSystem { #region Spawning // Interactions with spawned entities can not currently be predicted. // This means that when spawning a stack it should not be given directly to the player, but have some intermediary. /// /// Gets or spawns an entity with a stack count of 1. /// Useful when you don't know if something is a stack, and want to make sure you just have a single entity. /// /// An entity to pop one count off the stack. /// An entity with a stack count of 1, or a non-stack. [PublicAPI] public EntityUid GetOne(Entity stackEnt) { if (!Resolve(stackEnt.Owner, ref stackEnt.Comp, logMissing: false) // If it's not a stack, you already have the one || stackEnt.Comp.Count == 1) // If it's at one, just use this return stackEnt.Owner; ReduceCount(stackEnt, 1); var stackId = ProtoMan.Index(stackEnt.Comp.StackTypeId); var entityUid = PredictedSpawnNextToOrDrop(stackId.Spawn, stackEnt.Owner); SetCount((entityUid, null), 1); return entityUid; } #endregion #region Merge Stacks /// /// Merges together the counts from a set of stacks into the smallest number of entities. /// /// Entities to merge. Returns stack entities with non-zero count. [PublicAPI] public void MergeStacks(ref HashSet stacks) { // Filter out the non-stacks and separate them by their stack types var stacksByType = new Dictionary, List>>(); foreach (var uid in stacks) { if (!_stackQuery.TryComp(uid, out var stackComponent)) continue; if (stacksByType.TryGetValue(stackComponent.StackTypeId, out var list)) list.Add((uid, stackComponent)); else { list = new List>(); list.Add((uid, stackComponent)); stacksByType[stackComponent.StackTypeId] = list; } } stacks.Clear(); // Set the count foreach (var (type, stackList) in stacksByType) { var count = GetCount(stackList, type); foreach (var stack in stackList) { // We've already moved all our stacks, so we clear the count of the remaining ones. if (count == 0) { SetCount(stack.AsNullable(), count); continue; } var amount = Math.Min(count, GetMaxCount(stack.Comp)); SetCount(stack.AsNullable(), amount); count -= amount; stacks.Add(stack); } } } /// /// This will find all the stacks in an area and merge them together. /// /// /// Useful for when you're spawning an unknown number of stacks like from an entity table /// and want to combine them together at the end. /// /// Stack entities with non-zero counts. [PublicAPI] public HashSet MergeStacksAtPosition(MapCoordinates pos, float range = 0.5f, LookupFlags flags = EntityLookupSystem.DefaultFlags) { var entities = _entityLookup.GetEntitiesInRange(pos, range, flags); MergeStacks(ref entities); return entities; } /// /// Moves as much stack count as we can from the donor to the recipient. /// Deletes the donor if count goes to 0. /// /// Entity which will give the amount from its stack /// Entity which will receive the amount to its stack /// How much stack count was actually moved. /// Limits amount of stack count to move from the donor. Will not always be the actual amount moved /// True if transferred is greater than 0. [PublicAPI] public bool TryMergeStacks( Entity donor, Entity recipient, out int transferred, int? amount = null ) { transferred = 0; if (donor.Owner == recipient.Owner) return false; // Recipient is being torn down, don't give it anything. if (TerminatingOrDeleted(recipient) || EntityManager.IsQueuedForDeletion(recipient)) return false; // Check they're stacks of the same type if (!_stackQuery.Resolve(recipient, ref recipient.Comp, false) || !_stackQuery.Resolve(donor, ref donor.Comp, false) || 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); var ev = new StackMergeEvent(donor, recipient, transferred); RaiseLocalEvent(recipient, ref ev, true); SetCount(donor, donor.Comp.Count - transferred); SetCount(recipient, recipient.Comp.Count + transferred); return true; } /// /// If the given item is a stack, this attempts to find a matching stack in the users hand and merge with that. /// /// /// If the interaction fails to fully merge the stack, or if this is just not a stack, it will instead try /// to place it in the user's hand normally. /// [PublicAPI] public void TryMergeToHands(Entity item, Entity user) { if (!Resolve(user.Owner, ref user.Comp, false)) return; if (!_stackQuery.Resolve(item.Owner, ref item.Comp, false)) { // This isn't even a stack. Just try to pickup as normal. _hands.PickupOrDrop(user.Owner, item.Owner, handsComp: user.Comp); return; } foreach (var held in _hands.EnumerateHeld(user)) { TryMergeStacks(item, held, out _); if (item.Comp.Count == 0) return; } _hands.PickupOrDrop(user.Owner, item.Owner, handsComp: user.Comp); } /// /// Donor entity merges stack count into contacting entities. /// Deletes the donor if count goes to 0. /// /// True if donor moved any count to contacts. [PublicAPI] public bool TryMergeToContacts(Entity donor) { var (uid, stack, xform) = donor; // sue me if (!Resolve(uid, ref stack, ref xform, false)) return false; var map = xform.MapID; var bounds = _physics.GetWorldAABB(uid); var intersecting = new HashSet>(); // Should we reuse a HashSet instead of making a new one? _entityLookup.GetEntitiesIntersecting(map, bounds, intersecting, LookupFlags.Dynamic | LookupFlags.Sundries); return TryMergeToStacks((uid, stack), intersecting); } /// /// Moves the count from the donor into the collection of entities. /// /// True if anything moved. [PublicAPI] public bool TryMergeToStacks(Entity donor, HashSet> stacks) { if (!_stackQuery.Resolve(donor.Owner, ref donor.Comp, false)) return false; var count = GetCount(donor); foreach (var stack in stacks) { if (stack.Comp.StackTypeId != donor.Comp.StackTypeId) continue; TryMergeStacks(donor, stack.AsNullable(), out var transferred); count -= transferred; if (count == 0) break; } return true; } #endregion #region Setters /// /// Sets a stack count to an amount. Server will delete ent if count is 0. /// Clamps between zero and the stack's max size. /// /// All setter functions should end up here. public void SetCount(Entity ent, int amount) { if (!_stackQuery.Resolve(ent.Owner, ref ent.Comp)) return; // Do nothing if amount is already the same. if (amount == ent.Comp.Count) return; // Store old value for event-raising purposes... var old = ent.Comp.Count; // Clamp the value. amount = Math.Min(amount, GetMaxCount(ent.Comp)); amount = Math.Max(amount, 0); ent.Comp.Count = amount; ent.Comp.UiUpdateNeeded = true; Dirty(ent); _appearance.SetData(ent.Owner, StackVisuals.Actual, ent.Comp.Count); RaiseLocalEvent(ent.Owner, new StackCountChangedEvent(old, ent.Comp.Count)); // Queue delete stack if count reaches zero. if (ent.Comp.Count <= 0) PredictedQueueDel(ent.Owner); } /// [Obsolete("Use Entity method instead")] public void SetCount(EntityUid uid, int amount, StackComponent? component = null) { SetCount((uid, component), amount); } // TODO /// /// Increase a stack count by an amount, and spawn new entities if above the max. /// // public List RaiseCountAndSpawn(Entity ent, int amount); /// /// Reduce a stack count by an amount, even if it would go below 0. /// If it reaches 0 the stack will despawn. /// /// [PublicAPI] public void ReduceCount(Entity ent, int amount) { if (!_stackQuery.Resolve(ent.Owner, ref ent.Comp)) return; // Don't reduce unlimited stacks if (ent.Comp.Unlimited) return; SetCount(ent, ent.Comp.Count - amount); } /// /// Try to reduce a stack count by a whole amount. /// Won't reduce the stack count if the amount is larger than the stack. /// /// True if the count was lowered. Always true if the stack is unlimited. [PublicAPI] public bool TryUse(Entity ent, int amount) { if (!_stackQuery.Resolve(ent.Owner, ref ent.Comp)) return false; // We're unlimited and always greater than amount if (ent.Comp.Unlimited) return true; // Check if we have enough things in the stack for this... if (amount > ent.Comp.Count) return false; // We do have enough things in the stack, so remove them and change. SetCount(ent, ent.Comp.Count - amount); return true; } #endregion #region Getters /// /// Gets the count in a stack. If it cannot be stacked, returns 1. /// [PublicAPI] public int GetCount(Entity ent) { return _stackQuery.Resolve(ent.Owner, ref ent.Comp, false) ? ent.Comp.Count : 1; } /// /// Gets the total count from a list of stacks. /// private int GetCount(List> stacks, ProtoId id) { var count = 0; foreach (var (_, stack) in stacks) { if (stack.StackTypeId != id) continue; count += stack.Count; } return count; } /// /// Gets the maximum amount that can be fit on a stack. /// /// ///

/// if there's no StackComponent, this equals 1. Otherwise, if there's a max /// count override, it equals that. It then checks for a max count value /// on the stack prototype. If there isn't one, it defaults to the max integer /// value (unlimited). ///

///
[PublicAPI] public int GetMaxCount(StackComponent? component) { if (component == null) return 1; if (component.MaxCountOverride != null) return component.MaxCountOverride.Value; var stackProto = ProtoMan.Index(component.StackTypeId); return stackProto.MaxCount ?? int.MaxValue; } /// [PublicAPI] public int GetMaxCount(EntProtoId entityId) { var entProto = ProtoMan.Index(entityId); entProto.TryComp(out var stackComp, EntityManager.ComponentFactory); return GetMaxCount(stackComp); } /// [PublicAPI] public int GetMaxCount(EntityPrototype entityId) { if (!entityId.TryComp(out var stackComp, EntityManager.ComponentFactory)) return 1; return GetMaxCount(stackComp); } /// [PublicAPI] public int GetMaxCount(EntityUid uid) { return GetMaxCount(_stackQuery.CompOrNull(uid)); } /// /// Gets the maximum amount that can be fit on a stack, or int.MaxValue if no max value exists. /// [PublicAPI] public static int GetMaxCount(StackPrototype stack) { return stack.MaxCount ?? int.MaxValue; } /// [PublicAPI] public int GetMaxCount(ProtoId stackId) { return GetMaxCount(ProtoMan.Index(stackId)); } /// /// Gets the remaining space in a stack. /// [PublicAPI] public int GetAvailableSpace(StackComponent component) { return GetMaxCount(component) - component.Count; } #endregion }