From 03a3080223c4e47a262cbdc07e0377a616a70f54 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:29:17 +0100 Subject: [PATCH 1/4] chore: continue compatibility maintenance From 0fdf0f9f6210533729517d442c372a19c9d98509 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:34:14 +0100 Subject: [PATCH 2/4] feat(inventory): bind definition-owned storage filters Resolve retained storage policy maps through the native descriptor matcher and keep lazy name resolution on the selected definition. Apply available filters and nesting admission to ordinary Java bundle insertion before prediction, with the existing fallback for unverified expression contexts. Replay all 107 core patches. Verify 2,106 executed core tests, both Checkstyle tasks and the first-patch PR projection. Native physical-slot request execution and installed controller validation remain unfinished. --- ...ain-bedrock-creative-catalog-entries.patch | 540 +++++++++++++++++- ...ain-bedrock-creative-catalog-entries.pr.md | 4 + ...bedrock-merchant-inventories-in-core.patch | 4 +- 3 files changed, 533 insertions(+), 15 deletions(-) diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch index b92009f8..01f7b76d 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch @@ -1,4 +1,4 @@ -From 89148c3e1b51206ca3a78d2a275352da5a902ead Mon Sep 17 00:00:00 2001 +From abdeebfd9ffd85d1b428f129e91d793ce3005a8a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(viabedrock): retain Bedrock creative catalog entries @@ -324,7 +324,7 @@ An empty cursor admits that same craft and output uses the freed source slot. Targeted request/state tests preserve these boundaries and rejection rollback. Build ordinary auto-craft ingredients from their retained resolved recipe -descriptors, including wildcard auxiliary32767. Resolve tag/Molang inputs +descriptors, including wildcard auxiliary 32767. Resolve tag/Molang inputs from the selected source instead. Native protocol2225 CraftingData declares oak_log and stick as named wildcard ingredients, but wooden-sword planks as tags. The official client emits32767 for the former and concrete0 for @@ -430,23 +430,54 @@ vtable14f222710 to getter140084510 and returns true for is_container_screen. Inherit the common classification instead of gating the tooltip subtree on the workbench boolean. Authored-program replay tests cover both screen hosts; input lifetime and rendered tooltip integration remain separate work. + +Bind retained storage policy maps through the native descriptor matcher. +Preserve tags, name and item_tag precedence, named auxiliary suffixes and +per-definition lazy resolution even when numeric item IDs collide. Cache +bindings on the selected receiver; recipe-source locks do not control +storage filter matches. Banned descriptors override allowed descriptors. + +Bedrock 1.26.60.5 protocol2225 reader141a72880 and vector converter148f31150 +execute 15 map controls and one four-entry composition control. The original +default factory 141abfe00 initializes its version word to 65535. Executable +SHA256 is 988f6a61403b4381aa41e6c66d64cce441b14e561a9b10f647c2758ef3f2672a. +Name construction retains auxiliary 32767 for lazy lookup. Supplied services +capture Molang and tag construction; native compilation and the effective +post-default language context remain outside these controls. + +Require an explicit supported language context for expression bindings. +Unsupported expressions make the entire policy unavailable rather than +silently removing a banned filter. The live caller has no proven context, +so it binds only name and item-tag policies. Ordinary Java bundle clicks use +those filters and nesting admission before prediction, retaining their +existing fallback when binding is unavailable. Their weight and ordering +remain separate from native physical-slot planning and request execution. + +Regressions cover filter precedence, auxiliary values, tagged source stacks, +unknown expression contexts, unsupported banned expressions, retained +receiver identity and both accepted and rejected Java insertion directions. --- .../model/NativePlayerInventoryAction.java | 94 +++ .../model/NativePlayerInventoryMessage.java | 334 +++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ + .../api/model/container/Container.java | 4 +- .../model/container/NativeRecipeCounter.java | 119 +++ .../model/container/NativeRecipeCrafting.java | 286 ++++++++ .../container/NativeRecipeDescriptor.java | 44 ++ + .../model/container/NativeStoragePolicy.java | 103 +++ .../block/CraftingTableContainer.java | 2 +- + .../container/dynamic/BundleContainer.java | 17 +- .../resourcepack/ui/PlayerInventoryUi.java | 402 +++++++++++ .../viabedrock/protocol/BedrockProtocol.java | 1 + .../protocol/model/CreativeCatalog.java | 266 +++++++ .../model/CreativeInventoryModel.java | 198 +++++ + .../model/ItemDefinitionProperties.java | 31 + + .../protocol/model/ItemRegistry.java | 8 + .../protocol/model/PlayerRecipeContext.java | 96 +++ .../protocol/model/PlayerRecipeModel.java | 125 ++++ .../protocol/packet/InventoryPackets.java | 213 ++++-- .../protocol/packet/MultiStatePackets.java | 22 + - .../protocol/rewriter/ItemRewriter.java | 74 +- + .../protocol/rewriter/ItemRewriter.java | 79 +- .../protocol/storage/CraftingDataTracker.java | 217 +++++- .../storage/InventoryRequestTracker.java | 24 + .../protocol/storage/InventoryTracker.java | 21 + @@ -458,20 +489,23 @@ input lifetime and rendered tooltip integration remain separate work. .../container/NativeRecipeQuantityTest.java | 61 ++ .../ui/PlayerInventoryUiTest.java | 153 ++++ .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++ + .../model/NativeStoragePolicyTest.java | 113 +++ .../protocol/packet/InventoryPacketsTest.java | 266 +++++-- .../rewriter/PlayerRecipeModelTest.java | 611 ++++++++++++++++ ...egisteredPlayerRecipeAvailabilityTest.java | 5 +- .../RegisteredPlayerRecipeSearchTest.java | 3 +- + .../storage/BundleInventoryActionTest.java | 61 ++ .../storage/CreativeInventoryActionTest.java | 262 +++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ .../NativePlayerCreativeAuxiliaryTest.java | 333 +++++++++ .../storage/NativePlayerCreativePickTest.java | 138 ++++ .../storage/NativeRecipeCraftingTest.java | 500 +++++++++++++ - 36 files changed, 6233 insertions(+), 169 deletions(-) + 43 files changed, 6572 insertions(+), 172 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryMessage.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryResult.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCrafting.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeStoragePolicy.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUi.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeInventoryModel.java @@ -482,6 +516,7 @@ input lifetime and rendered tooltip integration remain separate work. create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeQuantityTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUiTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/NativeStoragePolicyTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CreativeInventoryActionTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java @@ -981,6 +1016,28 @@ index 00000000..e69bf410 + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/Container.java b/src/main/java/net/raphimc/viabedrock/api/model/container/Container.java +index 71856177..1f414860 100644 +--- a/src/main/java/net/raphimc/viabedrock/api/model/container/Container.java ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/Container.java +@@ -213,7 +213,7 @@ public abstract class Container { + if (cursorBundle != null && ((button == 0 && !item.isEmpty()) || (button == 1 && item.isEmpty()))) { + clickContext.snapshot(cursorBundle); + if (button == 0) { +- return cursorBundle.insert(container, bedrockSlot); ++ return cursorBundle.insert(container, bedrockSlot, cursorItem); + } + final BedrockItem extracted = cursorBundle.selectedItem(false); + return container.canPlaceItem(bedrockSlot, extracted, extracted.amount()) +@@ -223,7 +223,7 @@ public abstract class Container { + if (slotBundle != null) { + if (button == 0 && !cursorItem.isEmpty()) { + clickContext.snapshot(slotBundle); +- return slotBundle.insert(clickContext.inventoryTracker.getHudContainer(), 0); ++ return slotBundle.insert(clickContext.inventoryTracker.getHudContainer(), 0, item); + } + if (button == 1 && cursorItem.isEmpty()) { + clickContext.snapshot(slotBundle); diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java index fe874172..96547dc6 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java @@ -1472,6 +1529,115 @@ index 271d9fad..ca2d004b 100644 /** These are native registry/block interfaces, not a fallback policy for unavailable data. */ interface Registry { boolean hasComplexAlias(String name); +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeStoragePolicy.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeStoragePolicy.java +new file mode 100644 +index 00000000..83ed1b46 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeStoragePolicy.java +@@ -0,0 +1,103 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model.container; ++ ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.ItemStoragePolicy; ++import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeDescriptor; ++import net.raphimc.viabedrock.protocol.model.recipe.RecipeMolangExpression; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import java.util.function.Function; ++ ++/** Descriptor bindings belong to the storage definition and its connection registry. */ ++public final class NativeStoragePolicy { ++ ++ private final ItemStoragePolicy policy; ++ private final NativeRecipeDescriptor.Registry registry; ++ private final List allowed; ++ private final List banned; ++ private final Integer molangVersion; ++ ++ private NativeStoragePolicy(final ItemStoragePolicy policy, final NativeRecipeDescriptor.Registry registry, ++ final List allowed, final List banned, ++ final Integer molangVersion) { ++ this.policy = policy; ++ this.registry = registry; ++ this.allowed = List.copyOf(allowed); ++ this.banned = List.copyOf(banned); ++ this.molangVersion = molangVersion; ++ } ++ ++ /** The caller must supply a known language context; raw storage metadata does not establish one. */ ++ public static NativeStoragePolicy bind(final ItemStoragePolicy policy, final NativeRecipeDescriptor.Registry registry, ++ final Integer molangVersion) { ++ if (policy == null) { ++ return null; ++ } ++ final var allowed = descriptors(policy.allowedItems(), registry, molangVersion); ++ final var banned = descriptors(policy.bannedItems(), registry, molangVersion); ++ if (allowed == null || banned == null) { ++ return null; ++ } ++ final boolean expressions = allowed.stream().anyMatch(NativeRecipeDescriptor.Molang.class::isInstance) ++ || banned.stream().anyMatch(NativeRecipeDescriptor.Molang.class::isInstance); ++ return new NativeStoragePolicy(policy, registry, allowed, banned, expressions ? molangVersion : null); ++ } ++ ++ /** Filter admission is independent of recipe-source locks, quantity and slot permissions. */ ++ public boolean accepts(final BedrockItem item, final Function descriptor) { ++ final var candidate = descriptor.apply(item); ++ final Function snapshot = ignored -> candidate; ++ return (this.allowed.isEmpty() || this.allowed.stream().anyMatch(value -> value.matchesStack(item, this.registry, snapshot))) ++ && this.banned.stream().noneMatch(value -> value.matchesStack(item, this.registry, snapshot)); ++ } ++ ++ public int maxSlots() { ++ return this.policy.maxSlots(); ++ } ++ ++ public boolean allowNested() { ++ return this.policy.allowNested(); ++ } ++ ++ public Integer molangVersion() { ++ return this.molangVersion; ++ } ++ ++ private static List descriptors(final List> input, ++ final NativeRecipeDescriptor.Registry registry, ++ final Integer molangVersion) { ++ final List result = new ArrayList<>(input.size()); ++ for (final var fields : input) { ++ if (fields.containsKey("tags")) { ++ // The bounded tag-expression compiler targets the latest language context only. ++ // Unsupported compilation must not turn a banned predicate into an empty filter. ++ if (molangVersion == null || molangVersion != 65535) { ++ return null; ++ } ++ final var expression = RecipeMolangExpression.compile(fields.get("tags")); ++ if (expression.isEmpty()) { ++ return null; ++ } ++ result.add(new NativeRecipeDescriptor.Molang(expression.get())); ++ } else if (fields.containsKey("name")) { ++ final String name = fields.get("name"); ++ result.add(name.isEmpty() ? NativeRecipeDescriptor.Empty.INSTANCE ++ : new NativeRecipeDescriptor.Deferred(name, Short.MAX_VALUE)); ++ } else if (fields.containsKey("item_tag")) { ++ result.add(NativeRecipeDescriptor.resolveTag(fields.get("item_tag"), registry)); ++ } else { ++ // Storage vector conversion initializes each descriptor empty before reading its map. ++ result.add(NativeRecipeDescriptor.Empty.INSTANCE); ++ } ++ } ++ return result; ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java b/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java index cb10d8f5..93ee9a52 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java @@ -1485,6 +1651,47 @@ index cb10d8f5..93ee9a52 100644 this.updateOutput(); PacketFactory.sendJavaContainerSetContent(this.user, this); } +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/dynamic/BundleContainer.java b/src/main/java/net/raphimc/viabedrock/api/model/container/dynamic/BundleContainer.java +index dff59408..b6e78f0b 100644 +--- a/src/main/java/net/raphimc/viabedrock/api/model/container/dynamic/BundleContainer.java ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/dynamic/BundleContainer.java +@@ -22,6 +22,7 @@ import com.viaversion.viaversion.api.connection.UserConnection; + import com.viaversion.viaversion.api.minecraft.item.Item; + import com.viaversion.viaversion.util.Pair; + ++import net.raphimc.viabedrock.api.model.NativeItemStorageData; + import net.raphimc.viabedrock.api.model.container.Container; + import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerID; + import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; +@@ -146,13 +147,27 @@ public class BundleContainer extends Container { + } + + public List insert(final Container source, final int sourceSlot) { ++ return this.insert(source, sourceSlot, null); ++ } ++ ++ public List insert(final Container source, final int sourceSlot, final BedrockItem storageItem) { + final BedrockItem item = source.getItem(sourceSlot); + if (item.isEmpty() || item.netId() == null) { + return List.of(); + } + final ItemRewriter rewriter = this.user.get(ItemRewriter.class); + final String identifier = rewriter.itemIdentifier(item); +- if (identifier == null || identifier.equals("minecraft:shulker_box") || identifier.endsWith("_shulker_box")) { ++ if (storageItem != null && !this.containerName.equals(storageContainerName(storageItem))) { ++ return List.of(); ++ } ++ final var policy = storageItem != null ? rewriter.nativeStoragePolicy(storageItem) : null; ++ if (policy != null) { ++ if (!policy.accepts(item, rewriter::recipeDescriptor) ++ || !policy.allowNested() && NativeItemStorageData.containerIdentity(item) != null ++ && rewriter.getItemRegistry().storageProperties(item).complete()) { ++ return List.of(); ++ } ++ } else if (identifier == null || identifier.equals("minecraft:shulker_box") || identifier.endsWith("_shulker_box")) { + return List.of(); + } + final Set visited = new HashSet<>(); diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUi.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUi.java new file mode 100644 index 00000000..d3c9f229 @@ -2381,6 +2588,94 @@ index 00000000..e0ba0220 + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemDefinitionProperties.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemDefinitionProperties.java +index 5b4769aa..46d6e088 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemDefinitionProperties.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemDefinitionProperties.java +@@ -11,12 +11,15 @@ import com.viaversion.nbt.tag.IntTag; + import com.viaversion.nbt.tag.ListTag; + import com.viaversion.nbt.tag.StringTag; + import com.viaversion.viaversion.util.Key; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeDescriptor; ++import net.raphimc.viabedrock.api.model.container.NativeStoragePolicy; + import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; + + import java.util.ArrayList; + import java.util.LinkedHashSet; + import java.util.List; + import java.util.Locale; ++import java.util.Objects; + import java.util.function.Function; + + /** Compiled state belongs to a constructed definition, independently of its network declarations. */ +@@ -34,6 +37,10 @@ final class ItemDefinitionProperties { + private final List updates = new ArrayList<>(); + private ItemStackingProperties stacking; + private ItemStorageProperties storage = ItemStorageProperties.NONE; ++ private ItemStoragePolicy storagePolicySource; ++ private NativeStoragePolicy storagePolicy; ++ private Integer storagePolicyVersion; ++ private boolean storagePolicyAttempted; + private ItemCookingProperties cooking; + private ItemFoodProperties food = ItemFoodProperties.NONE; + private ItemBlockPlacer blockPlacer; +@@ -203,6 +210,30 @@ final class ItemDefinitionProperties { + return this.storage; + } + ++ NativeStoragePolicy storagePolicy(final NativeRecipeDescriptor.Registry registry, final Integer molangVersion) { ++ if (this.storagePolicySource != this.storage.policy()) { ++ this.storagePolicySource = this.storage.policy(); ++ this.storagePolicy = null; ++ this.storagePolicyVersion = null; ++ this.storagePolicyAttempted = false; ++ } ++ if (this.storagePolicySource == null) { ++ return null; ++ } ++ if (this.storagePolicy != null && this.storagePolicy.molangVersion() == null) { ++ return this.storagePolicy; ++ } ++ if (this.storagePolicy != null && molangVersion == null) { ++ return null; ++ } ++ if (!this.storagePolicyAttempted || !Objects.equals(this.storagePolicyVersion, molangVersion)) { ++ this.storagePolicy = NativeStoragePolicy.bind(this.storagePolicySource, registry, molangVersion); ++ this.storagePolicyVersion = molangVersion; ++ this.storagePolicyAttempted = true; ++ } ++ return this.storagePolicy; ++ } ++ + void category(final int category) { + this.category = category & 0xFF; + } +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java +index 03ebaf51..3562cb65 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemRegistry.java +@@ -7,6 +7,8 @@ package net.raphimc.viabedrock.protocol.model; + + import com.viaversion.nbt.tag.CompoundTag; + import net.raphimc.viabedrock.api.chunk.blockstate.BlockItemDescriptor; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeDescriptor; ++import net.raphimc.viabedrock.api.model.container.NativeStoragePolicy; + import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; + import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeDescriptor; + import net.raphimc.viabedrock.api.model.NativeItemNames; +@@ -176,6 +178,12 @@ public final class ItemRegistry { + return definition != null ? ((ItemDefinitionProperties) definition.receiver()).storage() : ItemStorageProperties.NONE; + } + ++ public NativeStoragePolicy storagePolicy(final BedrockItem item, final NativeRecipeDescriptor.Registry registry, ++ final Integer molangVersion) { ++ final var definition = this.definition(item); ++ return definition != null ? ((ItemDefinitionProperties) definition.receiver()).storagePolicy(registry, molangVersion) : null; ++ } ++ + public int creativeCategory(final int id) { + final var definition = this.definitions.numeric(id); + return definition != null ? definition.receiver().category() : 0; diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/PlayerRecipeContext.java b/src/main/java/net/raphimc/viabedrock/protocol/model/PlayerRecipeContext.java new file mode 100644 index 00000000..98f8cd41 @@ -2941,18 +3236,21 @@ index 885579ea..d36de2f5 100644 if (NativeCookingUiState.actionsEnabled(wrapper.user())) { try { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index 28413a56..08ed3680 100644 +index 28413a56..be6a1b36 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -@@ -53,7 +53,6 @@ import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; +@@ -51,9 +51,9 @@ import com.viaversion.nbt.tag.ByteArrayTag; + import net.raphimc.viabedrock.api.model.container.NativeItemStackMatcher; + import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; import net.raphimc.viabedrock.api.model.container.NativeRecipeDescriptor; ++import net.raphimc.viabedrock.api.model.container.NativeStoragePolicy; import net.raphimc.viabedrock.api.model.container.RecipeIngredientMatcher; import net.raphimc.viabedrock.api.model.container.Container; -import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; import net.raphimc.viabedrock.api.resourcepack.definition.ItemDefinitions; import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; -@@ -61,6 +60,10 @@ import net.raphimc.viabedrock.protocol.data.BedrockMappingData; +@@ -61,6 +61,10 @@ import net.raphimc.viabedrock.protocol.data.BedrockMappingData; import net.raphimc.viabedrock.protocol.data.ProtocolConstants; import net.raphimc.viabedrock.protocol.data.generated.bedrock.CustomItemTags; import net.raphimc.viabedrock.protocol.model.BedrockItem; @@ -2963,7 +3261,7 @@ index 28413a56..08ed3680 100644 import net.raphimc.viabedrock.protocol.model.ItemEntry; import net.raphimc.viabedrock.protocol.model.ItemStorageProperties; import net.raphimc.viabedrock.protocol.model.FullContainerName; -@@ -73,6 +76,7 @@ import net.raphimc.viabedrock.protocol.model.ItemNameContext; +@@ -73,6 +77,7 @@ import net.raphimc.viabedrock.protocol.model.ItemNameContext; import net.raphimc.viabedrock.protocol.model.ItemDescriptionContext; import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.GameType; import net.raphimc.viabedrock.protocol.storage.EntityTracker; @@ -2971,7 +3269,7 @@ index 28413a56..08ed3680 100644 import net.raphimc.viabedrock.protocol.model.ItemStackHeader; import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeDescriptor; -@@ -104,9 +108,10 @@ import java.util.HashMap; +@@ -104,9 +109,10 @@ import java.util.HashMap; import java.util.List; import java.util.HashSet; import java.util.Map; @@ -2983,7 +3281,7 @@ index 28413a56..08ed3680 100644 import java.util.logging.Level; public class ItemRewriter extends StoredObject implements NetworkItemStackContext { -@@ -138,7 +143,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -138,7 +144,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final Type optionalNewItemType; private final Type newItemArrayType; private final Type stackRequestResultType; @@ -2992,7 +3290,7 @@ index 28413a56..08ed3680 100644 static { // TODO: Add missing item nbt rewriters -@@ -591,29 +596,31 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -591,29 +597,31 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex "Translating Java items to Bedrock is not yet supported"); } @@ -3043,7 +3341,7 @@ index 28413a56..08ed3680 100644 } public BedrockItem[] bedrockItems(final Item[] javaItems) { -@@ -664,6 +671,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -664,6 +672,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex (selected, auxiliary) -> this.associatedBlock(blocks, selected, auxiliary)); } @@ -3062,7 +3360,7 @@ index 28413a56..08ed3680 100644 public BedrockItem createNamedItem(final String name, final int metadata, final int amount) { final var blocks = this.user().get(BlockStateRewriter.class); return this.registry.createNamedItem(name, metadata, amount, -@@ -714,11 +733,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -714,11 +734,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex public int playerRecipeAvailability(final Supplier> candidates, final BedrockItem output, final BedrockItem selectedOutput, final Container owner, final boolean filtering, final boolean includeCursor) { @@ -3083,6 +3381,17 @@ index 28413a56..08ed3680 100644 item -> { final ItemRecipeKey key = this.recipeKey(item); return key != null ? key.descriptorHash() : 0; +@@ -826,6 +853,10 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex + return this.registry; + } + ++ public NativeStoragePolicy nativeStoragePolicy(final BedrockItem item) { ++ return this.registry.storagePolicy(item, new NativeRecipeRegistry(this.registry, this.user().get(BlockStateRewriter.class)), null); ++ } ++ + /** Match network-loaded stack queries while their connection registry remains current. */ + public boolean matchesStackQuery(final BedrockItem query, final BedrockItem candidate) { + if (!this.registry.sameItem(query, candidate)) { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java index 1b68e72e..22d95e55 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java @@ -5249,6 +5558,125 @@ index 00000000..b420d6ae + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/NativeStoragePolicyTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/NativeStoragePolicyTest.java +new file mode 100644 +index 00000000..52ab237f +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/model/NativeStoragePolicyTest.java +@@ -0,0 +1,113 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.model; ++ ++import com.viaversion.nbt.tag.CompoundTag; ++import com.viaversion.nbt.tag.ListTag; ++import com.viaversion.nbt.tag.StringTag; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeDescriptor; ++import net.raphimc.viabedrock.api.model.container.NativeStoragePolicy; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; ++import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; ++import org.junit.jupiter.api.Test; ++ ++import java.util.List; ++import java.util.Map; ++import java.util.concurrent.atomic.AtomicReference; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class NativeStoragePolicyTest { ++ ++ @Test ++ void storageFiltersUseDescriptorAuxiliaryValuesAndBannedMatchesOverrideAllowedMatches() { ++ final var items = registry(); ++ final var context = new NativeRecipeRegistry(items, mock(BlockStateRewriter.class)); ++ final var policy = NativeStoragePolicy.bind(new ItemStoragePolicy(64, true, ++ List.of(Map.of("name", "test:accepted", "item_tag", "test:other")), ++ List.of(Map.of("name", "test:accepted:3"))), context, null); ++ assertNotNull(policy); ++ final var accepted = new BedrockItem(items.definition("test:accepted"), (short) 4, (byte) 1); ++ final var tag = new CompoundTag(); ++ tag.put("ench", new ListTag<>(CompoundTag.class)); ++ accepted.setTag(tag); ++ assertTrue(policy.accepts(accepted, item -> items.recipeDescriptor(item, null))); ++ accepted.setData((short) 3); ++ assertFalse(policy.accepts(accepted, item -> items.recipeDescriptor(item, null))); ++ assertFalse(policy.accepts(new BedrockItem(30032), item -> items.recipeDescriptor(item, null))); ++ } ++ ++ @Test ++ void expressionPrecedenceRequiresKnownVersionAndUnsupportedPredicatesCannotBecomeUnrestrictedFilters() { ++ final var items = registry(); ++ final var context = new NativeRecipeRegistry(items, mock(BlockStateRewriter.class)); ++ final var raw = new ItemStoragePolicy(64, true, ++ List.of(Map.of("tags", "q.any_tag('test:allowed')", "name", "test:other")), List.of()); ++ assertNull(NativeStoragePolicy.bind(raw, context, null)); ++ assertNull(NativeStoragePolicy.bind(raw, context, 37)); ++ final var bound = NativeStoragePolicy.bind(raw, context, 65535); ++ assertNotNull(bound); ++ assertTrue(bound.accepts(new BedrockItem(30031), item -> items.recipeDescriptor(item, null))); ++ assertFalse(bound.accepts(new BedrockItem(30032), item -> items.recipeDescriptor(item, null))); ++ assertNull(NativeStoragePolicy.bind(new ItemStoragePolicy(64, true, List.of(), ++ List.of(Map.of("tags", "q.unverified_storage_query()", "name", "test:accepted"))), context, 65535)); ++ final var byTag = NativeStoragePolicy.bind(new ItemStoragePolicy(64, true, ++ List.of(Map.of("item_tag", "test:allowed")), List.of()), context, null); ++ assertNotNull(byTag); ++ assertTrue(byTag.accepts(new BedrockItem(30031), item -> items.recipeDescriptor(item, null))); ++ assertFalse(byTag.accepts(new BedrockItem(30032), item -> items.recipeDescriptor(item, null))); ++ } ++ ++ @Test ++ void retainedDefinitionBindingsDoNotShareLazyNameResolutionAfterNumericCollision() { ++ final var policy = new CompoundTag(); ++ policy.putInt("max_slots", 64); ++ policy.putByte("allow_nested_storage_items", (byte) 1); ++ final var name = new CompoundTag(); ++ name.putString("name", "test:dynamic"); ++ final var allowed = new ListTag<>(CompoundTag.class); ++ allowed.add(name); ++ policy.put("allowed_items", allowed); ++ policy.put("banned_items", new ListTag<>(CompoundTag.class)); ++ final var components = new CompoundTag(); ++ components.put("minecraft:storage_item", policy); ++ final var items = new ItemRegistry(new ItemEntry[]{entry("test:accepted", 30031, new CompoundTag()), ++ entry("test:other", 30032, new CompoundTag()), entry("test:first", 30033, components), entry("test:second", 30033, components)}); ++ final var context = spy(new NativeRecipeRegistry(items, mock(BlockStateRewriter.class))); ++ final var target = new AtomicReference<>(new NativeRecipeDescriptor.Lookup(items.definition("test:accepted"), 0)); ++ doAnswer(ignored -> target.get()).when(context).lookupItem("test:dynamic", Short.MAX_VALUE); ++ final var retained = new BedrockItem(items.definition("test:first"), (short) 0, (byte) 1); ++ final var first = items.storagePolicy(retained, context, null); ++ assertNotNull(first); ++ assertTrue(first.accepts(new BedrockItem(30031), item -> items.recipeDescriptor(item, null))); ++ target.set(new NativeRecipeDescriptor.Lookup(items.definition("test:other"), 0)); ++ assertSame(first, items.storagePolicy(retained, context, null)); ++ assertSame(first, items.storagePolicy(retained, context, 65535)); ++ assertTrue(first.accepts(new BedrockItem(30031), item -> items.recipeDescriptor(item, null))); ++ final var second = items.storagePolicy(new BedrockItem(30033), context, null); ++ assertNotNull(second); ++ assertNotSame(first, second); ++ assertFalse(second.accepts(new BedrockItem(30031), item -> items.recipeDescriptor(item, null))); ++ assertTrue(second.accepts(new BedrockItem(30032), item -> items.recipeDescriptor(item, null))); ++ verify(context, times(2)).lookupItem("test:dynamic", Short.MAX_VALUE); ++ } ++ ++ private static ItemRegistry registry() { ++ final var tagged = new CompoundTag(); ++ final var tags = new ListTag<>(StringTag.class); ++ tags.add(new StringTag("test:allowed")); ++ tagged.put("item_tags", tags); ++ return new ItemRegistry(new ItemEntry[]{entry("test:accepted", 30031, tagged), entry("test:other", 30032, new CompoundTag())}); ++ } ++ ++ private static ItemEntry entry(final String name, final int id, final CompoundTag components) { ++ final var data = new CompoundTag(); ++ data.put("components", components); ++ return new ItemEntry(name, id, true, ItemVersion.DataDriven, data); ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java index 1cfb02f6..d8eb367f 100644 --- a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java @@ -6252,6 +6680,92 @@ index 5f911215..ed7bae20 100644 () -> fail("Unexpected empty-query name"), () -> fail("Unexpected empty-query description"))); } +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/BundleInventoryActionTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/BundleInventoryActionTest.java +index bb1e3d35..f36b4fa1 100644 +--- a/src/test/java/net/raphimc/viabedrock/protocol/storage/BundleInventoryActionTest.java ++++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/BundleInventoryActionTest.java +@@ -15,6 +15,7 @@ import com.viaversion.nbt.tag.CompoundTag; + import com.viaversion.viaversion.api.connection.UserConnection; + import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey; + import net.raphimc.viabedrock.api.model.container.Container; ++import net.raphimc.viabedrock.api.model.container.NativeStoragePolicy; + import net.raphimc.viabedrock.ViaBedrock; + import net.raphimc.viabedrock.platform.ViaBedrockPlatform; + import net.raphimc.viabedrock.api.util.PacketFactory; +@@ -28,6 +29,10 @@ import net.raphimc.viabedrock.protocol.model.BedrockItem; + import net.raphimc.viabedrock.protocol.model.FullContainerName; + import net.raphimc.viabedrock.protocol.model.ItemRegistry; + import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.ItemStoragePolicy; ++import net.raphimc.viabedrock.protocol.model.ItemStorageProperties; ++import net.raphimc.viabedrock.protocol.model.NativeRecipeRegistry; ++import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; + import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; + import net.raphimc.viabedrock.protocol.model.inventory.*; + import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; +@@ -45,6 +50,62 @@ import static org.mockito.Mockito.*; + + class BundleInventoryActionTest { + ++ @Test ++ void boundStorageFiltersAdmitAndRejectBothJavaInsertionDirectionsBeforePrediction() throws Exception { ++ for (final boolean heldOnCursor : new boolean[]{false, true}) { ++ final var f = new Fixture(); ++ final var bundle = f.bundle(0); ++ final var root = bundleItem(0); ++ final var rewriter = (ItemRewriter) f.storage.get(ItemRewriter.class); ++ final var items = rewriter.getItemRegistry(); ++ final var policy = NativeStoragePolicy.bind(new ItemStoragePolicy(64, true, List.of(), ++ List.of(Map.of("name", "minecraft:diamond"))), new NativeRecipeRegistry(items, mock(BlockStateRewriter.class)), null); ++ assertNotNull(policy); ++ when(rewriter.nativeStoragePolicy(any())).thenReturn(policy); ++ when(rewriter.recipeDescriptor(any())).thenAnswer(call -> items.recipeDescriptor(call.getArgument(0), null)); ++ final var source = heldOnCursor ? f.screen : f.inventory.getHudContainer(); ++ final int slot = heldOnCursor ? 1 : 0; ++ if (heldOnCursor) { ++ f.inventory.getHudContainer().setItem(0, root); ++ } else { ++ f.screen.setItem(0, root); ++ } ++ source.setItem(slot, item(306, 4, 4)); ++ assertNull(f.click(heldOnCursor ? 1 : 0, 0, ContainerInput.PICKUP)); ++ assertEquals(4, source.getItem(slot).amount()); ++ assertEquals(4, source.getItem(slot).netId()); ++ assertEquals(0, f.nonempty(bundle)); ++ assertFalse(f.requests.hasPending()); ++ source.setItem(slot, item(523, 3, 5)); ++ assertNotNull(f.click(heldOnCursor ? 1 : 0, 0, ContainerInput.PICKUP)); ++ assertTrue(source.getItem(slot).isEmpty()); ++ assertEquals(523, bundle.getItem(0).identifier()); ++ assertEquals(3, bundle.getItem(0).amount()); ++ assertTrue(f.requests.hasPending()); ++ verify(rewriter, times(2)).nativeStoragePolicy(same(root)); ++ } ++ } ++ ++ @Test ++ void knownNoNestingPolicyRejectsAnIdentifiedStorageItemBeforeJavaPrediction() throws Exception { ++ final var f = new Fixture(); ++ final var bundle = f.bundle(0); ++ f.bundle(1); ++ f.screen.setItem(0, bundleItem(0)); ++ f.inventory.getHudContainer().setItem(0, bundleItem(1)); ++ final var rewriter = (ItemRewriter) f.storage.get(ItemRewriter.class); ++ final var policy = NativeStoragePolicy.bind(new ItemStoragePolicy(64, false, List.of(), List.of()), ++ new NativeRecipeRegistry(rewriter.getItemRegistry(), mock(BlockStateRewriter.class)), null); ++ when(rewriter.nativeStoragePolicy(any())).thenReturn(policy); ++ final var properties = mock(ItemRegistry.class); ++ when(properties.storageProperties(any())).thenReturn(new ItemStorageProperties(true, 4, 64)); ++ when(rewriter.getItemRegistry()).thenReturn(properties); ++ assertNull(f.click(0, 0, ContainerInput.PICKUP)); ++ assertEquals(101, f.inventory.getHudContainer().getItem(0).netId()); ++ assertEquals(0, f.nonempty(bundle)); ++ assertFalse(f.requests.hasPending()); ++ } ++ + @BeforeAll + static void initializeStructuredDataKeys() throws ClassNotFoundException { + Class.forName(StructuredDataKey.class.getName()); diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/CreativeInventoryActionTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/CreativeInventoryActionTest.java new file mode 100644 index 00000000..084d6dc7 diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index 6c3aa144..468cf523 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -169,3 +169,7 @@ The native inventory graph references these controller variables in forced toggl Missing variables resolve to null and make category clicks request index zero. Screen regressions click category and layout controls through exported programs in both inventory layouts. These authored control indices remain independent of the selected profile values. + +Storage filter bindings belong to the retained item definition. Name and item-tag filters use the existing descriptor matcher, with banned matches taking precedence. The version-matched native map controls establish `tags`, `name`, then `item_tag` precedence and deferred auxiliary handling. Expression bindings require an explicit supported language context; the live caller does not infer that context from raw component data. Unsupported predicates leave the complete binding unavailable. + +Ordinary Java bundle insertion applies available filters and nesting rules before prediction. Regressions cover both insertion directions, accepted and rejected items, descriptor auxiliary values, and retained definitions sharing a numeric ID. The full stack and both Checkstyle tasks pass. Native physical-slot request execution and installed controller validation remain unfinished. diff --git a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch index 48e42b1d..3e6068df 100644 --- a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch +++ b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch @@ -1,4 +1,4 @@ -From bd0b9b36579519a25eded8f6682333034d89c132 Mon Sep 17 00:00:00 2001 +From a96b8c9b344ac33cd50789ee4eed9078ddeef382 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(trading): translate Bedrock merchant inventories in core @@ -57,7 +57,7 @@ owner and remove the duplicate merchant decoder. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/TradePacketTest.java diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/Container.java b/src/main/java/net/raphimc/viabedrock/api/model/container/Container.java -index 71856177..1fb99500 100644 +index 1f414860..41d0a0e7 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/Container.java +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/Container.java @@ -723,7 +723,6 @@ public abstract class Container { From c53bfa80e3e5fd9352620623d5a0d9e789c402d5 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:50:31 +0100 Subject: [PATCH 3/4] feat(inventory): execute native physical-slot bundle plans Add an opening-bound backend that resolves current storage owners, runs the native physical-slot planner and submits separately predicted transfer requests through the ordered request tracker. Chain changed-slot request IDs and preserve preparation requests when incoming-item admission fails. Keep unavailable insertion filters out of Java fallback admission. Preserve newer authoritative updates, replacement dynamic owners and replacement crafting grids during rollback. The backend is not yet connected to add-on controller input. Verify eleven new boundary cases, all 107 core patches, 2,117 executed core tests, both Checkstyle tasks and the first-patch PR projection. Native wire grouping and installed direct/proxy controls remain unfinished. --- ...ain-bedrock-creative-catalog-entries.patch | 694 +++++++++++++++++- ...ain-bedrock-creative-catalog-entries.pr.md | 4 +- 2 files changed, 695 insertions(+), 3 deletions(-) diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch index 01f7b76d..86548c1a 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch @@ -1,4 +1,4 @@ -From abdeebfd9ffd85d1b428f129e91d793ce3005a8a Mon Sep 17 00:00:00 2001 +From d20333283bad638cc6edb23be7fcc34399366080 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(viabedrock): retain Bedrock creative catalog entries @@ -456,11 +456,39 @@ remain separate from native physical-slot planning and request execution. Regressions cover filter precedence, auxiliary values, tagged source stacks, unknown expression contexts, unsupported banned expressions, retained receiver identity and both accepted and rejected Java insertion directions. + +Execute native physical-slot bundle plans through an opening-bound action +session. Resolve the root and dynamic owner from current server state, +retain the root snapshot and definition registry, and revalidate every read +container after planning. Require known insertion filters instead of using +the Java fallback for unavailable native policy. Extraction does not require +weight components that only insertion consumes. Keep this backend separate +from controller admission and Java bundle item ordinals. + +Publish each Take, Place or Swap as its own predicted request in the +existing ordered transfer batch. Chain negative request IDs through changed +slots, including emptied destinations, while unchanged empty slots use zero. +Preserve preparation requests even when incoming-item admission returns +false. The session reports that native result separately from request count. + +Use the physical transfer order established by original 1.26.60.5 handlers +143a28680 and 143a2c350 and the existing captured negative-ID convention. +Native wire grouping, the full gesture controller and installed routing +remain separate validation; Java boundary tests do not establish them. + +Reuse request prediction and response reconciliation for accepted/rejected +batches and dynamic-owner replacement. Restore only matching predicted slots +after construction/submission failure, preserving newer server revisions +and the crafting grid of a replacement opening. Regressions execute the +actual request tracker with out-of-order acknowledgments, rejected cleanup, +empty-slot ID chaining, unavailable filters, stale contexts, failed sends, +new authoritative contents and same-object reopening. --- .../model/NativePlayerInventoryAction.java | 94 +++ .../model/NativePlayerInventoryMessage.java | 334 +++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ .../api/model/container/Container.java | 4 +- + .../container/NativeBundleActionSession.java | 273 +++++++ .../model/container/NativeRecipeCounter.java | 119 +++ .../model/container/NativeRecipeCrafting.java | 286 ++++++++ .../container/NativeRecipeDescriptor.java | 44 ++ @@ -497,13 +525,15 @@ receiver identity and both accepted and rejected Java insertion directions. .../storage/BundleInventoryActionTest.java | 61 ++ .../storage/CreativeInventoryActionTest.java | 262 +++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ + .../NativeBundleActionSessionTest.java | 374 ++++++++++ .../NativePlayerCreativeAuxiliaryTest.java | 333 +++++++++ .../storage/NativePlayerCreativePickTest.java | 138 ++++ .../storage/NativeRecipeCraftingTest.java | 500 +++++++++++++ - 43 files changed, 6572 insertions(+), 172 deletions(-) + 45 files changed, 7219 insertions(+), 172 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryMessage.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryResult.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeBundleActionSession.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCrafting.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeStoragePolicy.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUi.java @@ -519,6 +549,7 @@ receiver identity and both accepted and rejected Java insertion directions. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/NativeStoragePolicyTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CreativeInventoryActionTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativeBundleActionSessionTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.java @@ -1038,6 +1069,285 @@ index 71856177..1f414860 100644 } if (button == 1 && cursorItem.isEmpty()) { clickContext.snapshot(slotBundle); +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeBundleActionSession.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeBundleActionSession.java +new file mode 100644 +index 00000000..1f987c6a +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeBundleActionSession.java +@@ -0,0 +1,273 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model.container; ++ ++import com.viaversion.viaversion.api.connection.UserConnection; ++import net.raphimc.viabedrock.api.model.NativeItemStorageData; ++import net.raphimc.viabedrock.api.model.container.dynamic.BundleContainer; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.TextProcessingEventOrigin; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.FullContainerName; ++import net.raphimc.viabedrock.protocol.model.ItemRegistry; ++import net.raphimc.viabedrock.protocol.model.ItemStorageProperties; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; ++import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; ++import net.raphimc.viabedrock.protocol.storage.InventoryRequestStorage; ++import net.raphimc.viabedrock.protocol.storage.InventoryRequestTracker; ++import net.raphimc.viabedrock.protocol.storage.InventoryTracker; ++ ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.HashMap; ++import java.util.LinkedHashMap; ++import java.util.List; ++import java.util.Map; ++ ++/** A trusted opening's physical bundle operations, separate from Java bundle item ordinals. */ ++public final class NativeBundleActionSession { ++ ++ private final UserConnection user; ++ private final InventoryTracker inventory; ++ private final ItemRewriter items; ++ private final ItemRegistry definitions; ++ private final Container menu; ++ private final long generation; ++ private final Container rootOwner; ++ private final int rootSlot; ++ private final BedrockItem root; ++ private final BundleContainer bundle; ++ ++ private NativeBundleActionSession(final UserConnection user, final InventoryTracker inventory, ++ final ItemRewriter items, final Container rootOwner, final int rootSlot, ++ final BundleContainer bundle) { ++ this.user = user; ++ this.inventory = inventory; ++ this.items = items; ++ this.definitions = items.getItemRegistry(); ++ this.menu = inventory.getCurrentContainer(); ++ this.generation = inventory.currentContainerGeneration(); ++ this.rootOwner = rootOwner; ++ this.rootSlot = rootSlot; ++ this.root = rootOwner.getItem(rootSlot).copy(); ++ this.bundle = bundle; ++ } ++ ++ /** Controller admission must resolve its source to a current server-owned slot first. */ ++ public static NativeBundleActionSession create(final UserConnection user, final Container rootOwner, final int rootSlot) { ++ final var inventory = user.get(InventoryTracker.class); ++ final var items = user.get(ItemRewriter.class); ++ if (!owned(inventory, rootOwner, rootSlot)) { ++ return null; ++ } ++ final var root = rootOwner.getItem(rootSlot); ++ final var properties = items.getItemRegistry().storageProperties(root); ++ final Integer identity = NativeItemStorageData.containerIdentity(root); ++ if (root.isEmpty() || root.netId() == null || root.netId() <= 0 || identity == null ++ || !properties.storage() || properties.viewableSlots() == null) { ++ return null; ++ } ++ final var bundle = inventory.getDynamicContainer(new FullContainerName(ContainerEnumName.DynamicContainer, identity)); ++ if (bundle == null || bundle == rootOwner) { ++ return null; ++ } ++ final var result = new NativeBundleActionSession(user, inventory, items, rootOwner, rootSlot, bundle); ++ return result.current() ? result : null; ++ } ++ ++ public Outcome insert(final Container external, final int slot, final Integer quantity, final int revision) { ++ return this.transfer(external, slot, quantity, -1, revision, true); ++ } ++ ++ public Outcome extract(final Container external, final int slot, final int selectedSlot, final int revision) { ++ return this.transfer(external, slot, null, selectedSlot, revision, false); ++ } ++ ++ private Outcome transfer(final Container external, final int slot, final Integer quantity, ++ final int selectedSlot, final int revision, final boolean insertion) { ++ final var requests = this.user.get(InventoryRequestTracker.class); ++ if (!this.current() || revision < 0 || !requests.canStartRecipePlacement() || requests.isPredictingClick() ++ || !owned(this.inventory, external, slot) || external == this.bundle ++ || external == this.rootOwner && slot == this.rootSlot ++ || external.requestSlot(slot) > 255 || quantity != null && (quantity < 0 || quantity > 255)) { ++ return null; ++ } ++ final var policy = this.items.nativeStoragePolicy(this.root); ++ if (insertion && policy == null) { ++ return null; ++ } ++ final Map before = new LinkedHashMap<>(); ++ before.put(this.rootOwner, this.rootOwner.copy()); ++ before.put(this.bundle, this.bundle.copy()); ++ before.put(external, external.copy()); ++ before.put(this.inventory.getHudContainer(), this.inventory.getHudContainer().copy()); ++ final var source = new NativeBundleTransferPlan.Source() { ++ @Override ++ public ItemStorageProperties properties(final BedrockItem item) { ++ return NativeBundleActionSession.this.definitions.storageProperties(item); ++ } ++ ++ @Override ++ public BedrockItem[] contents(final int identity) { ++ final var owner = NativeBundleActionSession.this.inventory.getDynamicContainer( ++ new FullContainerName(ContainerEnumName.DynamicContainer, identity)); ++ return owner == null ? null : before.computeIfAbsent(owner, Container::copy).getItems(); ++ } ++ ++ @Override ++ public int maximum(final BedrockItem item) { ++ return NativeBundleActionSession.this.items.maxStackSize(item); ++ } ++ ++ @Override ++ public boolean canMerge(final BedrockItem first, final BedrockItem second) { ++ return NativeBundleActionSession.this.items.canMerge(first, second); ++ } ++ ++ @Override ++ public boolean canTake(final NativeBundleTransferPlan.Slot from, final BedrockItem item, final int amount) { ++ // Internal slots may have been filled by an earlier planned transfer. ++ return from.bundle() ? !item.isEmpty() && amount > 0 && amount <= item.amount() ++ : external.canTakeItem(slot, amount); ++ } ++ ++ @Override ++ public boolean canPlace(final NativeBundleTransferPlan.Slot to, final BedrockItem item, final int amount) { ++ return (to.bundle() ? NativeBundleActionSession.this.bundle : external) ++ .canPlaceItem(to.bundle() ? to.index() : slot, item, amount); ++ } ++ ++ @Override ++ public boolean accepts(final BedrockItem bundle, final BedrockItem incoming) { ++ return policy.accepts(incoming, NativeBundleActionSession.this.items::recipeDescriptor); ++ } ++ ++ @Override ++ public boolean allowNested(final BedrockItem bundle) { ++ return policy.allowNested(); ++ } ++ }; ++ final NativeBundleTransferPlan.Result plan; ++ try { ++ plan = insertion ? NativeBundleTransferPlan.insert(this.root, this.bundle.getItems(), external.getItem(slot), slot, quantity, source) ++ : NativeBundleTransferPlan.extract(this.root, this.bundle.getItems(), external.getItem(slot), slot, selectedSlot, source); ++ } catch (final IllegalArgumentException unavailable) { ++ return null; ++ } ++ if (!this.current() || requests != this.user.get(InventoryRequestTracker.class) ++ || !requests.canStartRecipePlacement() || requests.isPredictingClick() ++ || before.entrySet().stream().anyMatch(entry -> !unchanged(this.inventory, entry.getKey(), entry.getValue()))) { ++ return null; ++ } ++ for (final var transfer : plan.transfers()) { ++ for (final var position : List.of(transfer.source(), transfer.destination())) { ++ final var item = this.owner(position, external).getItem(position.index()); ++ if (!item.isEmpty() && (item.netId() == null || item.netId() <= 0)) { ++ return null; ++ } ++ } ++ } ++ final var batch = new ArrayList(); ++ final Map changed = new HashMap<>(); ++ final Map predicted = new HashMap<>(); ++ requests.setPredictingClick(true); ++ try { ++ for (final var transfer : plan.transfers()) { ++ final int id = requests.nextRequestId(); ++ final var from = this.owner(transfer.source(), external); ++ final var to = this.owner(transfer.destination(), external); ++ final var sourceSlot = from.requestSlotInfo(transfer.source().index(), networkId(from, transfer.source(), changed)); ++ final var destinationSlot = to.requestSlotInfo(transfer.destination().index(), networkId(to, transfer.destination(), changed)); ++ final ItemStackRequestAction action = switch (transfer.kind()) { ++ case TAKE -> new ItemStackRequestAction.TakeAction(transfer.count(), sourceSlot, destinationSlot); ++ case PLACE -> new ItemStackRequestAction.PlaceAction(transfer.count(), sourceSlot, destinationSlot); ++ case SWAP -> new ItemStackRequestAction.SwapAction(sourceSlot, destinationSlot); ++ }; ++ final var previous = new ArrayList(); ++ previous.add(this.bundle.copy()); ++ if (external != this.inventory.getHudContainer()) { ++ previous.add(external.copy()); ++ } ++ final var request = new InventoryRequestStorage(new ItemStackRequestInfo(id, List.of(action), ++ List.of(), TextProcessingEventOrigin.unknown), revision, this.inventory.getHudContainer().copy(), previous); ++ final var sourceAfter = transfer.sourceAfter().copy(); ++ final var destinationAfter = transfer.destinationAfter().copy(); ++ sourceAfter.setNetId(id); ++ destinationAfter.setNetId(id); ++ predicted.put(transfer.source(), sourceAfter); ++ predicted.put(transfer.destination(), destinationAfter); ++ from.setItem(transfer.source().index(), sourceAfter); ++ to.setItem(transfer.destination().index(), destinationAfter); ++ changed.put(transfer.source(), id); ++ changed.put(transfer.destination(), id); ++ request.capturePrediction(this.inventory); ++ batch.add(request); ++ } ++ if (!batch.isEmpty()) { ++ requests.sendTransferBatch(batch); ++ } ++ } catch (final RuntimeException error) { ++ // The tracker restores submission failures; this also covers construction failures. ++ predicted.forEach((position, item) -> { ++ final var owner = this.owner(position, external); ++ final var snapshot = before.get(owner); ++ final boolean sameOpening = this.inventory.getCurrentContainer() == this.menu ++ && this.inventory.currentContainerGeneration() == this.generation; ++ if (owned(this.inventory, owner, position.index()) && owner.serverRevision() == snapshot.serverRevision() ++ && owner.getItem(position.index()).equals(item) ++ && (owner != this.menu || owner == this.inventory.getInventoryContainer() || sameOpening) ++ && (owner != this.inventory.getHudContainer() || position.index() == 0 || sameOpening)) { ++ owner.setItem(position.index(), snapshot.getItem(position.index()).copy()); ++ } ++ }); ++ throw error; ++ } finally { ++ requests.setPredictingClick(false); ++ } ++ return new Outcome(plan.successful(), batch.size()); ++ } ++ ++ private boolean current() { ++ return this.inventory == this.user.get(InventoryTracker.class) && this.items == this.user.get(ItemRewriter.class) ++ && this.definitions == this.items.getItemRegistry() ++ && this.user.get(GameSessionStorage.class).isInventoryServerAuthoritative() ++ && this.menu != null && this.generation > 0 && this.inventory.getCurrentContainer() == this.menu ++ && this.inventory.currentContainerGeneration() == this.generation && this.inventory.getPendingCloseContainer() == null ++ && owned(this.inventory, this.rootOwner, this.rootSlot) && this.root.equals(this.rootOwner.getItem(this.rootSlot)) ++ && this.inventory.getDynamicContainer(this.bundle.getFullContainerName(0)) == this.bundle; ++ } ++ ++ private Container owner(final NativeBundleTransferPlan.Slot slot, final Container external) { ++ return slot.bundle() ? this.bundle : external; ++ } ++ ++ private static int networkId(final Container owner, final NativeBundleTransferPlan.Slot slot, ++ final Map changed) { ++ final var item = owner.getItem(slot.index()); ++ return changed.containsKey(slot) ? changed.get(slot) : item.isEmpty() ? 0 : item.netId(); ++ } ++ ++ private static boolean owned(final InventoryTracker inventory, final Container owner, final int slot) { ++ if (owner == null || slot < 0 || slot >= owner.size()) { ++ return false; ++ } ++ final int requestSlot = owner.requestSlot(slot); ++ final var name = owner.getFullContainerName(slot); ++ return owner.ownsBedrockSlot(name, requestSlot) && owner.slotFromRequest(requestSlot) == slot ++ && inventory.getContainerFromName(name, requestSlot) == owner; ++ } ++ ++ private static boolean unchanged(final InventoryTracker inventory, final Container owner, final Container snapshot) { ++ return owned(inventory, owner, 0) && owner.serverRevision() == snapshot.serverRevision() ++ && Arrays.equals(owner.getItems(), snapshot.getItems()); ++ } ++ ++ /** A rejected insertion can still submit preparation requests. Null means the context is unavailable. */ ++ public record Outcome(boolean successful, int requests) { } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java index fe874172..96547dc6 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java @@ -7128,6 +7438,386 @@ index b69f1868..037328f2 100644 @Test void successfulResponseUpdatesNameAndCountWithoutTreatingDurabilityCorrectionAsDamage() { final BedrockItem current = new BedrockItem(276); +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeBundleActionSessionTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeBundleActionSessionTest.java +new file mode 100644 +index 00000000..2bcf98d2 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeBundleActionSessionTest.java +@@ -0,0 +1,374 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.storage; ++ ++import com.viaversion.nbt.tag.CompoundTag; ++import com.viaversion.nbt.tag.ListTag; ++import com.viaversion.viaversion.api.connection.UserConnection; ++import net.raphimc.viabedrock.ViaBedrock; ++import net.raphimc.viabedrock.api.model.container.Container; ++import net.raphimc.viabedrock.api.model.container.NativeBundleActionSession; ++import net.raphimc.viabedrock.api.model.container.NativeStoragePolicy; ++import net.raphimc.viabedrock.api.model.container.dynamic.BundleContainer; ++import net.raphimc.viabedrock.api.util.PacketFactory; ++import net.raphimc.viabedrock.platform.ViaBedrockPlatform; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemStackNetResult; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.FullContainerName; ++import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.ItemRegistry; ++import net.raphimc.viabedrock.protocol.model.ItemStoragePolicy; ++import net.raphimc.viabedrock.protocol.model.ItemStorageProperties; ++import net.raphimc.viabedrock.protocol.model.NativeRecipeRegistry; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseContainerInfo; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseInfo; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseSlotInfo; ++import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; ++import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import org.junit.jupiter.api.Test; ++import org.mockito.MockedStatic; ++ ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++import java.util.logging.Logger; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class NativeBundleActionSessionTest { ++ ++ @Test ++ void insertionChainsPreparationAndSwapIdsAndReconcilesOutOfOrderResponses() { ++ try (var f = new Fixture()) { ++ f.bundle.setItem(0, item(30031, 2, 100)); ++ f.bundle.setItem(2, item(30031, 3, 102)); ++ f.bundle.setItem(3, item(30032, 1, 103)); ++ f.player.setItem(10, item(30031, 4, 201)); ++ assertEquals(new NativeBundleActionSession.Outcome(true, 4), f.session().insert(f.player, 10, null, 7)); ++ assertEquals(List.of(-3, -5, -7, -9), f.sent.stream().map(ItemStackRequestInfo::requestId).toList()); ++ final var merge = (ItemStackRequestAction.PlaceAction) f.sent.get(0).actions().getFirst(); ++ assertEquals(f.bundle.requestSlotInfo(2, 102), merge.source()); ++ assertEquals(f.bundle.requestSlotInfo(0, 100), merge.destination()); ++ final var compact = (ItemStackRequestAction.PlaceAction) f.sent.get(1).actions().getFirst(); ++ assertEquals(f.bundle.requestSlotInfo(3, 103), compact.source()); ++ assertEquals(f.bundle.requestSlotInfo(1, 0), compact.destination()); ++ final var insert = (ItemStackRequestAction.TakeAction) f.sent.get(2).actions().getFirst(); ++ assertEquals(f.player.requestSlotInfo(10, 201), insert.source()); ++ assertEquals(f.bundle.requestSlotInfo(0, -3), insert.destination()); ++ final var swap = (ItemStackRequestAction.SwapAction) f.sent.get(3).actions().getFirst(); ++ assertEquals(f.bundle.requestSlotInfo(0, -7), swap.source()); ++ assertEquals(f.bundle.requestSlotInfo(1, -5), swap.destination()); ++ assertEquals(-9, f.bundle.getItem(1).netId()); ++ assertEquals(-7, f.player.getItem(10).netId()); ++ ++ f.requests.handleResponse(f.response(3, f.info(f.bundle, slot(0, 1, 304), slot(1, 9, 305)))); ++ f.requests.handleResponse(f.response(2, f.info(f.bundle, slot(0, 9, 303)), f.info(f.player, slot(10, 0, 0)))); ++ f.requests.handleResponse(f.response(1, f.info(f.bundle, slot(1, 1, 302), slot(3, 0, 0)))); ++ assertTrue(f.requests.hasPending()); ++ f.requests.handleResponse(f.response(0, f.info(f.bundle, slot(0, 5, 301), slot(2, 0, 0)))); ++ assertFalse(f.requests.hasPending()); ++ assertEquals(item(30032, 1, 304), f.bundle.getItem(0)); ++ assertEquals(item(30031, 9, 305), f.bundle.getItem(1)); ++ assertTrue(f.player.getItem(10).isEmpty()); ++ assertEquals(42, f.player.getItem(9).tag().getInt("bundle_id")); ++ } ++ } ++ ++ @Test ++ void extractionUsesPhysicalCellsAndChainsTheEmptiedSlotBeforeRejectionRestoresBothOwners() { ++ try (var f = new Fixture()) { ++ f.bundle.setItem(2, item(30031, 5, 102)); ++ f.bundle.setItem(3, item(30032, 4, 103)); ++ final var before = Arrays.stream(f.bundle.getItems()).map(BedrockItem::copy).toArray(BedrockItem[]::new); ++ final var cursor = f.inventory.getHudContainer(); ++ assertEquals(new NativeBundleActionSession.Outcome(true, 2), f.session().extract(cursor, 0, 2, 0)); ++ final var take = (ItemStackRequestAction.TakeAction) f.sent.getFirst().actions().getFirst(); ++ assertEquals(f.bundle.requestSlotInfo(2, 102), take.source()); ++ assertEquals(cursor.requestSlotInfo(0, 0), take.destination()); ++ final var compact = (ItemStackRequestAction.TakeAction) f.sent.getLast().actions().getFirst(); ++ assertEquals(f.bundle.requestSlotInfo(3, 103), compact.source()); ++ assertEquals(f.bundle.requestSlotInfo(2, -3), compact.destination()); ++ assertTrue(f.bundle.getItem(1).isEmpty()); ++ assertEquals(-5, f.bundle.getItem(3).netId()); ++ assertEquals(item(30031, 5, -3), cursor.getItem(0)); ++ for (final var request : f.sent.reversed()) { ++ f.requests.handleResponse(rejected(request)); ++ } ++ assertFalse(f.requests.hasPending()); ++ assertArrayEquals(before, f.bundle.getItems()); ++ assertTrue(cursor.getItem(0).isEmpty()); ++ } ++ } ++ ++ @Test ++ void extractionDoesNotRequireStorageWeightMetadata() { ++ try (var f = new Fixture()) { ++ final var definitions = mock(ItemRegistry.class); ++ when(definitions.storageProperties(any())).thenReturn(new ItemStorageProperties(true, null, null, 64)); ++ when(f.items.getItemRegistry()).thenReturn(definitions); ++ f.bundle.setItem(2, item(30031, 5, 102)); ++ assertEquals(new NativeBundleActionSession.Outcome(true, 1), f.session().extract(f.inventory.getHudContainer(), 0, 2, 0)); ++ assertEquals(item(30031, 5, -3), f.inventory.getHudContainer().getItem(0)); ++ assertInstanceOf(ItemStackRequestAction.TakeAction.class, f.sent.getFirst().actions().getFirst()); ++ } ++ } ++ ++ @Test ++ void rejectedIncomingItemStillSubmitsItsNativePreparationRequest() { ++ try (var f = new Fixture()) { ++ when(f.items.nativeStoragePolicy(any())).thenReturn(NativeStoragePolicy.bind( ++ new ItemStoragePolicy(64, true, List.of(), List.of(Map.of("name", "test:stone"))), f.context, null)); ++ f.bundle.setItem(2, item(30032, 3, 102)); ++ f.player.setItem(10, item(30031, 4, 201)); ++ assertEquals(new NativeBundleActionSession.Outcome(false, 1), f.session().insert(f.player, 10, null, 0)); ++ assertEquals(item(30032, 3, -3), f.bundle.getItem(0)); ++ assertEquals(item(30031, 4, 201), f.player.getItem(10)); ++ assertInstanceOf(ItemStackRequestAction.PlaceAction.class, f.sent.getFirst().actions().getFirst()); ++ f.requests.handleResponse(rejected(f.sent.getFirst())); ++ assertTrue(f.bundle.getItem(0).isEmpty()); ++ assertEquals(item(30032, 3, 102), f.bundle.getItem(2)); ++ } ++ } ++ ++ @Test ++ void failedSubmissionRestoresTheCompletePhysicalPlanAndClearsPendingOwnership() { ++ try (var f = new Fixture()) { ++ f.bundle.setItem(2, item(30031, 5, 102)); ++ f.bundle.setItem(3, item(30032, 4, 103)); ++ final var before = Arrays.stream(f.bundle.getItems()).map(BedrockItem::copy).toArray(BedrockItem[]::new); ++ final var error = new IllegalStateException("submission failed"); ++ f.packets.when(() -> PacketFactory.sendBedrockInventoryRequest(eq(f.user), any())).thenThrow(error); ++ assertSame(error, assertThrows(IllegalStateException.class, ++ () -> f.session().extract(f.inventory.getHudContainer(), 0, 2, 0))); ++ assertArrayEquals(before, f.bundle.getItems()); ++ assertTrue(f.inventory.getHudContainer().getItem(0).isEmpty()); ++ assertFalse(f.requests.hasPending()); ++ assertFalse(f.requests.isPredictingClick()); ++ assertTrue(f.requests.canStartRecipePlacement()); ++ } ++ } ++ ++ @Test ++ void unavailableStorageFiltersCannotFallBackToJavaAdmissionOrSubmitPreparation() { ++ try (var f = new Fixture()) { ++ when(f.items.nativeStoragePolicy(any())).thenReturn(null); ++ f.bundle.setItem(2, item(30032, 3, 102)); ++ f.player.setItem(10, item(30031, 4, 201)); ++ assertNull(f.session().insert(f.player, 10, null, 0)); ++ assertTrue(f.bundle.getItem(0).isEmpty()); ++ assertEquals(item(30032, 3, 102), f.bundle.getItem(2)); ++ assertEquals(item(30031, 4, 201), f.player.getItem(10)); ++ assertTrue(f.sent.isEmpty()); ++ assertFalse(f.requests.hasPending()); ++ } ++ } ++ ++ @Test ++ void submissionFailurePreservesNewerAuthoritativeDynamicContents() { ++ try (var f = new Fixture()) { ++ f.bundle.setItem(2, item(30031, 5, 102)); ++ f.packets.when(() -> PacketFactory.sendBedrockInventoryRequest(eq(f.user), any())).thenAnswer(call -> { ++ f.bundle.setItem(2, item(30032, 7, 700)); ++ f.bundle.markServerUpdate(); ++ throw new IllegalStateException("submission failed"); ++ }); ++ assertThrows(IllegalStateException.class, () -> f.session().extract(f.inventory.getHudContainer(), 0, 2, 0)); ++ assertEquals(item(30032, 7, 700), f.bundle.getItem(2)); ++ assertTrue(f.inventory.getHudContainer().getItem(0).isEmpty()); ++ assertFalse(f.requests.hasPending()); ++ assertFalse(f.requests.isPredictingClick()); ++ } ++ } ++ ++ @Test ++ void submissionFailureAfterReopeningPreservesTheNewCraftingGrid() { ++ try (var f = new Fixture()) { ++ f.bundle.setItem(2, item(30031, 5, 102)); ++ final var hud = f.inventory.getHudContainer(); ++ f.packets.when(() -> PacketFactory.sendBedrockInventoryRequest(eq(f.user), any())).thenAnswer(call -> { ++ f.inventory.setCurrentContainerClosed(false); ++ f.inventory.setCurrentContainer(f.player); ++ hud.setItem(28, item(30032, 7, 700)); ++ throw new IllegalStateException("submission failed"); ++ }); ++ assertThrows(IllegalStateException.class, () -> f.session().extract(hud, 28, 2, 0)); ++ assertEquals(item(30032, 7, 700), hud.getItem(28)); ++ assertEquals(item(30031, 5, 102), f.bundle.getItem(2)); ++ assertFalse(f.requests.hasPending()); ++ assertFalse(f.requests.isPredictingClick()); ++ } ++ } ++ ++ @Test ++ void lateAcceptedAndRejectedResponsesCannotOverwriteReplacementDynamicOwners() { ++ for (final boolean accepted : new boolean[]{false, true}) { ++ try (var f = new Fixture()) { ++ f.bundle.setItem(2, item(30031, 5, 102)); ++ assertEquals(new NativeBundleActionSession.Outcome(true, 1), f.session().extract(f.inventory.getHudContainer(), 0, 2, 0)); ++ final var replacement = new BundleContainer(f.user, f.bundle.getFullContainerName(0)); ++ replacement.setItem(2, item(30032, 7, 999)); ++ f.dynamic.put(replacement.getFullContainerName(0), replacement); ++ f.requests.handleResponse(accepted ? f.response(0, f.info(replacement, slot(2, 0, 0))) : rejected(f.sent.getFirst())); ++ assertEquals(item(30032, 7, 999), replacement.getItem(2)); ++ assertFalse(f.requests.hasPending()); ++ } ++ } ++ } ++ ++ @Test ++ void retiredOpeningChangedRootAndUnconfirmedStacksCannotAllocateRequests() { ++ try (var f = new Fixture()) { ++ final var session = f.session(); ++ f.player.setItem(10, item(30031, 5, -11)); ++ assertNull(session.insert(f.player, 10, null, 0)); ++ f.player.setItem(10, item(30031, 5, 201)); ++ f.player.getItem(9).tag().putInt("bundle_id", 43); ++ assertNull(session.insert(f.player, 10, null, 0)); ++ f.player.getItem(9).tag().putInt("bundle_id", 42); ++ f.inventory.setCurrentContainerClosed(false); ++ f.inventory.setCurrentContainer(f.player); ++ assertNull(session.insert(f.player, 10, null, 0)); ++ assertTrue(f.sent.isEmpty()); ++ assertFalse(f.requests.hasPending()); ++ assertEquals(-3, f.requests.nextRequestId()); ++ } ++ } ++ ++ @Test ++ void authoritativeChangeDuringPlanningIsPreservedWithoutSubmittingPrediction() { ++ try (var f = new Fixture()) { ++ f.bundle.setItem(0, item(30031, 2, 100)); ++ f.player.setItem(10, item(30031, 4, 201)); ++ doAnswer(call -> { ++ f.player.setItem(10, item(30031, 9, 700)); ++ f.player.markServerUpdate(); ++ return true; ++ }).when(f.items).canMerge(any(), any()); ++ assertNull(f.session().insert(f.player, 10, null, 0)); ++ assertEquals(item(30031, 9, 700), f.player.getItem(10)); ++ assertEquals(item(30031, 2, 100), f.bundle.getItem(0)); ++ assertTrue(f.sent.isEmpty()); ++ assertFalse(f.requests.hasPending()); ++ } ++ } ++ ++ private static ItemStackResponseSlotInfo slot(final int slot, final int amount, final int id) { ++ return new ItemStackResponseSlotInfo((byte) slot, (byte) slot, (byte) amount, id, "", null, 0); ++ } ++ ++ private static ItemStackResponseInfo rejected(final ItemStackRequestInfo request) { ++ return new ItemStackResponseInfo(ItemStackNetResult.Error, request.requestId(), List.of()); ++ } ++ ++ private static BedrockItem item(final int type, final int amount, final int id) { ++ final var item = new BedrockItem(type, (short) 0, (byte) amount); ++ item.setNetId(id); ++ return item; ++ } ++ ++ private static final class Fixture implements AutoCloseable { ++ private final UserConnection user = mock(UserConnection.class); ++ private final Map, Object> storage = new HashMap<>(); ++ private final Map dynamic = new HashMap<>(); ++ private final MockedStatic packets = mockStatic(PacketFactory.class); ++ private final MockedStatic viaBedrock = mockStatic(ViaBedrock.class); ++ private final ItemRewriter items = mock(ItemRewriter.class); ++ private final InventoryTracker inventory; ++ private final InventoryRequestTracker requests; ++ private final Container player; ++ private final BundleContainer bundle; ++ private final NativeRecipeRegistry context; ++ private final List sent = new ArrayList<>(); ++ ++ private Fixture() { ++ when(this.user.get(any())).thenAnswer(call -> this.storage.computeIfAbsent(call.getArgument(0), key -> mock(key))); ++ final var platform = mock(ViaBedrockPlatform.class); ++ when(platform.getLogger()).thenReturn(Logger.getAnonymousLogger()); ++ this.viaBedrock.when(ViaBedrock::getPlatform).thenReturn(platform); ++ this.inventory = new InventoryTracker(this.user) { ++ @Override ++ public BundleContainer getDynamicContainer(final FullContainerName name) { ++ return Fixture.this.dynamic.get(name); ++ } ++ ++ @Override ++ public Container getContainerFromName(final FullContainerName name, final int slot) { ++ return name.name() == ContainerEnumName.DynamicContainer ? Fixture.this.dynamic.get(name) : super.getContainerFromName(name, slot); ++ } ++ }; ++ this.requests = new InventoryRequestTracker(this.user); ++ this.storage.put(InventoryTracker.class, this.inventory); ++ this.storage.put(InventoryRequestTracker.class, this.requests); ++ this.storage.put(ItemRewriter.class, this.items); ++ when(this.user.get(GameSessionStorage.class).isInventoryServerAuthoritative()).thenReturn(true); ++ this.player = this.inventory.getInventoryContainer(); ++ this.inventory.setCurrentContainer(this.player); ++ final var components = new CompoundTag(); ++ final var policy = new CompoundTag(); ++ policy.putInt("max_slots", 64); ++ policy.putByte("allow_nested_storage_items", (byte) 1); ++ policy.put("allowed_items", new ListTag<>(CompoundTag.class)); ++ policy.put("banned_items", new ListTag<>(CompoundTag.class)); ++ components.put("minecraft:storage_item", policy); ++ for (final var field : Map.of("minecraft:storage_weight_modifier", "weight_in_storage_item", ++ "minecraft:storage_weight_limit", "max_weight_limit", "minecraft:bundle_interaction", "num_viewable_slots").entrySet()) { ++ final var value = new CompoundTag(); ++ value.putInt(field.getValue(), field.getValue().equals("weight_in_storage_item") ? 4 : 64); ++ components.put(field.getKey(), value); ++ } ++ final var data = new CompoundTag(); ++ data.put("components", components); ++ final var registry = new ItemRegistry(new ItemEntry[]{new ItemEntry("test:bundle", 30030, true, ItemVersion.DataDriven, data), ++ new ItemEntry("test:stone", 30031, true, ItemVersion.DataDriven, new CompoundTag()), ++ new ItemEntry("test:other", 30032, true, ItemVersion.DataDriven, new CompoundTag())}); ++ this.context = new NativeRecipeRegistry(registry, mock(BlockStateRewriter.class)); ++ when(this.items.getItemRegistry()).thenReturn(registry); ++ when(this.items.maxStackSize(any())).thenReturn(64); ++ when(this.items.nativeStoragePolicy(any())).thenAnswer(call -> registry.storagePolicy(call.getArgument(0), this.context, null)); ++ when(this.items.recipeDescriptor(any())).thenAnswer(call -> registry.recipeDescriptor(call.getArgument(0), null)); ++ when(this.items.canMerge(any(), any())).thenAnswer(call -> ((BedrockItem) call.getArgument(0)).identifier() ++ == ((BedrockItem) call.getArgument(1)).identifier()); ++ final var root = item(30030, 1, 99); ++ root.setTag(new CompoundTag()); ++ root.tag().putInt("bundle_id", 42); ++ this.player.setItem(9, root); ++ this.bundle = new BundleContainer(this.user, new FullContainerName(ContainerEnumName.DynamicContainer, 42)); ++ this.dynamic.put(this.bundle.getFullContainerName(0), this.bundle); ++ this.packets.when(() -> PacketFactory.sendBedrockInventoryRequest(eq(this.user), any())).thenAnswer(call -> { ++ this.sent.addAll(List.of((ItemStackRequestInfo[]) call.getArgument(1))); ++ return null; ++ }); ++ } ++ ++ private NativeBundleActionSession session() { ++ final var session = NativeBundleActionSession.create(this.user, this.player, 9); ++ assertNotNull(session); ++ return session; ++ } ++ ++ private ItemStackResponseContainerInfo info(final Container owner, final ItemStackResponseSlotInfo... slots) { ++ return new ItemStackResponseContainerInfo(owner.getFullContainerName(Byte.toUnsignedInt(slots[0].slot())), List.of(slots)); ++ } ++ ++ private ItemStackResponseInfo response(final int index, final ItemStackResponseContainerInfo... containers) { ++ return new ItemStackResponseInfo(ItemStackNetResult.Success, this.sent.get(index).requestId(), List.of(containers)); ++ } ++ ++ @Override ++ public void close() { ++ this.packets.close(); ++ this.viaBedrock.close(); ++ } ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java new file mode 100644 index 00000000..1d7a8938 diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index 468cf523..ba10a498 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -172,4 +172,6 @@ These authored control indices remain independent of the selected profile values Storage filter bindings belong to the retained item definition. Name and item-tag filters use the existing descriptor matcher, with banned matches taking precedence. The version-matched native map controls establish `tags`, `name`, then `item_tag` precedence and deferred auxiliary handling. Expression bindings require an explicit supported language context; the live caller does not infer that context from raw component data. Unsupported predicates leave the complete binding unavailable. -Ordinary Java bundle insertion applies available filters and nesting rules before prediction. Regressions cover both insertion directions, accepted and rejected items, descriptor auxiliary values, and retained definitions sharing a numeric ID. The full stack and both Checkstyle tasks pass. Native physical-slot request execution and installed controller validation remain unfinished. +Ordinary Java bundle insertion applies available filters and nesting rules before prediction. Regressions cover both insertion directions, accepted and rejected items, descriptor auxiliary values, and retained definitions sharing a numeric ID. + +The native bundle action session resolves current server-owned roots and physical containers, then submits the native transfer plan through the ordered request tracker. Each transfer captures its intermediate prediction and chains changed-slot request IDs, including emptied slots. A rejected incoming item can still submit preparation requests. Extraction remains independent of insertion weight metadata. Boundary tests cover out-of-order acceptance, rejection, unavailable filters, replacement owners, failed sends, authoritative updates, and reopening with a new crafting grid. Full controller delivery, native wire grouping, and installed direct/proxy validation remain unfinished; this backend does not enable bundle gestures in the add-on. From dfb197a15c4113953826751fbc2fa0cb319428ec Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 5 Nov 2026 01:19:39 +0100 Subject: [PATCH 4/4] feat(inventory): add native default bundle extraction Keep desktop default extraction separate from selected-cell extraction. Scan backward for the last physical entry, emit one Take without compaction and retain the transfer result. Preserve zero-count definition identity without treating userdata-only empty slots as occupied. Add planner and request-boundary regressions for partial merging, rejection, empty vectors, authoritative acceptance and full-cursor no-op. Record 328 version-matched native instruction controls distinguishing desktop state1 predicates from the touch state45 path. Verify all 107 core patches, 2,122 executed core tests, both Checkstyle tasks and the first-patch PR projection. Controller transport and installed direct/proxy comparisons remain unfinished. --- ...ain-bedrock-creative-catalog-entries.patch | 62 +++++++-- ...ain-bedrock-creative-catalog-entries.pr.md | 2 + ...rate-authored-native-ui-presentation.patch | 121 ++++++++++++++++-- ...rate-authored-native-ui-presentation.pr.md | 4 +- 4 files changed, 169 insertions(+), 20 deletions(-) diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch index 86548c1a..f0db8082 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.patch @@ -1,4 +1,4 @@ -From d20333283bad638cc6edb23be7fcc34399366080 Mon Sep 17 00:00:00 2001 +From fd0c5eb33733fbfe9ddb865f333652f54631e0d5 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(viabedrock): retain Bedrock creative catalog entries @@ -483,12 +483,28 @@ and the crafting grid of a replacement opening. Regressions execute the actual request tracker with out-of-order acknowledgments, rejected cleanup, empty-slot ID chaining, unavailable filters, stale contexts, failed sends, new authoritative contents and same-object reopening. + +Expose desktop default extraction separately from selected-cell extraction +through the trusted bundle session. Preserve the default helper's Take kind +when merging into an occupied cursor and return failure for a full cursor. +The request regression checks last physical selection, partial prediction, +authoritative acceptance and the following no-op without allocating a request. + +Original state1 dispatcher controls distinguish desktop predicates14503b3b0/ +14503b570 from the touch state45 predicates. With no retained primary source, +288 input-mode1/3, tooltip-mode0/2, phase and source cases establish activation +entry into state50 for storage insertion and state51 for default extraction. +Twenty-four original state50 controls establish selected-cursor versus clicked +source ownership and sound results, without desktop transfer flights. Admission, +source/model services and mutation results are supplied; predicate and handler +runs are separate. Full controller composition and installed routing remain +unfinished. Do not apply state45's input-mode2 restriction to state1. --- .../model/NativePlayerInventoryAction.java | 94 +++ .../model/NativePlayerInventoryMessage.java | 334 +++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ .../api/model/container/Container.java | 4 +- - .../container/NativeBundleActionSession.java | 273 +++++++ + .../container/NativeBundleActionSession.java | 278 ++++++++ .../model/container/NativeRecipeCounter.java | 119 +++ .../model/container/NativeRecipeCrafting.java | 286 ++++++++ .../container/NativeRecipeDescriptor.java | 44 ++ @@ -525,11 +541,11 @@ new authoritative contents and same-object reopening. .../storage/BundleInventoryActionTest.java | 61 ++ .../storage/CreativeInventoryActionTest.java | 262 +++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ - .../NativeBundleActionSessionTest.java | 374 ++++++++++ + .../NativeBundleActionSessionTest.java | 397 ++++++++++ .../NativePlayerCreativeAuxiliaryTest.java | 333 +++++++++ .../storage/NativePlayerCreativePickTest.java | 138 ++++ .../storage/NativeRecipeCraftingTest.java | 500 +++++++++++++ - 45 files changed, 7219 insertions(+), 172 deletions(-) + 45 files changed, 7247 insertions(+), 172 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryMessage.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryResult.java @@ -1071,10 +1087,10 @@ index 71856177..1f414860 100644 clickContext.snapshot(slotBundle); diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeBundleActionSession.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeBundleActionSession.java new file mode 100644 -index 00000000..1f987c6a +index 00000000..f4430e21 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeBundleActionSession.java -@@ -0,0 +1,273 @@ +@@ -0,0 +1,278 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -1165,8 +1181,12 @@ index 00000000..1f987c6a + return this.transfer(external, slot, null, selectedSlot, revision, false); + } + ++ public Outcome extractLast(final Container external, final int slot, final int revision) { ++ return this.transfer(external, slot, null, null, revision, false); ++ } ++ + private Outcome transfer(final Container external, final int slot, final Integer quantity, -+ final int selectedSlot, final int revision, final boolean insertion) { ++ final Integer selectedSlot, final int revision, final boolean insertion) { + final var requests = this.user.get(InventoryRequestTracker.class); + if (!this.current() || revision < 0 || !requests.canStartRecipePlacement() || requests.isPredictingClick() + || !owned(this.inventory, external, slot) || external == this.bundle @@ -1232,6 +1252,7 @@ index 00000000..1f987c6a + final NativeBundleTransferPlan.Result plan; + try { + plan = insertion ? NativeBundleTransferPlan.insert(this.root, this.bundle.getItems(), external.getItem(slot), slot, quantity, source) ++ : selectedSlot == null ? NativeBundleTransferPlan.extractLast(this.root, this.bundle.getItems(), external.getItem(slot), slot, source) + : NativeBundleTransferPlan.extract(this.root, this.bundle.getItems(), external.getItem(slot), slot, selectedSlot, source); + } catch (final IllegalArgumentException unavailable) { + return null; @@ -7440,10 +7461,10 @@ index b69f1868..037328f2 100644 final BedrockItem current = new BedrockItem(276); diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeBundleActionSessionTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeBundleActionSessionTest.java new file mode 100644 -index 00000000..2bcf98d2 +index 00000000..c8de7485 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeBundleActionSessionTest.java -@@ -0,0 +1,374 @@ +@@ -0,0 +1,397 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -7494,6 +7515,29 @@ index 00000000..2bcf98d2 +class NativeBundleActionSessionTest { + + @Test ++ void defaultExtractionUsesLastPhysicalCellAndKeepsTakeForPartialCursorMerge() { ++ try (var f = new Fixture()) { ++ f.bundle.setItem(0, item(30031, 2, 100)); ++ f.bundle.setItem(3, item(30032, 5, 103)); ++ final var cursor = f.inventory.getHudContainer(); ++ cursor.setItem(0, item(30032, 62, 200)); ++ assertEquals(new NativeBundleActionSession.Outcome(true, 1), f.session().extractLast(cursor, 0, 0)); ++ final var take = (ItemStackRequestAction.TakeAction) f.sent.getFirst().actions().getFirst(); ++ assertEquals(2, take.count()); ++ assertEquals(f.bundle.requestSlotInfo(3, 103), take.source()); ++ assertEquals(cursor.requestSlotInfo(0, 200), take.destination()); ++ assertEquals(item(30031, 2, 100), f.bundle.getItem(0)); ++ assertEquals(item(30032, 3, -3), f.bundle.getItem(3)); ++ f.requests.handleResponse(f.response(0, f.info(f.bundle, slot(3, 3, 303)), f.info(cursor, slot(0, 64, 304)))); ++ assertEquals(item(30032, 3, 303), f.bundle.getItem(3)); ++ assertEquals(item(30032, 64, 304), cursor.getItem(0)); ++ assertFalse(f.requests.hasPending()); ++ assertEquals(new NativeBundleActionSession.Outcome(false, 0), f.session().extractLast(cursor, 0, 0)); ++ assertEquals(1, f.sent.size()); ++ } ++ } ++ ++ @Test + void insertionChainsPreparationAndSwapIdsAndReconcilesOutOfOrderResponses() { + try (var f = new Fixture()) { + f.bundle.setItem(0, item(30031, 2, 100)); diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index ba10a498..1f52a0fc 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -175,3 +175,5 @@ Storage filter bindings belong to the retained item definition. Name and item-ta Ordinary Java bundle insertion applies available filters and nesting rules before prediction. Regressions cover both insertion directions, accepted and rejected items, descriptor auxiliary values, and retained definitions sharing a numeric ID. The native bundle action session resolves current server-owned roots and physical containers, then submits the native transfer plan through the ordered request tracker. Each transfer captures its intermediate prediction and chains changed-slot request IDs, including emptied slots. A rejected incoming item can still submit preparation requests. Extraction remains independent of insertion weight metadata. Boundary tests cover out-of-order acceptance, rejection, unavailable filters, replacement owners, failed sends, authoritative updates, and reopening with a new crafting grid. Full controller delivery, native wire grouping, and installed direct/proxy validation remain unfinished; this backend does not enable bundle gestures in the add-on. + +Desktop state 1 uses separate bundle predicates from touch state 45. Original controls cover 288 transition cases and 24 insertion-handler cases for input modes 1/3, tooltip modes 0/2, and absent retained primary sources. They establish activation entry and cursor/clicked source ownership with supplied model and admission services. The session exposes default extraction separately and tests its Take request, partial cursor merge, authoritative response, and full-cursor no-op. Complete controller composition and installed input/rendering remain unverified. diff --git a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch index bba5a156..303b2c73 100644 --- a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch +++ b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.patch @@ -1,4 +1,4 @@ -From 455632d9a1d43fd8905775eb6fc6c81df40464e8 Mon Sep 17 00:00:00 2001 +From 89dbbf8d733db412c0f6ef85a14ccb7870fc75a8 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(ui): resolve authored Bedrock server UI scenes @@ -774,6 +774,23 @@ map reader, four-entry vector conversion, and default Legacy construction. Raw policy retention does not resolve those descriptors or assume a Molang version context. Bind them in the runtime layer with the existing registry and descriptor contracts before enabling native transfer actions. + +Keep desktop default bundle extraction separate from selected-cell extraction. +Original Bedrock 1.26.60.5 protocol 2225 helper 143a2bf10 scans backward for +the last native-nonempty physical entry and submits one Take with all-quantity +selection and flags 0/0. It returns that transfer result without compaction. +Twelve original helper controls cover holes, trailing/empty vectors, rejection, +matching destinations and zero-count retained definitions. A userdata pointer +without a definition does not stop the backward empty-slot scan. Four complete +original generic-transfer controls verify partial Take merging, full/incompatible +destination rejection and zero-count source rejection with these flags. + +Do not skip a zero-count retained definition to expose an earlier entry. +Accept zero-length physical vectors as a no-op. Local regressions verify last +physical selection, Take identity, partial merging, rejection, empty containers +and owned before/after contents. The native controls use explicit model/item, +identity and publication boundaries; they do not establish controller delivery, +wire grouping or installed parity. --- Data Asset Sources.md | 33 + build.gradle | 1 + @@ -784,7 +801,7 @@ and descriptor contracts before enabling native transfer actions. .../api/model/NativeItemStacking.java | 85 + .../api/model/NativeItemStorageData.java | 123 + .../api/model/container/ChestContainer.java | 3 +- - .../container/NativeBundleTransferPlan.java | 289 + + .../container/NativeBundleTransferPlan.java | 306 + .../container/NativeContainerActionSlots.java | 75 + .../NativeContainerLegacyActions.java | 130 + .../container/NativeContainerTransfer.java | 63 + @@ -848,7 +865,7 @@ and descriptor contracts before enabling native transfer actions. .../api/model/NativeItemNamesTest.java | 83 + .../api/model/NativeItemStackingTest.java | 149 + .../api/model/NativeItemStorageDataTest.java | 112 + - .../NativeBundleTransferPlanTest.java | 188 + + .../NativeBundleTransferPlanTest.java | 257 + .../NativeContainerLegacyActionsTest.java | 323 + .../NativeContainerTransferTest.java | 115 + .../container/NativeItemStackMatcherTest.java | 191 + @@ -875,7 +892,7 @@ and descriptor contracts before enabling native transfer actions. .../native-bundle-tooltip-layout.json | 266 + .../inventory/native-bundle-transfers.json | 1622 ++ .../inventory/native-item-storage.json | 568 + - 100 files changed, 40355 insertions(+), 364 deletions(-) + 100 files changed, 40441 insertions(+), 364 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BuiltinBlockNames.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeBundleTooltipData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeBundleTooltipItems.java @@ -1599,10 +1616,10 @@ index a37f1b07..913255f8 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeBundleTransferPlan.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeBundleTransferPlan.java new file mode 100644 -index 00000000..fb69ab1a +index 00000000..dbb2797c --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeBundleTransferPlan.java -@@ -0,0 +1,289 @@ +@@ -0,0 +1,306 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -1624,6 +1641,23 @@ index 00000000..fb69ab1a + + private NativeBundleTransferPlan() { } + ++ /** Desktop default extraction uses one Take and returns its admission result. */ ++ public static Result extractLast(final BedrockItem bundle, final BedrockItem[] contents, ++ final BedrockItem destination, final int destinationSlot, final Source source) { ++ final var state = new State(contents, destination, destinationSlot, source); ++ if (NativeItemStorageData.containerIdentity(bundle) == null) { ++ return state.result(false); ++ } ++ for (int slot = state.items.length - 1; slot >= 0; slot--) { ++ final var item = state.items[slot]; ++ // A defined zero-count stack is not the native empty singleton. It must not expose an earlier item. ++ if (item.amount() != 0 || item.definition() != null || item.identifier() != 0 && item.identifier() != -1) { ++ return state.result(state.transfer(Kind.TAKE, Slot.bundle(slot), state.external, item.amount(), false)); ++ } ++ } ++ return state.result(false); ++ } ++ + public static Result extract(final BedrockItem bundle, final BedrockItem[] contents, + final BedrockItem destination, final int destinationSlot, + final int selectedSlot, final Source source) { @@ -1768,7 +1802,7 @@ index 00000000..fb69ab1a + + private State(final BedrockItem[] contents, final BedrockItem outside, + final int externalSlot, final Source source) { -+ if (contents.length == 0 || contents.length > 256) { ++ if (contents.length > 256) { + throw new IllegalArgumentException("Unsupported native dynamic container size"); + } + this.items = Arrays.stream(contents).map(BedrockItem::copy).toArray(BedrockItem[]::new); @@ -36571,10 +36605,10 @@ index 00000000..0332336d +} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/NativeBundleTransferPlanTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeBundleTransferPlanTest.java new file mode 100644 -index 00000000..23d4e9bb +index 00000000..72802600 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeBundleTransferPlanTest.java -@@ -0,0 +1,188 @@ +@@ -0,0 +1,257 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -36600,6 +36634,75 @@ index 00000000..23d4e9bb +class NativeBundleTransferPlanTest { + + @Test ++ void defaultExtractionTakesTheLastPhysicalEntryAndKeepsTakeWhenMerging() { ++ final var bundle = item(1001, 1, 99); ++ bundle.setTag(new CompoundTag()); ++ bundle.tag().putInt("bundle_id", 42); ++ final var contents = new BedrockItem[]{item(31, 2, 100), BedrockItem.empty(), item(44, 5, 102), BedrockItem.empty()}; ++ for (final int held : new int[]{0, 62}) { ++ final var result = NativeBundleTransferPlan.extractLast(bundle, contents, item(44, held, held == 0 ? 0 : 200), 5, ++ new FixtureSource(new JsonObject())); ++ assertTrue(result.successful()); ++ assertEquals(1, result.transfers().size()); ++ final var transfer = result.transfers().getFirst(); ++ assertEquals(NativeBundleTransferPlan.Kind.TAKE, transfer.kind()); ++ assertEquals(new NativeBundleTransferPlan.Slot(true, 2), transfer.source()); ++ assertEquals(new NativeBundleTransferPlan.Slot(false, 5), transfer.destination()); ++ assertEquals(held == 0 ? 5 : 2, transfer.count()); ++ assertEquals(contents[0], result.contents().getFirst()); ++ assertTrue(result.contents().get(1).isEmpty()); ++ assertEquals(5, contents[2].amount()); ++ } ++ } ++ ++ @Test ++ void defaultExtractionReturnsFailureWhenTheDestinationCannotAcceptItsLastEntry() { ++ final var bundle = item(1001, 1, 99); ++ bundle.setTag(new CompoundTag()); ++ bundle.tag().putInt("bundle_id", 42); ++ final var contents = new BedrockItem[]{item(31, 2, 100), item(44, 5, 101)}; ++ for (final var destination : List.of(item(44, 64, 200), item(55, 1, 201))) { ++ final var result = NativeBundleTransferPlan.extractLast(bundle, contents, destination, 5, new FixtureSource(new JsonObject())); ++ assertFalse(result.successful()); ++ assertTrue(result.transfers().isEmpty()); ++ assertEquals(List.of(contents), result.contents()); ++ assertEquals(destination, result.external()); ++ } ++ } ++ ++ @Test ++ void defaultExtractionDistinguishesZeroCountDefinitionsFromUserdataOnlyEmptySlots() { ++ final var bundle = item(1001, 1, 99); ++ bundle.setTag(new CompoundTag()); ++ bundle.tag().putInt("bundle_id", 42); ++ final var zero = new BedrockItem(44, (short) 0, (byte) 0); ++ final var contents = new BedrockItem[]{item(31, 2, 100), zero, BedrockItem.empty()}; ++ final var result = NativeBundleTransferPlan.extractLast(bundle, contents, BedrockItem.empty(), 5, new FixtureSource(new JsonObject())); ++ assertFalse(result.successful()); ++ assertTrue(result.transfers().isEmpty()); ++ assertEquals(List.of(contents), result.contents()); ++ final var userdataOnly = BedrockItem.empty(); ++ userdataOnly.setTag(new CompoundTag()); ++ contents[1] = userdataOnly; ++ final var withoutDefinition = NativeBundleTransferPlan.extractLast(bundle, contents, BedrockItem.empty(), 5, new FixtureSource(new JsonObject())); ++ assertTrue(withoutDefinition.successful()); ++ assertEquals(new NativeBundleTransferPlan.Slot(true, 0), withoutDefinition.transfers().getFirst().source()); ++ assertEquals(2, withoutDefinition.external().amount()); ++ } ++ ++ @Test ++ void emptyPhysicalContainersHaveNoDefaultTransfer() { ++ final var bundle = item(1001, 1, 99); ++ bundle.setTag(new CompoundTag()); ++ bundle.tag().putInt("bundle_id", 42); ++ for (final var contents : List.of(new BedrockItem[0], new BedrockItem[]{BedrockItem.empty(), BedrockItem.empty()})) { ++ final var result = NativeBundleTransferPlan.extractLast(bundle, contents, BedrockItem.empty(), 5, new FixtureSource(new JsonObject())); ++ assertFalse(result.successful()); ++ assertTrue(result.transfers().isEmpty()); ++ } ++ } ++ ++ @Test + void presentNonIntegerBundleIdentityCanInsertIntoItsZeroContainer() { + final var bundle = item(1001, 1, 99); + bundle.setTag(new CompoundTag()); diff --git a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md index bf68237d..b6a8d29f 100644 --- a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md +++ b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md @@ -31,7 +31,7 @@ Full protocol tag construction and installed controller behavior remain outside `ItemStoragePolicy` retains typed slot limits, nesting flags, and immutable lowercase descriptor maps on each selected definition. Registry regressions cover packet mutation, numeric-ID collisions, absent updates, and malformed replacements without inventing unrestricted filters. -Descriptor binding, its version context, and live request execution remain unfinished. +The full stack supplies descriptor binding and request execution in [deferred patch 0012](../deferred/0012-retain-bedrock-creative-catalog-entries.patch). Effective expression version context and controller delivery remain unfinished. Packet, menu-lifetime, and action tests cover supported adapters. Complete installed player/cooking controllers and live native parity remain unfinished. @@ -48,4 +48,4 @@ Bundle snapshots retain physical slots, view limits, native names, and localized `NativeBundleTransferPlan` retains physical child slots, native preparation order, and cleanup transfers after a rejected insertion. `NativeBundleTransferPlanTest` compares 47 original 1.26.60.5 mutation controls and checks owned item snapshots. The controls cover weights, filters, nesting, partial extraction, holes, and interleaved merges. -Trusted source-policy bindings, request identities, reconciliation, and live controller calls remain unfinished. +Desktop default extraction uses a separate single-Take path. Twelve original native helper controls and four generic-transfer controls establish last physical selection, partial merging, rejection, empty vectors, and zero-count retained definitions. Local regressions preserve this distinction from selected-cell extraction and its compaction. The full stack supplies trusted policy and request adapters in deferred patch 0012; live controller calls remain unfinished.