Skip to content

refactor: implement ISlottedItemStorage in Container and NeoForge storage#400

Merged
Indemnity83 merged 3 commits into
mc/26.1from
fix/neoforge-common-item-storage-virtual-slot
May 31, 2026
Merged

refactor: implement ISlottedItemStorage in Container and NeoForge storage#400
Indemnity83 merged 3 commits into
mc/26.1from
fix/neoforge-common-item-storage-virtual-slot

Conversation

@Indemnity83
Copy link
Copy Markdown
Owner

@Indemnity83 Indemnity83 commented May 30, 2026

Summary

This update introduces significant enhancements to the item storage capabilities within the logistics system. The implementation of the ISlottedItemStorage interface allows for more efficient and organized item management in containers, enhancing the flexibility of item insertion and extraction while maintaining compatibility with existing systems.

Changes

  • New Interface Implementation

    • Added ISlottedItemStorage interface to define a contract for item storages with addressable slots.
    • Updated ContainerItemStorage class to implement ISlottedItemStorage, allowing for operations on specific slots.
  • Item Insertion and Extraction

    • Refined the methods for inserting and extracting items to target specific slots while respecting the container's restrictions and capacity.
    • Integrated slot awareness into item management, resulting in improved behavior when interacting with storage systems.
  • Enhanced Testing

    • Established new unit tests for ContainerItemStorage to validate the new slot-based item operations.
    • Added test cases to ensure that the slot management operates as expected, including coverage for different item types and requested amounts.

Notes

  • This change is expected to enhance the modularity of the item management system, creating potential for improved implementation in future updates and user-configurable storage solutions.
  • Ensure that existing item storages are reviewed for compatibility with the new interface where necessary.
  • Closes NeoForge common item storage exposes only one virtual slot #394

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c34b1c57-3f6c-45d7-abc7-0089e67ca65e

📥 Commits

Reviewing files that changed from the base of the PR and between 855088e and 531adea.

📒 Files selected for processing (1)
  • neoforge/src/test/java/com/logistics/neoforge/storage/NeoForgeItemStorageTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • neoforge/src/test/java/com/logistics/neoforge/storage/NeoForgeItemStorageTest.java

📝 Walkthrough

Walkthrough

Adds ISlottedItemStorage and updates ContainerItemStorage to implement slotCount/slotView and slot-scoped insert/extract (exposed→actual slot mapping, face permission checks, simulation semantics). NeoForgeItemStorage.asNeoForge now returns a SlottedCommonItemHandler for ISlottedItemStorage inputs, backed by a SnapshotJournal that tracks per-(slot,item) pending deltas and applies net changes on commit. Adds unit tests for container and NeoForge adapters and adjusts NeoForge test classpath in Gradle.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SlottedCommonItemHandler
  participant SnapshotJournal
  participant ISlottedItemStorage

  Client->>SlottedCommonItemHandler: insert(slot, item, amount, simulate/tx)
  SlottedCommonItemHandler->>SnapshotJournal: record pending delta(slot,item,+amount)
  SlottedCommonItemHandler->>SnapshotJournal: simulate availability vs pending
  SnapshotJournal-->>SlottedCommonItemHandler: simulated result
  alt commit
    SlottedCommonItemHandler->>ISlottedItemStorage: insert(actualSlot, item, netAmount, simulate=false)
    ISlottedItemStorage-->>SlottedCommonItemHandler: applied amount
    SlottedCommonItemHandler->>SnapshotJournal: clear pending deltas
  end
Loading

Possibly related PRs

  • Indemnity83/logistics#349: Prior changes to the core IItemStorage/ContainerItemStorage abstraction that this PR extends with slot-aware APIs.
  • Indemnity83/logistics#369: Earlier NeoForge adapter changes (NeoForgeItemStorage) that this PR augments to detect and adapt ISlottedItemStorage.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing ISlottedItemStorage interface in container and NeoForge storage components.
Description check ✅ Passed The description clearly relates to the changeset, explaining the introduction of ISlottedItemStorage interface, slot-based operations, and testing additions.
Linked Issues check ✅ Passed The PR successfully addresses #394's requirement by introducing ISlottedItemStorage interface, implementing it in ContainerItemStorage with slot-scoped insert/extract, and adapting NeoForge storage to preserve slot identities via SlottedCommonItemHandler while maintaining fallback for non-slotted storage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the stated objectives: new interface definition, implementations in container and NeoForge storage, and corresponding test coverage. Minor build.gradle adjustment supports test compilation of new code.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/neoforge-common-item-storage-virtual-slot

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
common/src/main/java/com/logistics/core/lib/storage/ContainerItemStorage.java (1)

171-179: ⚡ Quick win

Cache sided slot array once in actualSlot for consistent bounds+mapping.

This avoids repeated getSlotsForFace(side) lookups and keeps bounds checks tied to the exact same array used for index mapping.

Proposed refactor
     private int actualSlot(int exposedSlot) {
-        if (exposedSlot < 0 || exposedSlot >= slotCount()) {
-            throw new IndexOutOfBoundsException(exposedSlot);
-        }
         if (side != null && container instanceof WorldlyContainer wc) {
-            return wc.getSlotsForFace(side)[exposedSlot];
+            int[] slots = wc.getSlotsForFace(side);
+            if (exposedSlot < 0 || exposedSlot >= slots.length) {
+                throw new IndexOutOfBoundsException(exposedSlot);
+            }
+            return slots[exposedSlot];
         }
+        if (exposedSlot < 0 || exposedSlot >= container.getContainerSize()) {
+            throw new IndexOutOfBoundsException(exposedSlot);
+        }
         return exposedSlot;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@common/src/main/java/com/logistics/core/lib/storage/ContainerItemStorage.java`
around lines 171 - 179, The actualSlot method currently calls
wc.getSlotsForFace(side) twice which can yield inconsistent bounds vs mapping;
cache the int[] slots = wc.getSlotsForFace(side) in actualSlot when container
instanceof WorldlyContainer and side != null, validate exposedSlot against
slots.length (instead of slotCount()), and then return slots[exposedSlot]; keep
the existing fallback return exposedSlot and the initial negative/upper bound
check for non-sided cases, updating checks to use the cached slots array so
bounds and mapping use the same data.
common/src/main/java/com/logistics/core/lib/storage/ISlottedItemStorage.java (1)

16-44: ⚡ Quick win

Document slot bounds and invalid-amount behavior in the public API.

Please specify contract behavior for out-of-range slot values and maxAmount <= 0 (e.g., throw IndexOutOfBoundsException vs return 0) so implementations/adapters stay consistent.

Proposed doc update
     /**
      * Read a single slot.
      *
      * `@param` slot the exposed slot index
      * `@return` the non-empty slot view, or {`@code` null} when the slot is empty
+     * `@throws` IndexOutOfBoundsException when {`@code` slot} is outside {`@code` [0, slotCount())}
      */
     `@Nullable` IItemView slotView(int slot);

     /**
      * Insert into a single exposed slot.
@@
      * `@param` maxAmount the maximum number of items to insert
      * `@param` simulate  whether to leave backing state unchanged
      * `@return` the amount inserted, or that would be inserted when simulating
+     * `@throws` IndexOutOfBoundsException when {`@code` slot} is outside {`@code` [0, slotCount())}
      */
     long insert(int slot, IItemKey item, long maxAmount, boolean simulate);

     /**
      * Extract from a single exposed slot.
@@
      * `@param` maxAmount the maximum number of items to extract
      * `@param` simulate  whether to leave backing state unchanged
      * `@return` the amount extracted, or that would be extracted when simulating
+     * `@throws` IndexOutOfBoundsException when {`@code` slot} is outside {`@code` [0, slotCount())}
      */
     long extract(int slot, IItemKey item, long maxAmount, boolean simulate);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/src/main/java/com/logistics/core/lib/storage/ISlottedItemStorage.java`
around lines 16 - 44, Update the Javadoc for ISlottedItemStorage's methods
(slotView, insert, extract) to explicitly document bounds and invalid-amount
behavior: state that an out-of-range slot index will throw
IndexOutOfBoundsException (rather than returning null or silently ignoring), and
that a non-positive maxAmount (maxAmount <= 0) is treated as a no-op and the
method returns 0 (no items inserted/extracted) even when simulate is true; keep
existing contract that slotView returns null for an empty slot but still throws
IndexOutOfBoundsException for invalid slot indexes.
neoforge/src/main/java/com/logistics/neoforge/storage/NeoForgeItemStorage.java (1)

136-138: 💤 Low value

Minor inefficiency: redundant map lookup after finding key.

firstPositivePending already iterates the map and finds the entry value, but then getAmountAsLong performs another lookup. Consider returning the amount directly from firstPositivePending or caching it.

♻️ Possible optimization
-        `@Nullable`
-        private SlotItemKey firstPositivePending(int slot) {
+        private long firstPositivePendingAmount(int slot) {
             for (var entry : pendingDeltas.entrySet()) {
                 if (entry.getKey().slot() == slot && entry.getValue() > 0) {
-                    return entry.getKey();
+                    return entry.getValue();
                 }
             }
-            return null;
+            return 0;
         }

Then update callers accordingly, or keep the current approach if the key is needed elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@neoforge/src/main/java/com/logistics/neoforge/storage/NeoForgeItemStorage.java`
around lines 136 - 138, firstPositivePending currently finds a SlotItemKey with
a positive pending delta but callers such as getAmountAsLong immediately do a
second lookup in pendingDeltas; change firstPositivePending (or add an overload)
to return the amount directly (e.g., return Long or OptionalLong alongside the
SlotItemKey) so callers like getAmountAsLong can use the returned amount without
a redundant pendingDeltas.getOrDefault call, and update any callers of
firstPositivePending/getAmountAsLong accordingly to consume the amount-returning
API.
neoforge/src/test/java/com/logistics/neoforge/storage/NeoForgeItemStorageTest.java (1)

17-37: ⚡ Quick win

Consider adding tests for insert/extract operations and transaction semantics.

The current tests verify slot count detection and empty slot behavior. Consider adding tests for:

  • Insert/extract with actual item resources and amounts
  • Transaction commit vs simulation (rollback)
  • Out-of-bounds index handling (should throw IndexOutOfBoundsException)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@neoforge/src/test/java/com/logistics/neoforge/storage/NeoForgeItemStorageTest.java`
around lines 17 - 37, Add tests exercising insert/extract and transaction
behavior using NeoForgeItemStorage.asNeoForge: create handlers from a populated
slotted storage (EmptySlottedStorage -> replace with a small test storage filled
via handler.insert) and from non-slotted storage (EmptyStorage) and assert
insert returns expected inserted amount, getResource/getAmountAsLong reflect
committed values after a committed Transaction and do not change after a
simulated Transaction (rollback). Also add tests that calling
handler.insert/handler.extract with an out-of-range index (e.g., index -1 or
index >= handler.size()) throws IndexOutOfBoundsException. Reference
ResourceHandler methods size(), insert(...), extract(...), getResource(int),
getAmountAsLong(int) and
NeoForgeItemStorage.asNeoForge/EmptySlottedStorage/EmptyStorage when adding
these tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@common/src/main/java/com/logistics/core/lib/storage/ContainerItemStorage.java`:
- Around line 171-179: The actualSlot method currently calls
wc.getSlotsForFace(side) twice which can yield inconsistent bounds vs mapping;
cache the int[] slots = wc.getSlotsForFace(side) in actualSlot when container
instanceof WorldlyContainer and side != null, validate exposedSlot against
slots.length (instead of slotCount()), and then return slots[exposedSlot]; keep
the existing fallback return exposedSlot and the initial negative/upper bound
check for non-sided cases, updating checks to use the cached slots array so
bounds and mapping use the same data.

In
`@common/src/main/java/com/logistics/core/lib/storage/ISlottedItemStorage.java`:
- Around line 16-44: Update the Javadoc for ISlottedItemStorage's methods
(slotView, insert, extract) to explicitly document bounds and invalid-amount
behavior: state that an out-of-range slot index will throw
IndexOutOfBoundsException (rather than returning null or silently ignoring), and
that a non-positive maxAmount (maxAmount <= 0) is treated as a no-op and the
method returns 0 (no items inserted/extracted) even when simulate is true; keep
existing contract that slotView returns null for an empty slot but still throws
IndexOutOfBoundsException for invalid slot indexes.

In
`@neoforge/src/main/java/com/logistics/neoforge/storage/NeoForgeItemStorage.java`:
- Around line 136-138: firstPositivePending currently finds a SlotItemKey with a
positive pending delta but callers such as getAmountAsLong immediately do a
second lookup in pendingDeltas; change firstPositivePending (or add an overload)
to return the amount directly (e.g., return Long or OptionalLong alongside the
SlotItemKey) so callers like getAmountAsLong can use the returned amount without
a redundant pendingDeltas.getOrDefault call, and update any callers of
firstPositivePending/getAmountAsLong accordingly to consume the amount-returning
API.

In
`@neoforge/src/test/java/com/logistics/neoforge/storage/NeoForgeItemStorageTest.java`:
- Around line 17-37: Add tests exercising insert/extract and transaction
behavior using NeoForgeItemStorage.asNeoForge: create handlers from a populated
slotted storage (EmptySlottedStorage -> replace with a small test storage filled
via handler.insert) and from non-slotted storage (EmptyStorage) and assert
insert returns expected inserted amount, getResource/getAmountAsLong reflect
committed values after a committed Transaction and do not change after a
simulated Transaction (rollback). Also add tests that calling
handler.insert/handler.extract with an out-of-range index (e.g., index -1 or
index >= handler.size()) throws IndexOutOfBoundsException. Reference
ResourceHandler methods size(), insert(...), extract(...), getResource(int),
getAmountAsLong(int) and
NeoForgeItemStorage.asNeoForge/EmptySlottedStorage/EmptyStorage when adding
these tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b181f40c-d98f-4c12-8448-d383f11defd4

📥 Commits

Reviewing files that changed from the base of the PR and between 33f95ee and c825631.

📒 Files selected for processing (6)
  • common/src/main/java/com/logistics/core/lib/storage/ContainerItemStorage.java
  • common/src/main/java/com/logistics/core/lib/storage/ISlottedItemStorage.java
  • common/src/test/java/com/logistics/core/lib/storage/ContainerItemStorageTest.java
  • neoforge/build.gradle
  • neoforge/src/main/java/com/logistics/neoforge/storage/NeoForgeItemStorage.java
  • neoforge/src/test/java/com/logistics/neoforge/storage/NeoForgeItemStorageTest.java

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
neoforge/src/test/java/com/logistics/neoforge/storage/NeoForgeItemStorageTest.java (1)

31-46: ⚡ Quick win

Consider consolidating the duplicate boundary tests.

Both slottedHandler_rejectsOutOfRangeTransferIndexes and nonSlottedHandler_rejectsOutOfRangeTransferIndexes test identical boundary conditions with only the storage type differing. Refactoring to a @ParameterizedTest with a @MethodSource would eliminate duplication and make it easier to add new storage variants in the future.

♻️ Example refactor using parameterized test
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import java.util.stream.Stream;
+
-@Test
-@DisplayName("slotted handler rejects out-of-range transfer indexes")
-void slottedHandler_rejectsOutOfRangeTransferIndexes() {
-    ResourceHandler<ItemResource> handler = NeoForgeItemStorage.asNeoForge(new EmptySlottedStorage(1));
-
-    try (Transaction tx = Transaction.openRoot()) {
-        assertThatThrownBy(() -> handler.insert(-1, ItemResource.EMPTY, 1, tx))
-                .isInstanceOf(IndexOutOfBoundsException.class);
-        assertThatThrownBy(() -> handler.insert(handler.size(), ItemResource.EMPTY, 1, tx))
-                .isInstanceOf(IndexOutOfBoundsException.class);
-        assertThatThrownBy(() -> handler.extract(-1, ItemResource.EMPTY, 1, tx))
-                .isInstanceOf(IndexOutOfBoundsException.class);
-        assertThatThrownBy(() -> handler.extract(handler.size(), ItemResource.EMPTY, 1, tx))
-                .isInstanceOf(IndexOutOfBoundsException.class);
-    }
-}
-
-@Test
-@DisplayName("non-slotted handler rejects out-of-range transfer indexes")
-void nonSlottedHandler_rejectsOutOfRangeTransferIndexes() {
-    ResourceHandler<ItemResource> handler = NeoForgeItemStorage.asNeoForge(new EmptyStorage());
+@ParameterizedTest(name = "{0}")
+@MethodSource("storageVariants")
+void handler_rejectsOutOfRangeTransferIndexes(String displayName, IItemStorage storage) {
+    ResourceHandler<ItemResource> handler = NeoForgeItemStorage.asNeoForge(storage);
 
     try (Transaction tx = Transaction.openRoot()) {
         assertThatThrownBy(() -> handler.insert(-1, ItemResource.EMPTY, 1, tx))
                 .isInstanceOf(IndexOutOfBoundsException.class);
         assertThatThrownBy(() -> handler.insert(handler.size(), ItemResource.EMPTY, 1, tx))
                 .isInstanceOf(IndexOutOfBoundsException.class);
         assertThatThrownBy(() -> handler.extract(-1, ItemResource.EMPTY, 1, tx))
                 .isInstanceOf(IndexOutOfBoundsException.class);
         assertThatThrownBy(() -> handler.extract(handler.size(), ItemResource.EMPTY, 1, tx))
                 .isInstanceOf(IndexOutOfBoundsException.class);
     }
 }
+
+private static Stream<org.junit.jupiter.params.provider.Arguments> storageVariants() {
+    return Stream.of(
+        org.junit.jupiter.params.provider.Arguments.of("slotted handler", new EmptySlottedStorage(1)),
+        org.junit.jupiter.params.provider.Arguments.of("non-slotted handler", new EmptyStorage())
+    );
+}

Also applies to: 58-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@neoforge/src/test/java/com/logistics/neoforge/storage/NeoForgeItemStorageTest.java`
around lines 31 - 46, Consolidate the two duplicate tests into a single
`@ParameterizedTest` that takes a ResourceHandler<ItemResource> (or a factory)
from a `@MethodSource`; replace the existing
slottedHandler_rejectsOutOfRangeTransferIndexes and
nonSlottedHandler_rejectsOutOfRangeTransferIndexes with one method (e.g.,
outOfRangeTransferIndexes) that, given a handler produced by MethodSource
(provide handlers like NeoForgeItemStorage.asNeoForge(new
EmptySlottedStorage(1)) and NeoForgeItemStorage.asNeoForge(new
EmptyNonSlottedStorage(1))), opens Transaction.openRoot() and asserts that
handler.insert(-1,...), handler.insert(handler.size(),...),
handler.extract(-1,...), and handler.extract(handler.size(),...) each throw
IndexOutOfBoundsException (use ItemResource.EMPTY and the same quantity and tx);
add the MethodSource provider method to return the two handler instances for
reuse.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@neoforge/src/test/java/com/logistics/neoforge/storage/NeoForgeItemStorageTest.java`:
- Around line 31-46: Consolidate the two duplicate tests into a single
`@ParameterizedTest` that takes a ResourceHandler<ItemResource> (or a factory)
from a `@MethodSource`; replace the existing
slottedHandler_rejectsOutOfRangeTransferIndexes and
nonSlottedHandler_rejectsOutOfRangeTransferIndexes with one method (e.g.,
outOfRangeTransferIndexes) that, given a handler produced by MethodSource
(provide handlers like NeoForgeItemStorage.asNeoForge(new
EmptySlottedStorage(1)) and NeoForgeItemStorage.asNeoForge(new
EmptyNonSlottedStorage(1))), opens Transaction.openRoot() and asserts that
handler.insert(-1,...), handler.insert(handler.size(),...),
handler.extract(-1,...), and handler.extract(handler.size(),...) each throw
IndexOutOfBoundsException (use ItemResource.EMPTY and the same quantity and tx);
add the MethodSource provider method to return the two handler instances for
reuse.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9e40bc84-382f-4d41-8399-f2751c7b3f55

📥 Commits

Reviewing files that changed from the base of the PR and between c825631 and 855088e.

📒 Files selected for processing (4)
  • common/src/main/java/com/logistics/core/lib/storage/ContainerItemStorage.java
  • common/src/main/java/com/logistics/core/lib/storage/ISlottedItemStorage.java
  • neoforge/src/main/java/com/logistics/neoforge/storage/NeoForgeItemStorage.java
  • neoforge/src/test/java/com/logistics/neoforge/storage/NeoForgeItemStorageTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • common/src/main/java/com/logistics/core/lib/storage/ISlottedItemStorage.java
  • common/src/main/java/com/logistics/core/lib/storage/ContainerItemStorage.java
  • neoforge/src/main/java/com/logistics/neoforge/storage/NeoForgeItemStorage.java

@Indemnity83 Indemnity83 merged commit 7638c98 into mc/26.1 May 31, 2026
6 checks passed
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.

NeoForge common item storage exposes only one virtual slot

1 participant