Fix items disappearing when inserted into chests on Fabric - #209
Merged
rubensworks merged 2 commits intoSep 7, 2026
Merged
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #208
Root cause
Fabric's
InventoryStorage.of(container, direction)keeps a globalContainer→ storage cache:MapMaker().weakValues()uses equals-based key semantics. CyclopsCore'sSimpleInventoryCommon(the base of every chest inventory here) overridesequals/hashCode: two inventories are equal when they have the same size, the same stack limit and the same item types per slot, andhashCode()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
InvWrapperper lookup, with no shared cache.Fix
Chest inventories are identified by instance instead of by contents, on Fabric only:
loader-common(InventoryColossalChest,InventoryUncolossalChest), and the block entities create them through overridable factory methods.loader-fabricsubclasses those with identityequals/hashCode, and its blocks and block entity types now create Fabric block entities that use them. Both the block'snewBlockEntityand 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 specialisesBlockEntityColossalChestNeoForge.NeoForge and Forge keep using the inventories exactly as they were.
The underlying problem is in
SimpleInventoryCommonitself: a mutable, content-basedhashCode/equalsmakes 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 pinnedcyclopscore_version=1.25.3-637.Three smaller changes come along:
ItemStorage.SIDEDproviders 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 — anint[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.UncolossalChestgains a block entity supplier constructor argument, likeColossalChestalready had, and with it an instancecodecin place of thepublic static final CODEC. The uncolossal chest's inventory is now created lazily, so the overridable factory is not called from the constructor.isFabric()inGameTestsCommonhad 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#testColossalItemStorageIsPerChestbuilds 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: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,
testColossalWood5x5HopperInsertfails too; with the fix all 95 game tests pass.Verified with
./gradlew :loader-fabric:buildand./gradlew :loader-fabric:runGameTestServeragainst 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