using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared.Rounding;
+using Robust.Shared.Collections;
+using Robust.Shared.Map.Enumerators;
namespace Content.Shared.Storage.EntitySystems;
public abstract class SharedStorageSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
- [Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] protected readonly IRobustRandom Random = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
protected readonly List<string> CantFillReasons = [];
+ // Caching for various checks
+ private readonly Dictionary<Vector2i, ulong> _ignored = new();
+ private List<Box2i> _itemShape = new();
+
/// <inheritdoc />
public override void Initialize()
{
return;
}
+ UpdateOccupied((container.Owner, storage));
+
if (!ItemFitsInGridLocation((itemEnt.Owner, itemEnt.Comp), (container.Owner, storage), loc))
{
ContainerSystem.Remove(itemEnt.Owner, container, force: true);
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
+ // TODO: This should update all entities in storage as well.
if (args.ByType.ContainsKey(typeof(ItemSizePrototype))
|| (args.Removed?.ContainsKey(typeof(ItemSizePrototype)) ?? false))
{
{
storageComp.Container = ContainerSystem.EnsureContainer<Container>(uid, StorageComponent.ContainerId);
UpdateAppearance((uid, storageComp, null));
+
+ // Make sure the initial starting grid is okay.
+ UpdateOccupied((uid, storageComp));
}
/// <summary>
/// <summary>
/// Tries to get the storage location of an item.
/// </summary>
- public bool TryGetStorageLocation(Entity<ItemComponent?> itemEnt, [NotNullWhen(true)] out BaseContainer? container, out StorageComponent? storage, out ItemStorageLocation loc)
+ public bool TryGetStorageLocation(Entity<ItemComponent?> itemEnt, [NotNullWhen(true)] out BaseContainer? container, [NotNullWhen(true)] out StorageComponent? storage, out ItemStorageLocation loc)
{
loc = default;
storage = null;
}
entity.Comp.StoredItems[args.Entity] = location.Value;
- Dirty(entity, entity.Comp);
+ AddOccupiedEntity(entity, args.Entity, location.Value);
}
UpdateAppearance((entity, entity.Comp, null));
if (args.Container.ID != StorageComponent.ContainerId)
return;
- entity.Comp.StoredItems.Remove(args.Entity);
+ if (entity.Comp.StoredItems.Remove(args.Entity, out var loc))
+ {
+ RemoveOccupiedEntity(entity, args.Entity, loc);
+ }
+
Dirty(entity, entity.Comp);
UpdateAppearance((entity, entity.Comp, null));
return false;
uid.Comp.StoredItems[insertEnt] = location;
- Dirty(uid, uid.Comp);
+ AddOccupiedEntity((uid.Owner, uid.Comp), insertEnt, location);
if (Insert(uid,
insertEnt,
return true;
}
+ RemoveOccupiedEntity((uid.Owner, uid.Comp), insertEnt, location);
uid.Comp.StoredItems.Remove(insertEnt);
return false;
}
if (!ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation))
return false;
- storageEnt.Comp.StoredItems[itemEnt] = location;
+ if (storageEnt.Comp.StoredItems.Remove(itemEnt, out var existing))
+ {
+ RemoveOccupiedEntity((storageEnt.Owner, storageEnt.Comp), itemEnt, existing);
+ }
+
+ storageEnt.Comp.StoredItems.Add(itemEnt, location);
+ AddOccupiedEntity((storageEnt.Owner, storageEnt.Comp), itemEnt, location);
UpdateUI(storageEnt);
- Dirty(storageEnt, storageEnt.Comp);
return true;
}
}
}
- for (var y = storageBounding.Bottom; y <= storageBounding.Top; y++)
+ // Ignore the item's existing location for fitting purposes.
+ _ignored.Clear();
+
+ if (storageEnt.Comp.StoredItems.TryGetValue(itemEnt.Owner, out var existing))
+ {
+ AddOccupied(itemEnt, existing, _ignored);
+ }
+
+ // This uses a faster path than the typical codepaths
+ // as we can cache a bunch more data and re-use it to avoid a bunch of component overhead.
+
+ // So if we have an item that occupies 0,0 we can assume that the tile itself we're checking
+ // is always in its shapes regardless of angle. This matches virtually every item in the game and
+ // means we can skip getting the item's rotated shape at all if the tile is occupied.
+ // This mostly makes heavy checks (e.g. area insert) much, much faster.
+ var fastPath = false;
+ var itemShape = ItemSystem.GetItemShape(itemEnt);
+ var fastAngles = itemShape.Count == 1;
+
+ foreach (var shape in itemShape)
+ {
+ if (shape.Contains(Vector2i.Zero))
+ {
+ fastPath = true;
+ break;
+ }
+ }
+
+ var chunkEnumerator = new ChunkIndicesEnumerator(storageBounding, StorageComponent.ChunkSize);
+ var angles = new ValueList<Angle>();
+
+ if (!fastAngles)
+ {
+ angles.Clear();
+
+ for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f)
+ {
+ angles.Add(angle);
+ }
+ }
+ else
+ {
+ var shape = itemShape[0];
+
+ // At least 1 check for a square.
+ angles.Add(startAngle);
+
+ // If it's a rectangle make it 2.
+ if (shape.Width != shape.Height)
+ {
+ // Idk if there's a preferred facing but + or - 90 pick one.
+ angles.Add(startAngle + Angle.FromDegrees(90));
+ }
+ }
+
+ while (chunkEnumerator.MoveNext(out var storageChunk))
{
- for (var x = storageBounding.Left; x <= storageBounding.Right; x++)
+ var storageChunkOrigin = storageChunk.Value * StorageComponent.ChunkSize;
+
+ var left = Math.Max(storageChunkOrigin.X, storageBounding.Left);
+ var bottom = Math.Max(storageChunkOrigin.Y, storageBounding.Bottom);
+ var top = Math.Min(storageChunkOrigin.Y + StorageComponent.ChunkSize - 1, storageBounding.Top);
+ var right = Math.Min(storageChunkOrigin.X + StorageComponent.ChunkSize - 1, storageBounding.Right);
+
+ // No data so assume empty.
+ if (!storageEnt.Comp.OccupiedGrid.TryGetValue(storageChunkOrigin, out var occupied))
+ continue;
+
+ // This has a lot of redundant tile checks but with the fast path it shouldn't matter for average ss14
+ // use cases.
+ for (var y = bottom; y <= top; y++)
{
- for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f)
+ for (var x = left; x <= right; x++)
{
- var location = new ItemStorageLocation(angle, (x, y));
- if (ItemFitsInGridLocation(itemEnt, storageEnt, location))
+ foreach (var angle in angles)
{
- storageLocation = location;
- return true;
+ var position = new Vector2i(x, y);
+
+ // This bit of code is how area inserts go from tanking frames to being negligible.
+ if (fastPath)
+ {
+ var flag = SharedMapSystem.ToBitmask(position, StorageComponent.ChunkSize);
+
+ // Occupied so skip.
+ if ((occupied & flag) == flag)
+ continue;
+ }
+
+ _itemShape.Clear();
+ ItemSystem.GetAdjustedItemShape(_itemShape, itemEnt, angle, position);
+
+ if (ItemFitsInGridLocation(storageEnt.Comp.OccupiedGrid, _itemShape, _ignored))
+ {
+ storageLocation = new ItemStorageLocation(angle, position);
+ return true;
+ }
}
}
}
return ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation);
}
+ private bool ItemFitsInGridLocation(
+ Dictionary<Vector2i, ulong> occupied,
+ IReadOnlyList<Box2i> itemShape,
+ Dictionary<Vector2i, ulong> ignored)
+ {
+ // We pre-cache the occupied / ignored tiles upfront and then can just check each tile 1-by-1.
+ // We do it by chunk so we can avoid dictionary overhead.
+ foreach (var box in itemShape)
+ {
+ var chunkEnumerator = new ChunkIndicesEnumerator(box, StorageComponent.ChunkSize);
+
+ while (chunkEnumerator.MoveNext(out var chunk))
+ {
+ var chunkOrigin = chunk.Value * StorageComponent.ChunkSize;
+
+ // Box may not necessarily be in 1 chunk so clamp it.
+ var left = Math.Max(chunkOrigin.X, box.Left);
+ var bottom = Math.Max(chunkOrigin.Y, box.Bottom);
+ var right = Math.Min(chunkOrigin.X + StorageComponent.ChunkSize - 1, box.Right);
+ var top = Math.Min(chunkOrigin.Y + StorageComponent.ChunkSize - 1, box.Top);
+
+ // Assume it's occupied if no data.
+ if (!occupied.TryGetValue(chunkOrigin, out var occupiedMask))
+ {
+ return false;
+ }
+
+ var ignoredMask = ignored.GetValueOrDefault(chunkOrigin);
+
+ for (var x = left; x <= right; x++)
+ {
+ for (var y = bottom; y <= top; y++)
+ {
+ var index = new Vector2i(x, y);
+ var chunkRelative = SharedMapSystem.GetChunkRelative(index, StorageComponent.ChunkSize);
+ var flag = SharedMapSystem.ToBitmask(chunkRelative, StorageComponent.ChunkSize);
+
+ // Ignore it
+ if ((ignoredMask & flag) == flag)
+ continue;
+
+ if ((occupiedMask & flag) == flag)
+ {
+ return false;
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
/// <summary>
/// Checks if an item fits into a specific spot on a storage grid.
/// </summary>
return false;
var itemShape = ItemSystem.GetAdjustedItemShape(itemEnt, rotation, position);
+ // Ignore the item's existing location for fitting purposes.
+ _ignored.Clear();
- foreach (var box in itemShape)
+ if (storageEnt.Comp.StoredItems.TryGetValue(itemEnt.Owner, out var existing))
{
- for (var offsetY = box.Bottom; offsetY <= box.Top; offsetY++)
- {
- for (var offsetX = box.Left; offsetX <= box.Right; offsetX++)
- {
- var pos = (offsetX, offsetY);
-
- if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos))
- return false;
- }
- }
+ AddOccupied(itemEnt, existing, _ignored);
}
- return true;
+ return ItemFitsInGridLocation(storageEnt.Comp.OccupiedGrid, itemShape, _ignored);
}
/// <summary>
/// Checks if a space on a grid is valid and not occupied by any other pieces.
/// </summary>
- public bool IsGridSpaceEmpty(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, Vector2i location)
+ public bool IsGridSpaceEmpty(Entity<StorageComponent?> storageEnt, Vector2i location, Dictionary<Vector2i, ulong>? ignored = null)
{
if (!Resolve(storageEnt, ref storageEnt.Comp))
return false;
- var validGrid = false;
- foreach (var grid in storageEnt.Comp.Grid)
+ var chunkOrigin = SharedMapSystem.GetChunkIndices(location, StorageComponent.ChunkSize) * StorageComponent.ChunkSize;
+
+ // No entry so assume it's occupied.
+ if (!storageEnt.Comp.OccupiedGrid.TryGetValue(chunkOrigin, out var occupiedMask))
+ return false;
+
+ var chunkRelative = SharedMapSystem.GetChunkRelative(location, StorageComponent.ChunkSize);
+ var occupiedIndex = SharedMapSystem.ToBitmask(chunkRelative);
+
+ if (ignored?.TryGetValue(chunkOrigin, out var ignoredMask) == true && (ignoredMask & occupiedIndex) == occupiedIndex)
{
- if (grid.Contains(location))
- {
- validGrid = true;
- break;
- }
+ return true;
}
- if (!validGrid)
+ if ((occupiedMask & occupiedIndex) != 0x0)
+ {
return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Updates the occupied grid mask for the entity.
+ /// </summary>
+ protected void UpdateOccupied(Entity<StorageComponent> ent)
+ {
+ ent.Comp.OccupiedGrid.Clear();
+ RemoveOccupied(ent.Comp.Grid, ent.Comp.OccupiedGrid);
+
+ Dirty(ent);
- foreach (var (ent, storedItem) in storageEnt.Comp.StoredItems)
+ foreach (var (stent, storedItem) in ent.Comp.StoredItems)
{
- if (ent == itemEnt.Owner)
+ if (!_itemQuery.TryGetComponent(stent, out var itemComp))
continue;
- if (!_itemQuery.TryGetComponent(ent, out var itemComp))
- continue;
+ AddOccupiedEntity(ent, (stent, itemComp), storedItem);
+ }
+ }
+
+ private void AddOccupiedEntity(Entity<StorageComponent> storageEnt, Entity<ItemComponent?> itemEnt, ItemStorageLocation location)
+ {
+ AddOccupied(itemEnt, location, storageEnt.Comp.OccupiedGrid);
+
+ Dirty(storageEnt);
+ }
+
+ private void AddOccupied(Entity<ItemComponent?> itemEnt, ItemStorageLocation location, Dictionary<Vector2i, ulong> occupied)
+ {
+ var adjustedShape = ItemSystem.GetAdjustedItemShape((itemEnt.Owner, itemEnt.Comp), location);
+ AddOccupied(adjustedShape, occupied);
+ }
- var adjustedShape = ItemSystem.GetAdjustedItemShape((ent, itemComp), storedItem);
- foreach (var box in adjustedShape)
+ private void RemoveOccupied(IReadOnlyList<Box2i> adjustedShape, Dictionary<Vector2i, ulong> occupied)
+ {
+ foreach (var box in adjustedShape)
+ {
+ var chunks = new ChunkIndicesEnumerator(box, StorageComponent.ChunkSize);
+
+ while (chunks.MoveNext(out var chunk))
{
- if (box.Contains(location))
- return false;
+ var chunkOrigin = chunk.Value * StorageComponent.ChunkSize;
+
+ var left = Math.Max(box.Left, chunkOrigin.X);
+ var bottom = Math.Max(box.Bottom, chunkOrigin.Y);
+ var right = Math.Min(box.Right, chunkOrigin.X + StorageComponent.ChunkSize - 1);
+ var top = Math.Min(box.Top, chunkOrigin.Y + StorageComponent.ChunkSize - 1);
+ var existing = occupied.GetValueOrDefault(chunkOrigin, ulong.MaxValue);
+
+ // Unmark all of the tiles that we actually have.
+ for (var x = left; x <= right; x++)
+ {
+ for (var y = bottom; y <= top; y++)
+ {
+ var index = new Vector2i(x, y);
+ var chunkRelative = SharedMapSystem.GetChunkRelative(index, StorageComponent.ChunkSize);
+
+ var flag = SharedMapSystem.ToBitmask(chunkRelative, StorageComponent.ChunkSize);
+ existing &= ~flag;
+ }
+ }
+
+ // My kingdom for collections.marshal
+ occupied[chunkOrigin] = existing;
}
}
+ }
- return true;
+ private void AddOccupied(IReadOnlyList<Box2i> adjustedShape, Dictionary<Vector2i, ulong> occupied)
+ {
+ foreach (var box in adjustedShape)
+ {
+ // Reduce dictionary access from every tile to just once per chunk.
+ // Makes this more complicated but dictionaries are slow af.
+ // This is how we get savings over IsGridSpaceEmpty.
+ var chunkEnumerator = new ChunkIndicesEnumerator(box, StorageComponent.ChunkSize);
+
+ while (chunkEnumerator.MoveNext(out var chunk))
+ {
+ var chunkOrigin = chunk.Value * StorageComponent.ChunkSize;
+ var existing = occupied.GetOrNew(chunkOrigin);
+
+ // Box may not necessarily be in 1 chunk so clamp it.
+ var left = Math.Max(chunkOrigin.X, box.Left);
+ var bottom = Math.Max(chunkOrigin.Y, box.Bottom);
+ var right = Math.Min(chunkOrigin.X + StorageComponent.ChunkSize - 1, box.Right);
+ var top = Math.Min(chunkOrigin.Y + StorageComponent.ChunkSize - 1, box.Top);
+
+ for (var x = left; x <= right; x++)
+ {
+ for (var y = bottom; y <= top; y++)
+ {
+ var index = new Vector2i(x, y);
+ var chunkRelative = SharedMapSystem.GetChunkRelative(index, StorageComponent.ChunkSize);
+ var flag = SharedMapSystem.ToBitmask(chunkRelative, StorageComponent.ChunkSize);
+ existing |= flag;
+ }
+ }
+
+ occupied[chunkOrigin] = existing;
+ }
+ }
+ }
+
+ private void RemoveOccupiedEntity(Entity<StorageComponent> storageEnt, Entity<ItemComponent?> itemEnt, ItemStorageLocation location)
+ {
+ var adjustedShape = ItemSystem.GetAdjustedItemShape((itemEnt.Owner, itemEnt.Comp), location);
+
+ RemoveOccupied(adjustedShape, storageEnt.Comp.OccupiedGrid);
+
+ Dirty(storageEnt);
}
/// <summary>