Optimise storage a quadrillion times (#37638)
* Optimise storage a quadrillion times * How sweaty can we get * Add fast angle checks * Fix chunk indices * Optimise the refresh method Helps on client a lot as the clientside is suboptimal atm. * Better name * wawawewa * Add single-angle path * Okay FINE rider
This commit is contained in:
@@ -63,6 +63,8 @@ public sealed class StorageSystem : SharedStorageSystem
|
|||||||
component.SavedLocations[loc.Key] = new(loc.Value);
|
component.SavedLocations[loc.Key] = new(loc.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateOccupied((uid, component));
|
||||||
|
|
||||||
var uiDirty = !component.StoredItems.SequenceEqual(_oldStoredItems);
|
var uiDirty = !component.StoredItems.SequenceEqual(_oldStoredItems);
|
||||||
|
|
||||||
if (uiDirty && UI.TryGetOpenUi<StorageBoundUserInterface>(uid, StorageComponent.StorageUiKey.Key, out var storageBui))
|
if (uiDirty && UI.TryGetOpenUi<StorageBoundUserInterface>(uid, StorageComponent.StorageUiKey.Key, out var storageBui))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Content.Shared.Examine;
|
|||||||
using Content.Shared.Item.ItemToggle.Components;
|
using Content.Shared.Item.ItemToggle.Components;
|
||||||
using Content.Shared.Storage;
|
using Content.Shared.Storage;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
@@ -206,15 +207,21 @@ public abstract class SharedItemSystem : EntitySystem
|
|||||||
public IReadOnlyList<Box2i> GetAdjustedItemShape(Entity<ItemComponent?> entity, Angle rotation, Vector2i position)
|
public IReadOnlyList<Box2i> GetAdjustedItemShape(Entity<ItemComponent?> entity, Angle rotation, Vector2i position)
|
||||||
{
|
{
|
||||||
if (!Resolve(entity, ref entity.Comp))
|
if (!Resolve(entity, ref entity.Comp))
|
||||||
return new Box2i[] { };
|
return [];
|
||||||
|
|
||||||
|
var adjustedShapes = new List<Box2i>();
|
||||||
|
GetAdjustedItemShape(adjustedShapes, entity, rotation, position);
|
||||||
|
return adjustedShapes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetAdjustedItemShape(List<Box2i> adjustedShapes, Entity<ItemComponent?> entity, Angle rotation, Vector2i position)
|
||||||
|
{
|
||||||
var shapes = GetItemShape(entity);
|
var shapes = GetItemShape(entity);
|
||||||
var boundingShape = shapes.GetBoundingBox();
|
var boundingShape = shapes.GetBoundingBox();
|
||||||
var boundingCenter = ((Box2) boundingShape).Center;
|
var boundingCenter = ((Box2) boundingShape).Center;
|
||||||
var matty = Matrix3Helpers.CreateTransform(boundingCenter, rotation);
|
var matty = Matrix3Helpers.CreateTransform(boundingCenter, rotation);
|
||||||
var drift = boundingShape.BottomLeft - matty.TransformBox(boundingShape).BottomLeft;
|
var drift = boundingShape.BottomLeft - matty.TransformBox(boundingShape).BottomLeft;
|
||||||
|
|
||||||
var adjustedShapes = new List<Box2i>();
|
|
||||||
foreach (var shape in shapes)
|
foreach (var shape in shapes)
|
||||||
{
|
{
|
||||||
var transformed = matty.TransformBox(shape).Translated(drift);
|
var transformed = matty.TransformBox(shape).Translated(drift);
|
||||||
@@ -223,8 +230,6 @@ public abstract class SharedItemSystem : EntitySystem
|
|||||||
|
|
||||||
adjustedShapes.Add(translated);
|
adjustedShapes.Add(translated);
|
||||||
}
|
}
|
||||||
|
|
||||||
return adjustedShapes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -42,13 +42,14 @@ using Robust.Shared.Serialization;
|
|||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using Content.Shared.Rounding;
|
using Content.Shared.Rounding;
|
||||||
|
using Robust.Shared.Collections;
|
||||||
|
using Robust.Shared.Map.Enumerators;
|
||||||
|
|
||||||
namespace Content.Shared.Storage.EntitySystems;
|
namespace Content.Shared.Storage.EntitySystems;
|
||||||
|
|
||||||
public abstract class SharedStorageSystem : EntitySystem
|
public abstract class SharedStorageSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
[Dependency] protected readonly IGameTiming Timing = default!;
|
|
||||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||||
[Dependency] protected readonly IRobustRandom Random = default!;
|
[Dependency] protected readonly IRobustRandom Random = default!;
|
||||||
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
|
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
|
||||||
@@ -115,6 +116,10 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
|
|
||||||
protected readonly List<string> CantFillReasons = [];
|
protected readonly List<string> CantFillReasons = [];
|
||||||
|
|
||||||
|
// Caching for various checks
|
||||||
|
private readonly Dictionary<Vector2i, ulong> _ignored = new();
|
||||||
|
private List<Box2i> _itemShape = new();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -183,6 +188,8 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateOccupied((container.Owner, storage));
|
||||||
|
|
||||||
if (!ItemFitsInGridLocation((itemEnt.Owner, itemEnt.Comp), (container.Owner, storage), loc))
|
if (!ItemFitsInGridLocation((itemEnt.Owner, itemEnt.Comp), (container.Owner, storage), loc))
|
||||||
{
|
{
|
||||||
ContainerSystem.Remove(itemEnt.Owner, container, force: true);
|
ContainerSystem.Remove(itemEnt.Owner, container, force: true);
|
||||||
@@ -237,6 +244,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
|
|
||||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
|
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
|
||||||
{
|
{
|
||||||
|
// TODO: This should update all entities in storage as well.
|
||||||
if (args.ByType.ContainsKey(typeof(ItemSizePrototype))
|
if (args.ByType.ContainsKey(typeof(ItemSizePrototype))
|
||||||
|| (args.Removed?.ContainsKey(typeof(ItemSizePrototype)) ?? false))
|
|| (args.Removed?.ContainsKey(typeof(ItemSizePrototype)) ?? false))
|
||||||
{
|
{
|
||||||
@@ -266,6 +274,9 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
{
|
{
|
||||||
storageComp.Container = ContainerSystem.EnsureContainer<Container>(uid, StorageComponent.ContainerId);
|
storageComp.Container = ContainerSystem.EnsureContainer<Container>(uid, StorageComponent.ContainerId);
|
||||||
UpdateAppearance((uid, storageComp, null));
|
UpdateAppearance((uid, storageComp, null));
|
||||||
|
|
||||||
|
// Make sure the initial starting grid is okay.
|
||||||
|
UpdateOccupied((uid, storageComp));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -341,7 +352,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to get the storage location of an item.
|
/// Tries to get the storage location of an item.
|
||||||
/// </summary>
|
/// </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;
|
loc = default;
|
||||||
storage = null;
|
storage = null;
|
||||||
@@ -862,7 +873,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
entity.Comp.StoredItems[args.Entity] = location.Value;
|
entity.Comp.StoredItems[args.Entity] = location.Value;
|
||||||
Dirty(entity, entity.Comp);
|
AddOccupiedEntity(entity, args.Entity, location.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateAppearance((entity, entity.Comp, null));
|
UpdateAppearance((entity, entity.Comp, null));
|
||||||
@@ -878,7 +889,11 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
if (args.Container.ID != StorageComponent.ContainerId)
|
if (args.Container.ID != StorageComponent.ContainerId)
|
||||||
return;
|
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);
|
Dirty(entity, entity.Comp);
|
||||||
|
|
||||||
UpdateAppearance((entity, entity.Comp, null));
|
UpdateAppearance((entity, entity.Comp, null));
|
||||||
@@ -1071,7 +1086,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
uid.Comp.StoredItems[insertEnt] = location;
|
uid.Comp.StoredItems[insertEnt] = location;
|
||||||
Dirty(uid, uid.Comp);
|
AddOccupiedEntity((uid.Owner, uid.Comp), insertEnt, location);
|
||||||
|
|
||||||
if (Insert(uid,
|
if (Insert(uid,
|
||||||
insertEnt,
|
insertEnt,
|
||||||
@@ -1085,6 +1100,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RemoveOccupiedEntity((uid.Owner, uid.Comp), insertEnt, location);
|
||||||
uid.Comp.StoredItems.Remove(insertEnt);
|
uid.Comp.StoredItems.Remove(insertEnt);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1247,9 +1263,14 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
if (!ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation))
|
if (!ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation))
|
||||||
return false;
|
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);
|
UpdateUI(storageEnt);
|
||||||
Dirty(storageEnt, storageEnt.Comp);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1294,17 +1315,102 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
{
|
{
|
||||||
for (var x = storageBounding.Left; x <= storageBounding.Right; x++)
|
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))
|
||||||
{
|
{
|
||||||
for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f)
|
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))
|
||||||
|
{
|
||||||
|
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 x = left; x <= right; x++)
|
||||||
{
|
{
|
||||||
var location = new ItemStorageLocation(angle, (x, y));
|
foreach (var angle in angles)
|
||||||
if (ItemFitsInGridLocation(itemEnt, storageEnt, location))
|
|
||||||
{
|
{
|
||||||
storageLocation = location;
|
var position = new Vector2i(x, y);
|
||||||
return true;
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1395,6 +1501,59 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
return ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation);
|
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>
|
/// <summary>
|
||||||
/// Checks if an item fits into a specific spot on a storage grid.
|
/// Checks if an item fits into a specific spot on a storage grid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1412,62 +1571,157 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var itemShape = ItemSystem.GetAdjustedItemShape(itemEnt, rotation, position);
|
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++)
|
AddOccupied(itemEnt, existing, _ignored);
|
||||||
{
|
}
|
||||||
for (var offsetX = box.Left; offsetX <= box.Right; offsetX++)
|
|
||||||
{
|
|
||||||
var pos = (offsetX, offsetY);
|
|
||||||
|
|
||||||
if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos))
|
return ItemFitsInGridLocation(storageEnt.Comp.OccupiedGrid, itemShape, _ignored);
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
}
|
/// <summary>
|
||||||
|
/// Checks if a space on a grid is valid and not occupied by any other pieces.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsGridSpaceEmpty(Entity<StorageComponent?> storageEnt, Vector2i location, Dictionary<Vector2i, ulong>? ignored = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(storageEnt, ref storageEnt.Comp))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((occupiedMask & occupiedIndex) != 0x0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if a space on a grid is valid and not occupied by any other pieces.
|
/// Updates the occupied grid mask for the entity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsGridSpaceEmpty(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, Vector2i location)
|
protected void UpdateOccupied(Entity<StorageComponent> ent)
|
||||||
{
|
{
|
||||||
if (!Resolve(storageEnt, ref storageEnt.Comp))
|
ent.Comp.OccupiedGrid.Clear();
|
||||||
return false;
|
RemoveOccupied(ent.Comp.Grid, ent.Comp.OccupiedGrid);
|
||||||
|
|
||||||
var validGrid = false;
|
Dirty(ent);
|
||||||
foreach (var grid in storageEnt.Comp.Grid)
|
|
||||||
|
foreach (var (stent, storedItem) in ent.Comp.StoredItems)
|
||||||
{
|
{
|
||||||
if (grid.Contains(location))
|
if (!_itemQuery.TryGetComponent(stent, out var itemComp))
|
||||||
{
|
|
||||||
validGrid = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validGrid)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
foreach (var (ent, storedItem) in storageEnt.Comp.StoredItems)
|
|
||||||
{
|
|
||||||
if (ent == itemEnt.Owner)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!_itemQuery.TryGetComponent(ent, out var itemComp))
|
AddOccupiedEntity(ent, (stent, itemComp), storedItem);
|
||||||
continue;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var adjustedShape = ItemSystem.GetAdjustedItemShape((ent, itemComp), storedItem);
|
private void AddOccupiedEntity(Entity<StorageComponent> storageEnt, Entity<ItemComponent?> itemEnt, ItemStorageLocation location)
|
||||||
foreach (var box in adjustedShape)
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
var chunkOrigin = chunk.Value * StorageComponent.ChunkSize;
|
||||||
return false;
|
|
||||||
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ namespace Content.Shared.Storage
|
|||||||
{
|
{
|
||||||
public static string ContainerId = "storagebase";
|
public static string ContainerId = "storagebase";
|
||||||
|
|
||||||
|
public const byte ChunkSize = 8;
|
||||||
|
|
||||||
|
// No datafield because we can just derive it from stored items.
|
||||||
|
/// <summary>
|
||||||
|
/// Bitmask of occupied tiles
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<Vector2i, ulong> OccupiedGrid = new();
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
public Container Container = default!;
|
public Container Container = default!;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user