Skip to content

Take the maximum stack size from the slot when extracting from large slots - #49

Merged
rubensworks merged 1 commit into
master-1.21-ltsfrom
fix/large-slot-extraction-stack-size
Sep 8, 2026
Merged

rubensworks merged 1 commit into
master-1.21-ltsfrom
fix/large-slot-extraction-stack-size

Conversation

@rubensworks

@rubensworks rubensworks commented Sep 7, 2026

Copy link
Copy Markdown
Member

Problem

IngredientComponentStorageWrapperHandlerItemStack.ComponentStorageWrapper.storageExtractItem is the workaround for inventories whose slots hold more than a vanilla stack (added for CyclopsMC/IntegratedCrafting#106, Sophisticated Barrels). It read its "one vanilla stack" value from ItemStack.EMPTY:

int maxStackSize = ItemStack.EMPTY.getMaxStackSize();

Since the data component rewrite that returns 1, not 64. ItemStack.getComponents() returns DataComponentMap.EMPTY for an empty stack, so NeoForge's IItemExtension.getMaxStackSize(stack) falls through to getOrDefault(DataComponents.MAX_STACK_SIZE, 1). Confirmed at runtime on 1.21.1 / NeoForge 21.1.247.

Two consequences:

  1. The branch now triggers for practically every extraction, since amount > 1 && getSlotLimit(slot) > 1 holds for any ordinary chest.
  2. The non-simulated path loops on Math.min(amount - count, maxStackSize), so it pulls one item per extractItem call. Measured with a counting handler: draining 1000 items out of one 4096-capacity slot took 1001 calls where 16 would do.

Items that stack below 64 were affected the same way even before this, since the constant never matched their actual cap. 100 ender pearls took ~101 calls rather than 7.

Cost

Measured against real block entities: extracting 1024 oak planks in a single extract call, 1 warmup then 3 timed runs, reported as the mean per run, whole configuration repeated twice. Before = stock 2.11.5-363, after = this branch published locally as 2.11.5-DEV; the mod list in each log confirms which jar loaded.

storage before after speedup
Sophisticated Storage limited_diamond_barrel_1 (1 slot, 8192 capacity) 7.87 ms / 7.74 ms 0.43 ms / 0.37 ms ~20x
vanilla chest (27 slots x 64) 2.79 ms / 2.06 ms 0.18 ms / 0.20 ms ~11-15x

At the old rate, draining a full 8192-capacity barrel slot in one extraction extrapolates to roughly 62 ms, more than a whole 50 ms server tick, for a single call. After the change that is about 3 ms.

Vanilla chests were affected too, ~11-15x, which confirms the branch was triggering for essentially every extraction rather than only for oversized slots. This was never drawer-specific.

Caveat on the numbers: n is small and these are dev-environment measurements on a single machine, so treat them as an order of magnitude rather than a precise figure. The variance across repeats is small relative to the effect.

Change

Take the maximum stack size from the stack in the slot, which is what item handlers actually cap a single extraction at (ItemStackHandler.extractItem does Math.min(amount, existing.getMaxStackSize())). An empty slot falls through to the plain extractItem as before, rather than short-circuiting, so handlers with an unusual getStackInSlot keep their old behaviour.

Tests

Two additions to TestItemStackComponentStorageWrapper, using a counting ItemStackHandler with a drawer-like slot limit:

  • testExtractLargeUsesFullStacksPerHandlerCall: 1000 apples out of one slot, asserts the extraction is correct and takes fewer than 40 handler calls.
  • testExtractLargeUsesFullStacksPerHandlerCallForSmallStacks: the same for ender pearls, which stack to 16.

Both fail on master-1.21-lts and pass with the change. The existing testExtractLarge and testExtractLargePartial pass either way, since the old code was correct, just slow.

Validation

  • ./gradlew build passes
  • ./gradlew runGameTestServer --rerun passes, 47 game tests

Note for anyone building this locally: src/main/java/org/cyclops/commoncapabilities/api is a git submodule, and compileJava fails with 223 errors until git submodule update --init has been run. That is unrelated to this change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ACuxaa5QeiZ8mVsEQtwSq1

…slots

The workaround for inventories with slots larger than a vanilla stack read
its maximum stack size from ItemStack.EMPTY. Since the component rewrite an
empty stack carries no MAX_STACK_SIZE component, so that reports 1 rather
than 64.

As a result the branch triggered for practically every extraction, and the
non-simulated path pulled a single item per extractItem call: draining 1000
items out of one slot took 1001 calls instead of 16. Items that stack below
64, such as ender pearls, were affected the same way.

The value now comes from the stack in the slot, which is what item handlers
actually cap a single extraction at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACuxaa5QeiZ8mVsEQtwSq1
@coveralls

coveralls commented Sep 7, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 69.684% (+35.2%) from 34.51% — fix/large-slot-extraction-stack-size into master-1.21-lts

Copy link
Copy Markdown
Member Author

Wall-clock measurement, replacing the caveat in the description

The PR description said the call count was measured but the wall-clock cost was not, since my counting handler was in-memory. I have now measured it against real block entities, so that caveat no longer applies.

Setup: game test in Integrated Crafting, extracting 1024 oak planks in a single extract call through IngredientComponentStorageWrapperHandlerItemStack from a real block entity's IItemHandler. 1 warmup, then 3 timed runs, reported as the mean per run. Whole configuration repeated twice. Dev environment, single machine, deobf jars. Before = stock 2.11.5-363, after = this branch published locally as 2.11.5-DEV; the mod list in each log confirms which jar loaded.

storage before after speedup
Sophisticated Storage limited_diamond_barrel_1 (1 slot, 8192 capacity) 7.87 ms / 7.74 ms 0.43 ms / 0.37 ms ~20x
vanilla chest (27 slots x 64) 2.79 ms / 2.06 ms 0.18 ms / 0.20 ms ~11-15x

Two things worth pulling out:

  1. The drawer case is worse than a tick. At the old rate, draining 8192 items out of one barrel slot in a single extraction extrapolates to roughly 62 ms, more than a whole 50 ms server tick, for one call. After the change that is about 3 ms.

  2. Vanilla chests were affected too, ~11-15x, which confirms the analysis that the "large slot" branch was triggering for essentially every extraction rather than only for oversized slots. This was never drawer-specific.

So this is a real performance fix rather than only a correctness tidy-up, and it scales with how much autocrafting pulls per operation. The variance across repeats is small relative to the effect, though n is small and these are dev-environment numbers, so treat them as an order of magnitude rather than a precise figure.


Generated by Claude Code

@rubensworks
rubensworks merged commit 8963d58 into master-1.21-lts Sep 8, 2026
5 checks passed
@rubensworks
rubensworks deleted the fix/large-slot-extraction-stack-size branch September 8, 2026 17:49
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.

3 participants