* 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 * Fix storage (#37714) The one path I forgot to get the relative index. * cleanup ring box * Fix 1x1 storage windows (#35985) * fix stupid lunchbox error --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: deltanedas <@deltanedas:kde.org>
This commit is contained in:
parent
2cdfb98768
commit
496d60448a
|
|
@ -63,6 +63,8 @@ public sealed class StorageSystem : SharedStorageSystem
|
|||
component.SavedLocations[loc.Key] = new(loc.Value);
|
||||
}
|
||||
|
||||
UpdateOccupied((uid, component));
|
||||
|
||||
var uiDirty = !component.StoredItems.SequenceEqual(_oldStoredItems);
|
||||
|
||||
if (uiDirty && UI.TryGetOpenUi<StorageBoundUserInterface>(uid, StorageComponent.StorageUiKey.Key, out var storageBui))
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ public sealed class StorageWindow : BaseWindow
|
|||
private ValueList<EntityUid> _contained = new();
|
||||
private ValueList<EntityUid> _toRemove = new();
|
||||
|
||||
// Manually store this because you can't have a 0x0 GridContainer but we still need to add child controls for 1x1 containers.
|
||||
private Vector2i _pieceGridSize;
|
||||
|
||||
private TextureButton? _backButton;
|
||||
|
||||
private bool _isDirty;
|
||||
|
|
@ -408,11 +411,14 @@ public sealed class StorageWindow : BaseWindow
|
|||
_contained.Clear();
|
||||
_contained.AddRange(storageComp.Container.ContainedEntities.Reverse());
|
||||
|
||||
var width = boundingGrid.Width + 1;
|
||||
var height = boundingGrid.Height + 1;
|
||||
|
||||
// Build the grid representation
|
||||
if (_pieceGrid.Rows - 1 != boundingGrid.Height || _pieceGrid.Columns - 1 != boundingGrid.Width)
|
||||
if (_pieceGrid.Rows != _pieceGridSize.Y || _pieceGrid.Columns != _pieceGridSize.X)
|
||||
{
|
||||
_pieceGrid.Rows = boundingGrid.Height + 1;
|
||||
_pieceGrid.Columns = boundingGrid.Width + 1;
|
||||
_pieceGrid.Rows = height;
|
||||
_pieceGrid.Columns = width;
|
||||
_controlGrid.Clear();
|
||||
|
||||
for (var y = boundingGrid.Bottom; y <= boundingGrid.Top; y++)
|
||||
|
|
@ -430,6 +436,7 @@ public sealed class StorageWindow : BaseWindow
|
|||
}
|
||||
}
|
||||
|
||||
_pieceGridSize = new(width, height);
|
||||
_toRemove.Clear();
|
||||
|
||||
// Remove entities no longer relevant / Update existing ones
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Content.Shared.Examine;
|
|||
using Content.Shared.Item.ItemToggle.Components;
|
||||
using Content.Shared.Storage;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
|
@ -205,15 +206,21 @@ public abstract class SharedItemSystem : EntitySystem
|
|||
public IReadOnlyList<Box2i> GetAdjustedItemShape(Entity<ItemComponent?> entity, Angle rotation, Vector2i position)
|
||||
{
|
||||
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 boundingShape = shapes.GetBoundingBox();
|
||||
var boundingCenter = ((Box2) boundingShape).Center;
|
||||
var matty = Matrix3Helpers.CreateTransform(boundingCenter, rotation);
|
||||
var drift = boundingShape.BottomLeft - matty.TransformBox(boundingShape).BottomLeft;
|
||||
|
||||
var adjustedShapes = new List<Box2i>();
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
var transformed = matty.TransformBox(shape).Translated(drift);
|
||||
|
|
@ -222,8 +229,6 @@ public abstract class SharedItemSystem : EntitySystem
|
|||
|
||||
adjustedShapes.Add(translated);
|
||||
}
|
||||
|
||||
return adjustedShapes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -43,13 +43,14 @@ using Robust.Shared.Serialization;
|
|||
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!;
|
||||
|
|
@ -116,6 +117,10 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
|
||||
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()
|
||||
{
|
||||
|
|
@ -184,6 +189,8 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
return;
|
||||
}
|
||||
|
||||
UpdateOccupied((container.Owner, storage));
|
||||
|
||||
if (!ItemFitsInGridLocation((itemEnt.Owner, itemEnt.Comp), (container.Owner, storage), loc))
|
||||
{
|
||||
ContainerSystem.Remove(itemEnt.Owner, container, force: true);
|
||||
|
|
@ -238,6 +245,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
|
||||
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))
|
||||
{
|
||||
|
|
@ -267,6 +275,9 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
{
|
||||
storageComp.Container = ContainerSystem.EnsureContainer<Container>(uid, StorageComponent.ContainerId);
|
||||
UpdateAppearance((uid, storageComp, null));
|
||||
|
||||
// Make sure the initial starting grid is okay.
|
||||
UpdateOccupied((uid, storageComp));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -342,7 +353,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
/// <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;
|
||||
|
|
@ -862,7 +873,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
}
|
||||
|
||||
entity.Comp.StoredItems[args.Entity] = location.Value;
|
||||
Dirty(entity, entity.Comp);
|
||||
AddOccupiedEntity(entity, args.Entity, location.Value);
|
||||
}
|
||||
|
||||
UpdateAppearance((entity, entity.Comp, null));
|
||||
|
|
@ -878,7 +889,11 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
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));
|
||||
|
|
@ -1074,7 +1089,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
return false;
|
||||
|
||||
uid.Comp.StoredItems[insertEnt] = location;
|
||||
Dirty(uid, uid.Comp);
|
||||
AddOccupiedEntity((uid.Owner, uid.Comp), insertEnt, location);
|
||||
|
||||
if (Insert(uid,
|
||||
insertEnt,
|
||||
|
|
@ -1088,6 +1103,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
return true;
|
||||
}
|
||||
|
||||
RemoveOccupiedEntity((uid.Owner, uid.Comp), insertEnt, location);
|
||||
uid.Comp.StoredItems.Remove(insertEnt);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1250,9 +1266,14 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1297,17 +1318,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));
|
||||
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(SharedMapSystem.GetChunkRelative(position, StorageComponent.ChunkSize), 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1398,6 +1504,59 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
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>
|
||||
|
|
@ -1415,62 +1574,157 @@ public abstract class SharedStorageSystem : EntitySystem
|
|||
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);
|
||||
AddOccupied(itemEnt, existing, _ignored);
|
||||
}
|
||||
|
||||
if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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<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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public bool IsGridSpaceEmpty(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, Vector2i location)
|
||||
protected void UpdateOccupied(Entity<StorageComponent> ent)
|
||||
{
|
||||
if (!Resolve(storageEnt, ref storageEnt.Comp))
|
||||
return false;
|
||||
ent.Comp.OccupiedGrid.Clear();
|
||||
RemoveOccupied(ent.Comp.Grid, ent.Comp.OccupiedGrid);
|
||||
|
||||
var validGrid = false;
|
||||
foreach (var grid in storageEnt.Comp.Grid)
|
||||
Dirty(ent);
|
||||
|
||||
foreach (var (stent, storedItem) in ent.Comp.StoredItems)
|
||||
{
|
||||
if (grid.Contains(location))
|
||||
{
|
||||
validGrid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validGrid)
|
||||
return false;
|
||||
|
||||
foreach (var (ent, storedItem) in storageEnt.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);
|
||||
}
|
||||
}
|
||||
|
||||
var adjustedShape = ItemSystem.GetAdjustedItemShape((ent, itemComp), storedItem);
|
||||
foreach (var box in adjustedShape)
|
||||
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);
|
||||
}
|
||||
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ namespace Content.Shared.Storage
|
|||
{
|
||||
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]
|
||||
public Container Container = default!;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@
|
|||
- type: Storage
|
||||
maxItemSize: Normal
|
||||
grid:
|
||||
- 0,0,1,1
|
||||
- 3,0,1,1
|
||||
- 0,0,2,1
|
||||
- 4,0,4,1
|
||||
- type: PhysicalComposition
|
||||
materialComposition:
|
||||
|
|
@ -44,70 +43,70 @@
|
|||
contents:
|
||||
#Main
|
||||
- id: FoodPizzaArnoldSlice
|
||||
orGroup: HealthyOrUnhealthyMain
|
||||
orGroup: Main
|
||||
prob: 0.2
|
||||
amount: 2
|
||||
- id: FoodBurgerCheese
|
||||
orGroup: HealthyOrUnhealthyMain
|
||||
orGroup: Main
|
||||
prob: 0.2
|
||||
- id: FoodCarrot
|
||||
orGroup: HealthyOrUnhealthyMain
|
||||
orGroup: Main
|
||||
prob: 0.2
|
||||
- id: FoodMothCapreseSalad
|
||||
orGroup: HealthyOrUnhealthyMain
|
||||
orGroup: Main
|
||||
prob: 0.2
|
||||
- id: FoodEggBoiled
|
||||
orGroup: HealthyOrUnhealthyMain
|
||||
orGroup: Main
|
||||
prob: 0.2
|
||||
#Drink
|
||||
- id: DrinkJuiceOrangeJuicebox
|
||||
orGroup: HealthyOrUnhealthyDrink
|
||||
orGroup: Drink
|
||||
prob: 0.15
|
||||
- id: DrinkJuicePineappleJuicebox
|
||||
orGroup: HealthyOrUnhealthyDrink
|
||||
orGroup: Drink
|
||||
prob: 0.15
|
||||
- id: DrinkJuiceAppleJuicebox
|
||||
orGroup: HealthyOrUnhealthyDrink
|
||||
orGroup: Drink
|
||||
prob: 0.15
|
||||
- id: DrinkJuiceGrapeJuicebox
|
||||
orGroup: HealthyOrUnhealthyDrink
|
||||
orGroup: Drink
|
||||
prob: 0.15
|
||||
- id: DrinkChocolateJuicebox
|
||||
orGroup: HealthyOrUnhealthyDrink
|
||||
orGroup: Drink
|
||||
prob: 0.15
|
||||
- id: DrinkWaterBottleFull
|
||||
orGroup: HealthyOrUnhealthyDrink
|
||||
orGroup: Drink
|
||||
prob: 0.15
|
||||
#Snack
|
||||
- id: FoodSnackCheesie
|
||||
orGroup: HealthyOrUnhealthySnack
|
||||
orGroup: Snack
|
||||
prob: 0.15
|
||||
- id: FoodSnackBoritos
|
||||
orGroup: HealthyOrUnhealthySnack
|
||||
orGroup: Snack
|
||||
prob: 0.15
|
||||
- id: FoodSnackChips
|
||||
orGroup: HealthyOrUnhealthySnack
|
||||
orGroup: Snack
|
||||
prob: 0.15
|
||||
- id: FoodSnackPistachios
|
||||
orGroup: HealthyOrUnhealthySnack
|
||||
orGroup: Snack
|
||||
prob: 0.15
|
||||
- id: FoodSnackChocolate
|
||||
orGroup: HealthyOrUnhealthySnack
|
||||
orGroup: Snack
|
||||
prob: 0.15
|
||||
- id: FoodSnackSus
|
||||
orGroup: HealthyOrUnhealthySnack
|
||||
orGroup: Snack
|
||||
prob: 0.15
|
||||
- id: FoodMothMoffin
|
||||
orGroup: HealthyOrUnhealthySnack
|
||||
orGroup: Snack
|
||||
prob: 0.05
|
||||
- id: FoodMothMothmallowSlice
|
||||
orGroup: HealthyOrUnhealthySnack
|
||||
orGroup: Snack
|
||||
prob: 0.05
|
||||
- id: FoodApple
|
||||
orGroup: HealthyOrUnhealthySnack
|
||||
orGroup: Snack
|
||||
prob: 0.15
|
||||
- id: FoodBanana
|
||||
orGroup: HealthyOrUnhealthySnack
|
||||
orGroup: Snack
|
||||
prob: 0.15
|
||||
#Note
|
||||
- id: PaperWrittenNoteFromMumGeneric
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@
|
|||
name: ring box
|
||||
description: Made from a high quality wood!
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: _DV/Objects/Specific/Chapel/ringbox.rsi
|
||||
layers:
|
||||
- state: ring-box-closed
|
||||
map: [ "closeLayer" ]
|
||||
- state: ring-box-open
|
||||
map: [ "openLayer" ]
|
||||
visible: false
|
||||
- type: StaticPrice
|
||||
price: 50
|
||||
- type: Appearance
|
||||
- type: Storage # No restrictions on purpose. If someone wants to propose with a clown horn let them!
|
||||
grid:
|
||||
- 0,0,0,0
|
||||
- type: Sprite
|
||||
sprite: _DV/Objects/Specific/Chapel/ringbox.rsi
|
||||
layers:
|
||||
- state: ring-box-closed
|
||||
map: [ "closeLayer" ]
|
||||
- state: ring-box-open
|
||||
map: [ "openLayer" ]
|
||||
visible: false
|
||||
- type: StaticPrice
|
||||
price: 50
|
||||
- type: Appearance
|
||||
- type: Storage # No whitelist on purpose. If someone wants to propose with a clown horn let them!
|
||||
grid:
|
||||
- 0,0,0,0
|
||||
|
|
|
|||
Loading…
Reference in New Issue