From 5d8b0f224ca6e7426dce65820dfbb36c03756489 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:32:39 +0100 Subject: [PATCH 1/5] chore: continue compatibility maintenance From 55adc05f858ffcab526a257d2614ccf637419491 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:56:50 +0100 Subject: [PATCH 2/5] feat(inventory): bind authored bundle tooltip state Resolve explicit native bundle views in storage, cooking, player inventory, and workbench scenes. Preserve physical source slots, input-mode visibility, localized text, selection, and native weight and overflow values through program export. Correct the container-screen classification that hid the player inventory tooltip subtree. Keep bundle sources outside Java menu and recipe action scopes. Original Bedrock 1.26.60.5 callback controls and authored-program tests cover these boundaries. Input lifetime, add-on rendering, packet actions, and live comparisons remain unfinished. --- ...ain-bedrock-creative-catalog-entries.patch | 61 +- ...rate-authored-native-ui-presentation.patch | 702 +++++++++++++++++- ...rate-authored-native-ui-presentation.pr.md | 2 +- 3 files changed, 752 insertions(+), 13 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 13cdae23..5c099774 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 5117b833daac2e7b600856929ad3fefacc4af7c6 Mon Sep 17 00:00:00 2001 +From 202abae09d82ca2ae04851e4f746585344011c84 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 @@ -421,6 +421,15 @@ Ignore placeholder candidates without any nonempty ingredient before counting. Tests preserve later candidates and the no-selection result, and cover generation-zero snapshots, unmapped preview inputs/output, search persistence, reopening and no request allocation. + +Resolve player and workbench scenes with the shared native bundle tooltip +view and transport the generic empty-description translation. Keep bundle +physical sources outside Java menu and recipe action scopes. Original +registered getter144c0aa90 dispatches through the actual player/crafting +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. --- .../model/NativePlayerInventoryAction.java | 94 +++ .../model/NativePlayerInventoryMessage.java | 334 +++++++++ @@ -429,7 +438,7 @@ search persistence, reopening and no request allocation. .../model/container/NativeRecipeCrafting.java | 286 ++++++++ .../container/NativeRecipeDescriptor.java | 44 ++ .../block/CraftingTableContainer.java | 2 +- - .../resourcepack/ui/PlayerInventoryUi.java | 397 ++++++++++ + .../resourcepack/ui/PlayerInventoryUi.java | 402 +++++++++++ .../viabedrock/protocol/BedrockProtocol.java | 1 + .../protocol/model/CreativeCatalog.java | 266 +++++++ .../model/CreativeInventoryModel.java | 198 +++++ @@ -447,7 +456,7 @@ search persistence, reopening and no request allocation. .../container/NativeRecipeCounterTest.java | 84 +++ .../container/NativeRecipeDescriptorTest.java | 24 + .../container/NativeRecipeQuantityTest.java | 61 ++ - .../ui/PlayerInventoryUiTest.java | 126 ++++ + .../ui/PlayerInventoryUiTest.java | 153 ++++ .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++ .../protocol/packet/InventoryPacketsTest.java | 266 +++++-- .../rewriter/PlayerRecipeModelTest.java | 611 ++++++++++++++++ @@ -458,7 +467,7 @@ search persistence, reopening and no request allocation. .../NativePlayerCreativeAuxiliaryTest.java | 333 +++++++++ .../storage/NativePlayerCreativePickTest.java | 138 ++++ .../storage/NativeRecipeCraftingTest.java | 500 +++++++++++++ - 36 files changed, 6201 insertions(+), 169 deletions(-) + 36 files changed, 6233 insertions(+), 169 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 @@ -1478,10 +1487,10 @@ index cb10d8f5..93ee9a52 100644 } 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..6bad8d57 +index 00000000..d3c9f229 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUi.java -@@ -0,0 +1,397 @@ +@@ -0,0 +1,402 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -1524,6 +1533,7 @@ index 00000000..6bad8d57 + if (!result.getAsJsonObject("definitions").has(host(workbench))) { + throw new IllegalArgumentException("Missing authored player inventory host"); + } ++ result.getAsJsonObject("translations").addProperty(BundleTooltipUi.EMPTY_DESCRIPTION, this.program.translate(BundleTooltipUi.EMPTY_DESCRIPTION)); + result.addProperty("player_workbench", workbench); + this.exportHelpTranslations(result.get("definitions"), result.getAsJsonObject("translations")); + return result; @@ -1570,10 +1580,15 @@ index 00000000..6bad8d57 + + /** Non-null windows opt into local cell views while retaining the complete menu model. */ + public JsonObject resolve(final MenuView menu, final Map windows) { ++ return this.resolve(menu, windows, BundleTooltipUi.View.inactive()); ++ } ++ ++ public JsonObject resolve(final MenuView menu, final Map windows, final BundleTooltipUi.View tooltip) { + if (this.workbench != null && this.workbench != menu.state.workbench) { + throw new IllegalArgumentException("Player inventory program belongs to another family"); + } -+ final var resolved = this.program.resolve(host(menu.state.workbench), new Controller(menu, windows)); ++ final var resolved = this.program.resolve(host(menu.state.workbench), new Controller(menu, windows) ++ .bundle(tooltip, this.program.translate(BundleTooltipUi.EMPTY_DESCRIPTION))); + if (resolved.root() == null || !resolved.missingTemplates().isEmpty()) { + throw new IllegalArgumentException("Incomplete authored player inventory scene"); + } @@ -1807,7 +1822,6 @@ index 00000000..6bad8d57 + this.globals.put("#is_creative_and_recipe_book_layout", new JsonPrimitive(this.state.creative && this.state.effectiveLayout() == 2)); + this.globals.put("#is_creative_and_creative_layout", new JsonPrimitive(this.state.creative && this.state.effectiveLayout() == 3)); + this.globals.put("#is_creative_layout_button_visible", new JsonPrimitive(this.state.creative && !this.state.workbench)); -+ this.globals.put("#is_container_screen", new JsonPrimitive(this.state.workbench)); + final List tabs = List.of("construct", "equipment", "items", "nature", "search", "inventory"); + for (int index = 0; index < tabs.size(); index++) { + this.globals.put("#is_left_tab_" + tabs.get(index), new JsonPrimitive(this.state.leftTab == index + 1)); @@ -4690,10 +4704,10 @@ index 00000000..f6d3290d +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUiTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUiTest.java new file mode 100644 -index 00000000..e8b3e47e +index 00000000..9500dee7 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUiTest.java -@@ -0,0 +1,126 @@ +@@ -0,0 +1,153 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -4704,6 +4718,8 @@ index 00000000..e8b3e47e +import com.viaversion.viaversion.libs.gson.JsonElement; +import com.viaversion.viaversion.libs.gson.JsonObject; +import com.viaversion.viaversion.libs.gson.JsonPrimitive; ++import net.raphimc.viabedrock.api.model.NativeBundleTooltipData; ++import net.raphimc.viabedrock.api.model.NativeItemStorageData; +import net.raphimc.viabedrock.api.resourcepack.ResourcePack; +import net.raphimc.viabedrock.api.resourcepack.content.InMemoryContent; +import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; @@ -4723,6 +4739,31 @@ index 00000000..e8b3e47e +class PlayerInventoryUiTest { + + @Test ++ void authoredPlayerAndWorkbenchTooltipsKeepPhysicalContentsSeparateFromMenuActions() { ++ final var ui = new PlayerInventoryUi(new ResourcePackStorage(List.of()).getJsonUiProgram()); ++ final var slots = IntStream.range(0, 46).mapToObj(id -> new ContainerUi.Slot(id, Map.of( ++ "#inventory_stack_count", new JsonPrimitive(0)))).toList(); ++ final var data = new NativeBundleTooltipData(17, 12, List.of(1, 0, 2)).withPresentation("§dRetained bundle", "Native description"); ++ final var items = IntStream.range(0, 3).mapToObj(slot -> new BundleTooltipUi.Item("Native item " + slot, ++ Map.of("#inventory_stack_count", new JsonPrimitive(data.amounts().get(slot))))).toList(); ++ final var tooltip = new BundleTooltipUi.View(2, 1, false, 2, data, new NativeItemStorageData(4, 3, 64), items); ++ for (final boolean table : new boolean[]{false, true}) { ++ final var state = new PlayerInventoryUi.State(table, false, 2, 2, 5, 2, true, "", "Recipes", "Crafting", ++ List.of(0, 0, 0, 0), List.of(), "", -1); ++ final var view = new PlayerInventoryUi.MenuView(table ? 7 : 0, state, slots, Map.of()); ++ final var scene = ui.resolve(view, null, tooltip); ++ assertEquals(scene, PlayerInventoryUi.fromProgram(ui.exportProgram(table)).resolve(view, null, tooltip)); ++ final var controls = nodes(scene.getAsJsonObject("root")); ++ final var contents = controls.stream().filter(node -> node.has("bundle_slot")).toList(); ++ assertFalse(contents.isEmpty()); ++ assertEquals(Set.of(-1, 0, 2), contents.stream().map(node -> node.get("bundle_slot").getAsInt()).collect(Collectors.toSet())); ++ assertTrue(contents.stream().noneMatch(node -> node.has("inventory_slot") || node.has("inventory_actions") || node.has("recipe_actions"))); ++ assertTrue(controls.stream().anyMatch(node -> "bundle_label".equals(JsonUiProgram.string(node, "name")) ++ && "§dRetained bundle".equals(JsonUiProgram.string(node, "text")))); ++ } ++ } ++ ++ @Test + void aFullCreativeSourceRetainsOriginalActionsAndMenuSlotsAcrossDistantViewWindows() { + final var ui = new PlayerInventoryUi(new ResourcePackStorage(List.of()).getJsonUiProgram()); + final var slots = IntStream.range(0, 46).mapToObj(id -> new ContainerUi.Slot(id, Map.of( 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 dfeb8209..21a3eefe 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 7856322b7410a5fe8de448df5b87237fe9dee953 Mon Sep 17 00:00:00 2001 +From 22ba101e69b35a3492c4152983b6286ef341f29d 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 @@ -698,6 +698,24 @@ The fallback key is established by original initializer14507683d-1450768a6. Original 1.26.60.5 controls preserve custom namespaces, empty translations, missing fallback results, and the native primary-name source. Keep formatting and newlines in transport rather than deriving text from a Java carrier. + +Bind explicit tooltip views through storage and cooking scene controllers. +Original 1.26.60.5 callbacks14503c5e0/14503c610 distinguish touch mode1 +from hover mode2; screen layout is independent of the input mode. Retain +the two trailing touch-title spaces, remaining-capacity ratio, exact full +comparison, and raw-prefix overflow quantity, including +0. Register the +bundle_items collection with physical source references instead of Java +menu slots. Callbacks14503d970/14503db10/14503c800/14503c020 establish +cell admission, final-cell overflow and selection independent of visibility. +Keep item presentation and native primary names explicit at this boundary. +Original instruction fixtures and exported-program scene tests establish +binding values and physical mapping. Input lifetime, renderer placement and live comparisons still require their own integration. + +Publish the actual common-container classification independently of tooltip +visibility. Original registration144b76e0e-144b76e58 installs getter144c0aa90; +bounded primary-vtable installation145000d60-145000d6a and the original +registered dispatch return true through slot0x130/getter140084510. Retaining +a missing binding would hide the entire authored bundle-tooltip wrapper. --- Data Asset Sources.md | 33 + build.gradle | 1 + @@ -717,6 +735,9 @@ and newlines in transport rather than deriving text from a Java carrier. .../container/dynamic/BundleContainer.java | 2 +- .../definition/ItemDefinitions.java | 5 - .../resourcepack/ui/BundleTooltipLayout.java | 62 + + .../api/resourcepack/ui/BundleTooltipUi.java | 111 + + .../api/resourcepack/ui/ContainerUi.java | 25 +- + .../api/resourcepack/ui/CookingUi.java | 8 +- .../viabedrock/api/util/NativeStringMap.java | 75 + .../viabedrock/protocol/BedrockProtocol.java | 3 + .../protocol/data/BedrockMappingData.java | 15 +- @@ -769,6 +790,7 @@ and newlines in transport rather than deriving text from a Java carrier. .../NativeContainerTransferTest.java | 115 + .../container/NativeItemStackMatcherTest.java | 191 + .../ui/BundleTooltipLayoutTest.java | 38 + + .../resourcepack/ui/BundleTooltipUiTest.java | 151 + .../resourcepack/ui/HudActionbarUiTest.java | 108 + .../api/resourcepack/ui/HudTitleUiTest.java | 176 + .../protocol/model/BuiltinItemNameTest.java | 159 + @@ -785,9 +807,10 @@ and newlines in transport rather than deriving text from a Java carrier. .../packet/WrittenBookUsePacketTest.java | 1 + .../storage/NativeContainerUiStateTest.java | 85 + .../inventory/native-bundle-description.json | 73 + + .../native-bundle-tooltip-bindings.json | 283 + .../native-bundle-tooltip-layout.json | 266 + .../inventory/native-item-storage.json | 568 + - 88 files changed, 37080 insertions(+), 362 deletions(-) + 93 files changed, 37656 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/NativeItemStacking.java @@ -800,6 +823,7 @@ and newlines in transport rather than deriving text from a Java carrier. create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/block/Generic3x3Container.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/block/HopperContainer.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/BundleTooltipLayout.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/BundleTooltipUi.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/util/NativeStringMap.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/BaseGameVersion.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/BuiltinItemAliases.java @@ -835,6 +859,7 @@ and newlines in transport rather than deriving text from a Java carrier. create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeContainerTransferTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeItemStackMatcherTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/BundleTooltipLayoutTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/BundleTooltipUiTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/HudActionbarUiTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/HudTitleUiTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/BuiltinItemNameTest.java @@ -849,6 +874,7 @@ and newlines in transport rather than deriving text from a Java carrier. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/NativeHudPacketTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativeContainerUiStateTest.java create mode 100644 src/test/resources/inventory/native-bundle-description.json + create mode 100644 src/test/resources/inventory/native-bundle-tooltip-bindings.json create mode 100644 src/test/resources/inventory/native-bundle-tooltip-layout.json create mode 100644 src/test/resources/inventory/native-item-storage.json @@ -1913,6 +1939,232 @@ index 00000000..966a11ab + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/BundleTooltipUi.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/BundleTooltipUi.java +new file mode 100644 +index 00000000..1f22a92d +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/BundleTooltipUi.java +@@ -0,0 +1,111 @@ ++/* ++ * 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.resourcepack.ui; ++ ++import com.viaversion.viaversion.libs.gson.JsonArray; ++import com.viaversion.viaversion.libs.gson.JsonElement; ++import com.viaversion.viaversion.libs.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonPrimitive; ++import net.raphimc.viabedrock.api.model.NativeBundleTooltipData; ++import net.raphimc.viabedrock.api.model.NativeItemStorageData; ++ ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++ ++/** Native tooltip bindings use physical contents and input mode independently of screen layout. */ ++public final class BundleTooltipUi { ++ ++ static final String EMPTY_DESCRIPTION = "item.bundle.empty.description"; ++ ++ private BundleTooltipUi() { } ++ ++ static Map bindings(final View view, final String emptyDescription) { ++ final var layout = view.layout(); ++ final boolean active = view.active(); ++ final var storage = view.storage; ++ final Map values = new HashMap<>(); ++ final JsonArray dimensions = new JsonArray(); ++ dimensions.add(layout.columns()); ++ dimensions.add(layout.rows()); ++ values.put("#bundle_tooltip_grid_dimensions", dimensions); ++ values.put("#using_touch", new JsonPrimitive(view.inputMode == 2)); ++ values.put("#show_bundle_tooltip_hover", new JsonPrimitive(view.mode == 2 && view.inputMode != 2)); ++ values.put("#show_bundle_tooltip_pocket", new JsonPrimitive(view.mode == 1 && view.inputMode == 2 && !view.hidden)); ++ values.put("#bundle_name", new JsonPrimitive(active ? view.contents.primaryName() + (view.inputMode == 2 ? " " : "") : "Bundle")); ++ values.put("#bundle_empty_description", new JsonPrimitive(active ? view.contents.emptyDescription() : emptyDescription)); ++ values.put("#bundle_has_selected_item", new JsonPrimitive(view.selectedCell != null)); ++ String selectedName = ""; ++ if (active && view.selectedCell != null && view.selectedCell >= 0 && view.selectedCell < layout.sourceSlots().size()) { ++ final int slot = layout.sourceSlots().get(view.selectedCell); ++ if (slot >= 0 && view.contents.amounts().get(slot) != 0) { ++ selectedName = view.items.get(slot).primaryName; ++ } ++ } ++ values.put("#selected_bundle_item_text", new JsonPrimitive(selectedName)); ++ values.put("#more_hidden_items_text", new JsonPrimitive(layout.hiddenEntries() == 0 ? "" : "+" + layout.hiddenItemCount())); ++ values.put("#is_selected_bundle_empty", new JsonPrimitive(active && (storage == null || storage.weight() == 0))); ++ values.put("#is_selected_bundle_full", new JsonPrimitive(active && storage != null && storage.weight() == storage.capacity())); ++ final float remaining = !active ? 1F : storage == null ? Float.NaN : (float) (storage.capacity() - storage.weight()) / storage.capacity(); ++ values.put("#bundle_weight_bar_ratio", new JsonPrimitive(remaining)); ++ return Map.copyOf(values); ++ } ++ ++ static UiBindings.Collection collection(final View view) { ++ final var layout = view.layout(); ++ final List> entries = new ArrayList<>(); ++ for (int cell = 0; cell < layout.sourceSlots().size(); cell++) { ++ final int slot = layout.sourceSlots().get(cell); ++ final Map values = new HashMap<>(slot < 0 ? Map.of() : view.items.get(slot).presentation); ++ values.put("#collection_index", new JsonPrimitive(cell)); ++ values.put("#show_bundle_item_slot", new JsonPrimitive(slot >= 0 && view.contents.amounts().get(slot) != 0)); ++ values.put("#is_hidden_items_label", new JsonPrimitive(layout.hiddenEntries() != 0 && cell == layout.sourceSlots().size() - 1)); ++ values.put("#is_selected_bundle_item_slot", new JsonPrimitive(view.selectedCell != null && view.selectedCell == cell)); ++ final JsonObject reference = new JsonObject(); ++ reference.addProperty("bundle_identity", view.contents.identity()); ++ reference.addProperty("bundle_slot", slot); ++ values.put("#item_renderer_data", reference); ++ entries.add(values); ++ } ++ return new UiBindings.Collection(entries, Map.of("#collection_total_items", new JsonPrimitive(entries.size()))); ++ } ++ ++ public record Item(String primaryName, Map presentation) { ++ public Item { ++ if (primaryName == null) { ++ throw new IllegalArgumentException("Missing native bundle item name"); ++ } ++ presentation = ContainerUi.copy(presentation); ++ } ++ } ++ ++ /** Modes 1 and 2 activate the retained tooltip source. Selection is a grid cell. */ ++ public record View(int mode, int inputMode, boolean hidden, Integer selectedCell, ++ NativeBundleTooltipData contents, NativeItemStorageData storage, List items) { ++ public View { ++ items = List.copyOf(items); ++ if (mode < 0 || mode > 2 || mode != 0 && (contents == null || contents.primaryName() == null ++ || contents.emptyDescription() == null || items.size() != contents.amounts().size())) { ++ throw new IllegalArgumentException("Incomplete native bundle tooltip view"); ++ } ++ } ++ ++ public static View inactive() { ++ return new View(0, 1, false, null, null, null, List.of()); ++ } ++ ++ boolean active() { ++ return this.mode == 1 || this.mode == 2; ++ } ++ ++ BundleTooltipLayout layout() { ++ return this.active() ? BundleTooltipLayout.resolve(this.contents) ++ : new BundleTooltipLayout(0, 4, 0, 0, 0, List.of()); ++ } ++ } ++ ++} +diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUi.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUi.java +index 76e62199..fe1f231b 100644 +--- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUi.java ++++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUi.java +@@ -61,6 +61,7 @@ public final class ContainerUi { + try { + final String host = kind.host; + final JsonObject exported = this.program.export(Set.of(host), new JsonPrimitive(title.raw)); ++ exported.getAsJsonObject("translations").addProperty(BundleTooltipUi.EMPTY_DESCRIPTION, this.program.translate(BundleTooltipUi.EMPTY_DESCRIPTION)); + final JsonObject definitions = exported.getAsJsonObject("definitions"); + if (!definitions.has(host)) { + return null; +@@ -78,15 +79,24 @@ public final class ContainerUi { + } + + public JsonObject resolve(final MenuView menu) { ++ return this.resolve(menu, BundleTooltipUi.View.inactive()); ++ } ++ ++ public JsonObject resolve(final MenuView menu, final BundleTooltipUi.View tooltip) { + try { +- return this.resolveScene(menu); ++ return this.resolveScene(menu, tooltip); + } catch (final IllegalArgumentException | IllegalStateException unsupported) { + return null; + } + } + + JsonObject resolveScene(final MenuView menu) { ++ return this.resolveScene(menu, BundleTooltipUi.View.inactive()); ++ } ++ ++ private JsonObject resolveScene(final MenuView menu, final BundleTooltipUi.View tooltip) { + final Controller controller = new Controller(menu, this.title == null ? new Title(menu.title, false) : this.title, this.nativeActions); ++ controller.bundle(tooltip, this.program.translate(BundleTooltipUi.EMPTY_DESCRIPTION)); + final String host = menu.kind.host; + final var resolved = this.program.resolve(host, controller); + if (resolved.root() == null) { +@@ -323,6 +333,7 @@ public final class ContainerUi { + this.globals.put("$pocket_screen", new JsonPrimitive(false)); + this.globals.put("#is_using_gamepad", new JsonPrimitive(false)); + this.globals.put("#is_using_touch", new JsonPrimitive(false)); ++ this.globals.put("#is_container_screen", new JsonPrimitive(true)); + this.globals.put("#close_button_visible", new JsonPrimitive(true)); + this.globals.putAll(cursor); + final JsonObject cursorReference = new JsonObject(); +@@ -330,6 +341,12 @@ public final class ContainerUi { + this.globals.put("#inventory_selected_item", cursorReference); + } + ++ final Controller bundle(final BundleTooltipUi.View tooltip, final String emptyDescription) { ++ this.globals.putAll(BundleTooltipUi.bindings(tooltip, emptyDescription)); ++ this.collections.put("bundle_items", BundleTooltipUi.collection(tooltip)); ++ return this; ++ } ++ + protected final void registerSingleton(final String name, final int slot, final String emptyBinding) { + this.collections.put(name, singleton(this.slots, slot, emptyBinding)); + this.singletonCollections.add(name); +@@ -401,6 +418,9 @@ public final class ContainerUi { + public Map bindings(final Scope scope) { + final Map values = new HashMap<>(this.globals); + values.putAll(this.variables(scope)); ++ if (scope == null || !"bundle_items".equals(scope.collection())) { ++ values.put("#is_selected_bundle_item_slot", new JsonPrimitive(false)); ++ } + if (scope != null && Set.of("brewing_result_items", "brewing_fuel_item").contains(scope.collection())) { + final JsonElement count = values.get("#inventory_stack_count"); + if (count == null || !count.isJsonPrimitive() || !count.getAsJsonPrimitive().isNumber()) { +@@ -428,6 +448,9 @@ public final class ContainerUi { + node.addProperty("collection_index", scope.index()); + if (this.variables(scope).get("#item_renderer_data") instanceof JsonObject reference && reference.has("java_slot")) { + node.addProperty("inventory_slot", reference.get("java_slot").getAsInt()); ++ } else if (this.variables(scope).get("#item_renderer_data") instanceof JsonObject reference && reference.has("bundle_slot")) { ++ node.addProperty("bundle_slot", reference.get("bundle_slot").getAsInt()); ++ node.addProperty("bundle_identity", reference.get("bundle_identity").getAsInt()); + } + } else if (this.singletonCollections.contains(UiExpression.text(resolved.get("$item_collection_name")))) { + final String collection = UiExpression.text(resolved.get("$item_collection_name")); +diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingUi.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingUi.java +index 8bd1da2a..8d51d187 100644 +--- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingUi.java ++++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingUi.java +@@ -47,6 +47,7 @@ public final class CookingUi { + if (!exported.getAsJsonObject("definitions").has(family.host)) { + throw new IllegalArgumentException("Missing native cooking host"); + } ++ exported.getAsJsonObject("translations").addProperty(BundleTooltipUi.EMPTY_DESCRIPTION, this.program.translate(BundleTooltipUi.EMPTY_DESCRIPTION)); + exported.addProperty("cooking_family", family.name()); + exported.getAsJsonObject("globals").addProperty("$container_title", title.raw()); + exported.getAsJsonObject("globals").addProperty("$localize_title", title.localize()); +@@ -76,11 +77,16 @@ public final class CookingUi { + } + + public JsonObject resolve(final MenuView menu) { ++ return this.resolve(menu, BundleTooltipUi.View.inactive()); ++ } ++ ++ public JsonObject resolve(final MenuView menu, final BundleTooltipUi.View tooltip) { + if (this.family != null && menu.family != this.family) { + throw new IllegalArgumentException("Cooking program belongs to another family"); + } + final var resolved = this.program.resolve(menu.family.host, +- new Controller(menu, this.title == null ? menu.title : this.title)); ++ new Controller(menu, this.title == null ? menu.title : this.title) ++ .bundle(tooltip, this.program.translate(BundleTooltipUi.EMPTY_DESCRIPTION))); + if (resolved.root() == null || !"screen".equals(UiExpression.text(resolved.root().get("type"))) + || !resolved.requiredDefinitions().contains(menu.family.host) || !resolved.missingTemplates().isEmpty()) { + throw new IllegalArgumentException("Incomplete native cooking program"); diff --git a/src/main/java/net/raphimc/viabedrock/api/util/NativeStringMap.java b/src/main/java/net/raphimc/viabedrock/api/util/NativeStringMap.java new file mode 100644 index 00000000..dd4f7744 @@ -36120,6 +36372,163 @@ index 00000000..da791492 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/BundleTooltipUiTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/BundleTooltipUiTest.java +new file mode 100644 +index 00000000..2ced731a +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/BundleTooltipUiTest.java +@@ -0,0 +1,151 @@ ++/* ++ * 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.resourcepack.ui; ++ ++import com.viaversion.viaversion.libs.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonParser; ++import com.viaversion.viaversion.libs.gson.JsonPrimitive; ++import net.raphimc.viabedrock.api.model.NativeBundleTooltipData; ++import net.raphimc.viabedrock.api.model.NativeItemStorageData; ++import org.junit.jupiter.api.Test; ++ ++import java.io.InputStreamReader; ++import java.nio.charset.StandardCharsets; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import java.util.stream.IntStream; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class BundleTooltipUiTest { ++ ++ private static final String UI = """ ++ {"namespace":"chest","small_chest_screen":{"type":"screen","controls":[ ++ {"title":{"type":"label","text":"#bundle_name","bindings":[{"binding_name":"#bundle_name"}]}}, ++ {"description":{"type":"label","text":"#bundle_empty_description","bindings":[{"binding_name":"#bundle_empty_description"}]}}, ++ {"selected":{"type":"label","text":"#selected_bundle_item_text","bindings":[{"binding_name":"#selected_bundle_item_text"}]}}, ++ {"items":{"type":"grid","collection_name":"bundle_items","grid_dimension_binding":"#bundle_tooltip_grid_dimensions", ++ "grid_item_template":"chest.cell","bindings":[{"binding_name":"#bundle_tooltip_grid_dimensions"}]}}]}, ++ "cell":{"type":"panel","controls":[ ++ {"item":{"type":"custom","renderer":"inventory_item_renderer","bindings":[ ++ {"binding_name":"#show_bundle_item_slot","binding_name_override":"#visible","binding_type":"collection","binding_collection_name":"bundle_items"}, ++ {"binding_name":"#item_renderer_data","binding_name_override":"#item_id_aux","binding_type":"collection","binding_collection_name":"bundle_items"}]}}, ++ {"overflow":{"type":"label","text":"#more_hidden_items_text","bindings":[ ++ {"binding_name":"#is_hidden_items_label","binding_name_override":"#visible","binding_type":"collection","binding_collection_name":"bundle_items"}, ++ {"binding_name":"#more_hidden_items_text"}]}}]}} ++ """; ++ ++ private static JsonObject fixture(final String name) throws Exception { ++ try (var stream = BundleTooltipUiTest.class.getResourceAsStream("/inventory/" + name + ".json")) { ++ assertNotNull(stream); ++ return JsonParser.parseReader(new InputStreamReader(stream, StandardCharsets.UTF_8)).getAsJsonObject(); ++ } ++ } ++ ++ private static BundleTooltipUi.View view(final int mode, final int input, final boolean hidden, final Integer selected, ++ final List amounts, final int limit, final NativeItemStorageData storage, final String name) { ++ final var data = new NativeBundleTooltipData(7, limit, amounts).withPresentation(name, "§7Translated description"); ++ final var items = IntStream.range(0, amounts.size()).mapToObj(slot -> new BundleTooltipUi.Item("Item " + slot, ++ Map.of("#inventory_stack_count", new JsonPrimitive(amounts.get(slot))))).toList(); ++ return new BundleTooltipUi.View(mode, input, hidden, selected, data, storage, items); ++ } ++ ++ @Test ++ void tooltipVisibilityNamesAndRemainingCapacityMatchOriginalCallbacks() throws Exception { ++ final var fixture = fixture("native-bundle-tooltip-bindings"); ++ for (final var element : fixture.getAsJsonArray("weights")) { ++ final var row = element.getAsJsonObject(); ++ final var input = row.getAsJsonObject("input"); ++ final var storage = input.get("valid").getAsBoolean() ++ ? new NativeItemStorageData(4, input.get("weight").getAsInt(), input.get("limit").getAsInt()) : null; ++ final var values = BundleTooltipUi.bindings(view(input.get("mode").getAsInt(), 1, false, null, List.of(), 12, storage, "Bundle"), "Fallback"); ++ final var expected = row.getAsJsonObject("bindings"); ++ final var ratio = expected.getAsJsonObject("ratio").get("value"); ++ if (ratio.isJsonPrimitive() && ratio.getAsJsonPrimitive().isString()) { ++ assertTrue(Float.isNaN(values.get("#bundle_weight_bar_ratio").getAsFloat())); ++ } else { ++ assertEquals(ratio.getAsFloat(), values.get("#bundle_weight_bar_ratio").getAsFloat()); ++ } ++ assertEquals(expected.get("empty").getAsBoolean(), values.get("#is_selected_bundle_empty").getAsBoolean()); ++ assertEquals(expected.get("full").getAsBoolean(), values.get("#is_selected_bundle_full").getAsBoolean()); ++ } ++ for (final var element : fixture.getAsJsonArray("visibility")) { ++ final var row = element.getAsJsonObject(); ++ final var values = BundleTooltipUi.bindings(view(row.get("rawModeAt1230").getAsInt(), row.get("rawProfileAtC04").getAsInt(), ++ row.get("rawFlagAt11f8").getAsBoolean(), null, List.of(), 12, new NativeItemStorageData(4, 0, 64), "Bundle"), "Fallback"); ++ assertEquals(row.get("hover").getAsBoolean(), values.get("#show_bundle_tooltip_hover").getAsBoolean()); ++ assertEquals(row.get("pocket").getAsBoolean(), values.get("#show_bundle_tooltip_pocket").getAsBoolean()); ++ } ++ for (final var element : fixture.getAsJsonArray("names")) { ++ final var row = element.getAsJsonObject(); ++ final var input = row.getAsJsonObject("input"); ++ final var values = BundleTooltipUi.bindings(view(input.get("mode").getAsInt(), input.get("input").getAsInt(), false, null, ++ List.of(), 12, new NativeItemStorageData(4, 0, 64), input.get("name").getAsString()), "Fallback"); ++ assertEquals(row.get("text").getAsString(), values.get("#bundle_name").getAsString()); ++ } ++ } ++ ++ @Test ++ void transportedContainerProgramRetainsSparsePhysicalCellsSelectionAndOverflow() throws Exception { ++ final var source = new ContainerUi(new JsonUiProgram(List.of(Map.of("ui/chest.json", UI.getBytes(StandardCharsets.UTF_8))), ++ key -> key.equals(BundleTooltipUi.EMPTY_DESCRIPTION) ? "Resource pack fallback" : key)); ++ final var program = source.exportProgram(ContainerUi.Kind.CHEST_3_ROWS, new ContainerUi.Title("Chest", false)); ++ final var imported = ContainerUi.fromProgram(program); ++ final var slots = IntStream.range(0, 63).mapToObj(slot -> new ContainerUi.Slot(slot, Map.of())).toList(); ++ final var menu = new ContainerUi.MenuView(7, ContainerUi.Kind.CHEST_3_ROWS, "Chest", slots, Map.of(), List.of()); ++ final var inactive = imported.resolve(menu); ++ assertNotNull(inactive); ++ assertEquals("Resource pack fallback", node(inactive, "description").get("text").getAsString()); ++ for (final int index : List.of(1, 2)) { ++ final var row = fixture("native-bundle-tooltip-layout").getAsJsonArray("rows").get(index).getAsJsonObject(); ++ final var amounts = row.getAsJsonArray("amounts").asList().stream().map(value -> value.getAsInt()).toList(); ++ final var view = view(2, 1, false, 2, amounts, row.get("viewableSlots").getAsInt(), new NativeItemStorageData(4, 8, 64), "§dCustom bundle"); ++ final var scene = imported.resolve(menu, view); ++ assertNotNull(scene); ++ assertEquals("§dCustom bundle", node(scene, "title").get("text").getAsString()); ++ assertEquals("§7Translated description", node(scene, "description").get("text").getAsString()); ++ final int selectedSlot = row.getAsJsonArray("cells").get(2).getAsInt(); ++ assertEquals("Item " + selectedSlot, node(scene, "selected").get("text").getAsString()); ++ final var grid = node(scene, "items"); ++ final var cells = grid.getAsJsonArray("children"); ++ assertEquals(row.getAsJsonArray("cells").size(), cells.size()); ++ for (int cell = 0; cell < cells.size(); cell++) { ++ final int physical = row.getAsJsonArray("cells").get(cell).getAsInt(); ++ final var item = node(cells.get(cell).getAsJsonObject(), "item"); ++ assertEquals(physical, item.get("bundle_slot").getAsInt()); ++ assertFalse(item.has("inventory_slot"), "Bundle contents must not become Java menu actions"); ++ assertEquals(physical >= 0, item.get("visible").getAsBoolean()); ++ final var overflow = node(cells.get(cell).getAsJsonObject(), "overflow"); ++ final boolean hidden = row.get("hiddenEntries").getAsInt() != 0 && cell == cells.size() - 1; ++ assertEquals(hidden, overflow.get("visible").getAsBoolean()); ++ if (hidden) { ++ assertEquals("+3", overflow.get("text").getAsString()); ++ } ++ } ++ if (row.get("hiddenEntries").getAsInt() != 0) { ++ final var overflowSelected = view(2, 1, false, cells.size() - 1, amounts, row.get("viewableSlots").getAsInt(), ++ new NativeItemStorageData(4, 8, 64), "§dCustom bundle"); ++ assertEquals("", node(imported.resolve(menu, overflowSelected), "selected").get("text").getAsString()); ++ } ++ } ++ } ++ ++ private static JsonObject node(final JsonObject root, final String name) { ++ return nodes(root.has("root") ? root.getAsJsonObject("root") : root).stream() ++ .filter(node -> name.equals(JsonUiProgram.string(node, "name"))).findFirst().orElseThrow(); ++ } ++ ++ private static List nodes(final JsonObject root) { ++ final List result = new ArrayList<>(); ++ result.add(root); ++ if (root.has("children")) { ++ root.getAsJsonArray("children").forEach(child -> result.addAll(nodes(child.getAsJsonObject()))); ++ } ++ return result; ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/HudActionbarUiTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/HudActionbarUiTest.java new file mode 100644 index 00000000..2aee20b8 @@ -38758,6 +39167,295 @@ index 00000000..dbf227f0 + } + ] +} +diff --git a/src/test/resources/inventory/native-bundle-tooltip-bindings.json b/src/test/resources/inventory/native-bundle-tooltip-bindings.json +new file mode 100644 +index 00000000..01f81555 +--- /dev/null ++++ b/src/test/resources/inventory/native-bundle-tooltip-bindings.json +@@ -0,0 +1,283 @@ ++{ ++ "version": "1.26.60.5", ++ "protocol": 2225, ++ "executableSha256": "988f6a61403b4381aa41e6c66d64cce441b14e561a9b10f647c2758ef3f2672a", ++ "scope": "Original callback values. Item lookup, layout, primary names and storage-weight production are supplied boundaries; layout has separate original native controls. These rows do not prove controller publication, rendered pixels or input parity.", ++ "functions": { ++ "visibility": [ ++ "14503c5e0", ++ "14503c610" ++ ], ++ "name": "14503cbe0", ++ "weight": [ ++ "14503dd30", ++ "14503dec0", ++ "14503dfe0" ++ ], ++ "cells": [ ++ "14503d970", ++ "14503db10", ++ "14503c800", ++ "14503c020" ++ ] ++ }, ++ "weights": [ ++ { ++ "input": { ++ "name": "inactive", ++ "mode": 0, ++ "weight": 8, ++ "limit": 64, ++ "valid": true, ++ "ratio": 1.0, ++ "full": false, ++ "empty": false ++ }, ++ "bindings": { ++ "ratio": { ++ "value": 1.0, ++ "bits": "3f800000" ++ }, ++ "full": false, ++ "empty": false ++ } ++ }, ++ { ++ "input": { ++ "name": "empty-mode1", ++ "mode": 1, ++ "weight": 0, ++ "limit": 64, ++ "valid": true, ++ "ratio": 1.0, ++ "full": false, ++ "empty": true ++ }, ++ "bindings": { ++ "ratio": { ++ "value": 1.0, ++ "bits": "3f800000" ++ }, ++ "full": false, ++ "empty": true ++ } ++ }, ++ { ++ "input": { ++ "name": "partial-mode1", ++ "mode": 1, ++ "weight": 8, ++ "limit": 64, ++ "valid": true, ++ "ratio": 0.875, ++ "full": false, ++ "empty": false ++ }, ++ "bindings": { ++ "ratio": { ++ "value": 0.875, ++ "bits": "3f600000" ++ }, ++ "full": false, ++ "empty": false ++ } ++ }, ++ { ++ "input": { ++ "name": "full-mode2", ++ "mode": 2, ++ "weight": 64, ++ "limit": 64, ++ "valid": true, ++ "ratio": 0.0, ++ "full": true, ++ "empty": false ++ }, ++ "bindings": { ++ "ratio": { ++ "value": 0.0, ++ "bits": "00000000" ++ }, ++ "full": true, ++ "empty": false ++ } ++ }, ++ { ++ "input": { ++ "name": "overfull", ++ "mode": 2, ++ "weight": 65, ++ "limit": 64, ++ "valid": true, ++ "ratio": -0.015625, ++ "full": false, ++ "empty": false ++ }, ++ "bindings": { ++ "ratio": { ++ "value": -0.015625, ++ "bits": "bc800000" ++ }, ++ "full": false, ++ "empty": false ++ } ++ }, ++ { ++ "input": { ++ "name": "invalid-weight", ++ "mode": 1, ++ "weight": 8, ++ "limit": 64, ++ "valid": false, ++ "ratio": null, ++ "full": false, ++ "empty": true ++ }, ++ "bindings": { ++ "ratio": { ++ "value": "NaN", ++ "bits": "ffc00000" ++ }, ++ "full": false, ++ "empty": true ++ } ++ } ++ ], ++ "visibility": [ ++ { ++ "rawProfileAtC04": 1, ++ "rawModeAt1230": 0, ++ "rawFlagAt11f8": false, ++ "hover": false, ++ "pocket": false ++ }, ++ { ++ "rawProfileAtC04": 1, ++ "rawModeAt1230": 0, ++ "rawFlagAt11f8": true, ++ "hover": false, ++ "pocket": false ++ }, ++ { ++ "rawProfileAtC04": 1, ++ "rawModeAt1230": 1, ++ "rawFlagAt11f8": false, ++ "hover": false, ++ "pocket": false ++ }, ++ { ++ "rawProfileAtC04": 1, ++ "rawModeAt1230": 1, ++ "rawFlagAt11f8": true, ++ "hover": false, ++ "pocket": false ++ }, ++ { ++ "rawProfileAtC04": 1, ++ "rawModeAt1230": 2, ++ "rawFlagAt11f8": false, ++ "hover": true, ++ "pocket": false ++ }, ++ { ++ "rawProfileAtC04": 1, ++ "rawModeAt1230": 2, ++ "rawFlagAt11f8": true, ++ "hover": true, ++ "pocket": false ++ }, ++ { ++ "rawProfileAtC04": 2, ++ "rawModeAt1230": 0, ++ "rawFlagAt11f8": false, ++ "hover": false, ++ "pocket": false ++ }, ++ { ++ "rawProfileAtC04": 2, ++ "rawModeAt1230": 0, ++ "rawFlagAt11f8": true, ++ "hover": false, ++ "pocket": false ++ }, ++ { ++ "rawProfileAtC04": 2, ++ "rawModeAt1230": 1, ++ "rawFlagAt11f8": false, ++ "hover": false, ++ "pocket": true ++ }, ++ { ++ "rawProfileAtC04": 2, ++ "rawModeAt1230": 1, ++ "rawFlagAt11f8": true, ++ "hover": false, ++ "pocket": false ++ }, ++ { ++ "rawProfileAtC04": 2, ++ "rawModeAt1230": 2, ++ "rawFlagAt11f8": false, ++ "hover": false, ++ "pocket": false ++ }, ++ { ++ "rawProfileAtC04": 2, ++ "rawModeAt1230": 2, ++ "rawFlagAt11f8": true, ++ "hover": false, ++ "pocket": false ++ } ++ ], ++ "names": [ ++ { ++ "input": { ++ "mode": 0, ++ "input": 1, ++ "valid": true, ++ "name": "My Bundle" ++ }, ++ "text": "Bundle", ++ "calls": [] ++ }, ++ { ++ "input": { ++ "mode": 1, ++ "input": 1, ++ "valid": true, ++ "name": "My Bundle" ++ }, ++ "text": "My Bundle", ++ "calls": [ ++ "item", ++ "primary-name" ++ ] ++ }, ++ { ++ "input": { ++ "mode": 1, ++ "input": 2, ++ "valid": true, ++ "name": "My Bundle" ++ }, ++ "text": "My Bundle ", ++ "calls": [ ++ "item", ++ "primary-name" ++ ] ++ }, ++ { ++ "input": { ++ "mode": 2, ++ "input": 2, ++ "valid": true, ++ "name": "A Longer Named Bundle" ++ }, ++ "text": "A Longer Named Bundle ", ++ "calls": [ ++ "item", ++ "primary-name" ++ ] ++ } ++ ] ++} diff --git a/src/test/resources/inventory/native-bundle-tooltip-layout.json b/src/test/resources/inventory/native-bundle-tooltip-layout.json new file mode 100644 index 00000000..ce03cd8b 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 80965395..2ea0d24b 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 @@ -34,4 +34,4 @@ This verifies the generic fallback defect; the original Lifeboat dialog report s The [owning patch](0095-integrate-authored-native-ui-presentation.patch) retains implementation rationale and versioned evidence. -Bundle items retain physical-slot counts, the server component's viewable-slot limit, native primary names, and localized empty descriptions. Original 1.26.60.5 fixtures cover sparse and overflow mapping, wider layouts, custom namespaces, and translation fallback. Authored tooltip state, rendering, and input remain unfinished. +Bundle snapshots retain physical slots, view limits, native names, and localized descriptions. Storage, cooking, and player scene controllers accept explicit tooltip views and preserve native visibility, remaining capacity, selection, and overflow values through program export. Original 1.26.60.5 callbacks and scene tests cover these boundaries. The native container classification keeps the authored tooltip wrapper available in the player inventory. Input lifetime, rendering, and live comparisons remain unfinished. From 614ee052757e40b928ac761260477612630587aa Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:29:30 +0100 Subject: [PATCH 3/5] feat(inventory): retain physical bundle item previews Carry bounded item component patches and native names at their original physical slots, independently of Java bundle compaction and capacity rules. Validate preview identity and amounts against the retained source snapshot. Emit tables only for outer stacks belonging to clients with native inventory support, and keep malformed or oversized tables unavailable instead of manufacturing empty contents. Keep nested conversion tables shallow to prevent recursive duplication. Input lifetime, add-on rendering, and live comparisons remain open. --- ...erver-authoritative-bedrock-requests.patch | 44 +- ...edrock-recipes-and-place-ingredients.patch | 31 +- ...08-signal-sustained-bedrock-item-use.patch | 16 +- ...ain-bedrock-creative-catalog-entries.patch | 20 +- ...rate-authored-native-ui-presentation.patch | 430 +++++++++++++++++- ...rate-authored-native-ui-presentation.pr.md | 2 +- 6 files changed, 464 insertions(+), 79 deletions(-) diff --git a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch index 5660b55d..7c3d196a 100644 --- a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch +++ b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch @@ -1,4 +1,4 @@ -From 7754375e15833c35fbbcba48b2ccdc9b519106f9 Mon Sep 17 00:00:00 2001 +From 766d63ec9879dcf7ec959d0905f3eb03b51f5dcd 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(inventory): translate server-authoritative Bedrock @@ -11621,7 +11621,7 @@ index f737c0b2..31341a0e 100644 final BlockEntity javaBlockEntity = BlockEntityRewriter.toJava(wrapper.user(), chunkTracker.getBlockState(bedrockBlockEntity.position()), bedrockBlockEntity); 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 bcf7ac89..988369db 100644 +index 36b2cf55..a5193c10 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java @@ -35,10 +35,13 @@ import com.viaversion.viaversion.libs.fastutil.ints.Int2ObjectMap; @@ -11638,7 +11638,7 @@ index bcf7ac89..988369db 100644 import net.raphimc.viabedrock.api.model.NativeItemStacking; import net.raphimc.viabedrock.api.model.NativeItemStackData; import net.raphimc.viabedrock.api.model.NativeItemStorageData; -@@ -50,12 +53,19 @@ import net.raphimc.viabedrock.protocol.data.BedrockMappingData; +@@ -54,12 +57,19 @@ 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; @@ -11660,7 +11660,7 @@ index bcf7ac89..988369db 100644 import net.raphimc.viabedrock.protocol.model.ItemCooldown; import net.raphimc.viabedrock.protocol.model.ItemAnimationData; import net.raphimc.viabedrock.protocol.model.ItemArmorData; -@@ -69,16 +79,19 @@ import net.raphimc.viabedrock.protocol.storage.MapTracker; +@@ -73,16 +83,19 @@ import net.raphimc.viabedrock.protocol.storage.MapTracker; import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; import net.raphimc.viabedrock.protocol.types.BedrockTypes; import net.raphimc.viabedrock.protocol.types.array.ArrayType; @@ -11680,8 +11680,8 @@ index bcf7ac89..988369db 100644 +public class ItemRewriter extends StoredObject implements NetworkItemStackContext { private final ItemRegistry registry; - -@@ -102,6 +115,8 @@ public class ItemRewriter extends StoredObject { + private int javaItemDepth; +@@ -107,6 +120,8 @@ public class ItemRewriter extends StoredObject { private final Type newItemType; private final Type optionalNewItemType; private final Type newItemArrayType; @@ -11690,7 +11690,7 @@ index bcf7ac89..988369db 100644 static { // TODO: Add missing item nbt rewriters -@@ -134,10 +149,11 @@ public class ItemRewriter extends StoredObject { +@@ -139,10 +154,11 @@ public class ItemRewriter extends StoredObject { super(user); final BlockStateRewriter blockStateRewriter = this.user().get(BlockStateRewriter.class); @@ -11704,7 +11704,7 @@ index bcf7ac89..988369db 100644 if (itemEntry.componentData() != null && itemEntry.componentData().get("components") instanceof CompoundTag components) { this.animationData.put(itemEntry.identifier(), ItemAnimationData.fromNetworkComponents(components)); -@@ -148,8 +164,10 @@ public class ItemRewriter extends StoredObject { +@@ -153,8 +169,10 @@ public class ItemRewriter extends StoredObject { this.cooldowns.put(itemEntry.identifier(), cooldown); } } @@ -11716,7 +11716,7 @@ index bcf7ac89..988369db 100644 blockItems.removeIf(identifier -> !this.items.containsKey(identifier)); this.blockItemValidBlockStates = new Int2ObjectOpenHashMap<>(blockItems.size()); -@@ -158,22 +176,37 @@ public class ItemRewriter extends StoredObject { +@@ -163,22 +181,37 @@ public class ItemRewriter extends StoredObject { if (validBlockStates == null) { final String[] components = identifier.split(":", 2); if (components[1].startsWith("item.")) { @@ -11759,7 +11759,7 @@ index bcf7ac89..988369db 100644 } public Item javaItem(final BedrockItem bedrockItem) { -@@ -183,13 +216,18 @@ public class ItemRewriter extends StoredObject { +@@ -197,13 +230,18 @@ public class ItemRewriter extends StoredObject { final String identifier = this.registry.identifier(bedrockItem); if (identifier == null) { @@ -11780,7 +11780,7 @@ index bcf7ac89..988369db 100644 if (blockItemMappings != null) { final BlockState displayState; if (bedrockItem.hasBlockState()) { -@@ -202,9 +240,13 @@ public class ItemRewriter extends StoredObject { +@@ -216,9 +254,13 @@ public class ItemRewriter extends StoredObject { javaItemMapping = displayState != null ? blockItemMappings.get(displayState) : null; } else { final int meta = bedrockItem.data() & 0xFFFF; @@ -11796,7 +11796,7 @@ index bcf7ac89..988369db 100644 if (newBlockItemMappings != null) { final IntSortedSet validBlockStates = this.blockItemValidBlockStates.get(bedrockItem.identifier()); javaItemMapping = validBlockStates != null && !validBlockStates.isEmpty() -@@ -213,7 +255,8 @@ public class ItemRewriter extends StoredObject { +@@ -227,7 +269,8 @@ public class ItemRewriter extends StoredObject { javaItemMapping = null; } } else { @@ -11806,7 +11806,7 @@ index bcf7ac89..988369db 100644 if (metaItemMappings != null) { if (!metaItemMappings.containsKey(meta)) { if ((metaItemMappings.size() != 1 || meta != 0) -@@ -245,8 +288,10 @@ public class ItemRewriter extends StoredObject { +@@ -259,8 +302,10 @@ public class ItemRewriter extends StoredObject { } javaItem = new StructuredItem(javaItemMapping.id(), bedrockItem.amount(), data); } else { @@ -11819,7 +11819,7 @@ index bcf7ac89..988369db 100644 final StructuredDataContainer data = ProtocolConstants.createStructuredDataContainer(); if (itemDefinition != null) { -@@ -264,13 +309,21 @@ public class ItemRewriter extends StoredObject { +@@ -278,13 +323,21 @@ public class ItemRewriter extends StoredObject { data.set(StructuredDataKey.ITEM_MODEL, CustomItemTextureResourceRewriter.getItemModel(resourcePackStorage, itemDefinition.iconComponent())); data.set(StructuredDataKey.CUSTOM_MODEL_DATA1_21_4, CustomItemTextureResourceRewriter.getCustomModelData("0")); } else { @@ -11843,7 +11843,7 @@ index bcf7ac89..988369db 100644 } if (ordinaryNameKey != null) { -@@ -294,8 +347,12 @@ public class ItemRewriter extends StoredObject { +@@ -308,8 +361,12 @@ public class ItemRewriter extends StoredObject { final CompoundTag bedrockTag = bedrockItem.tag(); if (bedrockTag != null) { @@ -11858,7 +11858,7 @@ index bcf7ac89..988369db 100644 } final String customName = this.registry.customName(identifier, bedrockTag); if (customName != null) { -@@ -310,12 +367,16 @@ public class ItemRewriter extends StoredObject { +@@ -324,12 +381,16 @@ public class ItemRewriter extends StoredObject { ITEM_NBT_REWRITERS.get(tag).toJava(this.user(), bedrockItem, javaItem); } @@ -11876,7 +11876,7 @@ index bcf7ac89..988369db 100644 if (this.registry.storageProperties(bedrockItem).storage()) { final var storageSource = new NativeItemStorageData.Source() { @Override -@@ -355,7 +416,8 @@ public class ItemRewriter extends StoredObject { +@@ -389,7 +450,8 @@ public class ItemRewriter extends StoredObject { return null; } final CompoundTag javaTag = new CompoundTag(); @@ -11886,7 +11886,7 @@ index bcf7ac89..988369db 100644 javaTag.put("id", new StringTag(javaId)); javaTag.put("count", new IntTag(javaItem.amount())); if (javaItem.tag() != null) { -@@ -369,28 +431,49 @@ public class ItemRewriter extends StoredObject { +@@ -403,28 +465,49 @@ public class ItemRewriter extends StoredObject { return null; } @@ -11955,7 +11955,7 @@ index bcf7ac89..988369db 100644 } /** Resolve primary text through the selected definition and current language sources. */ -@@ -414,7 +497,33 @@ public class ItemRewriter extends StoredObject { +@@ -448,7 +531,33 @@ public class ItemRewriter extends StoredObject { } public BedrockItem bedrockItem(final Item javaItem) { @@ -11990,7 +11990,7 @@ index bcf7ac89..988369db 100644 } public BedrockItem[] bedrockItems(final Item[] javaItems) { -@@ -437,8 +546,72 @@ public class ItemRewriter extends StoredObject { +@@ -471,8 +580,72 @@ public class ItemRewriter extends StoredObject { return this.registry.identifier(item); } @@ -12065,7 +12065,7 @@ index bcf7ac89..988369db 100644 } /** Match network-loaded stack queries while their connection registry remains current. */ -@@ -484,6 +657,17 @@ public class ItemRewriter extends StoredObject { +@@ -518,6 +691,17 @@ public class ItemRewriter extends StoredObject { return properties == null ? 0 : properties.maximum(); } @@ -12083,7 +12083,7 @@ index bcf7ac89..988369db 100644 public Type itemType() { return this.itemType; } -@@ -508,10 +692,13 @@ public class ItemRewriter extends StoredObject { +@@ -542,10 +726,13 @@ public class ItemRewriter extends StoredObject { return this.newItemArrayType; } diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch index 4254adac..f296b47b 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.patch @@ -1,4 +1,4 @@ -From 01b333512158392bff14393a6a288b466701f413 Mon Sep 17 00:00:00 2001 +From 01a39b69a548a1f5b2272188c53d76760b1c46aa 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(crafting): expose Bedrock recipes and place ingredients @@ -4366,10 +4366,10 @@ index c04231bd..ac448e28 100644 if (channels.contains(LocatorBarSnapshot.CHANNEL) && wrapper.user().has(PlayerWaypointTracker.class)) { wrapper.user().get(PlayerWaypointTracker.class).sendSnapshot(); 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 988369db..a044b07f 100644 +index a5193c10..a6a4073d 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -@@ -40,12 +40,16 @@ import net.raphimc.viabedrock.ViaBedrock; +@@ -40,8 +40,6 @@ import net.raphimc.viabedrock.ViaBedrock; import net.raphimc.viabedrock.api.model.BlockState; import net.raphimc.viabedrock.api.model.BedrockBlockState; import net.raphimc.viabedrock.api.model.NativeItemNames; @@ -4378,7 +4378,10 @@ index 988369db..a044b07f 100644 import net.raphimc.viabedrock.api.model.NativeItemStacking; import net.raphimc.viabedrock.api.model.NativeItemStackData; import net.raphimc.viabedrock.api.model.NativeItemStorageData; - import net.raphimc.viabedrock.api.model.NativeBundleTooltipData; +@@ -50,6 +48,12 @@ import net.raphimc.viabedrock.api.model.NativeBundleTooltipItems; + import net.raphimc.viabedrock.api.model.NativeContainerMessage; + import net.raphimc.viabedrock.protocol.storage.ChannelStorage; + 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; @@ -4388,7 +4391,7 @@ index 988369db..a044b07f 100644 import net.raphimc.viabedrock.api.resourcepack.definition.ItemDefinitions; import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; -@@ -62,9 +66,13 @@ import net.raphimc.viabedrock.protocol.model.SavedItemStack; +@@ -66,9 +70,13 @@ import net.raphimc.viabedrock.protocol.model.SavedItemStack; import net.raphimc.viabedrock.protocol.model.NativeItemDefinitions; import net.raphimc.viabedrock.protocol.model.NativeRecipeRegistry; import net.raphimc.viabedrock.protocol.model.ItemNameContext; @@ -4402,7 +4405,7 @@ index 988369db..a044b07f 100644 import net.raphimc.viabedrock.protocol.model.ItemCookingProperties; import net.raphimc.viabedrock.protocol.model.ItemCooldown; import net.raphimc.viabedrock.protocol.model.ItemAnimationData; -@@ -81,14 +89,19 @@ import net.raphimc.viabedrock.protocol.types.BedrockTypes; +@@ -85,14 +93,19 @@ import net.raphimc.viabedrock.protocol.types.BedrockTypes; import net.raphimc.viabedrock.protocol.types.array.ArrayType; import net.raphimc.viabedrock.protocol.types.inventory.ItemStackRequestResultType; import net.raphimc.viabedrock.protocol.types.item.BedrockItemType; @@ -4422,7 +4425,7 @@ index 988369db..a044b07f 100644 import java.util.logging.Level; public class ItemRewriter extends StoredObject implements NetworkItemStackContext { -@@ -110,6 +123,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -115,6 +128,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final Set componentItems; private final Int2ObjectMap blockItemValidBlockStates; private final Type itemType; @@ -4431,7 +4434,7 @@ index 988369db..a044b07f 100644 private final Type optionalItemType; private final Type itemArrayType; private final Type newItemType; -@@ -196,6 +211,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -201,6 +216,8 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex this.items.getOrDefault("minecraft:shield", 0), false, this); @@ -4440,7 +4443,7 @@ index 988369db..a044b07f 100644 this.optionalItemType = new OptionalType<>(this.itemType); this.itemArrayType = new ArrayType<>(this.itemType, BedrockTypes.UNSIGNED_VAR_INT); this.newItemType = -@@ -482,6 +499,27 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -516,6 +533,27 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex (key, arguments) -> this.user().get(ResourcePackStorage.class).getTexts().translate(key, arguments.toArray())); } @@ -4468,7 +4471,7 @@ index 988369db..a044b07f 100644 private ItemNameContext itemNameContext() { return new ItemNameContext((block, metadata) -> this.user().get(BlockStateRewriter.class).blockItemNameKey(block, metadata), key -> this.user().get(ResourcePackStorage.class).getTexts().translate(key), -@@ -560,6 +598,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -594,6 +632,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex (definition, auxiliary) -> this.associatedBlock(blocks, definition, auxiliary)); } @@ -4483,7 +4486,7 @@ index 988369db..a044b07f 100644 public BedrockItem createDefinitionItem(final NativeItemDefinitions.Definition definition, final int metadata, final int amount) { final var blocks = this.user().get(BlockStateRewriter.class); return this.registry.createDefinitionItem(definition, metadata, amount, -@@ -602,6 +648,89 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -636,6 +682,89 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex ? this.user().get(BlockStateRewriter.class).blockItemRecipeDescriptor(item.blockRuntimeId()) : null); } @@ -4573,7 +4576,7 @@ index 988369db..a044b07f 100644 public boolean matchesRecipeItem(final String identifier, final int metadata, final BedrockItem item) { if (item == null) { return false; -@@ -610,6 +739,37 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -644,6 +773,37 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return NativeRecipeDescriptor.resolveName(identifier, metadata, context).matchesStack(item, context, this::recipeDescriptor); } @@ -4611,7 +4614,7 @@ index 988369db..a044b07f 100644 public ItemRegistry getItemRegistry() { return this.registry; } -@@ -632,6 +792,22 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -666,6 +826,22 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return NativeItemStackMatcher.matchesIdentity(query, candidate, blocks::blockState, blocks::restrictionBlocks); } @@ -4634,7 +4637,7 @@ index 988369db..a044b07f 100644 public boolean canMerge(final BedrockItem source, final BedrockItem destination) { if (destination == null || destination.isEmpty() || !this.registry.sameDefinition(source, destination)) { return false; -@@ -672,6 +848,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -706,6 +882,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return this.itemType; } diff --git a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch index 17785f6a..747337d6 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch @@ -1,4 +1,4 @@ -From 4dc26e81629e599b15383f45616eeda0dbf9a75b Mon Sep 17 00:00:00 2001 +From d78d819393f2664b05deb799a227deb54fd4d840 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] fix(interaction): signal sustained Bedrock item use @@ -344,10 +344,10 @@ index d8e307df..b1ffa95c 100644 0, null, null, ComplexInventoryTransaction_Type.ItemUseTransaction, new InventoryTransactionData.UseItemTransactionData( 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 a044b07f..c5059d60 100644 +index a6a4073d..28413a56 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -@@ -76,6 +76,7 @@ import net.raphimc.viabedrock.protocol.model.recipe.RecipeOutputKey; +@@ -80,6 +80,7 @@ import net.raphimc.viabedrock.protocol.model.recipe.RecipeOutputKey; import net.raphimc.viabedrock.protocol.model.ItemCookingProperties; import net.raphimc.viabedrock.protocol.model.ItemCooldown; import net.raphimc.viabedrock.protocol.model.ItemAnimationData; @@ -355,15 +355,15 @@ index a044b07f..c5059d60 100644 import net.raphimc.viabedrock.protocol.model.ItemArmorData; import net.raphimc.viabedrock.protocol.model.ItemUseModifiers; import net.raphimc.viabedrock.protocol.rewriter.item.BundleItemRewriter; -@@ -107,6 +108,7 @@ import java.util.logging.Level; - public class ItemRewriter extends StoredObject implements NetworkItemStackContext { +@@ -112,6 +113,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final ItemRegistry registry; + private int javaItemDepth; + private final Map foodProperties; private final Map animationData = new HashMap<>(); private final Map armorData = new HashMap<>(); -@@ -146,6 +148,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -151,6 +153,14 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex return this.armorData.getOrDefault(identifier, ItemArmorData.vanilla(identifier)); } @@ -378,7 +378,7 @@ index a044b07f..c5059d60 100644 public ItemUseModifiers useModifiers(final BedrockItem item) { final String identifier = this.registry.identifier(item); if ("minecraft:shield".equals(identifier)) { -@@ -166,6 +176,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -171,6 +181,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex final BlockStateRewriter blockStateRewriter = this.user().get(BlockStateRewriter.class); this.registry = new ItemRegistry(itemEntries, blockStateRewriter::registeredBlockIdentifier, user.get(GameSessionStorage.class).getBedrockBaseGameVersion()); @@ -386,7 +386,7 @@ index a044b07f..c5059d60 100644 this.items = this.registry.ids(); this.componentItems = this.registry.componentItems(); for (ItemEntry itemEntry : this.registry.registrations()) { -@@ -386,10 +397,17 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -400,10 +411,17 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex javaItem.dataContainer().set(StructuredDataKey.MAX_STACK_SIZE, Math.max(1, Math.min(99, this.maxStackSize(bedrockItem)))); 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 5c099774..b92009f8 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 202abae09d82ca2ae04851e4f746585344011c84 Mon Sep 17 00:00:00 2001 +From 89148c3e1b51206ca3a78d2a275352da5a902ead 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 @@ -2941,10 +2941,10 @@ 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 c5059d60..d9d88d67 100644 +index 28413a56..08ed3680 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -@@ -49,7 +49,6 @@ import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; +@@ -53,7 +53,6 @@ import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; import net.raphimc.viabedrock.api.model.container.NativeRecipeDescriptor; import net.raphimc.viabedrock.api.model.container.RecipeIngredientMatcher; import net.raphimc.viabedrock.api.model.container.Container; @@ -2952,7 +2952,7 @@ index c5059d60..d9d88d67 100644 import net.raphimc.viabedrock.api.resourcepack.definition.ItemDefinitions; import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; -@@ -57,6 +56,10 @@ import net.raphimc.viabedrock.protocol.data.BedrockMappingData; +@@ -61,6 +60,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 +2963,7 @@ index c5059d60..d9d88d67 100644 import net.raphimc.viabedrock.protocol.model.ItemEntry; import net.raphimc.viabedrock.protocol.model.ItemStorageProperties; import net.raphimc.viabedrock.protocol.model.FullContainerName; -@@ -69,6 +72,7 @@ import net.raphimc.viabedrock.protocol.model.ItemNameContext; +@@ -73,6 +76,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 +2971,7 @@ index c5059d60..d9d88d67 100644 import net.raphimc.viabedrock.protocol.model.ItemStackHeader; import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeDescriptor; -@@ -100,9 +104,10 @@ import java.util.HashMap; +@@ -104,9 +108,10 @@ import java.util.HashMap; import java.util.List; import java.util.HashSet; import java.util.Map; @@ -2983,7 +2983,7 @@ index c5059d60..d9d88d67 100644 import java.util.logging.Level; public class ItemRewriter extends StoredObject implements NetworkItemStackContext { -@@ -133,7 +138,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -138,7 +143,7 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex private final Type optionalNewItemType; private final Type newItemArrayType; private final Type stackRequestResultType; @@ -2992,7 +2992,7 @@ index c5059d60..d9d88d67 100644 static { // TODO: Add missing item nbt rewriters -@@ -557,29 +562,31 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -591,29 +596,31 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex "Translating Java items to Bedrock is not yet supported"); } @@ -3043,7 +3043,7 @@ index c5059d60..d9d88d67 100644 } public BedrockItem[] bedrockItems(final Item[] javaItems) { -@@ -630,6 +637,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -664,6 +671,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex (selected, auxiliary) -> this.associatedBlock(blocks, selected, auxiliary)); } @@ -3062,7 +3062,7 @@ index c5059d60..d9d88d67 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, -@@ -680,11 +699,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -714,11 +733,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) { 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 21a3eefe..259e0692 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 22ba101e69b35a3492c4152983b6286ef341f29d Mon Sep 17 00:00:00 2001 +From 58810577c996c288bec094699f3b9105efee762a 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 @@ -716,11 +716,23 @@ visibility. Original registration144b76e0e-144b76e58 installs getter144c0aa90; bounded primary-vtable installation145000d60-145000d6a and the original registered dispatch return true through slot0x130/getter140084510. Retaining a missing binding would hide the entire authored bundle-tooltip wrapper. + +Carry physical item previews and native primary names beside each outer +bundle snapshot for clients advertising native inventory presentation. +Java bundle contents compact holes and cannot represent every native +storage graph, so they are not a sufficient tooltip source. Encode complete +Java component patches with explicit physical indices and bind their +identity, slot count and amounts to the retained native snapshot. Limit each +table to 64 slots and 1 MiB; reject malformed, truncated and mismatched data +without manufacturing an empty table. Only outer conversions carry a table, +so nested render previews cannot recursively duplicate it. Missing or +oversized preview data stays explicitly unavailable for native presentation. --- Data Asset Sources.md | 33 + build.gradle | 1 + .../chunk/blockstate/BuiltinBlockNames.java | 46 + .../api/model/NativeBundleTooltipData.java | 86 + + .../api/model/NativeBundleTooltipItems.java | 170 + .../viabedrock/api/model/NativeItemNames.java | 14 + .../api/model/NativeItemStacking.java | 85 + .../api/model/NativeItemStorageData.java | 116 + @@ -769,7 +781,7 @@ a missing binding would hide the entire authored bundle-tooltip wrapper. .../protocol/packet/NpcDialoguePackets.java | 417 +- .../protocol/rewriter/BlockStateRewriter.java | 7 + .../protocol/rewriter/ItemDataRewriter.java | 1 - - .../protocol/rewriter/ItemRewriter.java | 170 +- + .../protocol/rewriter/ItemRewriter.java | 204 +- .../protocol/storage/EntityTracker.java | 4 +- .../protocol/storage/GameSessionStorage.java | 19 + .../protocol/storage/InventoryTracker.java | 27 +- @@ -783,6 +795,7 @@ a missing binding would hide the entire authored bundle-tooltip wrapper. .../data/bedrock/item_registry.json | 14496 ++++++++++++++++ .../viabedrock/data/bedrock/potion_names.json | 194 + .../model/NativeBundleTooltipDataTest.java | 97 + + .../model/NativeBundleTooltipItemsTest.java | 147 + .../api/model/NativeItemNamesTest.java | 83 + .../api/model/NativeItemStackingTest.java | 149 + .../api/model/NativeItemStorageDataTest.java | 96 + @@ -810,9 +823,10 @@ a missing binding would hide the entire authored bundle-tooltip wrapper. .../native-bundle-tooltip-bindings.json | 283 + .../native-bundle-tooltip-layout.json | 266 + .../inventory/native-item-storage.json | 568 + - 93 files changed, 37656 insertions(+), 364 deletions(-) + 95 files changed, 38007 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 create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeItemStacking.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeItemStorageData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeContainerActionSlots.java @@ -853,6 +867,7 @@ a missing binding would hide the entire authored bundle-tooltip wrapper. create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/item_registry.json create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/potion_names.json create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/NativeBundleTooltipDataTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/NativeBundleTooltipItemsTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/NativeItemStackingTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/NativeItemStorageDataTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeContainerLegacyActionsTest.java @@ -1075,6 +1090,182 @@ index 00000000..eca249b8 + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/NativeBundleTooltipItems.java b/src/main/java/net/raphimc/viabedrock/api/model/NativeBundleTooltipItems.java +new file mode 100644 +index 00000000..23b80ff1 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/NativeBundleTooltipItems.java +@@ -0,0 +1,170 @@ ++/* ++ * 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; ++ ++import com.viaversion.nbt.tag.ByteArrayTag; ++import com.viaversion.nbt.tag.CompoundTag; ++import com.viaversion.viaversion.api.minecraft.item.Item; ++import com.viaversion.viaversion.api.type.Type; ++import com.viaversion.viaversion.api.type.Types; ++import com.viaversion.viaversion.api.type.types.version.VersionedTypes; ++import io.netty.buffer.ByteBuf; ++import io.netty.buffer.Unpooled; ++import net.raphimc.viabedrock.protocol.data.ProtocolConstants; ++ ++import java.nio.charset.StandardCharsets; ++import java.util.HashMap; ++import java.util.Map; ++import java.util.TreeMap; ++import java.util.function.Supplier; ++ ++/** Physical previews are independent of the compacted and capacity-limited Java bundle component. */ ++public record NativeBundleTooltipItems(int identity, int slots, Map entries) { ++ ++ public static final String TAG = "preview_items"; ++ public static final int MAX_BYTES = 1 << 20; ++ public static final Type TYPE = codec(() -> VersionedTypes.V26_3.item); ++ ++ public NativeBundleTooltipItems { ++ entries = Map.copyOf(entries); ++ if (slots < 0 || slots > 64 || entries.keySet().stream().anyMatch(slot -> slot < 0 || slot >= slots)) { ++ throw new IllegalArgumentException("Invalid native bundle preview slots"); ++ } ++ } ++ ++ public boolean matches(final NativeBundleTooltipData data) { ++ if (data == null || data.identity() != this.identity || data.amounts().size() != this.slots) { ++ return false; ++ } ++ for (int slot = 0; slot < this.slots; slot++) { ++ final var entry = this.entries.get(slot); ++ if (data.amounts().get(slot) != (entry == null ? 0 : entry.item.amount())) { ++ return false; ++ } ++ } ++ return true; ++ } ++ ++ public byte[] encode() { ++ final var buffer = Unpooled.buffer(256, MAX_BYTES); ++ try { ++ TYPE.write(buffer, this); ++ final byte[] bytes = new byte[buffer.readableBytes()]; ++ buffer.readBytes(bytes); ++ return bytes; ++ } finally { ++ buffer.release(); ++ } ++ } ++ ++ public static NativeBundleTooltipItems fromTag(final CompoundTag tag) { ++ if (tag == null || !(tag.get(TAG) instanceof ByteArrayTag bytes) || bytes.getValue().length > MAX_BYTES) { ++ return null; ++ } ++ return decode(bytes.getValue(), NativeBundleTooltipData.fromTag(tag)); ++ } ++ ++ public static NativeBundleTooltipItems decode(final byte[] bytes, final NativeBundleTooltipData data) { ++ if (bytes.length > MAX_BYTES) { ++ return null; ++ } ++ final var buffer = Unpooled.wrappedBuffer(bytes); ++ try { ++ final var value = TYPE.read(buffer); ++ return value.matches(data) ? value : null; ++ } catch (final RuntimeException malformed) { ++ return null; ++ } finally { ++ buffer.release(); ++ } ++ } ++ ++ static Type codec(final Supplier> itemType) { ++ return new Type<>(NativeBundleTooltipItems.class) { ++ @Override ++ public NativeBundleTooltipItems read(final ByteBuf buffer) { ++ if (buffer.readableBytes() > MAX_BYTES || Types.VAR_INT.readPrimitive(buffer) != 1 ++ || Types.VAR_INT.readPrimitive(buffer) != ProtocolConstants.BEDROCK_PROTOCOL_VERSION) { ++ throw new IllegalArgumentException("Unsupported native bundle previews"); ++ } ++ final int identity = Types.VAR_INT.readPrimitive(buffer); ++ final int slots = Types.VAR_INT.readPrimitive(buffer); ++ final int count = Types.VAR_INT.readPrimitive(buffer); ++ if (slots < 0 || slots > 64 || count < 0 || count > slots) { ++ throw new IllegalArgumentException("Invalid native bundle preview count"); ++ } ++ final Map entries = new HashMap<>(); ++ for (int entry = 0; entry < count; entry++) { ++ final int slot = Types.VAR_INT.readPrimitive(buffer); ++ if (slot < 0 || slot >= slots || entries.containsKey(slot)) { ++ throw new IllegalArgumentException("Invalid native bundle preview source"); ++ } ++ final String name = Types.STRING.read(buffer); ++ final int length = Types.VAR_INT.readPrimitive(buffer); ++ if (length <= 0 || length > buffer.readableBytes()) { ++ throw new IllegalArgumentException("Invalid native bundle preview length"); ++ } ++ final var encoded = buffer.readSlice(length); ++ final Item item = itemType.get().read(encoded); ++ if (encoded.isReadable()) { ++ throw new IllegalArgumentException("Trailing native bundle item data"); ++ } ++ entries.put(slot, new Preview(name, item)); ++ } ++ if (buffer.isReadable()) { ++ throw new IllegalArgumentException("Trailing native bundle preview data"); ++ } ++ return new NativeBundleTooltipItems(identity, slots, entries); ++ } ++ ++ @Override ++ public void write(final ByteBuf buffer, final NativeBundleTooltipItems value) { ++ final var encoded = Unpooled.buffer(256, MAX_BYTES); ++ try { ++ Types.VAR_INT.writePrimitive(encoded, 1); ++ Types.VAR_INT.writePrimitive(encoded, ProtocolConstants.BEDROCK_PROTOCOL_VERSION); ++ Types.VAR_INT.writePrimitive(encoded, value.identity); ++ Types.VAR_INT.writePrimitive(encoded, value.slots); ++ Types.VAR_INT.writePrimitive(encoded, value.entries.size()); ++ for (final var entry : new TreeMap<>(value.entries).entrySet()) { ++ Types.VAR_INT.writePrimitive(encoded, entry.getKey()); ++ Types.STRING.write(encoded, entry.getValue().primaryName); ++ final var itemBytes = Unpooled.buffer(256, MAX_BYTES); ++ try { ++ itemType.get().write(itemBytes, entry.getValue().item); ++ Types.VAR_INT.writePrimitive(encoded, itemBytes.readableBytes()); ++ encoded.writeBytes(itemBytes); ++ } finally { ++ itemBytes.release(); ++ } ++ } ++ buffer.writeBytes(encoded); ++ } catch (final IndexOutOfBoundsException oversized) { ++ throw new IllegalArgumentException("Native bundle previews exceed the payload limit", oversized); ++ } finally { ++ encoded.release(); ++ } ++ } ++ }; ++ } ++ ++ public record Preview(String primaryName, Item item) { ++ public Preview { ++ if (primaryName == null || !StandardCharsets.UTF_8.newEncoder().canEncode(primaryName) ++ || primaryName.getBytes(StandardCharsets.UTF_8).length > 8192 || item == null || item.isEmpty() ++ || item.amount() <= 0 || item.amount() > 255) { ++ throw new IllegalArgumentException("Invalid native bundle item preview"); ++ } ++ item = item.copy(); ++ } ++ ++ @Override ++ public Item item() { ++ return this.item.copy(); ++ } ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/NativeItemNames.java b/src/main/java/net/raphimc/viabedrock/api/model/NativeItemNames.java index 7b2cd8d2..9778e563 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/NativeItemNames.java @@ -5988,7 +6179,7 @@ index f9836514..1baab221 100644 && bedrockTag.get("customColor") instanceof IntTag color) { javaItem.dataContainer().set(StructuredDataKey.DYED_COLOR1_21_5, 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 d0af6440..bcf7ac89 100644 +index d0af6440..36b2cf55 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java @@ -17,8 +17,6 @@ @@ -6000,7 +6191,7 @@ index d0af6440..bcf7ac89 100644 import com.viaversion.nbt.tag.CompoundTag; import com.viaversion.nbt.tag.IntTag; import com.viaversion.nbt.tag.NumberTag; -@@ -39,16 +37,25 @@ import com.viaversion.viaversion.libs.fastutil.ints.IntSortedSet; +@@ -39,16 +37,29 @@ import com.viaversion.viaversion.libs.fastutil.ints.IntSortedSet; import com.viaversion.viaversion.util.Key; import net.raphimc.viabedrock.ViaBedrock; import net.raphimc.viabedrock.api.model.BlockState; @@ -6010,6 +6201,10 @@ index d0af6440..bcf7ac89 100644 import net.raphimc.viabedrock.api.model.NativeItemStackData; +import net.raphimc.viabedrock.api.model.NativeItemStorageData; +import net.raphimc.viabedrock.api.model.NativeBundleTooltipData; ++import net.raphimc.viabedrock.api.model.NativeBundleTooltipItems; ++import net.raphimc.viabedrock.api.model.NativeContainerMessage; ++import net.raphimc.viabedrock.protocol.storage.ChannelStorage; ++import com.viaversion.nbt.tag.ByteArrayTag; import net.raphimc.viabedrock.api.resourcepack.definition.ItemDefinitions; import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; @@ -6027,7 +6222,7 @@ index d0af6440..bcf7ac89 100644 import net.raphimc.viabedrock.protocol.model.ItemCooldown; import net.raphimc.viabedrock.protocol.model.ItemAnimationData; import net.raphimc.viabedrock.protocol.model.ItemArmorData; -@@ -56,6 +63,8 @@ import net.raphimc.viabedrock.protocol.model.ItemUseModifiers; +@@ -56,6 +67,8 @@ import net.raphimc.viabedrock.protocol.model.ItemUseModifiers; import net.raphimc.viabedrock.protocol.rewriter.item.BundleItemRewriter; import net.raphimc.viabedrock.protocol.rewriter.resourcepack.CustomAttachableResourceRewriter; import net.raphimc.viabedrock.protocol.rewriter.resourcepack.CustomItemTextureResourceRewriter; @@ -6036,16 +6231,17 @@ index d0af6440..bcf7ac89 100644 import net.raphimc.viabedrock.protocol.storage.MapTracker; import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; import net.raphimc.viabedrock.protocol.types.BedrockTypes; -@@ -71,6 +80,8 @@ import java.util.logging.Level; +@@ -71,6 +84,9 @@ import java.util.logging.Level; public class ItemRewriter extends StoredObject { + private final ItemRegistry registry; ++ private int javaItemDepth; + private final Map animationData = new HashMap<>(); private final Map armorData = new HashMap<>(); private final Map useModifiers = new HashMap<>(); -@@ -82,7 +93,7 @@ public class ItemRewriter extends StoredObject { +@@ -82,7 +98,7 @@ public class ItemRewriter extends StoredObject { private static final Map ITEM_NBT_REWRITERS = new HashMap<>(); @@ -6054,7 +6250,7 @@ index d0af6440..bcf7ac89 100644 private final Set componentItems; private final Int2ObjectMap blockItemValidBlockStates; private final Type itemType; -@@ -101,12 +112,12 @@ public class ItemRewriter extends StoredObject { +@@ -101,12 +117,12 @@ public class ItemRewriter extends StoredObject { if (item.isEmpty()) { return ItemArmorData.NONE; } @@ -6069,7 +6265,7 @@ index d0af6440..bcf7ac89 100644 if ("minecraft:shield".equals(identifier)) { // Bedrock shields use sneaking rather than ItemInUse slowdown. return ItemUseModifiers.NONE; -@@ -122,11 +133,11 @@ public class ItemRewriter extends StoredObject { +@@ -122,11 +138,11 @@ public class ItemRewriter extends StoredObject { public ItemRewriter(final UserConnection user, final ItemEntry[] itemEntries) { super(user); @@ -6085,7 +6281,7 @@ index d0af6440..bcf7ac89 100644 if (itemEntry.componentData() != null && itemEntry.componentData().get("components") instanceof CompoundTag components) { this.animationData.put(itemEntry.identifier(), ItemAnimationData.fromNetworkComponents(components)); -@@ -137,15 +148,11 @@ public class ItemRewriter extends StoredObject { +@@ -137,15 +153,11 @@ public class ItemRewriter extends StoredObject { this.cooldowns.put(itemEntry.identifier(), cooldown); } } @@ -6101,7 +6297,20 @@ index d0af6440..bcf7ac89 100644 for (String identifier : blockItems) { IntSortedSet validBlockStates = blockStateRewriter.validBlockStates(identifier); if (validBlockStates == null) { -@@ -174,7 +181,7 @@ public class ItemRewriter extends StoredObject { +@@ -170,11 +182,20 @@ public class ItemRewriter extends StoredObject { + } + + public Item javaItem(final BedrockItem bedrockItem) { ++ this.javaItemDepth++; ++ try { ++ return this.convertJavaItem(bedrockItem); ++ } finally { ++ this.javaItemDepth--; ++ } ++ } ++ ++ private Item convertJavaItem(final BedrockItem bedrockItem) { + if (bedrockItem.isEmpty()) { return StructuredItem.empty(); } @@ -6110,7 +6319,7 @@ index d0af6440..bcf7ac89 100644 if (identifier == null) { ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Missing item identifier for id: " + bedrockItem.identifier()); return StructuredItem.empty(); -@@ -200,7 +207,8 @@ public class ItemRewriter extends StoredObject { +@@ -200,7 +221,8 @@ public class ItemRewriter extends StoredObject { final Map newBlockItemMappings = BedrockProtocol.MAPPINGS.getBedrockToJavaBlockItems().get(newIdentifier); if (newBlockItemMappings != null) { final IntSortedSet validBlockStates = this.blockItemValidBlockStates.get(bedrockItem.identifier()); @@ -6120,7 +6329,7 @@ index d0af6440..bcf7ac89 100644 } else { javaItemMapping = null; } -@@ -222,13 +230,15 @@ public class ItemRewriter extends StoredObject { +@@ -222,13 +244,15 @@ public class ItemRewriter extends StoredObject { } } @@ -6137,7 +6346,7 @@ index d0af6440..bcf7ac89 100644 final ResourcePackStorage resourcePackStorage = this.user().get(ResourcePackStorage.class); data.set(StructuredDataKey.ITEM_NAME, TextUtil.stringToNbt(resourcePackStorage.getTexts().get(javaItemMapping.name()))); data.set(StructuredDataKey.LORE, new Tag[]{TextUtil.stringToNbt("§7[ViaBedrock] Mapped item: " + identifier)}); -@@ -240,12 +250,14 @@ public class ItemRewriter extends StoredObject { +@@ -240,12 +264,14 @@ public class ItemRewriter extends StoredObject { final StructuredDataContainer data = ProtocolConstants.createStructuredDataContainer(); if (itemDefinition != null) { @@ -6158,7 +6367,7 @@ index d0af6440..bcf7ac89 100644 } if (itemDefinition.iconComponent() != null && resourcePackStorage.isLoadedOnJavaClient()) { -@@ -254,13 +266,21 @@ public class ItemRewriter extends StoredObject { +@@ -254,13 +280,21 @@ public class ItemRewriter extends StoredObject { } else { data.set(StructuredDataKey.LORE, new Tag[]{TextUtil.stringToNbt("§7[ViaBedrock] Custom item: " + identifier)}); } @@ -6181,7 +6390,7 @@ index d0af6440..bcf7ac89 100644 ItemCooldown cooldown = this.cooldowns.get(identifier); if (cooldown == null) { final ItemDefinitions.ItemDefinition definition = this.user().get(ResourcePackStorage.class).getItems().get(identifier); -@@ -277,10 +297,9 @@ public class ItemRewriter extends StoredObject { +@@ -277,10 +311,9 @@ public class ItemRewriter extends StoredObject { if (identifier.equals("minecraft:filled_map") && bedrockTag.get("map_uuid") instanceof NumberTag mapUuid) { javaItem.dataContainer().set(StructuredDataKey.MAP_ID, this.user().get(MapTracker.class).itemJavaId(mapUuid.asLong())); } @@ -6195,7 +6404,7 @@ index d0af6440..bcf7ac89 100644 } } -@@ -297,6 +316,36 @@ public class ItemRewriter extends StoredObject { +@@ -297,6 +330,56 @@ public class ItemRewriter extends StoredObject { javaItem.dataContainer().set(StructuredDataKey.CUSTOM_DATA, ItemDataRewriter.withBedrockItemData(javaItem.dataContainer().get(StructuredDataKey.CUSTOM_DATA), identifier, this.animationData.getOrDefault(identifier, ItemAnimationData.EMPTY), nativeStack)); @@ -6224,15 +6433,35 @@ index d0af6440..bcf7ac89 100644 + storage != null ? storage.toTag() : new CompoundTag()); + final var tooltip = NativeBundleTooltipData.resolve(bedrockItem, this.registry.storageProperties(bedrockItem), storageSource::contents); + final var texts = this.user().get(ResourcePackStorage.class).getTexts(); -+ javaItem.dataContainer().get(StructuredDataKey.CUSTOM_DATA).put(NativeBundleTooltipData.TAG, -+ tooltip != null ? tooltip.withPresentation(this.itemPrimaryName(bedrockItem), -+ NativeItemNames.bundleEmptyDescription(identifier, texts::translate)).toTag() -+ : new CompoundTag()); ++ final var tooltipTag = tooltip != null ? tooltip.withPresentation(this.itemPrimaryName(bedrockItem), ++ NativeItemNames.bundleEmptyDescription(identifier, texts::translate)).toTag() : new CompoundTag(); ++ final var channels = this.user().get(ChannelStorage.class); ++ if (tooltip != null && this.javaItemDepth == 1 && channels != null && channels.hasChannel(NativeContainerMessage.CHANNEL)) { ++ // Only outer stacks carry a physical table. Nested Java components and cell previews cannot recursively duplicate it. ++ try { ++ final var contents = storageSource.contents(tooltip.identity()); ++ final Map previews = new HashMap<>(); ++ for (int slot = 0; slot < contents.length; slot++) { ++ if (!contents[slot].isEmpty()) { ++ previews.put(slot, new NativeBundleTooltipItems.Preview(this.itemPrimaryName(contents[slot]), this.javaItem(contents[slot]))); ++ } ++ } ++ final var preview = new NativeBundleTooltipItems(tooltip.identity(), contents.length, previews); ++ if (!preview.matches(tooltip)) { ++ throw new IllegalArgumentException("Native bundle previews differ from the source snapshot"); ++ } ++ tooltipTag.put(NativeBundleTooltipItems.TAG, new ByteArrayTag(preview.encode())); ++ } catch (final IllegalArgumentException unsupported) { ++ // Keep the source snapshot, but do not substitute incomplete Java bundle contents. ++ tooltipTag.put(NativeBundleTooltipItems.TAG, new ByteArrayTag(new byte[0])); ++ } ++ } ++ javaItem.dataContainer().get(StructuredDataKey.CUSTOM_DATA).put(NativeBundleTooltipData.TAG, tooltipTag); + } return javaItem; } -@@ -344,6 +393,18 @@ public class ItemRewriter extends StoredObject { +@@ -344,6 +427,18 @@ public class ItemRewriter extends StoredObject { return this.javaItem(item); } @@ -6251,7 +6480,7 @@ index d0af6440..bcf7ac89 100644 public Item[] javaItems(final BedrockItem[] bedrockItems) { final Item[] javaItems = new Item[bedrockItems.length]; for (int i = 0; i < bedrockItems.length; i++) { -@@ -364,14 +425,65 @@ public class ItemRewriter extends StoredObject { +@@ -364,14 +459,65 @@ public class ItemRewriter extends StoredObject { return bedrockItems; } @@ -35310,6 +35539,159 @@ index 00000000..6ee62355 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/api/model/NativeBundleTooltipItemsTest.java b/src/test/java/net/raphimc/viabedrock/api/model/NativeBundleTooltipItemsTest.java +new file mode 100644 +index 00000000..bd711739 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/model/NativeBundleTooltipItemsTest.java +@@ -0,0 +1,147 @@ ++/* ++ * 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; ++ ++import com.viaversion.nbt.tag.ByteArrayTag; ++import com.viaversion.nbt.tag.CompoundTag; ++import com.viaversion.viaversion.api.data.FullMappings; ++import com.viaversion.viaversion.api.data.MappingData; ++import com.viaversion.viaversion.api.minecraft.data.StructuredData; ++import com.viaversion.viaversion.api.minecraft.data.StructuredDataContainer; ++import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey; ++import com.viaversion.viaversion.api.minecraft.item.Item; ++import com.viaversion.viaversion.api.minecraft.item.StructuredItem; ++import com.viaversion.viaversion.api.protocol.Protocol; ++import com.viaversion.viaversion.api.type.Type; ++import com.viaversion.viaversion.api.type.types.item.ItemType1_20_5; ++import com.viaversion.viaversion.api.type.types.item.StructuredDataType; ++import io.netty.buffer.Unpooled; ++import org.junit.jupiter.api.BeforeAll; ++import org.junit.jupiter.api.Test; ++ ++import java.util.Collections; ++import java.util.ArrayList; ++import java.util.stream.Collectors; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class NativeBundleTooltipItemsTest { ++ ++ @BeforeAll ++ static void initializeItemCodecs() throws ClassNotFoundException { ++ Class.forName(StructuredDataKey.class.getName()); ++ } ++ ++ private static Type codec() { ++ final var serializers = mock(FullMappings.class); ++ when(serializers.mappedSize()).thenReturn(1); ++ when(serializers.mappedId(anyString())).thenReturn(0); ++ final var mappings = mock(MappingData.class); ++ when(mappings.getDataComponentSerializerMappings()).thenReturn(serializers); ++ final var protocol = mock(Protocol.class); ++ when(protocol.getMappingData()).thenReturn(mappings); ++ final var components = new StructuredDataType(); ++ components.filler(protocol).add(StructuredDataKey.CUSTOM_DATA); ++ return NativeBundleTooltipItems.codec(() -> new ItemType1_20_5(components)); ++ } ++ ++ private static Item item(final int id, final int amount, final CompoundTag tag) { ++ return new StructuredItem(id, amount, new StructuredDataContainer(new StructuredData[]{ ++ StructuredData.of(StructuredDataKey.CUSTOM_DATA, tag, 0)})); ++ } ++ ++ @Test ++ void physicalPreviewsRetainSparseOverCapacityItemsNamesAndComponentOwnership() { ++ final var tag = new CompoundTag(); ++ tag.putString("native", "original"); ++ final var item = item(320, 255, tag); ++ final var entries = new HashMap(); ++ entries.put(1, new NativeBundleTooltipItems.Preview("§dCustom item", item)); ++ entries.put(63, new NativeBundleTooltipItems.Preview("Last physical slot", item(7, 2, new CompoundTag()))); ++ final var preview = new NativeBundleTooltipItems(Integer.MIN_VALUE, 64, entries); ++ entries.clear(); ++ tag.putString("native", "changed"); ++ preview.entries().get(1).item().setAmount(1); ++ final var codec = codec(); ++ final var wire = Unpooled.buffer(); ++ try { ++ codec.write(wire, preview); ++ final var received = codec.read(wire); ++ assertFalse(wire.isReadable()); ++ assertEquals(Map.of(1, "§dCustom item", 63, "Last physical slot"), received.entries().entrySet().stream() ++ .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().primaryName()))); ++ assertEquals(255, received.entries().get(1).item().amount()); ++ assertEquals("original", received.entries().get(1).item().dataContainer().get(StructuredDataKey.CUSTOM_DATA).getStringTag("native").getValue()); ++ final var counts = new ArrayList<>(Collections.nCopies(64, 0)); ++ counts.set(1, 255); ++ counts.set(63, 2); ++ assertTrue(received.matches(new NativeBundleTooltipData(Integer.MIN_VALUE, 12, counts))); ++ counts.set(1, 1); ++ assertFalse(received.matches(new NativeBundleTooltipData(Integer.MIN_VALUE, 12, counts))); ++ assertFalse(received.matches(new NativeBundleTooltipData(7, 12, counts))); ++ assertFalse(received.matches(new NativeBundleTooltipData(Integer.MIN_VALUE, 12, List.of(255, 2)))); ++ } finally { ++ wire.release(); ++ } ++ } ++ ++ @Test ++ void anEmptyPhysicalTableIsAvailableWhileMissingOrMalformedTablesStayUnavailable() { ++ final var data = new NativeBundleTooltipData(7, 12, List.of(0, 0, 0, 0)); ++ final var tag = data.toTag(); ++ assertNull(NativeBundleTooltipItems.fromTag(tag)); ++ final var empty = new NativeBundleTooltipItems(7, 4, Map.of()); ++ tag.put(NativeBundleTooltipItems.TAG, new ByteArrayTag(empty.encode())); ++ assertEquals(empty, NativeBundleTooltipItems.fromTag(tag)); ++ tag.put(NativeBundleTooltipItems.TAG, new ByteArrayTag(new byte[0])); ++ assertNull(NativeBundleTooltipItems.fromTag(tag)); ++ } ++ ++ @Test ++ void malformedPhysicalTablesCannotBecomeEmptyOrPartiallyDecodedPreviews() { ++ final var counts = new NativeBundleTooltipData(7, 12, List.of(0, 2)); ++ assertNull(NativeBundleTooltipItems.decode(new byte[0], counts)); ++ assertNull(NativeBundleTooltipItems.decode(new byte[NativeBundleTooltipItems.MAX_BYTES + 1], counts)); ++ final var codec = codec(); ++ final var wire = Unpooled.buffer(); ++ try { ++ codec.write(wire, new NativeBundleTooltipItems(7, 2, Map.of(1, ++ new NativeBundleTooltipItems.Preview("Native", item(7, 2, new CompoundTag()))))); ++ final var truncated = wire.copy(0, wire.writerIndex() - 1); ++ try { ++ assertThrows(RuntimeException.class, () -> codec.read(truncated)); ++ } finally { ++ truncated.release(); ++ } ++ wire.writeByte(0); ++ assertThrows(IllegalArgumentException.class, () -> codec.read(wire)); ++ } finally { ++ wire.release(); ++ } ++ } ++ ++ @Test ++ void oversizedComponentDataDoesNotPartiallyAppendPreviewData() { ++ final var tag = new CompoundTag(); ++ tag.put("large", new ByteArrayTag(new byte[NativeBundleTooltipItems.MAX_BYTES])); ++ final var preview = new NativeBundleTooltipItems(7, 1, Map.of(0, ++ new NativeBundleTooltipItems.Preview("Native", item(7, 1, tag)))); ++ final var wire = Unpooled.buffer(); ++ try { ++ wire.writeByte(91); ++ assertThrows(IllegalArgumentException.class, () -> codec().write(wire, preview)); ++ assertEquals(1, wire.readableBytes()); ++ assertEquals(91, wire.readUnsignedByte()); ++ } finally { ++ wire.release(); ++ } ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/NativeItemNamesTest.java b/src/test/java/net/raphimc/viabedrock/api/model/NativeItemNamesTest.java index fa493282..ff361708 100644 --- a/src/test/java/net/raphimc/viabedrock/api/model/NativeItemNamesTest.java 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 2ea0d24b..0d9e0815 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 @@ -34,4 +34,4 @@ This verifies the generic fallback defect; the original Lifeboat dialog report s The [owning patch](0095-integrate-authored-native-ui-presentation.patch) retains implementation rationale and versioned evidence. -Bundle snapshots retain physical slots, view limits, native names, and localized descriptions. Storage, cooking, and player scene controllers accept explicit tooltip views and preserve native visibility, remaining capacity, selection, and overflow values through program export. Original 1.26.60.5 callbacks and scene tests cover these boundaries. The native container classification keeps the authored tooltip wrapper available in the player inventory. Input lifetime, rendering, and live comparisons remain unfinished. +Bundle snapshots retain physical slots, view limits, native names, and localized descriptions. Bounded physical preview tables preserve full item components independently of Java bundle contents; identity and amounts must match before a client can use them. Storage, cooking, and player scene controllers accept explicit tooltip views and preserve native visibility, remaining capacity, selection, and overflow values through program export. Original 1.26.60.5 callbacks and scene tests cover these boundaries. The native container classification keeps the authored tooltip wrapper available in the player inventory. Input lifetime, rendering, and live comparisons remain unfinished. From bac8c57b49c84c82fedf7f725a42e01403fca535 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:34:36 +0100 Subject: [PATCH 4/5] docs(ui): distinguish deferred tooltip consumers --- .../0095-integrate-authored-native-ui-presentation.pr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0d9e0815..60d9d848 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 @@ -34,4 +34,4 @@ This verifies the generic fallback defect; the original Lifeboat dialog report s The [owning patch](0095-integrate-authored-native-ui-presentation.patch) retains implementation rationale and versioned evidence. -Bundle snapshots retain physical slots, view limits, native names, and localized descriptions. Bounded physical preview tables preserve full item components independently of Java bundle contents; identity and amounts must match before a client can use them. Storage, cooking, and player scene controllers accept explicit tooltip views and preserve native visibility, remaining capacity, selection, and overflow values through program export. Original 1.26.60.5 callbacks and scene tests cover these boundaries. The native container classification keeps the authored tooltip wrapper available in the player inventory. Input lifetime, rendering, and live comparisons remain unfinished. +Bundle snapshots retain physical slots, view limits, native names, and localized descriptions. Bounded physical preview tables preserve full item components independently of Java bundle contents; identity and amounts must match before a client can use them. Storage and cooking scene controllers accept explicit tooltip views and preserve native visibility, remaining capacity, selection, and overflow values through program export. Original 1.26.60.5 callbacks and local scene tests cover these boundaries. The shared controller publishes the native container classification. Player and workbench integration and their scene tests live in [deferred patch 0012](../deferred/0012-retain-bedrock-creative-catalog-entries.patch), outside this upstream PR. Input lifetime, rendering, and live comparisons remain unfinished. From 22f05671fbd316b45aadcb5d6f2a74b0a5472a17 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Wed, 4 Nov 2026 15:05:13 +0100 Subject: [PATCH 5/5] feat(inventory): decode physical bundle tooltip views Validate retained bundle identity and counts before decoding owned Minecraft item stacks. Preserve sparse physical slots and native names independently of Java bundle contents, then forward explicit views through all native inventory presentation families. Reject malformed or count-changing conversions and keep existing renderer admission guards. Record the core preview API prerequisite. Tests cover the real Minecraft item codec, authored scene resolution, item ownership, and invalid-table boundaries; source lifetime, renderer placement, actions, and live comparisons remain open. --- patches/viafabricplus-bedrock/series.json | 5 + ...9-present-authored-native-containers.patch | 257 ++++++++++++++++-- ...9-present-authored-native-containers.pr.md | 1 + 3 files changed, 235 insertions(+), 28 deletions(-) diff --git a/patches/viafabricplus-bedrock/series.json b/patches/viafabricplus-bedrock/series.json index 498f3ea2..74617bb3 100644 --- a/patches/viafabricplus-bedrock/series.json +++ b/patches/viafabricplus-bedrock/series.json @@ -249,6 +249,11 @@ "project": "viafabricplus-bedrock", "patch": "0029-share-native-ui-scene-rendering.patch", "reason": "Land shared scene and GUI actor rendering before container presentation." + }, + { + "project": "viabedrock", + "patch": "0095-integrate-authored-native-ui-presentation.patch", + "reason": "Land the native container scene contracts and physical bundle preview API before the client presentation." } ] }, diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.patch b/patches/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.patch index e71ec159..63b3d942 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.patch @@ -1,4 +1,4 @@ -From dae7b4abdd6fc01d39ddced40c09423b3c40d406 Mon Sep 17 00:00:00 2001 +From c3f1c007f3415769a9b3aa644462d93846fa3ee3 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:36 +0200 Subject: [PATCH] feat(inventory): present authored native containers @@ -781,6 +781,19 @@ order must not decide the display stack. Share the same empty-slot and active-drag admission with item rendering, while retaining real stacks and vanilla draw overrides. A renderer-boundary test draws count first, then verifies real-item precedence and suppression during drag selection. + +Decode physical bundle preview tables directly from retained typed item +metadata. Bind the table to its native identity and slot amounts before +converting complete Java component patches through the Minecraft item +codec. Keep holes and native primary names, and own copies of decoded +stacks so caller mutations cannot change later scene bindings. Reject +incomplete, malformed or count-changing conversions instead of substituting +Java bundle contents. Forward explicit native tooltip views through storage, +cooking, player and workbench presentation boundaries; existing callers +retain inactive views until source lifetime and renderer admission are wired. +Tests exercise the actual Minecraft item codec and authored scene resolution. +The screen's storage-tooltip guard remains in place; this does not establish +native input, placement, physical-slot actions or installed pixel parity. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -808,7 +821,7 @@ then verifies real-item precedence and suppression during drag selection. .../container/NativeContainerInput.java | 509 ++++++ .../container/NativeContainerPayload.java | 22 + .../container/NativeContainerPending.java | 65 + - .../NativeContainerPresentation.java | 44 + + .../NativeContainerPresentation.java | 45 + ...NativeContainerPresentationController.java | 95 ++ .../container/NativeContainerScroll.java | 143 ++ .../container/NativeContainerSession.java | 37 + @@ -820,21 +833,21 @@ then verifies real-item precedence and suppression during drag selection. .../container/NativeCookingNavigation.java | 52 + .../container/NativeCookingPayload.java | 23 + .../NativeCookingPreferenceControl.java | 63 + - .../container/NativeCookingPresentation.java | 135 ++ + .../container/NativeCookingPresentation.java | 136 ++ .../NativeCookingPresentationController.java | 119 ++ .../NativeCookingProgressPayload.java | 23 + .../NativeFurnaceOptionsPayload.java | 27 + .../NativeFurnacePreferenceState.java | 44 + .../container/NativeInventoryFlights.java | 65 + - .../NativeInventoryItemPresentation.java | 154 ++ + .../NativeInventoryItemPresentation.java | 220 +++ .../container/NativeInventoryItemStyle.java | 55 + - .../NativeInventoryPresentation.java | 45 + + .../NativeInventoryPresentation.java | 52 + .../container/NativeInventoryProgress.java | 70 + .../container/NativePlayerHelpText.java | 64 + .../NativePlayerInventoryActionPayload.java | 23 + .../NativePlayerInventoryPayload.java | 22 + .../NativePlayerInventoryResultPayload.java | 22 + - .../container/NativePlayerPresentation.java | 215 +++ + .../container/NativePlayerPresentation.java | 216 +++ .../PlayerInventoryScreenClosePayload.java | 22 + .../PlayerInventoryScreenLifecycle.java | 98 ++ .../PlayerInventoryScreenReadyPayload.java | 22 + @@ -856,7 +869,7 @@ then verifies real-item precedence and suppression during drag selection. .../container/NativeCookingScreenTest.java | 1409 ++++++++++++++++ .../NativeFurnacePreferenceStateTest.java | 45 + .../container/NativeInventoryFlightsTest.java | 70 + - .../NativeInventoryItemPresentationTest.java | 126 ++ + .../NativeInventoryItemPresentationTest.java | 238 +++ .../NativeInventoryItemStyleTest.java | 84 + .../NativeInventoryProgressTest.java | 83 + .../container/NativePlayerHelpTextTest.java | 34 + @@ -865,7 +878,7 @@ then verifies real-item precedence and suppression during drag selection. .../container/NativePlayerScreenTest.java | 720 +++++++++ .../PlayerInventoryAdmissionTest.java | 147 ++ .../PlayerInventoryScreenLifecycleTest.java | 139 ++ - 83 files changed, 11526 insertions(+), 62 deletions(-) + 83 files changed, 11714 insertions(+), 62 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiItemStyle.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinNativeContainerGeometry.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinNativeContainerScreenAdmission.java @@ -4681,10 +4694,10 @@ index 0000000..3408df8 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerPresentation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerPresentation.java new file mode 100644 -index 0000000..b9d1733 +index 0000000..0f3738a --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerPresentation.java -@@ -0,0 +1,44 @@ +@@ -0,0 +1,45 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -4696,6 +4709,7 @@ index 0000000..b9d1733 +import java.util.List; +import java.util.Map; +import net.raphimc.viabedrock.api.resourcepack.ui.ContainerUi; ++import net.raphimc.viabedrock.api.resourcepack.ui.BundleTooltipUi; + +final class NativeContainerPresentation implements NativeInventoryPresentation { + private final ContainerUi program; @@ -4723,8 +4737,8 @@ index 0000000..b9d1733 + + @Override + public Frame resolve(final int menuId, final String title, final List slots, -+ final Map cursor, final Boolean showTags) { -+ final var scene = this.program.resolve(new ContainerUi.MenuView(menuId, this.kind, title, slots, cursor, this.data)); ++ final Map cursor, final Boolean showTags, final BundleTooltipUi.View tooltip) { ++ final var scene = this.program.resolve(new ContainerUi.MenuView(menuId, this.kind, title, slots, cursor, this.data), tooltip); + if (scene == null) throw new IllegalArgumentException("Unresolved native inventory presentation"); + return new Frame(scene, this.kind.containerSlots(), Map.of(), Map.of()); + } @@ -5471,10 +5485,10 @@ index 0000000..636f3bd +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeCookingPresentation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeCookingPresentation.java new file mode 100644 -index 0000000..636a73e +index 0000000..e5b8fc6 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeCookingPresentation.java -@@ -0,0 +1,135 @@ +@@ -0,0 +1,136 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5495,6 +5509,7 @@ index 0000000..636a73e +import net.raphimc.viabedrock.api.model.NativeCookingMessage; +import net.raphimc.viabedrock.api.model.NativeCookingProgress; +import net.raphimc.viabedrock.api.resourcepack.ui.ContainerUi; ++import net.raphimc.viabedrock.api.resourcepack.ui.BundleTooltipUi; +import net.raphimc.viabedrock.api.resourcepack.ui.CookingUi; + +/** Authority and progress stay server-owned; genuine client stacks supply rendering and hover data. */ @@ -5594,7 +5609,7 @@ index 0000000..636a73e + + @Override + public Frame resolve(final int menuId, final String title, final List slots, -+ final Map cursor, final Boolean showTags) { ++ final Map cursor, final Boolean showTags, final BundleTooltipUi.View tooltip) { + if (menuId != this.message.menuId()) throw new IllegalArgumentException("Cooking view belongs to another menu"); + final var source = this.view.book(); + final var catalog = source.catalog().stream().map(recipe -> { @@ -5604,7 +5619,7 @@ index 0000000..636a73e + }).toList(); + final var book = new CookingUi.Book(source.layout(), source.forcedRecipeBookLayout(), source.tab(), source.filtering(), + source.foodCount(), source.itemCount(), source.blockCount(), source.tabLabel(), source.searchText(), this.selection, catalog, source.visibleRecipes()); -+ final var scene = this.program.resolve(new CookingUi.MenuView(menuId, this.view.family(), this.view.title(), slots, cursor, this.progress, book)); ++ final var scene = this.program.resolve(new CookingUi.MenuView(menuId, this.view.family(), this.view.title(), slots, cursor, this.progress, book), tooltip); + final var scope = CookingUi.actionScope(scene, book); + this.actions.presented(this.message, scope); + return new Frame(scene, 3, this.recipes, scope.actions()); @@ -5920,10 +5935,10 @@ index 0000000..0ea5137 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryItemPresentation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryItemPresentation.java new file mode 100644 -index 0000000..a8c0658 +index 0000000..72636f1 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryItemPresentation.java -@@ -0,0 +1,154 @@ +@@ -0,0 +1,220 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5938,11 +5953,18 @@ index 0000000..a8c0658 + +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.ByteTag; ++import net.minecraft.nbt.ByteArrayTag; ++import net.minecraft.nbt.IntArrayTag; ++import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.item.ItemStack; +import net.raphimc.viabedrock.api.model.NativeItemHoverText; ++import net.raphimc.viabedrock.api.model.NativeBundleTooltipData; ++import net.raphimc.viabedrock.api.model.NativeBundleTooltipItems; ++import net.raphimc.viabedrock.api.resourcepack.ui.BundleTooltipUi; ++import com.viaversion.viaversion.api.minecraft.item.Item; +import net.raphimc.viabedrock.api.model.NativeItemStackData; +import net.raphimc.viabedrock.api.model.NativeItemStorageData; +import net.raphimc.viabedrock.api.resourcepack.ui.InventoryStackCount; @@ -5954,6 +5976,12 @@ index 0000000..a8c0658 +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.HashMap; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collections; ++import java.util.List; ++import java.util.function.Function; ++import java.util.stream.IntStream; +import java.util.Map; + +/** Actual Java damage/count plus the original typed lock byte retained before conversion. */ @@ -6009,6 +6037,35 @@ index 0000000..a8c0658 + return null; + } + ++ static BundleTooltip bundleTooltip(final ItemStack stack, final Function decode) { ++ if (stack.isEmpty()) return null; ++ final var custom = stack.get(DataComponents.CUSTOM_DATA); ++ final var data = custom == null ? new CompoundTag() : custom.copyTag(); ++ if (!data.contains(NativeItemStorageData.TAG) && !bundle(stack, data)) return null; ++ if (!(data.get(NativeBundleTooltipData.TAG) instanceof CompoundTag tag) ++ || !(tag.get("version") instanceof IntTag version) || version.intValue() != NativeBundleTooltipData.VERSION ++ || !(tag.get("identity") instanceof IntTag identity) || !(tag.get("viewable_slots") instanceof IntTag viewable) ++ || !(tag.get("amounts") instanceof IntArrayTag counts) || counts.size() > 64 ++ || !(tag.get("primary_name") instanceof StringTag name) ++ || !(tag.get("empty_description") instanceof StringTag description) ++ || !(tag.get(NativeBundleTooltipItems.TAG) instanceof ByteArrayTag bytes)) ++ throw new IllegalArgumentException("Native bundle tooltip snapshot is incomplete"); ++ final var contents = new NativeBundleTooltipData(identity.intValue(), viewable.intValue(), ++ Arrays.stream(counts.getAsIntArray()).boxed().toList(), name.value(), description.value()); ++ final var previews = NativeBundleTooltipItems.decode(bytes.getAsByteArray(), contents); ++ if (previews == null) throw new IllegalArgumentException("Native bundle previews differ from the source snapshot"); ++ final List items = new ArrayList<>(Collections.nCopies(contents.amounts().size(), ItemStack.EMPTY)); ++ final List names = new ArrayList<>(Collections.nCopies(contents.amounts().size(), "")); ++ for (final var entry : previews.entries().entrySet()) { ++ final var item = decode.apply(entry.getValue().item()); ++ if (item == null || item.isEmpty() || item.getCount() != contents.amounts().get(entry.getKey())) ++ throw new IllegalArgumentException("Decoded native bundle preview changed its source count"); ++ items.set(entry.getKey(), item); ++ names.set(entry.getKey(), entry.getValue().primaryName()); ++ } ++ return new BundleTooltip(contents, storage(stack, data), names, items); ++ } ++ + static void requireTooltip(final ItemStack stack) { + if (stack.isEmpty()) return; + final var custom = stack.get(DataComponents.CUSTOM_DATA); @@ -6077,6 +6134,30 @@ index 0000000..a8c0658 + ? Byte.toUnsignedInt(value.byteValue()) + : 0; + } ++ record BundleTooltip(NativeBundleTooltipData contents, NativeItemStorageData storage, List names, List items) { ++ BundleTooltip { ++ names = List.copyOf(names); ++ items = items.stream().map(ItemStack::copy).toList(); ++ if (names.size() != contents.amounts().size() || items.size() != names.size()) ++ throw new IllegalArgumentException("Incomplete decoded native bundle contents"); ++ for (int slot = 0; slot < items.size(); slot++) { ++ if (items.get(slot).getCount() != contents.amounts().get(slot)) ++ throw new IllegalArgumentException("Decoded native bundle source count changed"); ++ } ++ } ++ ++ @Override ++ public List items() { return this.items.stream().map(ItemStack::copy).toList(); } ++ ++ ItemStack item(final int physicalSlot) { return this.items.get(physicalSlot).copy(); } ++ ++ BundleTooltipUi.View view(final int mode, final int inputMode, final boolean hidden, final Integer selectedCell, final Boolean showTags) { ++ final var cells = IntStream.range(0, this.items.size()).mapToObj(slot -> new BundleTooltipUi.Item(this.names.get(slot), ++ NativeInventoryItemPresentation.values(this.items.get(slot), false, showTags))).toList(); ++ return new BundleTooltipUi.View(mode, inputMode, hidden, selectedCell, this.contents, this.storage, cells); ++ } ++ } ++ +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryItemStyle.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryItemStyle.java new file mode 100644 @@ -6141,10 +6222,10 @@ index 0000000..98ed38d +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryPresentation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryPresentation.java new file mode 100644 -index 0000000..973ce0e +index 0000000..efc2831 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryPresentation.java -@@ -0,0 +1,45 @@ +@@ -0,0 +1,52 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -6159,12 +6240,19 @@ index 0000000..973ce0e +import java.util.Objects; +import net.minecraft.world.item.ItemStack; +import net.raphimc.viabedrock.api.resourcepack.ui.ContainerUi; ++import net.raphimc.viabedrock.api.resourcepack.ui.BundleTooltipUi; +import java.util.Set; +import java.util.HashMap; + +/** Different native menu controllers share one renderer and the genuine Java inventory event path. */ +sealed interface NativeInventoryPresentation permits NativeContainerPresentation, NativeCookingPresentation, NativePlayerPresentation { -+ Frame resolve(int menuId, String title, List slots, Map cursor, Boolean showTags); ++ default Frame resolve(final int menuId, final String title, final List slots, ++ final Map cursor, final Boolean showTags) { ++ return this.resolve(menuId, title, slots, cursor, showTags, BundleTooltipUi.View.inactive()); ++ } ++ ++ Frame resolve(int menuId, String title, List slots, Map cursor, ++ Boolean showTags, BundleTooltipUi.View tooltip); + + long revision(); + @@ -6423,10 +6511,10 @@ index 0000000..8482173 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerPresentation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerPresentation.java new file mode 100644 -index 0000000..0c4a0a5 +index 0000000..a116b55 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerPresentation.java -@@ -0,0 +1,215 @@ +@@ -0,0 +1,216 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -6450,6 +6538,7 @@ index 0000000..0c4a0a5 +import net.raphimc.viabedrock.api.model.NativePlayerInventoryMessage; +import net.raphimc.viabedrock.api.model.NativePlayerInventoryResult; +import net.raphimc.viabedrock.api.resourcepack.ui.ContainerUi; ++import net.raphimc.viabedrock.api.resourcepack.ui.BundleTooltipUi; +import net.raphimc.viabedrock.api.resourcepack.ui.PlayerInventoryUi; +import net.raphimc.viabedrock.api.resourcepack.ui.UiBindings; +import net.raphimc.viabedrock.protocol.model.InventoryOptions; @@ -6598,7 +6687,7 @@ index 0000000..0c4a0a5 + + @Override + public Frame resolve(final int menuId, final String title, final List slots, -+ final Map cursor, final Boolean showTags) { ++ final Map cursor, final Boolean showTags, final BundleTooltipUi.View tooltip) { + if (menuId != this.message.menuId() || this.retired) throw new IllegalArgumentException("Retired player menu"); + if (this.helping()) return new Frame(this.program.help(this.helpCreative, this.helpTopic), 46, Map.of(), Map.of()); + final var source = this.state; @@ -6620,7 +6709,7 @@ index 0000000..0c4a0a5 + ghosts.put(slot.javaSlot(), ghost); + return new ContainerUi.Slot(slot.javaSlot(), values); + }).toList(); -+ final var scene = this.program.resolve(new PlayerInventoryUi.MenuView(menuId, state, displayedSlots, cursor), this.windows); ++ final var scene = this.program.resolve(new PlayerInventoryUi.MenuView(menuId, state, displayedSlots, cursor), this.windows, tooltip); + final Map> actions = new HashMap<>(); + final Map visible = new HashMap<>(); + this.scope(scene.getAsJsonObject("root"), actions, visible); @@ -11370,10 +11459,10 @@ index 0000000..9646a61 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryItemPresentationTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryItemPresentationTest.java new file mode 100644 -index 0000000..c05f81d +index 0000000..58a774e --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryItemPresentationTest.java -@@ -0,0 +1,126 @@ +@@ -0,0 +1,238 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -11382,7 +11471,23 @@ index 0000000..c05f81d +package com.viaversion.viafabricplus.bedrock.screen.container; + +import java.util.Map; ++import java.util.List; ++import java.util.ArrayList; ++import java.util.stream.IntStream; ++import com.viaversion.viaversion.api.minecraft.item.StructuredItem; ++import com.viaversion.viaversion.api.minecraft.data.StructuredDataContainer; ++import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey; ++import com.viaversion.viaversion.libs.gson.JsonObject; ++import net.minecraft.nbt.ByteArrayTag; ++import net.minecraft.nbt.IntArrayTag; ++import net.raphimc.viabedrock.api.model.NativeBundleTooltipData; ++import net.raphimc.viabedrock.api.model.NativeBundleTooltipItems; ++import net.raphimc.viabedrock.api.resourcepack.ui.ContainerUi; ++import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; +import net.minecraft.SharedConstants; ++import net.minecraft.core.RegistryAccess; ++import net.minecraft.core.registries.BuiltInRegistries; ++import com.viaversion.viafabricplus.bedrock.creative.BedrockCreativeCatalog; +import net.minecraft.server.Bootstrap; +import org.junit.jupiter.api.BeforeAll; +import net.minecraft.core.component.DataComponents; @@ -11401,13 +11506,109 @@ index 0000000..c05f81d + +final class NativeInventoryItemPresentationTest { + @BeforeAll -+ static void bootstrap() { ++ static void bootstrap() throws Exception { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); ++ Class.forName(StructuredDataKey.class.getName()); ++ if (!Items.IRON_INGOT.builtInRegistryHolder().areComponentsBound()) Items.IRON_INGOT.builtInRegistryHolder().bindComponents(DataComponentMap.EMPTY); + if (!Items.BUNDLE.builtInRegistryHolder().areComponentsBound()) Items.BUNDLE.builtInRegistryHolder().bindComponents(DataComponentMap.EMPTY); + } + + @Test ++ void physicalPreviewsDecodeIndependentlyOfJavaBundleContentsAndFeedAuthoredScenes() { ++ final var source = bundle(List.of(0, 255, 0, 2), Map.of( ++ 1, new NativeBundleTooltipItems.Preview("§dFirst physical item", new StructuredItem(BuiltInRegistries.ITEM.getId(Items.IRON_INGOT), 255, new StructuredDataContainer())), ++ 3, new NativeBundleTooltipItems.Preview("Last physical item", new StructuredItem(BuiltInRegistries.ITEM.getId(Items.IRON_INGOT), 2, new StructuredDataContainer())))); ++ final List converted = new ArrayList<>(); ++ final var decoded = NativeInventoryItemPresentation.bundleTooltip(source, item -> { ++ final var preview = BedrockCreativeCatalog.toNative(item, RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY)); ++ converted.add(preview); ++ return preview; ++ }); ++ assertNotNull(decoded); ++ assertEquals(0, source.get(DataComponents.BUNDLE_CONTENTS).size()); ++ assertEquals(4, decoded.items().size()); ++ assertTrue(decoded.item(0).isEmpty()); ++ assertTrue(decoded.item(2).isEmpty()); ++ converted.forEach(item -> item.setCount(1)); ++ decoded.items().get(1).setCount(1); ++ assertSame(Items.IRON_INGOT, decoded.item(1).getItem()); ++ assertEquals(255, decoded.item(1).getCount()); ++ assertEquals(2, decoded.item(3).getCount()); ++ final var view = decoded.view(2, 1, false, 2, false); ++ assertEquals(List.of(0, 255, 0, 2), view.contents().amounts()); ++ assertEquals("Last physical item", view.items().get(3).primaryName()); ++ assertEquals(255, view.items().get(1).presentation().get("#inventory_stack_count").getAsInt()); ++ final var ui = new ResourcePackStorage(List.of()).getContainerUi(); ++ final var kind = ContainerUi.Kind.CHEST_3_ROWS; ++ final var program = ui.exportProgram(kind, new ContainerUi.Title("Chest", false)); ++ final var presentation = new NativeContainerPresentation(ContainerUi.fromProgram(program), kind, List.of()); ++ final var empty = NativeInventoryItemPresentation.values(ItemStack.EMPTY, false, false); ++ final var slots = IntStream.range(0, 63).mapToObj(slot -> new ContainerUi.Slot(slot, empty)).toList(); ++ final var frame = presentation.resolve(7, "Chest", slots, Map.of(), false, view); ++ final var controls = nodes(frame.scene().getAsJsonObject("root")); ++ assertTrue(controls.stream().anyMatch(node -> node.has("text") && node.get("text").isJsonPrimitive() ++ && node.get("text").getAsString().equals("§dOriginal bundle"))); ++ final var physical = controls.stream().filter(node -> node.has("bundle_slot")).toList(); ++ assertFalse(physical.isEmpty()); ++ assertTrue(physical.stream().noneMatch(node -> node.has("inventory_slot") || node.has("inventory_actions"))); ++ assertTrue(physical.stream().anyMatch(node -> node.get("bundle_slot").getAsInt() == 3)); ++ // Rendering and input admission remain explicit until the native screen can consume these views. ++ assertThrows(IllegalArgumentException.class, () -> NativeInventoryItemPresentation.requireTooltip(source)); ++ } ++ ++ @Test ++ void missingMalformedAndMismatchedTablesNeverBecomeEmptyOrCompactedNativeViews() { ++ final var empty = bundle(List.of(0, 0), Map.of()); ++ assertNotNull(NativeInventoryItemPresentation.bundleTooltip(empty, item -> { throw new AssertionError("Empty table decoded an item"); })); ++ final var source = bundle(List.of(0, 2), Map.of(1, ++ new NativeBundleTooltipItems.Preview("Native", new StructuredItem(7, 2, new StructuredDataContainer())))); ++ assertThrows(IllegalArgumentException.class, () -> NativeInventoryItemPresentation.bundleTooltip(source, item -> new ItemStack(Items.IRON_INGOT, 1))); ++ final var custom = source.get(DataComponents.CUSTOM_DATA).copyTag(); ++ final var tooltip = (CompoundTag) custom.get(NativeBundleTooltipData.TAG); ++ tooltip.putIntArray("amounts", new int[]{0, 1}); ++ source.set(DataComponents.CUSTOM_DATA, CustomData.of(custom)); ++ assertThrows(IllegalArgumentException.class, () -> NativeInventoryItemPresentation.bundleTooltip(source, item -> new ItemStack(Items.IRON_INGOT, item.amount()))); ++ tooltip.putIntArray("amounts", new int[]{0, 2}); ++ tooltip.put(NativeBundleTooltipItems.TAG, new ByteArrayTag(new byte[0])); ++ source.set(DataComponents.CUSTOM_DATA, CustomData.of(custom)); ++ assertThrows(IllegalArgumentException.class, () -> NativeInventoryItemPresentation.bundleTooltip(source, item -> new ItemStack(Items.IRON_INGOT, item.amount()))); ++ tooltip.remove(NativeBundleTooltipItems.TAG); ++ source.set(DataComponents.CUSTOM_DATA, CustomData.of(custom)); ++ assertThrows(IllegalArgumentException.class, () -> NativeInventoryItemPresentation.bundleTooltip(source, item -> new ItemStack(Items.IRON_INGOT, item.amount()))); ++ } ++ ++ private static ItemStack bundle(final List counts, final Map items) { ++ final var stack = new ItemStack(Items.BUNDLE); ++ stack.set(DataComponents.BUNDLE_CONTENTS, BundleContents.EMPTY); ++ final var custom = new CompoundTag(); ++ final var data = new CompoundTag(); ++ data.putInt("version", NativeBundleTooltipData.VERSION); ++ data.putInt("identity", 7); ++ data.putInt("viewable_slots", 12); ++ data.put("amounts", new IntArrayTag(counts.stream().mapToInt(Integer::intValue).toArray())); ++ data.putString("primary_name", "§dOriginal bundle"); ++ data.putString("empty_description", "Native description"); ++ data.put(NativeBundleTooltipItems.TAG, new ByteArrayTag(new NativeBundleTooltipItems(7, counts.size(), items).encode())); ++ custom.put(NativeBundleTooltipData.TAG, data); ++ final var storage = new CompoundTag(); ++ storage.putInt("version", NativeItemStorageData.VERSION); ++ storage.putInt("modifier", 4); ++ storage.putInt("capacity", 64); ++ storage.putInt("weight", counts.stream().mapToInt(Integer::intValue).sum()); ++ custom.put(NativeItemStorageData.TAG, storage); ++ stack.set(DataComponents.CUSTOM_DATA, CustomData.of(custom)); ++ return stack; ++ } ++ ++ private static List nodes(final JsonObject root) { ++ final List result = new ArrayList<>(); ++ result.add(root); ++ if (root.has("children")) root.getAsJsonArray("children").forEach(child -> result.addAll(nodes(child.getAsJsonObject()))); ++ return result; ++ } ++ ++ @Test + void authoredLabelsCapNativeCountsWithoutChangingNumericBindingsOrQuickcraftText() { + final Map reference = Map.of( + -1, "", 0, "", 1, "", 2, "2", 9, "9", 10, "10", 99, "99", 100, "99+", 255, "99+", Integer.MAX_VALUE, "99+"); diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.pr.md index 43967f6b..0146fe36 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.pr.md @@ -77,6 +77,7 @@ Server responses confirm placement of the retained eight-log cursor stack. Inactive recipe icons carry separate lighting and opacity through extraction, cached atlas entries, and oversized item submission. Regressions retain a real unavailable recipe and restore both properties after nested extraction failures. Closed storage-item previews and bars read the core's native weight snapshot. +Physical bundle preview tables decode into owned Minecraft stacks and explicit tooltip views without using Java bundle compaction. Identity and counts must match the retained source snapshot. Storage, cooking, player, and workbench presentations accept these views. The decoder requires the core preview API; source lifetime, renderer admission, placement, and physical-slot input remain unfinished. The default-screen regression retains an empty bundle alongside an inactive recipe. Installed direct and ViaProxy controls retain a server-issued empty bundle in Survival after reopening. Unknown snapshots restore ordinary controls. Hovering or carrying storage items also restores ordinary controls until native tooltip rendering and selection are complete.