Skip to content

Fix items disappearing when inserted into chests on Fabric - #209

Merged
rubensworks merged 2 commits into
master-1.21-ltsfrom
fix-208-fabric-chest-item-storage-aliasing
Sep 7, 2026
Merged

Fix items disappearing when inserted into chests on Fabric#209
rubensworks merged 2 commits into
master-1.21-ltsfrom
fix-208-fabric-chest-item-storage-aliasing

Conversation

@rubensworks

@rubensworks rubensworks commented Sep 7, 2026

Copy link
Copy Markdown
Member

Closes #208

Root cause

Fabric's InventoryStorage.of(container, direction) keeps a global Container → storage cache:

private static final Map<Inventory, InventoryStorageImpl> WRAPPERS = new MapMaker().weakValues().makeMap();
// TODO: should have identity semantics?

MapMaker().weakValues() uses equals-based key semantics. CyclopsCore's SimpleInventoryCommon (the base of every chest inventory here) overrides equals/hashCode: two inventories are equal when they have the same size, the same stack limit and the same item types per slot, and hashCode() is a modification counter.

So two chests that are empty and equally sized are "equal" and hash the same, and the second one to be looked up gets handed the first chest's storage. Every insert and every read then goes to the wrong chest, which is exactly what the issue reports: a turtle drops 64 items into a colossal chest core, the items leave the turtle, and never show up in the chest — and reading the chest's item list returns another chest's contents.

Only Fabric is affected. NeoForge and Forge wrap the inventory in a fresh InvWrapper per lookup, with no shared cache.

Fix

Chest inventories are identified by instance instead of by contents, on Fabric only:

  • The anonymous inventories of both chests become named classes in loader-common (InventoryColossalChest, InventoryUncolossalChest), and the block entities create them through overridable factory methods.
  • loader-fabric subclasses those with identity equals/hashCode, and its blocks and block entity types now create Fabric block entities that use them. Both the block's newBlockEntity and the block entity type's supplier are covered, so a chest keeps its identity-based inventory across a chunk reload. This mirrors how NeoForge already specialises BlockEntityColossalChestNeoForge.

NeoForge and Forge keep using the inventories exactly as they were.

The underlying problem is in SimpleInventoryCommon itself: a mutable, content-based hashCode/equals makes it unusable as a map key for any mod using it. CyclopsMC/CyclopsCore#242 fixes that at the source, which will make this loader-specific workaround redundant once ColossalChests moves off the pinned cyclopscore_version=1.25.3-637.

Three smaller changes come along:

  • The ItemStorage.SIDED providers now look up the storage without a direction. These inventories expose all slots to every side and accept everything through every face, so a direction only made Fabric build a fresh sided wrapper — an int[slots] plus one wrapper object per slot — on every lookup. For a chest with tens of thousands of slots that is a lot of garbage per hopper tick.
  • UncolossalChest gains a block entity supplier constructor argument, like ColossalChest already had, and with it an instance codec in place of the public static final CODEC. The uncolossal chest's inventory is now created lazily, so the overridable factory is not called from the constructor.
  • isFabric() in GameTestsCommon had no callers left after re-enabling the tests below, so it is removed.

One incidental behaviour change: the creative-mode debug inventory (GeneralConfig.creativeChests) now goes through the same factory as the normal one, so it gains the open/close hooks it was missing.

Reproduction and tests

New game test GameTestsFabric#testColossalItemStorageIsPerChest builds two 3×3×3 chests, looks up the second chest's storage first, then inserts 64 apples through the first chest's storage. Without the fix the apples land in the second chest:

gametestsfabric.testcolossalitemstorageisperchest failed! Inserted items did not end up in the targeted chest

The three Fabric hopper game tests that were disabled with "For some unknown reason, this test does not work in Fabric" were failing for this same reason, and are re-enabled. With the fix reverted, testColossalWood5x5HopperInsert fails too; with the fix all 95 game tests pass.

Verified with ./gradlew :loader-fabric:build and ./gradlew :loader-fabric:runGameTestServer against an unmodified CyclopsCore 1.25.3, so the tests exercise this fix and not the CyclopsCore one. The NeoForge and Forge loaders could not be built in this environment (their CyclopsCore and CommonCapabilities artifacts need GitHub Packages credentials); they are untouched by this change beyond the shared plumbing above, and CI covers them.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wbskd84GPZPE1rB7spHkdn

Closes #208

Fabric's InventoryStorage keeps a global Container-to-storage cache whose
keys are compared with equals/hashCode. CyclopsCore's SimpleInventoryCommon
considers two inventories with equally-sized and equal contents to be equal,
and its hash code is a counter that changes on every modification. Two empty
chests of the same size therefore collided in that cache, and both were
handed the storage of whichever chest was looked up first: items inserted
into one chest silently ended up in the other one, and reading a chest's
contents returned the other chest's items.

Chest inventories now identify by instance instead of by contents, so every
chest gets its own storage. This also fixes the Fabric hopper insertion game
tests that were disabled for an "unknown reason", which failed for exactly
this reason.

While here, look up the storage without a direction. Our inventories expose
all their slots to every side, so a direction only made Fabric allocate a
sided wrapper per slot on every single lookup, which is costly for chests
with tens of thousands of slots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbskd84GPZPE1rB7spHkdn
Only Fabric needs chest inventories to be identified by instance, because
only its InventoryStorage caches wrappers in a map keyed on the container.
NeoForge and Forge build a fresh InvWrapper per lookup, so they can keep
using the inventories as they were.

The anonymous inventories of both chests become named classes in
loader-common, and the block entities create them through overridable
factory methods. The Fabric loader subclasses those inventories to compare
by identity, and its blocks and block entity types now create the Fabric
block entities, mirroring how NeoForge already specialises its own.

UncolossalChest gains a block entity supplier constructor argument, like
ColossalChest already had, and with it an instance codec instead of a
static one. Its uncolossal chest inventory is now created lazily, so that
the overridable factory is not called from the constructor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wbskd84GPZPE1rB7spHkdn
@rubensworks
rubensworks merged commit cf5a301 into master-1.21-lts Sep 7, 2026
3 checks passed
@rubensworks
rubensworks deleted the fix-208-fabric-chest-item-storage-aliasing branch September 7, 2026 15:06
rubensworks added a commit that referenced this pull request Sep 7, 2026
Ports the Fabric item-storage fix (#209, closes #208) to the MC 26 APIs:
* CyclopsCore dropped the `Common` suffix (SimpleInventory, LargeInventory,
  IndexedInventory, CyclopsBlockEntity, BlockWithEntityGui).
* Inventory open/close callbacks now take ContainerUser instead of Player.
* Block entity NBT goes through ValueInput/ValueOutput.
* Fabric's InventoryStorage is now ContainerStorage; its wrapper cache is
  still keyed on the container with equals, so the identity-based
  equals/hashCode overrides are still required.
* UncolossalChestConfig gained an overridable getBlockEntitySupplier(),
  since the 26 block constructor lambda takes (config, properties).
* Registered GameTestsFabric via getGameTestClasses(), which replaced the
  fabric.mod.json gametest entrypoint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122pkULdS2VrgpHNRUpZutV
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant