From b4f3c4472fb4033fe81cd8ad49fc05e5e2eb2f16 Mon Sep 17 00:00:00 2001
From: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Date: Mon, 24 Nov 2025 08:51:14 +0100
Subject: [PATCH] Add test checking for multiple container fills, mark
StorageFillComponent as obsolete (#41562)
add test, obsolete storagefill
---
.../Tests/Storage/StorageTest.cs | 263 ++++++++++++++++++
Content.IntegrationTests/Tests/StorageTest.cs | 240 ----------------
.../Components/StorageFillComponent.cs | 5 +-
3 files changed, 266 insertions(+), 242 deletions(-)
create mode 100644 Content.IntegrationTests/Tests/Storage/StorageTest.cs
delete mode 100644 Content.IntegrationTests/Tests/StorageTest.cs
diff --git a/Content.IntegrationTests/Tests/Storage/StorageTest.cs b/Content.IntegrationTests/Tests/Storage/StorageTest.cs
new file mode 100644
index 0000000000..95d94906bb
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Storage/StorageTest.cs
@@ -0,0 +1,263 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using Content.Shared.Containers;
+using Content.Shared.Item;
+using Content.Shared.Prototypes;
+using Content.Shared.Storage;
+using Content.Shared.Storage.Components;
+using Content.Shared.Storage.EntitySystems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Storage;
+
+public sealed class StorageTest
+{
+ ///
+ /// Can an item store more than itself weighs.
+ /// In an ideal world this test wouldn't need to exist because sizes would be recursive.
+ ///
+ [Test]
+ public async Task StorageSizeArbitrageTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var protoManager = server.ResolveDependency();
+ var entMan = server.ResolveDependency();
+
+ var itemSys = entMan.System();
+
+ await server.WaitAssertion(() =>
+ {
+ foreach (var proto in protoManager.EnumeratePrototypes())
+ {
+ if (!proto.TryGetComponent("Storage", out var storage) ||
+ storage.Whitelist != null ||
+ storage.MaxItemSize == null ||
+ !proto.TryGetComponent("Item", out var item))
+ continue;
+
+ Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
+ Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
+ $"Found storage arbitrage on {proto.ID}");
+ }
+ });
+ await pair.CleanReturnAsync();
+ }
+
+ [Test]
+ public async Task TestStorageFillPrototypes()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var protoManager = server.ResolveDependency();
+
+ await server.WaitAssertion(() =>
+ {
+ Assert.Multiple(() =>
+ {
+ foreach (var proto in protoManager.EnumeratePrototypes())
+ {
+ if (!proto.TryGetComponent("StorageFill", out var storage))
+ continue;
+
+ foreach (var entry in storage.Contents)
+ {
+ Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
+ Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
+ }
+ }
+ });
+ });
+ await pair.CleanReturnAsync();
+ }
+
+ [Test]
+ public async Task TestSufficientSpaceForFill()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.ResolveDependency();
+ var protoMan = server.ResolveDependency();
+ var compFact = server.ResolveDependency();
+ var id = compFact.GetComponentName();
+
+ var itemSys = entMan.System();
+
+ var allSizes = protoMan.EnumeratePrototypes().ToList();
+ allSizes.Sort();
+
+ await Assert.MultipleAsync(async () =>
+ {
+ foreach (var (proto, fill) in pair.GetPrototypesWithComponent())
+ {
+ if (proto.HasComponent(compFact))
+ continue;
+
+ StorageComponent? storage = null;
+ ItemComponent? item = null;
+ var size = 0;
+ await server.WaitAssertion(() =>
+ {
+ if (!proto.TryGetComponent("Storage", out storage))
+ {
+ Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
+ return;
+ }
+
+ proto.TryGetComponent("Item", out item);
+ size = GetFillSize(fill, false, protoMan, itemSys);
+ });
+
+ if (storage == null)
+ continue;
+
+ var maxSize = storage.MaxItemSize;
+ if (storage.MaxItemSize == null)
+ {
+ if (item?.Size == null)
+ {
+ maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
+ }
+ else
+ {
+ var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
+ var index = Math.Max(0, curIndex - 1);
+ maxSize = allSizes[index].ID;
+ }
+ }
+
+ if (maxSize == null)
+ continue;
+
+ Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
+
+ foreach (var entry in fill.Contents)
+ {
+ if (entry.PrototypeId == null)
+ continue;
+
+ if (!protoMan.TryIndex(entry.PrototypeId, out var fillItem))
+ continue;
+
+ ItemComponent? entryItem = null;
+ await server.WaitPost(() =>
+ {
+ fillItem.TryGetComponent("Item", out entryItem);
+ });
+
+ if (entryItem == null)
+ continue;
+
+ Assert.That(protoMan.Index(entryItem.Size).Weight,
+ Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
+ $"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
+ }
+ }
+ });
+
+ await pair.CleanReturnAsync();
+ }
+
+ [Test]
+ public async Task TestSufficientSpaceForEntityStorageFill()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.ResolveDependency();
+ var protoMan = server.ResolveDependency();
+ var compFact = server.ResolveDependency();
+ var id = compFact.GetComponentName();
+
+ var itemSys = entMan.System();
+
+ foreach (var (proto, fill) in pair.GetPrototypesWithComponent())
+ {
+ if (proto.HasComponent(compFact))
+ continue;
+
+ await server.WaitAssertion(() =>
+ {
+ if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
+ Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
+
+ if (entStorage == null)
+ return;
+
+ var size = GetFillSize(fill, true, protoMan, itemSys);
+ Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
+ $"{proto.ID} storage fill is too large.");
+ });
+ }
+ await pair.CleanReturnAsync();
+ }
+
+ private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
+ {
+ if (entry.PrototypeId == null)
+ return 0;
+
+ if (!protoMan.TryIndex(entry.PrototypeId, out var proto))
+ {
+ Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
+ return 0;
+ }
+
+ if (getCount)
+ return entry.Amount;
+
+
+ if (proto.TryGetComponent("Item", out var item))
+ return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
+
+ Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
+ return 0;
+ }
+
+ private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
+ {
+ var totalSize = 0;
+ var groups = new Dictionary();
+ foreach (var entry in fill.Contents)
+ {
+ var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
+
+ if (entry.GroupId == null)
+ totalSize += size;
+ else
+ groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
+ }
+
+ return totalSize + groups.Values.Sum();
+ }
+
+ ///
+ /// Tests that prototypes are not using multiple container fill components at the same time.
+ ///
+ [Test]
+ public async Task NoMultipleContainerFillsTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var compFact = pair.Server.ResolveDependency();
+
+ Assert.Multiple(() =>
+ {
+ foreach (var (proto, fill) in pair.GetPrototypesWithComponent())
+ {
+ Assert.That(!proto.HasComponent(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(StorageFillComponent)}.");
+ Assert.That(!proto.HasComponent(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(ContainerFillComponent)}.");
+ }
+
+ foreach (var (proto, fill) in pair.GetPrototypesWithComponent())
+ {
+ Assert.That(!proto.HasComponent(compFact), $"Prototype {proto.ID} has both {nameof(ContainerFillComponent)} and {nameof(StorageFillComponent)}.");
+ }
+ });
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/StorageTest.cs b/Content.IntegrationTests/Tests/StorageTest.cs
deleted file mode 100644
index 13ddc56567..0000000000
--- a/Content.IntegrationTests/Tests/StorageTest.cs
+++ /dev/null
@@ -1,240 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using System.Linq;
-using Content.Server.Storage.Components;
-using Content.Shared.Item;
-using Content.Shared.Prototypes;
-using Content.Shared.Storage;
-using Content.Shared.Storage.Components;
-using Content.Shared.Storage.EntitySystems;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Prototypes;
-
-namespace Content.IntegrationTests.Tests
-{
- [TestFixture]
- public sealed class StorageTest
- {
- ///
- /// Can an item store more than itself weighs.
- /// In an ideal world this test wouldn't need to exist because sizes would be recursive.
- ///
- [Test]
- public async Task StorageSizeArbitrageTest()
- {
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var protoManager = server.ResolveDependency();
- var entMan = server.ResolveDependency();
-
- var itemSys = entMan.System();
-
- await server.WaitAssertion(() =>
- {
- foreach (var proto in protoManager.EnumeratePrototypes())
- {
- if (!proto.TryGetComponent("Storage", out var storage) ||
- storage.Whitelist != null ||
- storage.MaxItemSize == null ||
- !proto.TryGetComponent("Item", out var item))
- continue;
-
- Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
- Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
- $"Found storage arbitrage on {proto.ID}");
- }
- });
- await pair.CleanReturnAsync();
- }
-
- [Test]
- public async Task TestStorageFillPrototypes()
- {
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var protoManager = server.ResolveDependency();
-
- await server.WaitAssertion(() =>
- {
- Assert.Multiple(() =>
- {
- foreach (var proto in protoManager.EnumeratePrototypes())
- {
- if (!proto.TryGetComponent("StorageFill", out var storage))
- continue;
-
- foreach (var entry in storage.Contents)
- {
- Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
- Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
- }
- }
- });
- });
- await pair.CleanReturnAsync();
- }
-
- [Test]
- public async Task TestSufficientSpaceForFill()
- {
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.ResolveDependency();
- var protoMan = server.ResolveDependency();
- var compFact = server.ResolveDependency();
- var id = compFact.GetComponentName();
-
- var itemSys = entMan.System();
-
- var allSizes = protoMan.EnumeratePrototypes().ToList();
- allSizes.Sort();
-
- await Assert.MultipleAsync(async () =>
- {
- foreach (var (proto, fill) in pair.GetPrototypesWithComponent())
- {
- if (proto.HasComponent(compFact))
- continue;
-
- StorageComponent? storage = null;
- ItemComponent? item = null;
- var size = 0;
- await server.WaitAssertion(() =>
- {
- if (!proto.TryGetComponent("Storage", out storage))
- {
- Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
- return;
- }
-
- proto.TryGetComponent("Item", out item);
- size = GetFillSize(fill, false, protoMan, itemSys);
- });
-
- if (storage == null)
- continue;
-
- var maxSize = storage.MaxItemSize;
- if (storage.MaxItemSize == null)
- {
- if (item?.Size == null)
- {
- maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
- }
- else
- {
- var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
- var index = Math.Max(0, curIndex - 1);
- maxSize = allSizes[index].ID;
- }
- }
-
- if (maxSize == null)
- continue;
-
- Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
-
- foreach (var entry in fill.Contents)
- {
- if (entry.PrototypeId == null)
- continue;
-
- if (!protoMan.TryIndex(entry.PrototypeId, out var fillItem))
- continue;
-
- ItemComponent? entryItem = null;
- await server.WaitPost(() =>
- {
- fillItem.TryGetComponent("Item", out entryItem);
- });
-
- if (entryItem == null)
- continue;
-
- Assert.That(protoMan.Index(entryItem.Size).Weight,
- Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
- $"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
- }
- }
- });
-
- await pair.CleanReturnAsync();
- }
-
- [Test]
- public async Task TestSufficientSpaceForEntityStorageFill()
- {
- await using var pair = await PoolManager.GetServerClient();
- var server = pair.Server;
-
- var entMan = server.ResolveDependency();
- var protoMan = server.ResolveDependency();
- var compFact = server.ResolveDependency();
- var id = compFact.GetComponentName();
-
- var itemSys = entMan.System();
-
- foreach (var (proto, fill) in pair.GetPrototypesWithComponent())
- {
- if (proto.HasComponent(compFact))
- continue;
-
- await server.WaitAssertion(() =>
- {
- if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
- Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
-
- if (entStorage == null)
- return;
-
- var size = GetFillSize(fill, true, protoMan, itemSys);
- Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
- $"{proto.ID} storage fill is too large.");
- });
- }
- await pair.CleanReturnAsync();
- }
-
- private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
- {
- if (entry.PrototypeId == null)
- return 0;
-
- if (!protoMan.TryIndex(entry.PrototypeId, out var proto))
- {
- Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
- return 0;
- }
-
- if (getCount)
- return entry.Amount;
-
-
- if (proto.TryGetComponent("Item", out var item))
- return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
-
- Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
- return 0;
- }
-
- private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
- {
- var totalSize = 0;
- var groups = new Dictionary();
- foreach (var entry in fill.Contents)
- {
- var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
-
- if (entry.GroupId == null)
- totalSize += size;
- else
- groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
- }
-
- return totalSize + groups.Values.Sum();
- }
- }
-}
diff --git a/Content.Shared/Storage/Components/StorageFillComponent.cs b/Content.Shared/Storage/Components/StorageFillComponent.cs
index e112368a49..22ae32c4a8 100644
--- a/Content.Shared/Storage/Components/StorageFillComponent.cs
+++ b/Content.Shared/Storage/Components/StorageFillComponent.cs
@@ -3,8 +3,9 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Storage.Components;
-// TODO:
-// REPLACE THIS WITH CONTAINERFILL
+// Don't remove before December 2026.
+// A lot of forks still use this for their prototypes.
+[Obsolete("Use ContainerFillComponent or EntityTableContainerFillComponent instead")]
[RegisterComponent, NetworkedComponent, Access(typeof(SharedStorageSystem))]
public sealed partial class StorageFillComponent : Component
{
--
2.52.0