From 6ee0050b36bd24ddb989a414aa81392c15e29533 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 14:10:39 +0100 Subject: [PATCH 01/33] perf(ui): bound inventory grids to their visible rows Retain complete inventory collections while expanding fixed-grid cells near the viewport and keyboard selection. Preserve source indices, scroll extent, action targets, and focus across layout updates so large Creative catalogs fit the existing scene budget. Keep unsupported hotbar destruction on the ordinary screen before item changes, and construct Creative item previews through the connection-owned native item factory so metadata-only items do not acquire a false block identity. Installed Creative search and full native parity still require validation. --- docs/native-player-inventory.md | 13 + ...edrock-recipes-and-place-ingredients.patch | 165 +++++++- ...edrock-recipes-and-place-ingredients.pr.md | 5 + ...ain-bedrock-creative-catalog-entries.patch | 99 +++-- ...ain-bedrock-creative-catalog-entries.pr.md | 3 + ...bedrock-merchant-inventories-in-core.patch | 8 +- ...ate-bedrock-loom-inventories-in-core.patch | 6 +- ...ranslate-bedrock-cartography-in-core.patch | 18 +- ...1-confirm-survival-offhand-transfers.patch | 28 +- ...are-native-ui-programs-and-resources.patch | 385 ++++++++++++++++-- ...are-native-ui-programs-and-resources.pr.md | 6 + ...9-present-authored-native-containers.patch | 277 ++++++++++--- ...9-present-authored-native-containers.pr.md | 6 + ...0029-share-native-ui-scene-rendering.patch | 366 ++++++++++++++++- ...0029-share-native-ui-scene-rendering.pr.md | 4 + 15 files changed, 1215 insertions(+), 174 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 38768c90..fc9952e9 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -54,6 +54,18 @@ A change in raw text can retire selection even when normalized text is unchanged Survival counts combine candidate recipes and share their source budget. Creative counts read the current game mode and source output amount. The displayed total can exceed the quantity that one selected candidate can craft. +Fixed recipe grids use cell windows over the complete retained collection. +The source keeps its original order, item identities, and total scroll extent. +Each view expands the visible rows and a one-row buffer on each side. +An offscreen keyboard selection also retains its neighboring rows. +Resize and scroll changes preserve the original collection indices and action targets. +Window changes do not advance the authoritative snapshot revision. + +Only horizontal grids with fixed cell geometry support this contract. +Row-dependent dimensions and redirected root indices restore ordinary controls. +The node budget remains unchanged. +These windows preserve authored geometry but do not establish identical native pooling or frame timing. + ## Actions and presentation Core owns requests, network stack IDs, transfer quantities, prediction, and server response reconciliation. @@ -98,6 +110,7 @@ Filled-bundle bars still need installed native comparison. Unknown storage snapshots restore ordinary controls. Hovering or carrying storage items also restores ordinary controls until native tooltips and selection are complete. Unsupported visible Help glyphs, recipe auxiliary actions, and held-selection commands also restore ordinary controls. +Creative hotbar destruction restores ordinary controls before item changes or requests. Large Creative catalogs exceed the current scene work budget. Viewport expansion remains unfinished. Initial bundle insertion, bulk recipe actions, Creative cursor replacement, and complete installed native parity remain unfinished. Native constructor timing relative to the open acknowledgment and frame-level population timing remain unverified. 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 25f3ce45..ee948b79 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 d7d0d970f1cbc7880dca0b388407462bc03effbb Mon Sep 17 00:00:00 2001 +From 99504877696882cc572173a75ac3d84c1eaeeee0 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 @@ -819,6 +819,20 @@ bytes. Preserve raw query bytes through the String API as well. This supplies the query-update boundary required by the player model; installed screen ownership and text-edit delivery remain unfinished. + +Construct Creative catalog descriptors through the connection-owned native +Item constructor, as inventory descriptors already do. The raw Creative +reader previously attached the wire runtime value zero as a real Block to +ordinary items. Potion secondary search then requested a Block auxiliary +that did not exist and disconnected the session by unboxing null. Retain +the selected definition and real optional Block association instead of +substituting auxiliary data in the description producer. Consume the full +header/user-data slice even when the native constructor rejects the Item. + +The packet regression preserves potion metadata and definition identity, +keeps an absent Block absent, and reads secondary search text. Existing +constructor controls retain associated Blocks at runtime zero. The live +protocol-2225 Creative search supplied the disconnect regression. --- .../api/model/NativeFurnaceOptions.java | 44 + .../container/CookingRecipeAvailability.java | 30 + @@ -851,7 +865,7 @@ installed screen ownership and text-edit delivery remain unfinished. .../protocol/model/recipe/ShapedRecipe.java | 65 +- .../model/recipe/ShapelessRecipe.java | 6 +- .../protocol/model/recipe/SmithingRecipe.java | 4 +- - .../protocol/packet/InventoryPackets.java | 146 ++- + .../protocol/packet/InventoryPackets.java | 160 +++- .../protocol/packet/JoinPackets.java | 1 + .../protocol/packet/MultiStatePackets.java | 55 ++ .../protocol/rewriter/ItemRewriter.java | 188 +++- @@ -887,6 +901,7 @@ installed screen ownership and text-edit delivery remain unfinished. .../model/recipe/RecipeOwnershipTest.java | 52 ++ .../packet/FurnaceOptionsPacketTest.java | 162 ++++ .../packet/InventoryOptionsPacketTest.java | 211 +++++ + .../protocol/packet/InventoryPacketsTest.java | 51 +- .../NativeFurnaceOptionsPacketTest.java | 214 +++++ .../NativeRecipeConcreteCounterTest.java | 165 ++++ .../rewriter/NativeRecipePaletteTest.java | 203 +++++ @@ -911,7 +926,7 @@ installed screen ownership and text-edit delivery remain unfinished. .../types/model/FurnaceOptionsTypeTest.java | 75 ++ .../types/model/InventoryOptionsTypeTest.java | 46 + .../types/recipe/CraftingDataReaderTest.java | 84 +- - 91 files changed, 12928 insertions(+), 180 deletions(-) + 92 files changed, 12984 insertions(+), 189 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeFurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeAvailability.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeCatalog.java @@ -3835,7 +3850,7 @@ index 11c4d002..9dc86539 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 20e14855..2d0e3d35 100644 +index 20e14855..0672f7a7 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -93,6 +93,7 @@ import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.*; @@ -3846,7 +3861,15 @@ index 20e14855..2d0e3d35 100644 import net.raphimc.viabedrock.protocol.model.ItemCooldown; import net.raphimc.viabedrock.protocol.model.FullContainerName; import net.raphimc.viabedrock.protocol.model.inventory.BedrockInventoryTransaction; -@@ -165,11 +166,69 @@ public final class InventoryPackets { +@@ -109,6 +110,7 @@ import net.raphimc.viabedrock.protocol.types.entitydata.EntityDataTypesBedrock; + import net.raphimc.viabedrock.protocol.types.BedrockTypes; + import net.raphimc.viabedrock.protocol.types.InventoryTypes; + import net.raphimc.viabedrock.protocol.types.item.BedrockItemType; ++import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackContext; + import net.raphimc.viabedrock.protocol.types.recipe.CraftingDataReader; + + import java.util.ArrayList; +@@ -165,11 +167,69 @@ public final class InventoryPackets { final short javaId = container.translateContainerData(id); if (javaId < 0) { wrapper.cancel(); @@ -3916,7 +3939,7 @@ index 20e14855..2d0e3d35 100644 } static void handleContainerClose(final PacketWrapper wrapper) { -@@ -527,7 +586,13 @@ public final class InventoryPackets { +@@ -527,7 +587,13 @@ public final class InventoryPackets { if (container instanceof LecternContainer lectern) { lectern.sendInitialState(); } @@ -3931,7 +3954,7 @@ index 20e14855..2d0e3d35 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.CONTAINER_CLOSE, -@@ -564,6 +629,9 @@ public final class InventoryPackets { +@@ -564,6 +630,9 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeContent(container); @@ -3941,7 +3964,7 @@ index 20e14855..2d0e3d35 100644 if (container instanceof StonecutterContainer stonecutter) { stonecutter.refreshInput(); wrapper.cancel(); -@@ -571,6 +639,7 @@ public final class InventoryPackets { +@@ -571,6 +640,7 @@ public final class InventoryPackets { } PacketFactory.writeJavaContainerSetContent(wrapper, container); PacketFactory.sendJavaCursorForHudUpdate(wrapper.user(), container); @@ -3949,7 +3972,7 @@ index 20e14855..2d0e3d35 100644 } else { wrapper.cancel(); } -@@ -599,6 +668,12 @@ public final class InventoryPackets { +@@ -599,6 +669,12 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeSlot(container, slot); @@ -3962,7 +3985,7 @@ index 20e14855..2d0e3d35 100644 if (container instanceof StonecutterContainer stonecutter && slot == 3) { stonecutter.refreshInput(); wrapper.cancel(); -@@ -622,6 +697,7 @@ public final class InventoryPackets { +@@ -622,6 +698,7 @@ public final class InventoryPackets { } wrapper.write( VersionedTypes.V26_3.item, container.getJavaItem(slot)); // item @@ -3970,7 +3993,7 @@ index 20e14855..2d0e3d35 100644 } else { wrapper.cancel(); } -@@ -683,6 +759,14 @@ public final class InventoryPackets { +@@ -683,6 +760,14 @@ public final class InventoryPackets { inventoryTracker.removeDynamicContainer(containerName); } }); @@ -3985,13 +4008,13 @@ index 20e14855..2d0e3d35 100644 protocol.registerClientbound( ClientboundBedrockPackets.CRAFTING_DATA, null, -@@ -692,7 +776,27 @@ public final class InventoryPackets { +@@ -692,7 +777,27 @@ public final class InventoryPackets { wrapper.user().get(CraftingDataTracker.class); final CraftingDataReader.Data data = CraftingDataReader.read(wrapper); tracker.updateCraftingDataList(data.recipes(), data.clearRecipes()); + wrapper.user().get(InventoryTracker.class).getInventoryContainer() + .refreshCraftingOutput(); - tracker.sendJavaUpdateRecipes(wrapper.user()); ++ tracker.sendJavaUpdateRecipes(wrapper.user()); + NativeCookingUiState.sendUpdate(wrapper.user()); + }); + protocol.registerClientbound( @@ -4008,12 +4031,12 @@ index 20e14855..2d0e3d35 100644 + final CraftingDataTracker tracker = + wrapper.user().get(CraftingDataTracker.class); + tracker.updateUnlockedRecipes(type, ids); -+ tracker.sendJavaUpdateRecipes(wrapper.user()); + tracker.sendJavaUpdateRecipes(wrapper.user()); + NativeCookingUiState.sendUpdate(wrapper.user()); }); protocol.registerClientbound( ClientboundBedrockPackets.CONTAINER_SET_DATA, -@@ -905,6 +1009,46 @@ public final class InventoryPackets { +@@ -905,6 +1010,46 @@ public final class InventoryPackets { PacketFactory.sendJavaContainerSetContent(wrapper.user(), container); } }); @@ -4060,6 +4083,37 @@ index 20e14855..2d0e3d35 100644 protocol.registerServerbound( ServerboundPackets26_3.CONTAINER_BUTTON_CLICK, null, +@@ -1727,10 +1872,10 @@ public final class InventoryPackets { + + private static BedrockItem readCreativeItem(final PacketWrapper wrapper, final ItemRewriter itemRewriter) + throws InformativeException { +- return readCreativeItem(wrapper, itemRewriter.getItems().getOrDefault("minecraft:shield", 0)); ++ return readCreativeItem(wrapper, itemRewriter.getItems().getOrDefault("minecraft:shield", 0), itemRewriter); + } + +- static BedrockItem readCreativeItem(final PacketWrapper wrapper, final int blockingId) ++ static BedrockItem readCreativeItem(final PacketWrapper wrapper, final int blockingId, final NetworkItemStackContext context) + throws InformativeException { + final int id = wrapper.read(BedrockTypes.VAR_INT); + final int amount = wrapper.read(BedrockTypes.UNSIGNED_SHORT_LE); +@@ -1740,13 +1885,12 @@ public final class InventoryPackets { + if (id == 0) { + return BedrockItem.empty(); + } +- final BedrockItem item = new BedrockItem(id); +- item.setAmount(amount); +- item.setData(data); +- item.setBlockRuntimeId(blockRuntimeId); ++ final BedrockItem item = context.createNetworkItem(id, (short) data, (byte) amount, blockRuntimeId); + final ByteBuf buffer = Unpooled.wrappedBuffer(userData); + try { +- BedrockItemType.readUserData(buffer, item, blockingId); ++ if (!item.isEmpty()) { ++ BedrockItemType.readUserData(buffer, item, blockingId); ++ } + } finally { + buffer.release(); + } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java index 91f2662a..7d828823 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -9742,6 +9796,87 @@ index 00000000..3560ff25 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java +index 7dccb3cc..44b29c83 100644 +--- a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java ++++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java +@@ -36,6 +36,14 @@ import net.raphimc.viabedrock.protocol.model.inventory.InventoryActionData; + import net.raphimc.viabedrock.protocol.storage.InventoryRequestTracker; + import net.raphimc.viabedrock.protocol.storage.InventoryTracker; + import net.raphimc.viabedrock.protocol.types.BedrockTypes; ++import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackContext; ++import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; ++import net.raphimc.viabedrock.protocol.model.ItemRegistry; ++import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.BaseGameVersion; ++import net.raphimc.viabedrock.protocol.model.ItemDescriptionContext; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; + import org.junit.jupiter.api.Test; + import org.junit.jupiter.api.BeforeAll; + +@@ -50,6 +58,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; + import static org.junit.jupiter.api.Assertions.assertSame; + import static org.junit.jupiter.api.Assertions.assertTrue; + ++import static org.mockito.Mockito.*; ++ + class InventoryPacketsTest { + + @BeforeAll +@@ -107,13 +117,50 @@ class InventoryPacketsTest { + writeCreativeDescriptor(descriptors, 529, fireworkData); + final PacketWrapper wrapper = new PacketWrapperImpl(0, descriptors, null); + +- assertTrue(InventoryPackets.readCreativeItem(wrapper, 0).isEmpty()); +- final BedrockItem firework = InventoryPackets.readCreativeItem(wrapper, 0); ++ final var context = mock(NetworkItemStackContext.class); ++ when(context.createNetworkItem(529, (short) 0, (byte) 1, 0)).thenReturn(new BedrockItem(529)); ++ assertTrue(InventoryPackets.readCreativeItem(wrapper, 0, context).isEmpty()); ++ final BedrockItem firework = InventoryPackets.readCreativeItem(wrapper, 0, context); + assertEquals(529, firework.identifier()); + assertEquals(1, firework.tag().getCompoundTag("Fireworks").getByte("Flight")); + assertEquals(0, descriptors.readableBytes()); + } + ++ @Test ++ void creativePotionMetadataStaysNonBlockAndSupportsSecondarySearchText() throws Exception { ++ final var registry = new ItemRegistry(new ItemEntry[]{new ItemEntry("minecraft:potion", 30001, false, ItemVersion.None, null)}); ++ final var user = mock(UserConnection.class); ++ final var blocks = mock(BlockStateRewriter.class); ++ final var items = mock(ItemRewriter.class, CALLS_REAL_METHODS); ++ doReturn(user).when(items).user(); ++ when(user.get(BlockStateRewriter.class)).thenReturn(blocks); ++ final var field = ItemRewriter.class.getDeclaredField("registry"); ++ field.setAccessible(true); ++ field.set(items, registry); ++ final var data = Unpooled.buffer(); ++ try { ++ BedrockTypes.VAR_INT.write(data, 30001); ++ data.writeShortLE(1); ++ BedrockTypes.UNSIGNED_VAR_INT.write(data, 5); ++ BedrockTypes.VAR_INT.write(data, 0); ++ BedrockTypes.BYTE_ARRAY.write(data, new byte[0]); ++ final var item = InventoryPackets.readCreativeItem(new PacketWrapperImpl(0, data, user), 0, items); ++ assertSame(registry.definition(30001), item.definition()); ++ assertFalse(item.hasBlockState()); ++ assertEquals(5, item.data()); ++ final var description = registry.secondaryDescription(item, true, new ItemDescriptionContext(key -> key, ++ () -> BaseGameVersion.INVALID, stack -> { ++ assertFalse(stack.hasBlockState()); ++ return stack.data(); ++ }, ignored -> BedrockItem.empty(), ignored -> "", (key, value) -> Integer.toString(value))); ++ assertFalse(description.isEmpty()); ++ assertEquals(0, data.readableBytes()); ++ verifyNoInteractions(blocks); ++ } finally { ++ data.release(); ++ } ++ } ++ + private static void writeCreativeDescriptor( + final ByteBuf buffer, final int id, final byte[] userData) { + BedrockTypes.VAR_INT.write(buffer, id); diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/NativeFurnaceOptionsPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/NativeFurnaceOptionsPacketTest.java new file mode 100644 index 00000000..d66d6110 diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md index 1c1f597f..e94c4f2a 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md @@ -15,3 +15,8 @@ Regression coverage includes `NativeFurnaceOptionsTest`, `CookingRecipeAvailabil Controlled native recipe and naming references cover their declared boundaries. Complete installed player/cooking controllers and arbitrary recipe admission remain unfinished. The [owning patch](0004-expose-bedrock-recipes-and-place-ingredients.patch) retains implementation rationale and versioned evidence. + +Creative descriptor parsing uses the same connection-owned Item construction as inventory descriptors. +The packet regression retains potion metadata and definition identity without inventing a Block association for wire runtime zero. +It also reads secondary search text and preserves packet alignment after empty group icons. +Associated Blocks at runtime zero retain their separate constructor regression. 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 98dd690f..b20abc78 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 824a7ac150ba90ab07c8fd1231e547298da84705 Mon Sep 17 00:00:00 2001 +From 6e667d7fc79c738b7d4ccf52660435bee5fefb3e 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 @@ -218,12 +218,20 @@ category variables, including their nested controls. Literal text-key scanning misses those concatenated keys, leaving topic/category names untranslated after program transport even when paragraphs and titles work. The translated scene round trip covers both navigation and styled text. + +Allow installed player presenters to request local recipe-grid windows. +Keep the complete retained collection and all source/action identities in +the snapshot. The initial one-cell view establishes authored geometry; +subsequent ranges follow the real viewport rather than removing catalog +rows or changing search results. Other callers keep full expansion. +A 2,000-cell Creative source regression checks distant ranges, actual menu +slots and exported-program replay without replacing the source model. --- .../model/NativePlayerInventoryAction.java | 88 ++++ .../model/NativePlayerInventoryMessage.java | 271 ++++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ .../model/container/NativeRecipeCounter.java | 74 +++ - .../resourcepack/ui/PlayerInventoryUi.java | 375 ++++++++++++++ + .../resourcepack/ui/PlayerInventoryUi.java | 387 ++++++++++++++ .../viabedrock/protocol/BedrockProtocol.java | 1 + .../protocol/model/CreativeCatalog.java | 266 ++++++++++ .../model/CreativeInventoryModel.java | 198 ++++++++ @@ -240,15 +248,15 @@ The translated scene round trip covers both navigation and styled text. .../NativePlayerInventoryMessageTest.java | 160 ++++++ .../container/NativeRecipeCounterTest.java | 27 + .../container/NativeRecipeQuantityTest.java | 61 +++ - .../ui/PlayerInventoryUiTest.java | 101 ++++ + .../ui/PlayerInventoryUiTest.java | 126 +++++ .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++++++ - .../protocol/packet/InventoryPacketsTest.java | 268 ++++++++-- + .../protocol/packet/InventoryPacketsTest.java | 267 ++++++++-- .../rewriter/PlayerRecipeModelTest.java | 412 +++++++++++++++ ...egisteredPlayerRecipeAvailabilityTest.java | 5 +- .../RegisteredPlayerRecipeSearchTest.java | 3 +- .../storage/CreativeInventoryActionTest.java | 214 ++++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ - 29 files changed, 4065 insertions(+), 170 deletions(-) + 29 files changed, 4101 insertions(+), 170 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 @@ -783,10 +791,10 @@ index fe874172..3ccb1532 100644 /** A captured native collection. Slots in results are relative to this collection. */ 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..fb187c33 +index 00000000..19993219 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUi.java -@@ -0,0 +1,375 @@ +@@ -0,0 +1,387 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -868,10 +876,15 @@ index 00000000..fb187c33 + } + + public JsonObject resolve(final MenuView menu) { ++ return this.resolve(menu, null); ++ } ++ ++ /** Non-null windows opt into local cell views while retaining the complete menu model. */ ++ public JsonObject resolve(final MenuView menu, final Map windows) { + 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)); ++ final var resolved = this.program.resolve(host(menu.state.workbench), new Controller(menu, windows)); + if (resolved.root() == null || !resolved.missingTemplates().isEmpty()) { + throw new IllegalArgumentException("Incomplete authored player inventory scene"); + } @@ -1054,10 +1067,12 @@ index 00000000..fb187c33 + private static final class Controller extends ContainerUi.Controller { + private static final Set RECIPE_ACTIONS = Set.of("button.recipe_select", "button.recipe_secondary", "button.recipe_tertiary"); + private final State state; ++ private final Map windows; + -+ private Controller(final MenuView menu) { ++ private Controller(final MenuView menu, final Map windows) { + super(menu.state.workbench ? 10 : 9, menu.slots, menu.cursor, new ContainerUi.Title("", false), true); + this.state = menu.state; ++ this.windows = windows == null ? null : Map.copyOf(windows); + final Map slots = new HashMap<>(); + menu.slots.forEach(slot -> slots.put(slot.javaSlot(), slot)); + this.collections.put("crafting_input_items", collection(slots, 1, this.state.workbench ? 9 : 4)); @@ -1108,6 +1123,11 @@ index 00000000..fb187c33 + } + + @Override ++ public Window window(final String key, final String collection, final Scope parent) { ++ return this.windows != null && collection.equals("recipe_book") ? this.windows.getOrDefault(key, new Window(0, 1)) : null; ++ } ++ ++ @Override + public String defaultCollection() { + return "inventory_items"; + } @@ -1884,7 +1904,7 @@ index 00000000..5b3cdb3b + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index b05868fe..9cbfd9e4 100644 +index 9d4ce882..8c64cdce 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -92,14 +92,20 @@ import net.raphimc.viabedrock.protocol.data.enums.bedrock.ComplexInventoryTransa @@ -1908,7 +1928,7 @@ index b05868fe..9cbfd9e4 100644 import net.raphimc.viabedrock.protocol.model.recipe.EnchantData; import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; -@@ -114,6 +120,7 @@ import net.raphimc.viabedrock.protocol.types.recipe.CraftingDataReader; +@@ -115,6 +121,7 @@ import net.raphimc.viabedrock.protocol.types.recipe.CraftingDataReader; import java.util.ArrayList; import java.util.List; @@ -1916,7 +1936,7 @@ index b05868fe..9cbfd9e4 100644 import java.util.logging.Level; public final class InventoryPackets { -@@ -357,22 +364,33 @@ public final class InventoryPackets { +@@ -358,22 +365,33 @@ public final class InventoryPackets { wrapper.cancel(); final ItemRewriter itemRewriter = wrapper.user().get(ItemRewriter.class); final int groups = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); @@ -1957,7 +1977,7 @@ index b05868fe..9cbfd9e4 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.CONTAINER_OPEN, -@@ -1148,41 +1166,7 @@ public final class InventoryPackets { +@@ -1149,41 +1167,7 @@ public final class InventoryPackets { protocol.registerServerbound( ServerboundPackets26_3.SET_CREATIVE_MODE_SLOT, null, @@ -2000,7 +2020,7 @@ index b05868fe..9cbfd9e4 100644 protocol.registerServerbound( ServerboundPackets26_3.CUSTOM_CLICK_ACTION, ServerboundBedrockPackets.MODAL_FORM_RESPONSE, -@@ -1294,7 +1278,7 @@ public final class InventoryPackets { +@@ -1295,7 +1279,7 @@ public final class InventoryPackets { if (container == null) { if (containerId == ContainerID.CONTAINER_ID_INVENTORY.getValue()) { wrapper.user().get(InventoryRequestTracker.class) @@ -2009,7 +2029,7 @@ index b05868fe..9cbfd9e4 100644 } wrapper.cancel(); return; -@@ -1866,31 +1850,126 @@ public final class InventoryPackets { +@@ -1867,31 +1851,126 @@ public final class InventoryPackets { return "container." + name; } @@ -3499,10 +3519,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..ce655f04 +index 00000000..bb241279 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUiTest.java -@@ -0,0 +1,101 @@ +@@ -0,0 +1,126 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -3525,12 +3545,37 @@ index 00000000..ce655f04 +import java.util.Set; +import java.util.UUID; +import java.util.stream.IntStream; ++import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +class PlayerInventoryUiTest { + + @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( ++ "#inventory_stack_count", new JsonPrimitive(0)))).toList(); ++ final var cells = IntStream.range(0, 2000).mapToObj(id -> new PlayerInventoryUi.Cell(id, id + 10, -1, 0, 0, 1, false, false, "", Map.of())).toList(); ++ final var state = new PlayerInventoryUi.State(false, true, 2, 2, 5, 2, false, "", "All", "Crafting", ++ List.of(2000, 0, 0, 0), cells, "", -1); ++ final var view = new PlayerInventoryUi.MenuView(0, state, slots, Map.of()); ++ final var initial = ui.resolve(view, Map.of()); ++ final var grid = nodes(initial.getAsJsonObject("root")).stream().filter(node -> node.has("collection_window_key")).findFirst().orElseThrow(); ++ assertEquals(2000, grid.get("collection_size").getAsInt()); ++ assertEquals(1, grid.getAsJsonArray("children").size()); ++ final var windows = Map.of(grid.get("collection_window_key").getAsString(), new UiBindings.Window(1700, 40)); ++ final var scene = ui.resolve(view, windows); ++ final var visible = nodes(scene.getAsJsonObject("root")); ++ assertEquals(IntStream.range(1700, 1740).boxed().collect(Collectors.toSet()), visible.stream() ++ .filter(node -> node.has("recipe_id")).map(node -> node.get("recipe_id").getAsInt()).collect(Collectors.toSet())); ++ assertTrue(visible.stream().filter(node -> node.has("inventory_slot")).map(node -> node.get("inventory_slot").getAsInt()) ++ .collect(Collectors.toSet()).containsAll(IntStream.range(9, 45).boxed().toList())); ++ assertEquals(scene, PlayerInventoryUi.fromProgram(ui.exportProgram(false)).resolve(view, windows)); ++ assertEquals(2000, state.cells().size()); ++ } ++ ++ @Test + void actualAuthoredPlayerAndWorkbenchProgramsRetainRealInventoryAndRecipeIdentities() { + 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( @@ -3993,7 +4038,7 @@ index 00000000..b420d6ae + +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java -index a17e52fd..8980e433 100644 +index 19fe63ab..8cc84c93 100644 --- a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java @@ -17,15 +17,25 @@ package net.raphimc.viabedrock.protocol.packet; @@ -4030,7 +4075,7 @@ index a17e52fd..8980e433 100644 import net.raphimc.viabedrock.protocol.storage.InventoryRequestTracker; import net.raphimc.viabedrock.protocol.storage.InventoryTracker; import net.raphimc.viabedrock.protocol.types.BedrockTypes; -@@ -40,7 +51,9 @@ import org.junit.jupiter.api.Test; +@@ -48,7 +59,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeAll; import java.lang.reflect.Proxy; @@ -4040,15 +4085,7 @@ index a17e52fd..8980e433 100644 import java.util.Map; import java.util.UUID; -@@ -49,6 +62,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; - import static org.junit.jupiter.api.Assertions.assertNull; - import static org.junit.jupiter.api.Assertions.assertSame; - import static org.junit.jupiter.api.Assertions.assertTrue; -+import static org.mockito.Mockito.*; - - class InventoryPacketsTest { - -@@ -57,6 +71,31 @@ class InventoryPacketsTest { +@@ -67,6 +80,31 @@ class InventoryPacketsTest { Class.forName(StructuredDataKey.class.getName()); } @@ -4080,7 +4117,7 @@ index a17e52fd..8980e433 100644 @Test void nativePlayerInventoryCloseAcknowledgementDoesNotCloseTheJavaCreativeScreen() { final CloseFixture fixture = new CloseFixture(); -@@ -219,62 +258,174 @@ class InventoryPacketsTest { +@@ -266,62 +304,174 @@ class InventoryPacketsTest { } @Test @@ -4304,7 +4341,7 @@ index a17e52fd..8980e433 100644 } } -@@ -285,7 +436,7 @@ class InventoryPacketsTest { +@@ -332,7 +482,7 @@ class InventoryPacketsTest { UserConnection.class.getClassLoader(), new Class[]{UserConnection.class}, (proxy, method, args) -> method.getName().equals("get") ? this.storage.get(args[0]) : null); private final ClosingInventoryTracker inventory = new ClosingInventoryTracker(this.user); @@ -4313,7 +4350,7 @@ index a17e52fd..8980e433 100644 private CloseFixture() { this.storage.put(InventoryTracker.class, this.inventory); -@@ -317,4 +468,21 @@ class InventoryPacketsTest { +@@ -364,4 +514,21 @@ class InventoryPacketsTest { } } diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index dfee5688..18b34ff2 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -66,6 +66,9 @@ Missing profile preferences remain absent; the initial filtering value follows t Source, recipe, unlock, ability, rule, and preference changes invalidate the retained collections. Catalog replacement refreshes source collections while preserving the backend's installed policies and owned query. The state publishes the retained rows and full item previews through a bounded, compressed protocol-2225 snapshot. +The view adapter can expand a fixed recipe-grid window while retaining every source row and item preview. +Local windows preserve original recipe IDs, all real menu slots, and the authoritative snapshot revision. +A 2,000-row Creative regression covers distant windows and exported-program round trips. Each snapshot carries its opening, container generation, revision, and both independent native layouts. Search and preference commands can use a pending opening. Item requests require the acknowledged generation. diff --git a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch index b56c2ba9..af42d710 100644 --- a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch +++ b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch @@ -1,4 +1,4 @@ -From e9fbc6f19c1899c56e23a9aad6f1a00cb78e0fe3 Mon Sep 17 00:00:00 2001 +From 98eee333b2a4b3d34cebd1224f3d9eccd3ad95a2 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(trading): translate Bedrock merchant inventories in core @@ -884,7 +884,7 @@ index 00000000..ea4d9561 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 9cbfd9e4..5cae5577 100644 +index 8c64cdce..e0723f97 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -79,6 +79,7 @@ import net.raphimc.viabedrock.api.model.container.NativeStorageContainers; @@ -895,7 +895,7 @@ index 9cbfd9e4..5cae5577 100644 import net.raphimc.viabedrock.api.model.container.HorseContainer; import net.raphimc.viabedrock.api.model.container.block.*; import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; -@@ -430,6 +431,11 @@ public final class InventoryPackets { +@@ -431,6 +432,11 @@ public final class InventoryPackets { return; } @@ -907,7 +907,7 @@ index 9cbfd9e4..5cae5577 100644 if (inventoryTracker.isAnyScreenOpen()) { ViaBedrock.getPlatform() .getLogger() -@@ -1293,6 +1299,11 @@ public final class InventoryPackets { +@@ -1294,6 +1300,11 @@ public final class InventoryPackets { inventoryTracker.setCurrentContainerClosed(false); return; } diff --git a/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch b/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch index b034927e..12624951 100644 --- a/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch +++ b/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch @@ -1,4 +1,4 @@ -From 9114b5cc2fd812ac9d8e15bf962bbfc319dc6220 Mon Sep 17 00:00:00 2001 +From 6fcfe3e86a415f991576b98f19d1646d5348e505 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 Bedrock loom inventories in core @@ -368,10 +368,10 @@ index a3b933c2..6ef8f93d 100644 + } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 5cae5577..0f9e3a2b 100644 +index e0723f97..eda923f3 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -@@ -534,6 +534,7 @@ public final class InventoryPackets { +@@ -535,6 +535,7 @@ public final class InventoryPackets { container = new SmithingContainer( wrapper.user(), containerId, title, containerPosition); diff --git a/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch b/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch index 229a4a58..53885827 100644 --- a/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch +++ b/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch @@ -1,4 +1,4 @@ -From 3a27e0220cda59d39a15a657e34166dcf3fde95c Mon Sep 17 00:00:00 2001 +From 2975be474c327ac7e25d3667bb3f520eb73fb272 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 Bedrock cartography in core @@ -634,7 +634,7 @@ index 6ef8f93d..9d1c1819 100644 public static final String CREAKING_HEART = "creaking_heart"; public static final String DAYLIGHT_DETECTOR = "daylight_detector"; diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 0f9e3a2b..5cb4419d 100644 +index eda923f3..e71eb224 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -73,6 +73,7 @@ import net.lenni0451.mcstructs_bedrock.text.utils.BedrockTextUtils; @@ -645,7 +645,7 @@ index 0f9e3a2b..5cb4419d 100644 import net.raphimc.viabedrock.api.chunk.BedrockBlockEntity; import net.raphimc.viabedrock.api.model.container.ChestContainer; import net.raphimc.viabedrock.api.model.container.NativeStorageContainers; -@@ -277,6 +278,9 @@ public final class InventoryPackets { +@@ -278,6 +279,9 @@ public final class InventoryPackets { } final var screens = wrapper.user().get(PlayerInventoryScreenTracker.class); final boolean correlatedClose = container instanceof InventoryContainer && screens != null && screens.ownsClose(); @@ -655,7 +655,7 @@ index 0f9e3a2b..5cb4419d 100644 inventoryTracker.setCurrentContainerClosed(serverInitiated); if (correlatedClose) { wrapper.cancel(); -@@ -534,6 +538,7 @@ public final class InventoryPackets { +@@ -535,6 +539,7 @@ public final class InventoryPackets { container = new SmithingContainer( wrapper.user(), containerId, title, containerPosition); @@ -663,7 +663,7 @@ index 0f9e3a2b..5cb4419d 100644 case LOOM -> container = new LoomContainer(wrapper.user(), containerId, title, containerPosition); case STONECUTTER -> container = -@@ -598,6 +603,13 @@ public final class InventoryPackets { +@@ -599,6 +604,13 @@ public final class InventoryPackets { } container.resolveBlockBinding(); inventoryTracker.setCurrentContainer(container, openContext); @@ -677,7 +677,7 @@ index 0f9e3a2b..5cb4419d 100644 wrapper.write(Types.VAR_INT, (int) containerId); // container id final int javaMenuId = screenType == ContainerType.CONTAINER && size == 54 -@@ -651,6 +663,13 @@ public final class InventoryPackets { +@@ -652,6 +664,13 @@ public final class InventoryPackets { (byte) containerId, containerName, storageItem); if (container != null && container.setItems(items)) { container.markServerUpdate(); @@ -691,7 +691,7 @@ index 0f9e3a2b..5cb4419d 100644 wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeContent(container); wrapper.user().get(InventoryRequestTracker.class) -@@ -821,6 +840,9 @@ public final class InventoryPackets { +@@ -822,6 +841,9 @@ public final class InventoryPackets { .refreshCraftingOutput(); tracker.sendJavaUpdateRecipes(wrapper.user()); NativeCookingUiState.sendUpdate(wrapper.user()); @@ -701,7 +701,7 @@ index 0f9e3a2b..5cb4419d 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.UNLOCKED_RECIPES, -@@ -1147,6 +1169,9 @@ public final class InventoryPackets { +@@ -1148,6 +1170,9 @@ public final class InventoryPackets { if (wrapper.user().get(InventoryTracker.class).getCurrentContainer() instanceof AnvilContainer anvil) { anvil.setRenameText(name); @@ -711,7 +711,7 @@ index 0f9e3a2b..5cb4419d 100644 } }); protocol.registerServerbound( -@@ -1181,6 +1206,13 @@ public final class InventoryPackets { +@@ -1182,6 +1207,13 @@ public final class InventoryPackets { final String id = wrapper.read(Types.STRING); // id final CompoundTag payload = (CompoundTag) wrapper.read(Types.CUSTOM_CLICK_ACTION_TAG); // payload diff --git a/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch b/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch index 0471ba66..ce2dfabb 100644 --- a/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch +++ b/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch @@ -1,4 +1,4 @@ -From 9506014924446c226222f91175598083f64b5e92 Mon Sep 17 00:00:00 2001 +From 8b7d8579b5d77b4a503c52755a5aa42e576415d2 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(inventory): confirm survival offhand transfers from @@ -51,10 +51,10 @@ server acceptance and recovery remain separate controls. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/NativeContainerOffhandGuardTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 2d0e3d35..b05868fe 100644 +index 0672f7a7..9d4ce882 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -@@ -619,7 +619,6 @@ public final class InventoryPackets { +@@ -620,7 +620,6 @@ public final class InventoryPackets { wrapper.read(BedrockTypes.FULL_CONTAINER_NAME); // container name final BedrockItem storageItem = wrapper.read(itemRewriter.newItemType()); // storage item @@ -62,7 +62,7 @@ index 2d0e3d35..b05868fe 100644 final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); final Container container = -@@ -629,6 +628,10 @@ public final class InventoryPackets { +@@ -630,6 +629,10 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeContent(container); @@ -73,7 +73,7 @@ index 2d0e3d35..b05868fe 100644 if (container.type() == ContainerType.HUD) { inventoryTracker.getInventoryContainer().updateCraftingOutput(); } -@@ -658,7 +661,6 @@ public final class InventoryPackets { +@@ -659,7 +662,6 @@ public final class InventoryPackets { final BedrockItem storageItem = wrapper.read(itemRewriter.optionalNewItemType()); // storage item final BedrockItem item = wrapper.read(itemRewriter.newItemType()); // item @@ -81,7 +81,7 @@ index 2d0e3d35..b05868fe 100644 final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); final Container container = -@@ -668,6 +670,20 @@ public final class InventoryPackets { +@@ -669,6 +671,20 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeSlot(container, slot); @@ -102,7 +102,7 @@ index 2d0e3d35..b05868fe 100644 if (container.type() == ContainerType.HUD && ((slot >= 28 && slot <= 31) || slot == 50)) { inventoryTracker.getInventoryContainer().refreshCraftingOutput(); -@@ -961,6 +977,8 @@ public final class InventoryPackets { +@@ -962,6 +978,8 @@ public final class InventoryPackets { } final Container container = inventoryTracker.getContainerServerbound((byte) containerId); @@ -111,7 +111,7 @@ index 2d0e3d35..b05868fe 100644 final var nativeContainer = wrapper.user().get(NativeContainerUiState.class); if (nativeContainer != null && nativeContainer.blocksVanillaClicks()) { if (container != null) { -@@ -968,10 +986,11 @@ public final class InventoryPackets { +@@ -969,10 +987,11 @@ public final class InventoryPackets { } return; } @@ -125,7 +125,7 @@ index 2d0e3d35..b05868fe 100644 final boolean requestOpen = !requestTracker.isWaitingForInventoryOpen(); if (!requestTracker.queueInventoryOpenClick( revision, slot, button, action)) { -@@ -994,13 +1013,15 @@ public final class InventoryPackets { +@@ -995,13 +1014,15 @@ public final class InventoryPackets { wrapper.cancel(); return; } @@ -143,7 +143,7 @@ index 2d0e3d35..b05868fe 100644 if (!container.handleClick(revision, slot, button, action)) { if (container.type() != ContainerType.INVENTORY) { PacketFactory.sendJavaContainerSetContent( -@@ -1065,7 +1086,10 @@ public final class InventoryPackets { +@@ -1066,7 +1087,10 @@ public final class InventoryPackets { return; } final InventoryRequestTracker requestTracker = wrapper.user().get(InventoryRequestTracker.class); @@ -155,7 +155,7 @@ index 2d0e3d35..b05868fe 100644 if (!requestTracker.queueButtonClick(container, button)) { PacketFactory.sendJavaContainerSetContent(wrapper.user(), container); } -@@ -1869,6 +1893,120 @@ public final class InventoryPackets { +@@ -1870,6 +1894,120 @@ public final class InventoryPackets { new InventoryTransactionData.NormalTransactionData()); } @@ -275,7 +275,7 @@ index 2d0e3d35..b05868fe 100644 + private static BedrockItem readCreativeItem(final PacketWrapper wrapper, final ItemRewriter itemRewriter) throws InformativeException { - return readCreativeItem(wrapper, itemRewriter.getItems().getOrDefault("minecraft:shield", 0)); + return readCreativeItem(wrapper, itemRewriter.getItems().getOrDefault("minecraft:shield", 0), itemRewriter); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java index f5e0d204..f5cbbaf0 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java @@ -572,10 +572,10 @@ index 9f149ccb..06d9fd00 100644 } diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java -index 7dccb3cc..a17e52fd 100644 +index 44b29c83..19fe63ab 100644 --- a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java -@@ -123,6 +123,72 @@ class InventoryPacketsTest { +@@ -170,6 +170,72 @@ class InventoryPacketsTest { BedrockTypes.BYTE_ARRAY.write(buffer, userData); } diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch index add5ba7b..cd5b148b 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch @@ -1,4 +1,4 @@ -From e73f3276eee431d2d20635f3ef0382c30f758a70 Mon Sep 17 00:00:00 2001 +From 6f2e3e079e6260d4be5060fe2e4fe16a9b7c837e 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): share bounded native UI programs and resource @@ -347,16 +347,40 @@ conversion execute; object property lookup and ABI backing are supplied. Fifty object-root controls ship as a regression fixture. Directional lighting, item geometry, material submission and pixels remain separate comparisons. Dynamic binding/program transport has its own regression. + +Resolve uniform collection grids through optional local view windows. +Retain the complete controller collection, its logical extent and every +original row index while instantiating only the requested range. Carry +fixed cell geometry and the stable grid path into the resolved scene so +the client can retain scroll extents and request newly visible rows. +Full expansion remains the default for other scene consumers. + +Reject row-dependent or unproven cell dimensions before applying a uniform +window. The 1.26.60.5 authored inventory grid supplies fixed 18 by 18 cells +with horizontal rescaling. Compiler regressions cover a 5,000-row source, +late ranges, replacement collections, program transport and off-window +conditional geometry. The actual pinned grid is exercised separately. +These tests preserve compilation and source identity; installed scrolling, +focus, actions and native pixels require consumer comparisons. + +A window can retain a second bounded range around logical keyboard focus. +Union overlapping ranges by original index, so scrolling does not recycle +the focused row into a different item or require an unbounded contiguous +span between focus and the viewport. Retained rows stay ordinary source +rows with the same bindings and action identities. + +Reject windowed templates that redirect a cell root to another collection +index, so viewport replacement cannot change the cell action identity. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + .../api/model/NativeContainerData.java | 70 + .../api/model/NativeContainerMessage.java | 112 ++ .../api/model/NativeContainerStatus.java | 60 + - .../api/model/NativeCookingAction.java | 96 ++ + .../api/model/NativeCookingAction.java | 96 + .../api/model/NativeCookingActionResult.java | 43 + .../api/model/NativeCookingControl.java | 73 + - .../api/model/NativeCookingMessage.java | 304 ++++ + .../api/model/NativeCookingMessage.java | 304 +++ .../api/model/NativeCookingProgress.java | 55 + .../api/model/NativeCookingStatus.java | 41 + .../api/model/NativeFormMessage.java | 56 + @@ -366,19 +390,19 @@ comparisons. Dynamic binding/program transport has its own regression. .../api/model/NativeNpcDialogueMessage.java | 60 + .../http/ConvertedResourcePackCache.java | 6 +- .../api/resourcepack/ui/BrewingStandUi.java | 44 + - .../api/resourcepack/ui/ContainerUi.java | 511 ++++++ + .../api/resourcepack/ui/ContainerUi.java | 511 +++++ .../resourcepack/ui/CookingNavigation.java | 83 + .../resourcepack/ui/CookingRecipeOrder.java | 62 + .../resourcepack/ui/CookingSearchText.java | 45 + .../api/resourcepack/ui/CookingUi.java | 433 +++++ - .../api/resourcepack/ui/FormBindings.java | 290 ++++ + .../api/resourcepack/ui/FormBindings.java | 290 +++ .../api/resourcepack/ui/HudActionbarUi.java | 20 + .../api/resourcepack/ui/HudFactoryUi.java | 132 ++ .../api/resourcepack/ui/HudRootUi.java | 35 + .../api/resourcepack/ui/HudTitleUi.java | 28 + .../resourcepack/ui/InventoryItemFilter.java | 43 + .../resourcepack/ui/InventoryStackCount.java | 21 + - .../api/resourcepack/ui/JsonUiProgram.java | 1514 +++++++++++++++++ + .../api/resourcepack/ui/JsonUiProgram.java | 1654 +++++++++++++++++ .../resourcepack/ui/NativeRecipeSearch.java | 124 ++ .../ui/NativeSearchNormalization.java | 189 ++ .../api/resourcepack/ui/NpcDialogueUi.java | 316 ++++ @@ -386,11 +410,11 @@ comparisons. Dynamic binding/program transport has its own regression. .../api/resourcepack/ui/NpcEditorState.java | 322 ++++ .../api/resourcepack/ui/RecipeCollection.java | 266 +++ .../api/resourcepack/ui/ServerFormUi.java | 253 +++ - .../resourcepack/ui/UiAsepriteMetadata.java | 95 ++ - .../api/resourcepack/ui/UiBindings.java | 43 + + .../resourcepack/ui/UiAsepriteMetadata.java | 95 + + .../api/resourcepack/ui/UiBindings.java | 79 + .../api/resourcepack/ui/UiBitmapFonts.java | 282 +++ - .../api/resourcepack/ui/UiExpression.java | 308 ++++ - .../api/resourcepack/ui/UiFonts.java | 286 ++++ + .../api/resourcepack/ui/UiExpression.java | 308 +++ + .../api/resourcepack/ui/UiFonts.java | 286 +++ .../api/resourcepack/ui/UiTextures.java | 94 + .../viabedrock/api/util/NativeSort.java | 168 ++ .../rewriter/ResourcePackRewriter.java | 2 + @@ -401,10 +425,10 @@ comparisons. Dynamic binding/program transport has its own regression. .../viabedrock/data/search_unicode_5_0.bin.gz | Bin 0 -> 45618 bytes .../api/model/NativeContainerActionTest.java | 88 + .../api/model/NativeContainerDataTest.java | 45 + - .../api/model/NativeContainerMessageTest.java | 98 ++ + .../api/model/NativeContainerMessageTest.java | 98 + .../model/NativeCookingActionResultTest.java | 63 + .../api/model/NativeCookingActionTest.java | 170 ++ - .../api/model/NativeCookingControlTest.java | 99 ++ + .../api/model/NativeCookingControlTest.java | 99 + .../api/model/NativeCookingMessageTest.java | 424 +++++ .../api/model/NativeCookingProgressTest.java | 38 + .../api/model/NativeFormMessageTest.java | 51 + @@ -413,16 +437,16 @@ comparisons. Dynamic binding/program transport has its own regression. .../api/model/NativeItemNamesTest.java | 123 ++ .../model/NativeNpcDialogueMessageTest.java | 59 + .../resourcepack/ui/BrewingStandUiTest.java | 49 + - .../api/resourcepack/ui/ContainerUiTest.java | 403 +++++ + .../api/resourcepack/ui/ContainerUiTest.java | 403 ++++ .../ui/CookingNavigationTest.java | 87 + .../resourcepack/ui/CookingProgressTest.java | 47 + .../ui/CookingRecipeOrderTest.java | 82 + .../ui/CookingSearchTextTest.java | 90 + .../api/resourcepack/ui/CookingUiTest.java | 646 +++++++ - .../ui/FormCollectionActivationTest.java | 97 ++ + .../ui/FormCollectionActivationTest.java | 97 + .../api/resourcepack/ui/HudRootUiTest.java | 90 + .../ui/InventoryItemFilterTest.java | 33 + - .../resourcepack/ui/JsonUiProgramTest.java | 588 +++++++ + .../resourcepack/ui/JsonUiProgramTest.java | 588 ++++++ .../ui/NativeRecipeSearchTest.java | 110 ++ .../ui/NativeSearchNormalizationTest.java | 46 + .../resourcepack/ui/NpcDialogueUiTest.java | 108 ++ @@ -432,13 +456,14 @@ comparisons. Dynamic binding/program transport has its own regression. .../ui/ServerFormUiSliderStateTest.java | 126 ++ .../api/resourcepack/ui/ServerFormUiTest.java | 427 +++++ .../ui/UiAsepriteMetadataTest.java | 112 ++ - .../resourcepack/ui/UiBitmapFontsTest.java | 285 ++++ + .../resourcepack/ui/UiBitmapFontsTest.java | 285 +++ + .../ui/UiCollectionWindowTest.java | 131 ++ .../api/resourcepack/ui/UiExpressionTest.java | 68 + - .../api/resourcepack/ui/UiFontsTest.java | 192 +++ + .../api/resourcepack/ui/UiFontsTest.java | 192 ++ .../api/resourcepack/ui/UiTexturesTest.java | 175 ++ .../ui/native-inventory-item-filter.json | 311 ++++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 90 files changed, 14037 insertions(+), 60 deletions(-) + 91 files changed, 14344 insertions(+), 60 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerMessage.java @@ -519,6 +544,7 @@ comparisons. Dynamic binding/program transport has its own regression. create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ServerFormUiTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiAsepriteMetadataTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiBitmapFontsTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiCollectionWindowTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpressionTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiFontsTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTexturesTest.java @@ -3791,10 +3817,10 @@ index 00000000..5dfbb3f8 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java new file mode 100644 -index 00000000..d19a0f46 +index 00000000..acb78beb --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java -@@ -0,0 +1,1514 @@ +@@ -0,0 +1,1654 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -3828,6 +3854,7 @@ index 00000000..d19a0f46 +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.IntStream; ++import java.util.regex.Pattern; +import java.util.zip.ZipInputStream; +import net.raphimc.viabedrock.api.resourcepack.ResourcePack; +import net.raphimc.viabedrock.api.util.TextUtil; @@ -3839,6 +3866,7 @@ index 00000000..d19a0f46 + public static final int MAX_NODES = 8192; + public static final int MAX_PAYLOAD_BYTES = 1 << 20; + private static final int MAX_FILE_BYTES = 1 << 20; ++ private static final Pattern BINDING_REFERENCE = Pattern.compile("[#$][A-Za-z_][A-Za-z0-9_]*"); + private static final Set HOST_FILES = Set.of("ui/server_form.json", "ui/chest_screen.json", "ui/redstone_screen.json", "ui/inventory_screen.json", "ui/npc_interact_screen.json", "ui/hud_screen.json", "ui/ui_common.json", "ui/_global_variables.json"); + private final Map definitions = new LinkedHashMap<>(); + private final Map nativeDefinitions = new LinkedHashMap<>(); @@ -4745,6 +4773,7 @@ index 00000000..d19a0f46 + } + applyModifications(control); + final Map values = new HashMap<>(inherited); ++ values.put("$__instance_path", new JsonPrimitive(UiExpression.text(inherited.get("$__instance_path")) + "/" + name)); + final Set paths = new LinkedHashSet<>(templatePaths); + paths.addAll(instancePaths); + if (paths.size() > MAX_NODES) { @@ -4968,7 +4997,7 @@ index 00000000..d19a0f46 + this.factory(control, node, children, values, selectedScope, depth); + } + if ("grid".equals(string(control, "type")) && control.has("grid_item_template")) { -+ this.collection(UiExpression.text(resolveValue(control.get("grid_item_template"), values, 0)), children, values, selectedScope, string(node, "collection_name"), depth); ++ this.gridCollection(UiExpression.text(resolveValue(control.get("grid_item_template"), values, 0)), node, children, values, selectedScope, depth); + } + node.add("children", children); + return node; @@ -5162,6 +5191,143 @@ index 00000000..d19a0f46 + this.collection(template, children, values, scope, collection, depth, null); + } + ++ private void gridCollection(final String template, final JsonObject node, final JsonArray children, ++ final Map values, final UiBindings.Scope scope, final int depth) { ++ final String declaredCollection = string(node, "collection_name"); ++ final String collection = declaredCollection.isEmpty() ? this.bindings.defaultCollection() : declaredCollection; ++ final String key = UiExpression.text(values.get("$__instance_path")); ++ final UiBindings.Window window = this.bindings.window(key, collection, this.controllerScope(scope)); ++ if (window == null) { ++ this.collection(template, children, values, scope, collection, depth); ++ return; ++ } ++ final List rows = this.collectionScopes(collection, scope, null); ++ if (rows.isEmpty()) { ++ return; ++ } ++ if (!"horizontal".equals(string(node, "grid_rescaling_type")) || node.has("grid_dimensions") ++ || "vertical".equals(string(node, "grid_fill_direction")) || !children.isEmpty()) { ++ throw new IllegalArgumentException("Unsupported windowed UI grid geometry"); ++ } ++ final Set rowBindings = new HashSet<>(); ++ for (final var entry : this.collection(collection, scope).entries()) { ++ rowBindings.addAll(entry.keySet()); ++ } ++ rowBindings.add("#collection_index"); ++ final JsonObject cell = JsonUiProgram.this.template(template, this.requiredDefinitions); ++ if (cell == null) { ++ throw new MissingControlException(template); ++ } ++ final Map dimensions = new HashMap<>(values); ++ declarations(cell, dimensions); ++ for (final String property : List.of("size", "min_size", "max_size", "offset")) { ++ if (this.rowDependent(cell.get(property), cell, values, rowBindings, new HashSet<>())) { ++ throw new IllegalArgumentException("Windowed UI grid has row-dependent cell geometry"); ++ } ++ } ++ final JsonElement size = resolveValue(cell.get("size"), dimensions, 0); ++ if (!fixedPair(size, true) || cell.has("min_size") || cell.has("max_size") ++ || cell.has("offset") && !zeroPair(resolveValue(cell.get("offset"), dimensions, 0))) { ++ throw new IllegalArgumentException("Windowed UI grid requires fixed cell dimensions"); ++ } ++ final int start = Math.min(window.start(), Math.max(0, rows.size() - 1)); ++ final int end = Math.min(rows.size(), start + window.count()); ++ node.addProperty("collection_window_key", key); ++ node.addProperty("collection_size", rows.size()); ++ node.addProperty("collection_start", start); ++ node.addProperty("collection_count", end - start); ++ if (window.retainedCount() > 0) { ++ final int retained = Math.min(window.retainedStart(), rows.size() - 1); ++ node.addProperty("collection_retained_start", retained); ++ node.addProperty("collection_retained_count", Math.min(window.retainedCount(), rows.size() - retained)); ++ } ++ node.add("collection_cell_size", size); ++ for (final int index : window.indices(rows.size())) { ++ final var childScope = rows.get(index); ++ final Map context = new HashMap<>(values); ++ context.put("#collection_index", new JsonPrimitive(childScope.index())); ++ final JsonObject child = this.expandChild(template, new JsonObject(), context, childScope, null, depth + 1); ++ if (child == null || !child.get("size").equals(size)) { ++ throw new IllegalArgumentException("Windowed UI grid cell geometry changed"); ++ } ++ if (child.has("collection_index") && child.get("collection_index").getAsInt() != childScope.index()) { ++ throw new IllegalArgumentException("Windowed UI grid redirects its cell collection index"); ++ } ++ child.addProperty("collection_index", childScope.index()); ++ children.add(child); ++ } ++ } ++ ++ private static boolean fixedPair(final JsonElement value, final boolean positive) { ++ if (value == null || !value.isJsonArray() || value.getAsJsonArray().size() != 2) { ++ return false; ++ } ++ for (final var axis : value.getAsJsonArray()) { ++ if (!axis.isJsonPrimitive() || !axis.getAsJsonPrimitive().isNumber()) { ++ return false; ++ } ++ final double number = axis.getAsDouble(); ++ if (!Double.isFinite(number) || (positive ? (float) number <= 0 : number < 0) || number > 16384) { ++ return false; ++ } ++ } ++ return true; ++ } ++ ++ private static boolean zeroPair(final JsonElement value) { ++ return fixedPair(value, false) && value.getAsJsonArray().asList().stream().allMatch(axis -> axis.getAsDouble() == 0); ++ } ++ ++ private boolean rowDependent(final JsonElement value, final JsonObject control, final Map inherited, ++ final Set rowBindings, final Set visited) { ++ if (value == null || value.isJsonNull()) { ++ return false; ++ } ++ if (value.isJsonArray()) { ++ for (final var child : value.getAsJsonArray()) { ++ if (this.rowDependent(child, control, inherited, rowBindings, visited)) { ++ return true; ++ } ++ } ++ return false; ++ } ++ if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) { ++ return false; ++ } ++ final String expression = value.getAsString(); ++ final var names = BINDING_REFERENCE.matcher(expression); ++ while (names.find()) { ++ final String name = names.group(); ++ if (rowBindings.contains(name)) { ++ return true; ++ } ++ if (!name.startsWith("$") || !visited.add(name)) { ++ continue; ++ } ++ final JsonElement declaration = control.has(name) ? control.get(name) ++ : control.has(name + "|default") ? control.get(name + "|default") : inherited.get(name); ++ if (this.rowDependent(declaration, control, inherited, rowBindings, visited)) { ++ return true; ++ } ++ if (control.has("variables")) { ++ final JsonElement variants = control.get("variables"); ++ final List sets = variants.isJsonArray() ? variants.getAsJsonArray().asList() : List.of(variants); ++ for (final var variant : sets) { ++ if (!variant.isJsonObject()) { ++ continue; ++ } ++ final JsonObject variables = variant.getAsJsonObject(); ++ if ((variables.has(name) || variables.has(name + "|default")) ++ && (this.rowDependent(variables.has(name) ? variables.get(name) : variables.get(name + "|default"), control, inherited, rowBindings, visited) ++ || this.rowDependent(variables.get("requires"), control, inherited, rowBindings, visited))) { ++ return true; ++ } ++ } ++ } ++ } ++ return false; ++ } ++ + private static int collectionCount(final JsonElement value) { + if (value.isJsonArray()) { + return value.getAsJsonArray().size(); @@ -7253,10 +7419,10 @@ index 00000000..9c8d204f +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiBindings.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiBindings.java new file mode 100644 -index 00000000..ded11b88 +index 00000000..03dd0797 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiBindings.java -@@ -0,0 +1,43 @@ +@@ -0,0 +1,79 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -7268,6 +7434,7 @@ index 00000000..ded11b88 +import com.viaversion.viaversion.libs.gson.JsonObject; +import java.util.List; +import java.util.Map; ++import java.util.stream.IntStream; + +/** Controller data and semantic control annotations for one JSON UI resolution. */ +public interface UiBindings { @@ -7281,6 +7448,11 @@ index 00000000..ded11b88 + + Collection collection(String name, Scope parent); + ++ /** Null retains full expansion; a window changes instantiated views, not collection identity or length. */ ++ default Window window(final String key, final String collection, final Scope parent) { ++ return null; ++ } ++ + Map variables(Scope scope); + + Map bindings(Scope scope); @@ -7291,6 +7463,36 @@ index 00000000..ded11b88 + + record Scope(String collection, int index, Scope parent) { } + ++ record Window(int start, int count, int retainedStart, int retainedCount) { ++ public Window(final int start, final int count) { ++ this(start, count, 0, 0); ++ } ++ ++ public Window { ++ if (start < 0 || start > JsonUiProgram.MAX_NODES || count < 1 || count > JsonUiProgram.MAX_NODES ++ || retainedStart < 0 || retainedStart > JsonUiProgram.MAX_NODES || retainedCount < 0 || retainedCount > JsonUiProgram.MAX_NODES ++ || retainedCount == 0 && retainedStart != 0) { ++ throw new IllegalArgumentException("Invalid UI collection window"); ++ } ++ } ++ ++ public List indices(final int size) { ++ if (size < 0 || size > JsonUiProgram.MAX_NODES) { ++ throw new IllegalArgumentException("Invalid UI collection size"); ++ } ++ return IntStream.concat(range(this.start, this.count, size), range(this.retainedStart, this.retainedCount, size)) ++ .distinct().sorted().boxed().toList(); ++ } ++ ++ private static IntStream range(final int start, final int count, final int size) { ++ if (size == 0 || count == 0) { ++ return IntStream.empty(); ++ } ++ final int first = Math.min(start, size - 1); ++ return IntStream.range(first, Math.min(size, first + count)); ++ } ++ } ++ + record Collection(List> entries, Map details) { + + public Collection { @@ -15125,6 +15327,143 @@ index 00000000..11028d9b + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiCollectionWindowTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiCollectionWindowTest.java +new file mode 100644 +index 00000000..70718538 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiCollectionWindowTest.java +@@ -0,0 +1,131 @@ ++/* ++ * 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.JsonElement; ++import com.viaversion.viaversion.libs.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonPrimitive; ++import org.junit.jupiter.api.Test; ++import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; ++ ++import java.nio.charset.StandardCharsets; ++import java.util.List; ++import java.util.Map; ++import java.util.stream.IntStream; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class UiCollectionWindowTest { ++ ++ private static final String UI = """ ++ {"namespace":"example","root":{"type":"grid","size":[72,"default"], ++ "collection_name":"items","grid_rescaling_type":"horizontal","grid_item_template":"example.item"}, ++ "item":{"type":"panel","size":"$cell_size","$cell_size|default":[18,18],"controls":[ ++ {"caption":{"type":"label","text":"#value"}}, {"icon":{"type":"image","size":[16,16]}}]}} ++ """; ++ ++ @Test ++ void aLargeCollectionKeepsItsFullExtentAndOriginalRowBindingsAcrossWindows() { ++ final var program = program(UI); ++ assertThrows(IllegalArgumentException.class, () -> program.resolve("example.root", new Rows(5000, null))); ++ final var resolved = program.resolve("example.root", new Rows(5000, new UiBindings.Window(4500, 6))); ++ final var root = resolved.root(); ++ assertEquals(5000, root.get("collection_size").getAsInt()); ++ final var children = root.getAsJsonArray("children"); ++ assertEquals(List.of(4500, 4501, 4502, 4503, 4504, 4505), children.asList().stream() ++ .map(child -> child.getAsJsonObject().get("collection_index").getAsInt()).toList()); ++ assertEquals("4500", children.get(0).getAsJsonObject().getAsJsonArray("children").get(0).getAsJsonObject().get("text").getAsString()); ++ final var imported = JsonUiProgram.fromProgram(program.export(resolved.requiredDefinitions(), new JsonObject())); ++ assertEquals(root, imported.resolve("example.root", new Rows(5000, new UiBindings.Window(4500, 6))).root()); ++ final var retained = imported.resolve("example.root", new Rows(5000, new UiBindings.Window(4500, 6, 0, 8))).root(); ++ assertEquals(List.of(0, 1, 2, 3, 4, 5, 6, 7, 4500, 4501, 4502, 4503, 4504, 4505), retained.getAsJsonArray("children").asList().stream() ++ .map(child -> child.getAsJsonObject().get("collection_index").getAsInt()).toList()); ++ final var replaced = imported.resolve("example.root", new Rows(3, new UiBindings.Window(4500, 6))).root(); ++ assertEquals(3, replaced.get("collection_size").getAsInt()); ++ assertEquals(2, replaced.getAsJsonArray("children").get(0).getAsJsonObject().get("collection_index").getAsInt()); ++ } ++ ++ @Test ++ void rowDependentCellsCannotUseUniformGridWindows() { ++ final var source = UI.replace("\"$cell_size|default\":[18,18]", "\"$cell_size|default\":[\"(#value + 1)\",18]"); ++ assertThrows(IllegalArgumentException.class, ++ () -> program(source).resolve("example.root", new Rows(3, new UiBindings.Window(0, 1)))); ++ final var conditional = UI.replace("\"$cell_size|default\":[18,18]", """ ++ "$cell_size|default":[18,18],"variables":{"requires":"(#value > 10)","$cell_size":[20,18]} ++ """.strip()); ++ assertThrows(IllegalArgumentException.class, ++ () -> program(conditional).resolve("example.root", new Rows(40, new UiBindings.Window(0, 1)))); ++ } ++ ++ @Test ++ void windowingCannotRedirectCellsToAnotherSourceRow() { ++ final var source = UI.replace("\"item\":{\"type\":\"panel\"", "\"item\":{\"collection_index\":0,\"type\":\"panel\""); ++ assertThrows(IllegalArgumentException.class, ++ () -> program(source).resolve("example.root", new Rows(3, new UiBindings.Window(1, 1)))); ++ } ++ ++ @Test ++ void theAuthoredInventoryGridRetainsItsFixedCellGeometryWhileWindowed() { ++ final var program = new ResourcePackStorage(List.of()).getJsonUiProgram(); ++ final var root = program.resolve("crafting.scroll_grid", new Rows(2000, new UiBindings.Window(1700, 6))).root(); ++ assertEquals(2000, root.get("collection_size").getAsInt()); ++ final var children = root.getAsJsonArray("children"); ++ assertEquals(6, children.size()); ++ assertEquals(1700, children.get(0).getAsJsonObject().get("collection_index").getAsInt()); ++ assertEquals(18, root.getAsJsonArray("collection_cell_size").get(0).getAsInt()); ++ assertEquals(18, root.getAsJsonArray("collection_cell_size").get(1).getAsInt()); ++ } ++ ++ private static JsonUiProgram program(final String source) { ++ return new JsonUiProgram(List.of(Map.of("ui/example.json", source.getBytes(StandardCharsets.UTF_8)))); ++ } ++ ++ private record Rows(int count, Window selected) implements UiBindings { ++ @Override ++ public Map globals() { ++ return Map.of("$collection_name", new JsonPrimitive("items")); ++ } ++ ++ @Override ++ public String defaultCollection() { ++ return "items"; ++ } ++ ++ @Override ++ public boolean ownsCollection(final String name) { ++ return name.equals("items"); ++ } ++ ++ @Override ++ public Collection collection(final String name, final Scope parent) { ++ return new Collection(IntStream.range(0, this.count).mapToObj(index -> Map.of("#value", new JsonPrimitive(index))).toList(), Map.of()); ++ } ++ ++ @Override ++ public Window window(final String key, final String collection, final Scope parent) { ++ return this.selected; ++ } ++ ++ @Override ++ public Map variables(final Scope scope) { ++ return scope == null ? Map.of() : Map.of("#value", new JsonPrimitive(scope.index())); ++ } ++ ++ @Override ++ public Map bindings(final Scope scope) { ++ return this.variables(scope); ++ } ++ ++ @Override ++ public String factoryKey() { ++ return ""; ++ } ++ ++ @Override ++ public void decorate(final JsonObject node, final JsonObject control, final Map resolved, final Scope scope) { } ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpressionTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpressionTest.java new file mode 100644 index 00000000..f43ac37d diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index e7d7564d..03974ba8 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -26,6 +26,12 @@ Current 1.26.60.5 instruction controls cover 150 typed property combinations and The packaged regression retains 50 object-property cases, including booleans, numeric extremes, and invalid types. These controls supply property lookup and ABI storage. Complete item geometry, materials, and final pixels remain unverified. +`UiBindings.Window` selects cell views from a complete collection without changing its size or source indices. +A separate retained range supports offscreen keyboard focus and its neighbors. +Only fixed horizontal grids qualify. Row-dependent dimensions and redirected root indices are rejected. +Regression coverage compares complete and windowed sources, exported programs, source replacement, and the pinned inventory grid. +The scene node budget remains unchanged. + Program compilation, resource, and transport tests cover supported content. Transport acceptance does not establish screen interaction or visual parity. Dynamic domain text uses the same native palette as ordinary labels. 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 d8cf7e44..6e44891f 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 04b3540ddc6a35959c70d2c160ea5882e655598a Mon Sep 17 00:00:00 2001 +From fd96c68c8fe2aa3322774497e0db78b08f56fa31 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 @@ -523,6 +523,31 @@ not establish native item pixels or interactive bundle-tooltip parity. Until the tooltip collection, placement and selection renderer is ported, hovering or carrying storage items explicitly restores ordinary controls before an incomplete native tooltip can silently consume its input. + +Resolve fixed recipe grids through bounded cell windows over the complete +retained catalog. Converge the window after measuring the authored scroll +viewport and retain neighboring rows around logical keyboard focus. Resize +and scroll changes keep original collection indices, action identities, +menu state, and the full logical scroll extent. Local window changes do +not advance the authoritative snapshot revision. + +Copy only instantiated previews into each view frame while retaining the +complete preview cache. Rebuild windows after keyboard delivery so focus +callbacks do not replace component state during nested event dispatch. +Passive source-less hover bindings are not recipe action transitions and +must not retire the screen during directional navigation. Actual +unsupported recipe commands continue to restore ordinary controls. + +The fixed 18 by 18 cell contract follows crafting.scroll_grid and its +recipe-book template in the pinned 1.26.60.5 inventory_screen.json. This +preserves authored geometry without claiming identical native pooling. + +The pinned Creative layout binds hotbar auto-place to auto-destroy. Keep +that declared command in the explicit unsupported-host-action path so a +closed catalog can render without authorizing destruction. Both mapped +host commands and Java quick-move on those authored hotbar slots restore +ordinary controls before changing items or sending a request. Test both +recipe-book and Creative layouts, including the refused destruction path. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -535,7 +560,7 @@ before an incomplete native tooltip can silently consume its input. .../render/MixinNativeGuiItemState.java | 20 + .../render/MixinNativeGuiOversizedItem.java | 31 + .../container/BedrockFurnacePreferences.java | 60 + - .../BedrockNativeContainerScreen.java | 1216 ++++++++++++++ + .../BedrockNativeContainerScreen.java | 1242 +++++++++++++++ .../container/BedrockNativeContainers.java | 278 ++++ .../container/BedrockNativeCooking.java | 145 ++ .../BedrockNativePlayerInventory.java | 155 ++ @@ -544,7 +569,7 @@ before an incomplete native tooltip can silently consume its input. .../NativeContainerCurrentPayload.java | 30 + .../container/NativeContainerDataPayload.java | 30 + .../NativeContainerFamiliesPayload.java | 30 + - .../container/NativeContainerGeometry.java | 256 +++ + .../container/NativeContainerGeometry.java | 258 +++ .../container/NativeContainerInput.java | 501 ++++++ .../container/NativeContainerPayload.java | 22 + .../container/NativeContainerPending.java | 65 + @@ -574,7 +599,7 @@ before an incomplete native tooltip can silently consume its input. .../NativePlayerInventoryActionPayload.java | 22 + .../NativePlayerInventoryPayload.java | 22 + .../NativePlayerInventoryResultPayload.java | 22 + - .../container/NativePlayerPresentation.java | 180 +++ + .../container/NativePlayerPresentation.java | 195 +++ .../PlayerInventoryScreenClosePayload.java | 22 + .../PlayerInventoryScreenLifecycle.java | 74 + .../PlayerInventoryScreenRequestPayload.java | 22 + @@ -597,9 +622,9 @@ before an incomplete native tooltip can silently consume its input. .../NativeInventoryProgressTest.java | 83 + .../container/NativePlayerHelpTextTest.java | 34 + .../NativePlayerPresentationTest.java | 153 ++ - .../container/NativePlayerScreenTest.java | 168 ++ + .../container/NativePlayerScreenTest.java | 269 ++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 75 files changed, 9953 insertions(+), 62 deletions(-) + 75 files changed, 10097 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 @@ -1104,10 +1129,10 @@ index 0000000..1583ce0 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java new file mode 100644 -index 0000000..46282d3 +index 0000000..d5ea53e --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java -@@ -0,0 +1,1216 @@ +@@ -0,0 +1,1242 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1119,6 +1144,7 @@ index 0000000..46282d3 +import com.viaversion.viafabricplus.bedrock.ViaFabricPlusBedrock; +import com.viaversion.viafabricplus.bedrock.injection.access.IBedrockGuiItemStyle; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiAnimationLifecycle; ++import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiCollectionWindow; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiImages; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiInput; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiToggle; @@ -1327,6 +1353,48 @@ index 0000000..46282d3 + NativeInventoryItemPresentation.requireTooltip(this.menu.getCarried()); + final List slots = new ArrayList<>(); + for (final Slot slot : this.menu.slots) slots.add(new ContainerUi.Slot(slot.index, NativeInventoryItemPresentation.values(slot.getItem(), false, this.showTags))); ++ for (int pass = 0; ; pass++) { ++ this.refreshScene(slots); ++ if (!this.updateWindows()) break; ++ if (pass >= 2) throw new IllegalArgumentException("Native inventory collection windows did not converge"); ++ } ++ for (final var draw : this.draws) { ++ final var node = draw.box().node(); ++ if (this.presentation instanceof NativeCookingPresentation ++ && (Set.of("edit_box", "slider", "slider_box").contains(node.type()) ++ || !Set.of("button", "toggle").contains(node.type()) && node.properties().has("button_mappings") ++ && !NativeCookingNavigation.host(node))) ++ throw new IllegalArgumentException("Unsupported native cooking control"); ++ if (this.authoredController()) { ++ for (final var mapping : NativeUiInput.parse(node.properties()).mappings()) { ++ if (mapping.source() == NativeUiInput.symbol("button.menu_ok") && !this.supportsKeyboardInput(node, mapping)) ++ throw new IllegalArgumentException("Unsupported native inventory keyboard action on " + node.name() + ": " + mapping.target()); ++ } ++ } ++ final String custom = node.string("renderer", ""); ++ if (custom.equals("bundle_tooltip_renderer") && node.children().stream() ++ .anyMatch(child -> child.flag("visible", true) && !child.flag("ignored", false))) ++ throw new IllegalArgumentException("Native bundle cursor tooltip contents are not supported"); ++ if (custom.equals("progress_bar_renderer")) ++ NativeInventoryProgress.quads(draw.box(), draw.alpha(), 1); ++ } ++ final var current = this.menu.slots.stream().map(slot -> slot.getItem().copy()).toList(); ++ if (!this.previous.isEmpty()) this.flights.update(this.previous, current, this.anchors, this.clock.getAsLong()); ++ this.previous = this.menu.slots.stream().map(slot -> slot.getItem().copy()).toList(); ++ this.previousCarried = this.menu.getCarried().copy(); ++ this.previousPresentationRevision = this.presentation.revision(); ++ this.imageRevision = this.images.revision(); ++ if (this.activated && !this.actionScope.subset()) ++ throw new IllegalArgumentException("Native inventory action scope changed"); ++ if (this.authoredController()) this.nativeInput.initialize(); ++ } ++ ++ private boolean updateWindows() { ++ return this.presentation instanceof NativePlayerPresentation player && !player.helping() ++ && player.windows(NativeUiCollectionWindow.visible(this.root, this.nativeInput.focused())); ++ } ++ ++ private void refreshScene(final List slots) { + final var frame = this.presentation.resolve(this.menu.containerId, this.title.getString(), slots, + NativeInventoryItemPresentation.values(this.menu.getCarried(), true, this.showTags), this.showTags); + final var resolved = frame.scene(); @@ -1368,35 +1436,6 @@ index 0000000..46282d3 + this.lifecycle.animations().event(this.scene, "screen.entrance_push", 0); + } else this.lifecycle.replaceRoot(this.scene, this.animationTime()); + this.layout(this.animationTime(), -1, -1); -+ for (final var draw : this.draws) { -+ final var node = draw.box().node(); -+ if (this.presentation instanceof NativeCookingPresentation -+ && (Set.of("edit_box", "slider", "slider_box").contains(node.type()) -+ || !Set.of("button", "toggle").contains(node.type()) && node.properties().has("button_mappings") -+ && !NativeCookingNavigation.host(node))) -+ throw new IllegalArgumentException("Unsupported native cooking control"); -+ if (this.authoredController()) { -+ for (final var mapping : NativeUiInput.parse(node.properties()).mappings()) { -+ if (mapping.source() == NativeUiInput.symbol("button.menu_ok") && !this.supportsKeyboardInput(node, mapping)) -+ throw new IllegalArgumentException("Unsupported native inventory keyboard action on " + node.name() + ": " + mapping.target()); -+ } -+ } -+ final String custom = node.string("renderer", ""); -+ if (custom.equals("bundle_tooltip_renderer") && node.children().stream() -+ .anyMatch(child -> child.flag("visible", true) && !child.flag("ignored", false))) -+ throw new IllegalArgumentException("Native bundle cursor tooltip contents are not supported"); -+ if (custom.equals("progress_bar_renderer")) -+ NativeInventoryProgress.quads(draw.box(), draw.alpha(), 1); -+ } -+ final var current = this.menu.slots.stream().map(slot -> slot.getItem().copy()).toList(); -+ if (!this.previous.isEmpty()) this.flights.update(this.previous, current, this.anchors, this.clock.getAsLong()); -+ this.previous = this.menu.slots.stream().map(slot -> slot.getItem().copy()).toList(); -+ this.previousCarried = this.menu.getCarried().copy(); -+ this.previousPresentationRevision = this.presentation.revision(); -+ this.imageRevision = this.images.revision(); -+ if (this.activated && !this.actionScope.subset()) -+ throw new IllegalArgumentException("Native inventory action scope changed"); -+ if (this.authoredController()) this.nativeInput.initialize(); + } + + private long animationTime() { return this.clock.getAsLong() - this.animationStarted; } @@ -1553,7 +1592,7 @@ index 0000000..46282d3 + handled = true; + } else if (event.button() != null && event.button().transition() + && playerUnsupportedHostAction(event.button().target())) { -+ this.fallback(new IllegalArgumentException("Native held-selection input is not supported")); ++ this.fallback(new IllegalArgumentException("Native inventory host action is not supported")); + handled = true; + } else if (event.button() != null && event.button().target() == NativeUiInput.symbol("button.container_slot_hovered")) { + if (event.button().pressed()) player.select(event.node().string("collection", ""), event.collectionIndex()); @@ -1713,7 +1752,10 @@ index 0000000..46282d3 + final long now = this.animationTime(); + this.lifecycle.advance(now); + if (this.lifecycle.animations().layoutRevision() != this.layoutRevision -+ || this.nativeInput.visualRevision() != this.nativeInputRevision) this.layout(now, mouseX, mouseY); ++ || this.nativeInput.visualRevision() != this.nativeInputRevision) { ++ this.layout(now, mouseX, mouseY); ++ } ++ if (this.updateWindows()) this.refresh(); + this.draws = NativeUiPresentation.draws(this.root, box -> box.visibleBounds().contains(mouseX, mouseY), + node -> this.lifecycle.animations().opacityAt(node, now), this.nativeInput::hasButtonState); + if (this.authoredController()) this.nativeInput.layout(this.root, this.draws, this.width, this.height); @@ -1864,6 +1906,14 @@ index 0000000..46282d3 + protected void slotClicked(final Slot slot, final int index, final int button, final ContainerInput input) { + if (!this.current()) return; + if (this.actionScope.subset()) return; ++ if (this.presentation instanceof NativePlayerPresentation && input == ContainerInput.QUICK_MOVE ++ && this.draws.stream().map(draw -> draw.box().node()).anyMatch(node -> node.properties().has("inventory_slot") ++ && node.properties().get("inventory_slot").getAsInt() == index ++ && NativeUiInput.parse(node.properties()).mappings().stream() ++ .anyMatch(mapping -> mapping.target() == NativeUiInput.symbol("button.container_auto_destroy")))) { ++ this.fallback(new IllegalArgumentException("Native Creative hotbar destruction is not supported")); ++ return; ++ } + super.slotClicked(slot, index, button, input); + this.refreshSafely(); + } @@ -2148,11 +2198,11 @@ index 0000000..46282d3 + final int recipe = node.properties().get("recipe_id").getAsInt(); + return this.recipeActions.getOrDefault(recipe, Set.of()).stream().anyMatch(action -> NativeUiInput.symbol(action) == target); + } ++ if (this.presentation instanceof NativePlayerPresentation && playerUnsupportedHostAction(target)) return true; + if (node.properties().has("inventory_slot")) + return this.presentation instanceof NativePlayerPresentation && playerSlotInput(target) != null + || this.nativeActions && this.actionScope.subset() && target == NativeUiInput.symbol("button.container_auto_place_one"); + if (this.presentation instanceof NativePlayerPresentation && target == NativeUiInput.symbol("button.help")) return true; -+ if (this.presentation instanceof NativePlayerPresentation && playerUnsupportedHostAction(target)) return true; + if (this.presentation instanceof NativePlayerPresentation && cursorDropButton(target) >= 0) return true; + if (this.presentation instanceof NativePlayerPresentation && node.type().equals("edit_box") + && target == NativeUiInput.symbol("button.search_bar_selected")) return true; @@ -2176,7 +2226,7 @@ index 0000000..46282d3 + private record SlotInput(ContainerInput input, int button) {} + + private static boolean playerUnsupportedHostAction(final int target) { -+ return target == NativeUiInput.symbol("button.container_reset_held") || target == NativeUiInput.symbol("button.destroy_selection"); ++ return NativeContainerGeometry.PLAYER_FALLBACK_ACTIONS.stream().anyMatch(action -> NativeUiInput.symbol(action) == target); + } + + private static int cursorDropButton(final int target) { @@ -2195,7 +2245,7 @@ index 0000000..46282d3 + final int recipe = node.properties().get("recipe_id").getAsInt(); + final var mode = java.util.Arrays.stream(NativeCookingAction.Mode.values()) + .filter(candidate -> NativeUiInput.symbol(candidate.button()) == button.target()).findFirst().orElse(null); -+ if (mode == null && this.presentation instanceof NativePlayerPresentation && NativeContainerGeometry.RECIPE_AUXILIARY_ACTIONS.stream() ++ if (mode == null && button.transition() && this.presentation instanceof NativePlayerPresentation && NativeContainerGeometry.RECIPE_AUXILIARY_ACTIONS.stream() + .anyMatch(action -> NativeUiInput.symbol(action) == button.target())) { + this.fallback(new IllegalArgumentException("Native recipe auxiliary input is not supported")); + handled = true; @@ -2242,6 +2292,7 @@ index 0000000..46282d3 + final var previous = this.nativeInput.focused(); + final var result = this.nativeInput.button(source, pressed, 1, this.clock.getAsLong(), this.componentEnabled()); + final boolean handled = !this.waitingForPreference() && this.keyboardEvents(result); ++ if (this.updateWindows()) this.refreshSafely(); + return handled || previous != this.nativeInput.focused() || result.consumed(); + } + @@ -3164,10 +3215,10 @@ index 0000000..3387649 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometry.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometry.java new file mode 100644 -index 0000000..cf198d5 +index 0000000..e0257f3 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometry.java -@@ -0,0 +1,256 @@ +@@ -0,0 +1,258 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3191,6 +3242,7 @@ index 0000000..cf198d5 + +/** Authored, clipped controls share one front-to-back policy for slot and command input. */ +final class NativeContainerGeometry { ++ static final Set PLAYER_FALLBACK_ACTIONS = Set.of("button.container_reset_held", "button.destroy_selection", "button.container_auto_destroy"); + static final Set RECIPE_AUXILIARY_ACTIONS = Set.of("button.drop_one", "button.drop_all", "button.shape_drawing"); + private static final Set RECIPE_MAPPING_FIELDS = Set.of("from_button_id", "to_button_id", "mapping_type", "ignored", "consume_event"); + private final List controls; @@ -3326,6 +3378,7 @@ index 0000000..cf198d5 + if (recipe != null) recipe.inputs.values().forEach(mode -> supported.add(mode.button())); + if (recipe != null) supported.add("button.container_slot_hovered"); + if (recipe != null && player) supported.addAll(RECIPE_AUXILIARY_ACTIONS); ++ if (player) supported.addAll(PLAYER_FALLBACK_ACTIONS); + if (player) supported.addAll(Set.of("button.help", "button.search_bar_clear", "button.left_panel_tab_increment", "button.left_panel_tab_decrement")); + if (node.properties().has("inventory_actions")) { + for (final var action : node.properties().getAsJsonArray("inventory_actions")) @@ -5764,10 +5817,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..05949f7 +index 0000000..f83d636 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerPresentation.java -@@ -0,0 +1,180 @@ +@@ -0,0 +1,195 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5791,6 +5844,7 @@ index 0000000..05949f7 +import net.raphimc.viabedrock.api.model.NativePlayerInventoryResult; +import net.raphimc.viabedrock.api.resourcepack.ui.ContainerUi; +import net.raphimc.viabedrock.api.resourcepack.ui.PlayerInventoryUi; ++import net.raphimc.viabedrock.api.resourcepack.ui.UiBindings; +import net.raphimc.viabedrock.protocol.model.InventoryOptions; + +/** One installed menu retains its actual catalog and serializes edits against acknowledged snapshots. */ @@ -5800,6 +5854,7 @@ index 0000000..05949f7 + private PlayerInventoryUi program; + private PlayerInventoryUi.State state; + private Map previews; ++ private Map windows = Map.of(); + private long revision; + private long sequence; + private NativePlayerInventoryAction pending; @@ -5845,6 +5900,13 @@ index 0000000..05949f7 + String query() { return this.queuedQuery != null ? this.queuedQuery : this.pending != null && this.pending.kind() == NativePlayerInventoryAction.Kind.SEARCH + ? this.pending.query() : this.state.query(); } + ++ boolean windows(final Map windows) { ++ final var replacement = Map.copyOf(windows); ++ if (this.windows.equals(replacement)) return false; ++ this.windows = replacement; ++ return true; ++ } ++ + boolean helping() { return this.helpTopic >= 0; } + + void openHelp() { @@ -5932,20 +5994,26 @@ index 0000000..05949f7 + }).toList(); + final var state = new PlayerInventoryUi.State(source.workbench(), source.creative(), source.inventoryLayout(), source.craftingLayout(), source.leftTab(), source.rightTab(), + source.filtering(), this.query(), source.tabLabel(), source.craftingLabel(), source.categoryCounts(), cells, this.selectedCollection, this.selectedIndex); -+ final var scene = this.program.resolve(new PlayerInventoryUi.MenuView(menuId, state, slots, cursor)); ++ final var scene = this.program.resolve(new PlayerInventoryUi.MenuView(menuId, state, slots, cursor), this.windows); + final Map> actions = new HashMap<>(); -+ scope(scene.getAsJsonObject("root"), actions, state); -+ return new Frame(scene, 46, this.previews, actions); ++ final Map visible = new HashMap<>(); ++ this.scope(scene.getAsJsonObject("root"), actions, visible); ++ return new Frame(scene, 46, visible, actions); + } + -+ private static void scope(final JsonObject node, final Map> actions, final PlayerInventoryUi.State state) { ++ private void scope(final JsonObject node, final Map> actions, final Map visible) { ++ if (node.has("recipe_id")) { ++ final int id = node.get("recipe_id").getAsInt(); ++ final var item = this.previews.get(id); ++ if (item == null) throw new IllegalArgumentException("Unknown player cell"); ++ visible.put(id, item); ++ } + if (node.has("recipe_actions")) { + final int id = node.get("recipe_id").getAsInt(); -+ if (state.cells().stream().noneMatch(cell -> cell.id() == id)) throw new IllegalArgumentException("Unknown player cell"); + final Set values = actions.computeIfAbsent(id, ignored -> new HashSet<>()); + for (final var action : node.getAsJsonArray("recipe_actions")) values.add(action.getAsJsonObject().get("to_button_id").getAsString()); + } -+ if (node.has("children")) for (final var child : node.getAsJsonArray("children")) scope(child.getAsJsonObject(), actions, state); ++ if (node.has("children")) for (final var child : node.getAsJsonArray("children")) this.scope(child.getAsJsonObject(), actions, visible); + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenClosePayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenClosePayload.java @@ -10896,10 +10964,10 @@ index 0000000..d3902fa +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java new file mode 100644 -index 0000000..265ff43 +index 0000000..a248a1b --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java -@@ -0,0 +1,168 @@ +@@ -0,0 +1,269 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -10910,10 +10978,17 @@ index 0000000..265ff43 +import com.mojang.blaze3d.platform.InputConstants; +import com.viaversion.viafabricplus.bedrock.ViaFabricPlusBedrock; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiPresentation; ++import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiLayout; ++import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiFocus; ++import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiCollectionWindow; +import com.viaversion.viaversion.util.GsonUtil; +import com.viaversion.viaversion.api.minecraft.data.StructuredDataContainer; +import com.viaversion.viaversion.api.minecraft.item.StructuredItem; ++import com.viaversion.viaversion.api.minecraft.item.Item; +import java.util.List; ++import java.util.Arrays; ++import java.util.stream.Collectors; ++import java.util.stream.IntStream; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Predicate; @@ -10948,6 +11023,8 @@ index 0000000..265ff43 +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; @@ -11050,6 +11127,98 @@ index 0000000..265ff43 + } + } + ++ @ParameterizedTest ++ @ValueSource(ints = {2, 3}) ++ void creativeScrollingAndResizeKeepTheCompleteCatalogAndOriginalFocusedCell(final int layout) throws Exception { ++ final var game = mock(Minecraft.class); ++ final var player = mock(LocalPlayer.class); ++ final var inventory = new Inventory(player, new EntityEquipment()); ++ final var menu = new InventoryMenu(inventory, true, player); ++ final var owner = mock(ClientPacketListener.class); ++ final var ordinary = mock(AbstractContainerScreen.class); ++ final var options = mock(Options.class); ++ final var font = mock(Font.class); ++ final var clock = new AtomicLong(); ++ final var logger = mock(Logger.class); ++ final var impl = mock(ViaFabricPlusBedrock.class); ++ when(impl.logger()).thenReturn(logger); ++ game.player = player; ++ game.gameMode = mock(MultiPlayerGameMode.class); ++ player.containerMenu = menu; ++ when(ordinary.getMenu()).thenReturn(menu); ++ when(owner.getConnection()).thenReturn(mock(Connection.class)); ++ for (final String name : List.of("keyPickItem", "keyDrop", "keyInventory")) ++ set(Options.class, options, name, mock(KeyMapping.class)); ++ set(Minecraft.class, game, "options", options); ++ set(Minecraft.class, game, "gui", mock(Gui.class)); ++ try (final var singleton = mockStatic(Minecraft.class); final var entrypoint = mockStatic(ViaFabricPlusBedrock.class)) { ++ singleton.when(Minecraft::getInstance).thenReturn(game); ++ entrypoint.when(ViaFabricPlusBedrock::impl).thenReturn(impl); ++ final var ui = new PlayerInventoryUi(new ResourcePackStorage(List.of()).getJsonUiProgram()); ++ final var cells = IntStream.range(0, 2000).mapToObj(id -> ++ new PlayerInventoryUi.Cell(id, id + 10, -1, 0, 0, 1, false, false, "", Map.of())).toList(); ++ final var state = new PlayerInventoryUi.State(false, true, layout, 2, 5, 2, false, "", "All", "Crafting", ++ List.of(2000, 0, 0, 0), cells, "", -1); ++ final Map items = IntStream.range(0, 2000).boxed().collect(Collectors.toMap(id -> id, ++ id -> new StructuredItem(320, 1, new StructuredDataContainer()))); ++ final var previews = IntStream.range(0, 2000).boxed().collect(Collectors.toMap(id -> id, id -> new ItemStack(Items.IRON_INGOT))); ++ final var source = new NativePlayerInventoryMessage(0, 1, 8, 1, ui.exportProgram(false).toString(), ++ GsonUtil.getGson().toJson(state), items); ++ final var presentation = new NativePlayerPresentation(source, previews, clock::get); ++ final var screen = new BedrockNativeContainerScreen(owner, ordinary, inventory, Component.empty(), presentation, ++ false, 1, clock::get, () -> true); ++ set(Screen.class, screen, "minecraft", game); ++ set(Screen.class, screen, "font", font); ++ try { ++ screen.prepare(font, 640, 360); ++ clock.set(1_000_000_000); ++ screen.prepare(font, 640, 360); ++ final var first = pointer(screen, draw -> draw.box().node().properties().has("recipe_actions")); ++ screen.mouseMoved(first.x(), first.y()); ++ final var input = (NativeContainerInput) get(screen, "nativeInput"); ++ final var initialRoot = (NativeUiLayout.Box) get(screen, "root"); ++ final var target = NativeUiFocus.targets(initialRoot, node -> node.properties().has("recipe_actions")).getFirst(); ++ final var focus = NativeContainerInput.class.getDeclaredMethod("focus", NativeUiFocus.Target.class); ++ focus.setAccessible(true); ++ focus.invoke(input, target); ++ final int selected = input.focused().properties().get("recipe_id").getAsInt(); ++ assertTrue(screen.mouseScrolled(first.x(), first.y(), 0, -180)); ++ final var geometry = (NativeContainerGeometry) get(screen, "geometry"); ++ final var distant = pointer(screen, draw -> draw.box().node().properties().has("recipe_actions")); ++ assertTrue(geometry.recipe(distant.x(), distant.y()).id() > 1000); ++ assertEquals(selected, input.focused().properties().get("recipe_id").getAsInt()); ++ screen.prepare(font, 480, 270); ++ assertEquals(selected, input.focused().properties().get("recipe_id").getAsInt()); ++ assertTrue(screen.keyPressed(new KeyEvent(InputConstants.KEY_DOWN, 0, 0))); ++ screen.keyReleased(new KeyEvent(InputConstants.KEY_DOWN, 0, 0)); ++ assertTrue(input.focused().properties().get("recipe_id").getAsInt() > selected); ++ final var root = (NativeUiLayout.Box) get(screen, "root"); ++ assertFalse(presentation.windows(NativeUiCollectionWindow.visible(root, input.focused()))); ++ assertEquals(2000, presentation.state().cells().size()); ++ assertEquals(2000, presentation.message().items().size()); ++ assertTrue(((Map) get(screen, "recipePreviews")).size() < 200); ++ assertTrue(mockingDetails(logger).getInvocations().isEmpty(), () -> mockingDetails(logger).getInvocations().stream() ++ .map(invocation -> Arrays.toString(invocation.getArguments())).collect(Collectors.joining("\n"))); ++ verifyNoInteractions(game.gameMode); ++ if (layout == 3) { ++ final var stack = new ItemStack(Items.IRON_INGOT, 3); ++ menu.getSlot(36).set(stack); ++ screen.slotClicked(menu.getSlot(36), 36, 0, ContainerInput.QUICK_MOVE); ++ assertEquals(3, menu.getSlot(36).getItem().getCount()); ++ assertFalse(presentation.idle()); ++ assertEquals(1, mockingDetails(logger).getInvocations().size()); ++ verifyNoInteractions(game.gameMode); ++ } ++ } finally { screen.discard(); } ++ } ++ } ++ ++ private static Object get(final Object target, final String name) throws Exception { ++ final var field = target.getClass().getDeclaredField(name); ++ field.setAccessible(true); ++ return field.get(target); ++ } ++ + private static void set(final Class type, final Object target, final String name, final Object value) throws Exception { + final var field = type.getDeclaredField(name); + field.setAccessible(true); 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 b9824df3..86d844d0 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 @@ -39,6 +39,12 @@ Installed direct and ViaProxy controls retain a server-issued empty bundle in Su Unknown snapshots restore ordinary controls. Hovering or carrying storage items also restores ordinary controls until native tooltip rendering and selection are complete. Unsupported visible glyphs, recipe auxiliary actions, and held-selection commands also restore ordinary controls. Rejected presentations remain retired until the next opening, so subsequent updates do not repeat the failed installation. +Fixed recipe grids expand visible cells and retained keyboard neighbors over the complete catalog. +Local window changes preserve the menu opening and authoritative revision. +Screen regressions use 2,000 retained items in the recipe-book and Creative layouts. +They cover distant scrolling, resize, original action IDs, and keyboard navigation. +Creative hotbar destruction restores ordinary controls before item changes or requests. +Passive hover notifications do not trigger the fallback for unsupported recipe actions. Full Creative catalog expansion, initial bundle insertion, bulk recipe actions, and Creative cursor replacement remain unresolved. Complete cooking integration, live transfers, filled-bundle pixels, and the original E-key background report also remain unresolved. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index b20faad1..d3d5653b 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From 117eb0aa7de7079f2cfbe4567881ba26eeca691b Mon Sep 17 00:00:00 2001 +From b541289db9117312f094b9d59faf5fbd3a5d066a 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(ui): share native scene rendering and GUI actors @@ -592,6 +592,27 @@ animation reached a partial alpha and immediately retired the presentation. Color regression controls cover partial existing alpha, transparent frames and independent perspectives. Live installed entrance and Help comparisons remain required; this is not an exact native blend or timing claim. + +Lay out bounded uniform collection views over their complete logical grid. +Use the original collection indices for placement and keep full content +height for scrollbars even when only a few rows are instantiated. Derive +the next view range from the clipped viewport with an adjacent-row buffer. +Keep source counts and instantiated-node work independently bounded; long +logical content does not allocate off-screen controls. + +The fixed-cell window contract rejects changed geometry or redirected +source indices. Preserve animation clocks by collection index rather than +by a reused position inside the view window. Regressions compare windowed +geometry with full grids, distant offsets, resizing, end clamping and +animation identity across overlapping windows. Native inventory actions, +installed focus and final pixels remain consumer comparisons. + +Retain a separate bounded span around keyboard focus when it lies outside +the visible window. Place the union by each original collection index, +not by its current position in the instantiated list. This keeps focus +and adjacent navigation targets available after a distant wheel scroll +or column reflow. A focus regression compares the next row against the +full grid while the viewport is hundreds of rows away. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../appearance/BedrockBoneAnimation.java | 3 + @@ -634,6 +655,7 @@ remain required; this is not an exact native blend or timing claim. .../bedrock/screen/ui/NativeUiAnimations.java | 382 +++++++ .../bedrock/screen/ui/NativeUiAseprite.java | 54 + .../bedrock/screen/ui/NativeUiButton.java | 167 +++ + .../screen/ui/NativeUiCollectionWindow.java | 92 ++ .../bedrock/screen/ui/NativeUiColor.java | 55 + .../bedrock/screen/ui/NativeUiController.java | 37 + .../bedrock/screen/ui/NativeUiDimension.java | 93 ++ @@ -646,7 +668,7 @@ remain required; this is not an exact native blend or timing claim. .../bedrock/screen/ui/NativeUiImages.java | 171 ++++ .../bedrock/screen/ui/NativeUiInput.java | 248 +++++ .../screen/ui/NativeUiInputRouter.java | 72 ++ - .../bedrock/screen/ui/NativeUiLayout.java | 524 ++++++++++ + .../bedrock/screen/ui/NativeUiLayout.java | 551 ++++++++++ .../screen/ui/NativeUiMultilineInput.java | 267 +++++ .../screen/ui/NativeUiPlayerCamera.java | 72 ++ .../screen/ui/NativeUiPresentation.java | 162 +++ @@ -681,10 +703,11 @@ remain required; this is not an exact native blend or timing claim. .../screen/ui/NativeHudOverlayStateTest.java | 153 +++ .../screen/ui/NativeUiActorCameraTest.java | 102 ++ .../screen/ui/NativeUiActorPortraitsTest.java | 164 +++ - .../ui/NativeUiAnimationLifecycleTest.java | 131 +++ + .../ui/NativeUiAnimationLifecycleTest.java | 155 +++ .../screen/ui/NativeUiAnimationsTest.java | 264 +++++ .../screen/ui/NativeUiAsepriteTest.java | 108 ++ .../bedrock/screen/ui/NativeUiButtonTest.java | 127 +++ + .../ui/NativeUiCollectionWindowTest.java | 144 +++ .../screen/ui/NativeUiColorAnimationTest.java | 114 +++ .../bedrock/screen/ui/NativeUiFocusTest.java | 124 +++ .../bedrock/screen/ui/NativeUiFontTest.java | 148 +++ @@ -705,7 +728,7 @@ remain required; this is not an exact native blend or timing claim. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ - 112 files changed, 12614 insertions(+), 36 deletions(-) + 114 files changed, 12901 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearance.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiActorRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinNativeUiInput.java @@ -733,6 +756,7 @@ remain required; this is not an exact native blend or timing claim. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAseprite.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiButton.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiCollectionWindow.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiColor.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiController.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiDimension.java @@ -775,6 +799,7 @@ remain required; this is not an exact native blend or timing claim. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationsTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAsepriteTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiButtonTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiCollectionWindowTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiColorAnimationTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiFocusTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiFontTest.java @@ -3375,7 +3400,7 @@ index 0000000..286546d +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycle.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycle.java new file mode 100644 -index 0000000..d368b46 +index 0000000..687730c --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycle.java @@ -0,0 +1,131 @@ @@ -3461,10 +3486,10 @@ index 0000000..d368b46 + + private static Identity identity(final NativeUiScene.Node node, final int occurrence) { + return new Identity(node.name(), node.type(), String.valueOf(node.properties().get("inventory_slot")), -+ String.valueOf(node.properties().get("collection")), occurrence); ++ String.valueOf(node.properties().get("collection")), String.valueOf(node.properties().get("collection_index")), occurrence); + } + -+ private record Identity(String name, String type, String slot, String collection, int occurrence) {} ++ private record Identity(String name, String type, String slot, String collection, String index, int occurrence) {} + + boolean active() { return active(this.root); } + @@ -4131,6 +4156,104 @@ index 0000000..bc593e4 + private record Reference(NativeUiScene.Node node, List path) {} + } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiCollectionWindow.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiCollectionWindow.java +new file mode 100644 +index 0000000..67d2f90 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiCollectionWindow.java +@@ -0,0 +1,92 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.screen.ui; ++ ++import com.google.gson.JsonElement; ++import java.util.HashMap; ++import java.util.Map; ++import net.raphimc.viabedrock.api.resourcepack.ui.JsonUiProgram; ++import net.raphimc.viabedrock.api.resourcepack.ui.UiBindings; ++ ++/** A bounded set of cell views retains the complete source's indices and scroll extent. */ ++public record NativeUiCollectionWindow(String key, int size, UiBindings.Window range, float cellWidth, float cellHeight) { ++ public static NativeUiCollectionWindow from(final NativeUiScene.Node node) { ++ if (!node.properties().has("collection_window_key")) return null; ++ final String key = node.string("collection_window_key", ""); ++ final int size = integer(node.properties().get("collection_size")); ++ final int start = integer(node.properties().get("collection_start")); ++ final int count = integer(node.properties().get("collection_count")); ++ final int retainedStart = node.properties().has("collection_retained_start") ? integer(node.properties().get("collection_retained_start")) : 0; ++ final int retainedCount = node.properties().has("collection_retained_count") ? integer(node.properties().get("collection_retained_count")) : 0; ++ final float width = NativeUiLayout.vector(node, "collection_cell_size", 0, 0); ++ final float height = NativeUiLayout.vector(node, "collection_cell_size", 1, 0); ++ if (!node.type().equals("grid") || !node.string("grid_rescaling_type", "").equals("horizontal") ++ || node.string("grid_fill_direction", "horizontal").equals("vertical") || node.properties().has("grid_dimensions") ++ || key.isEmpty() || key.length() > 16384 || size < 1 || size > JsonUiProgram.MAX_NODES ++ || start < 0 || start >= size || count < 1 || count > size - start || width <= 0 || height <= 0 ++ || retainedStart < 0 || retainedStart >= size || retainedCount < 0 || retainedCount > size - retainedStart) ++ throw new IllegalArgumentException("Invalid native collection window"); ++ final var range = new UiBindings.Window(start, count, retainedStart, retainedCount); ++ final var indices = range.indices(size); ++ if (indices.size() != node.children().size()) throw new IllegalArgumentException("Native collection window lost its cell views"); ++ for (int index = 0; index < node.children().size(); index++) ++ if (integer(node.children().get(index).properties().get("collection_index")) != indices.get(index)) ++ throw new IllegalArgumentException("Native collection window lost its source index"); ++ return new NativeUiCollectionWindow(key, size, range, width, height); ++ } ++ ++ public static Map visible(final NativeUiLayout.Box root) { ++ return visible(root, null); ++ } ++ ++ public static Map visible(final NativeUiLayout.Box root, final NativeUiScene.Node focused) { ++ final Map result = new HashMap<>(); ++ collect(root, focused, result); ++ return Map.copyOf(result); ++ } ++ ++ private static void collect(final NativeUiLayout.Box box, final NativeUiScene.Node focused, final Map result) { ++ final var window = from(box.node()); ++ if (window != null) { ++ final var clip = box.visibleBounds(); ++ final int columns = Math.clamp((int) Math.floor(box.bounds().width() / window.cellWidth), 1, 128); ++ final int rows = Math.ceilDiv(window.size, columns); ++ final int firstRow = (int) Math.clamp(Math.floor(((double) clip.y() - box.bounds().y()) / window.cellHeight) - 1, 0, rows - 1); ++ final int endRow = (int) Math.clamp(Math.ceil(((double) clip.y() + clip.height() - box.bounds().y()) / window.cellHeight) + 1, firstRow + 1, rows); ++ final int start = clip.width() <= 0 || clip.height() <= 0 ? 0 : Math.min(window.size - 1, firstRow * columns); ++ final int end = clip.width() <= 0 || clip.height() <= 0 ? 1 : Math.min(window.size, endRow * columns); ++ int retainedStart = 0, retainedCount = 0; ++ if (focused != null && focused.properties().has("collection_index") && contains(box.node(), focused)) { ++ final int selected = integer(focused.properties().get("collection_index")); ++ if (selected >= 0 && selected < window.size) { ++ final int focusRow = selected / columns; ++ final int first = Math.max(0, focusRow - 1) * columns; ++ final int last = Math.min(window.size, (focusRow + 2) * columns); ++ if (first < start || last > end) { ++ retainedStart = first; ++ retainedCount = last - first; ++ } ++ } ++ } ++ if (result.put(window.key, new UiBindings.Window(start, Math.max(1, end - start), retainedStart, retainedCount)) != null) ++ throw new IllegalArgumentException("Duplicate native collection window identity"); ++ } ++ for (final var child : box.children()) collect(child, focused, result); ++ } ++ ++ private static boolean contains(final NativeUiScene.Node node, final NativeUiScene.Node selected) { ++ if (node == selected) return true; ++ for (final var child : node.children()) if (contains(child, selected)) return true; ++ return false; ++ } ++ ++ private static int integer(final JsonElement value) { ++ if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()) ++ throw new IllegalArgumentException("Invalid native collection window integer"); ++ try { return value.getAsBigDecimal().intValueExact(); } ++ catch (final ArithmeticException invalid) { throw new IllegalArgumentException("Invalid native collection window integer", invalid); } ++ } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiColor.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiColor.java new file mode 100644 index 0000000..83c9295 @@ -5723,10 +5846,10 @@ index 0000000..8c0c6f6 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayout.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayout.java new file mode 100644 -index 0000000..987106d +index 0000000..2af142c --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayout.java -@@ -0,0 +1,524 @@ +@@ -0,0 +1,551 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5749,6 +5872,8 @@ index 0000000..987106d + private final NativeUiAnimations animations; + private final Map> measured = new IdentityHashMap<>(); + private final Map visibleNodes = new IdentityHashMap<>(); ++ private final Map windows = new IdentityHashMap<>(); ++ private float verticalLimit; + private int work; + + public NativeUiLayout(final Metrics metrics) { @@ -5772,6 +5897,8 @@ index 0000000..987106d + final Map scroll) { + this.measured.clear(); + this.visibleNodes.clear(); ++ this.windows.clear(); ++ this.verticalLimit = LIMIT; + this.visibility(node, true); + this.work = 0; + final Size size = this.measure(node, width, height); @@ -5782,6 +5909,11 @@ index 0000000..987106d + private void visibility(final NativeUiScene.Node node, final boolean inherited) { + final boolean visible = inherited && node.flag("visible", true) && !node.flag("ignored", false); + this.visibleNodes.put(node, visible); ++ final var window = NativeUiCollectionWindow.from(node); ++ if (window != null) { ++ this.windows.put(node, window); ++ this.verticalLimit += window.size() * window.cellHeight(); ++ } + for (final var child : node.children()) this.visibility(child, visible); + } + @@ -5917,6 +6049,13 @@ index 0000000..987106d + final boolean vertical = !node.string("orientation", "vertical").equals("horizontal"); + if (node.type().equals("grid")) { + final Grid grid = this.grid(node, parentWidth, parentHeight); ++ final var window = this.windows.get(node); ++ if (window != null) { ++ for (final var child : children) ++ if (child.width != window.cellWidth() || child.height != window.cellHeight()) ++ throw new IllegalArgumentException("Native collection cell geometry changed"); ++ return new Size(grid.columns * window.cellWidth(), grid.rows * window.cellHeight()); ++ } + final Map widths = new java.util.HashMap<>(); + final Map heights = new java.util.HashMap<>(); + for (int i = 0; i < children.size(); i++) { @@ -6055,8 +6194,9 @@ index 0000000..987106d + final Measurement environment, final float width, final float height, + final float content, final float maximum) { + final float minimum = dimension(node, "min_size", axis, environment, width, height, content, maximum, 0); -+ final float declaredUpper = dimension(node, "max_size", axis, environment, width, height, content, maximum, LIMIT); -+ final float upper = declaredUpper == 0 ? LIMIT : declaredUpper; ++ final float limit = axis == 1 ? this.verticalLimit : LIMIT; ++ final float declaredUpper = dimension(node, "max_size", axis, environment, width, height, content, maximum, limit); ++ final float upper = declaredUpper == 0 ? limit : declaredUpper; + return Math.clamp(size, Math.min(minimum, upper), upper); + } + @@ -6064,13 +6204,21 @@ index 0000000..987106d + final Measurement environment, final float width, final float height, + final float content, final float maximum, final float fallback) { + final NativeUiDimension dimension = this.dimensionValue(node, key, axis); -+ if (dimension == null) return bounded(fallback); ++ final float limit = axis == 1 ? this.verticalLimit : LIMIT; ++ if (dimension == null) { ++ if (!Float.isFinite(fallback) || Math.abs(fallback) > limit) ++ throw new IllegalArgumentException("Native UI dimension exceeds layout limits"); ++ return Math.max(0, fallback); ++ } + final float parent = axis == 0 ? environment.width : environment.height; + final float sibling = axis == 0 ? environment.siblingWidth : environment.siblingHeight; + try { + final float value = dimension.resolve(parent, content, maximum, width, height, sibling, fallback); + // A loose maximum does not request geometry beyond our supported layout range. -+ return bounded(key.equals("max_size") ? Math.min(value, LIMIT) : value); ++ final float bounded = key.equals("max_size") ? Math.min(value, limit) : value; ++ if (!Float.isFinite(bounded) || Math.abs(bounded) > limit) ++ throw new IllegalArgumentException("Native UI dimension exceeds layout limits"); ++ return Math.max(0, bounded); + } catch (final IllegalArgumentException error) { + throw new IllegalArgumentException("Invalid native UI dimension: " + node.name() + "." + key + "[" + axis + "]", error); + } @@ -6118,9 +6266,10 @@ index 0000000..987106d + } + + private Grid grid(final NativeUiScene.Node node, final float width, final float height) { -+ if (!node.type().equals("grid")) return new Grid(1, 1, true, false, false); ++ if (!node.type().equals("grid")) return new Grid(1, 1, true, false, false, false); + final boolean horizontal = !node.string("grid_fill_direction", "horizontal").equals("vertical"); -+ final int count = node.children().size(); ++ final var window = this.windows.get(node); ++ final int count = window == null ? node.children().size() : window.size(); + if (count > 8192) throw new IllegalArgumentException("Native grid exceeds child limits"); + final boolean explicit = node.properties().has("grid_dimensions"); + final String rescaling = node.string("grid_rescaling_type", "none"); @@ -6159,7 +6308,7 @@ index 0000000..987106d + if (horizontal) rows = Math.max(rows, (int) Math.ceilDiv((long) count, columns)); + else columns = Math.max(columns, (int) Math.ceilDiv((long) count, rows)); + } -+ return new Grid(columns, rows, horizontal, rescaled, manual); ++ return new Grid(columns, rows, horizontal, rescaled, manual, window != null); + } + + private static int gridCapacity(final NativeUiScene.Node node, final int axis, final boolean manual) { @@ -6199,11 +6348,12 @@ index 0000000..987106d + return position; + } + -+ private record Grid(int columns, int rows, boolean horizontal, boolean rescaled, boolean manual) { ++ private record Grid(int columns, int rows, boolean horizontal, boolean rescaled, boolean manual, boolean windowed) { + int[] position(final NativeUiScene.Node node, final int index) { + if (this.manual) return gridPosition(node); -+ return this.horizontal ? new int[]{index % this.columns, index / this.columns} -+ : new int[]{index / this.rows, index % this.rows}; ++ final int original = this.windowed ? node.properties().get("collection_index").getAsInt() : index; ++ return this.horizontal ? new int[]{original % this.columns, original / this.columns} ++ : new int[]{original / this.rows, original % this.rows}; + } + } + @@ -10102,10 +10252,10 @@ index 0000000..835f559 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycleTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycleTest.java new file mode 100644 -index 0000000..694fcb7 +index 0000000..e407bbc --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycleTest.java -@@ -0,0 +1,131 @@ +@@ -0,0 +1,155 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -10219,6 +10369,30 @@ index 0000000..694fcb7 + assertEquals(.5F, lifecycle.animations().opacityAt(changed, 2_500_000_000L).current()); + } + ++ @Test ++ void overlappingCollectionWindowsRetainOnlyTheSameSourceRowsAnimationClock() { ++ final var first = animatedCollection(40); ++ final var retained = animatedCollection(41); ++ final var lifecycle = new NativeUiAnimationLifecycle(node("grid", List.of(first, retained))); ++ lifecycle.advance(250_000_000); ++ final var rebound = animatedCollection(41); ++ final var inserted = animatedCollection(42); ++ lifecycle.replaceRoot(node("grid", List.of(rebound, inserted)), 250_000_000); ++ assertEquals(.25F, lifecycle.animations().opacityAt(rebound, 250_000_000).current()); ++ assertEquals(0, lifecycle.animations().opacityAt(inserted, 250_000_000).current()); ++ lifecycle.advance(500_000_000); ++ assertEquals(.5F, lifecycle.animations().opacityAt(rebound, 500_000_000).current()); ++ assertEquals(.25F, lifecycle.animations().opacityAt(inserted, 500_000_000).current()); ++ } ++ ++ private static NativeUiScene.Node animatedCollection(final int index) { ++ final var node = animatedSlot(index); ++ node.properties().remove("inventory_slot"); ++ node.properties().addProperty("collection", "recipe_book"); ++ node.properties().addProperty("collection_index", index); ++ return node; ++ } ++ + private static NativeUiScene.Node animatedSlot(final int slot) { + final var node = node("slot", List.of()); + node.properties().addProperty("inventory_slot", slot); @@ -10754,6 +10928,156 @@ index 0000000..a6a275d + return new NativeUiInput.ButtonEvent(0, 0, 0, phase, false, 1, NativeUiInput.Scope.DEFAULT, update, update); + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiCollectionWindowTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiCollectionWindowTest.java +new file mode 100644 +index 0000000..359b641 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiCollectionWindowTest.java +@@ -0,0 +1,144 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.screen.ui; ++ ++import com.google.gson.JsonArray; ++import com.google.gson.JsonObject; ++import java.util.Map; ++import java.util.Set; ++import java.util.List; ++import java.util.stream.Stream; ++import net.raphimc.viabedrock.api.resourcepack.ui.UiBindings; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class NativeUiCollectionWindowTest { ++ @Test ++ void distantCellsKeepTheirFullScrollExtentAndOriginalPositions() { ++ final var root = scene(5000, 716, 20, true); ++ final var box = layout(root, 72, 3240); ++ assertEquals(22500, box.content().height()); ++ final var grid = box.children().getFirst(); ++ assertEquals(-18, grid.children().getFirst().bounds().y()); ++ assertEquals(0, grid.children().get(4).bounds().y()); ++ assertEquals(new UiBindings.Window(716, 20), NativeUiCollectionWindow.visible(box).get("/view/grid")); ++ final var resized = layout(root, 108, 3240); ++ assertEquals(15012, resized.content().height()); ++ assertEquals(new UiBindings.Window(1074, 30), NativeUiCollectionWindow.visible(resized).get("/view/grid")); ++ final var end = layout(scene(5000, 4984, 16, true), 72, 1_000_000); ++ assertEquals(new UiBindings.Window(4984, 16), NativeUiCollectionWindow.visible(end).get("/view/grid")); ++ assertEquals(36, end.children().getFirst().children().getLast().bounds().y()); ++ } ++ ++ @Test ++ void visibleWindowGeometryMatchesTheSameCellsInTheFullGrid() { ++ final var full = layout(scene(1000, 0, 1000, false), 72, 3240); ++ final var windowed = layout(scene(1000, 716, 20, true), 72, 3240); ++ assertEquals(full.content(), windowed.content()); ++ assertEquals(full.children().getFirst().bounds(), windowed.children().getFirst().bounds()); ++ for (int index = 0; index < 20; index++) { ++ final var expected = full.children().getFirst().children().get(716 + index); ++ final var actual = windowed.children().getFirst().children().get(index); ++ assertEquals(expected.bounds(), actual.bounds()); ++ assertEquals(expected.visibleBounds(), actual.visibleBounds()); ++ } ++ } ++ ++ @Test ++ void aWindowCannotRedirectAnInstantiatedCellToAnotherSourceIndex() { ++ final var root = scene(1000, 716, 20, true); ++ root.children().getFirst().children().getFirst().properties().addProperty("collection_index", 4); ++ assertThrows(IllegalArgumentException.class, () -> layout(root, 72, 3240)); ++ } ++ ++ @Test ++ void aDistantViewportRetainsLogicalFocusAndTheSameNextRowAsTheFullGrid() { ++ final var original = scene(1000, 716, 20, true); ++ final var grid = original.children().getFirst(); ++ final var properties = grid.properties().deepCopy(); ++ properties.addProperty("collection_retained_start", 0); ++ properties.addProperty("collection_retained_count", 8); ++ final var retained = scene(1000, 0, 8, false).children().getFirst().children(); ++ final var cells = Stream.concat(retained.stream(), grid.children().stream()).toList(); ++ final var windowGrid = new NativeUiScene.Node(grid.name(), grid.type(), properties, cells, grid.action()); ++ final var windowed = layout(scrollViewport(original, windowGrid), 72, 3240); ++ final var selected = retained.getFirst(); ++ assertEquals(new UiBindings.Window(716, 20, 0, 8), NativeUiCollectionWindow.visible(windowed, selected).get("/view/grid")); ++ final var full = scene(1000, 0, 1000, false); ++ final var fullGrid = full.children().getFirst(); ++ final var complete = layout(scrollViewport(full, fullGrid), 72, 3240); ++ final var reference = NativeUiFocus.next(NativeUiFocus.targets(complete, _ -> true), fullGrid.children().getFirst(), ++ NativeUiFocus.Direction.DOWN, 72, 54, new NativeUiFocus.History()); ++ final var actual = NativeUiFocus.next(NativeUiFocus.targets(windowed, _ -> true), selected, ++ NativeUiFocus.Direction.DOWN, 72, 54, new NativeUiFocus.History()); ++ assertEquals(4, reference.box().node().properties().get("collection_index").getAsInt()); ++ assertEquals(4, actual.box().node().properties().get("collection_index").getAsInt()); ++ } ++ ++ private static NativeUiScene.Node scrollViewport(final NativeUiScene.Node root, final NativeUiScene.Node grid) { ++ final var properties = root.properties().deepCopy(); ++ properties.addProperty("scroll_content", "grid"); ++ properties.addProperty("scroll_view_port", "viewport"); ++ final var viewport = node("viewport", "panel", "100%", "100%"); ++ final var child = new NativeUiScene.Node("viewport", "panel", viewport, List.of(grid), Integer.MIN_VALUE); ++ return new NativeUiScene.Node(root.name(), root.type(), properties, List.of(child), root.action()); ++ } ++ ++ private static NativeUiLayout.Box layout(final NativeUiScene.Node root, final int width, final float scroll) { ++ return new NativeUiLayout(_ -> new NativeUiLayout.Size(0, 0)).layout(root, width, 54, Map.of(root, scroll)); ++ } ++ ++ private static NativeUiScene.Node scene(final int total, final int start, final int count, final boolean windowed) { ++ final var root = node("view", "scroll_view", "100%", 54); ++ final var grid = node("grid", "grid", "100%", "default"); ++ grid.addProperty("grid_item_template", "example.cell"); ++ grid.addProperty("grid_rescaling_type", "horizontal"); ++ grid.addProperty("anchor_from", "top_left"); ++ grid.addProperty("anchor_to", "top_left"); ++ if (windowed) { ++ grid.addProperty("collection_window_key", "/view/grid"); ++ grid.addProperty("collection_size", total); ++ grid.addProperty("collection_start", start); ++ grid.addProperty("collection_count", count); ++ grid.add("collection_cell_size", pair(18, 18)); ++ } ++ final var cells = new JsonArray(); ++ for (int index = start; index < start + count; index++) { ++ final var cell = node("cell", "panel", 18, 18); ++ cell.addProperty("collection", "recipe_book"); ++ cell.addProperty("collection_index", index); ++ cell.addProperty("focus_enabled", true); ++ cells.add(cell); ++ } ++ grid.add("children", cells); ++ final var children = new JsonArray(); ++ children.add(grid); ++ root.add("children", children); ++ final var scene = new JsonObject(); ++ scene.addProperty("format", 1); ++ scene.add("root", root); ++ return NativeUiScene.parse(scene.toString(), Set.of()).root(); ++ } ++ ++ private static JsonObject node(final String name, final String type, final Object width, final Object height) { ++ final var result = new JsonObject(); ++ result.addProperty("name", name); ++ result.addProperty("type", type); ++ final var size = new JsonArray(); ++ if (width instanceof Number value) size.add(value); else size.add(width.toString()); ++ if (height instanceof Number value) size.add(value); else size.add(height.toString()); ++ result.add("size", size); ++ return result; ++ } ++ ++ private static JsonArray pair(final int width, final int height) { ++ final var result = new JsonArray(); ++ result.add(width); ++ result.add(height); ++ return result; ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiColorAnimationTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiColorAnimationTest.java new file mode 100644 index 0000000..a4d6db9 diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index cd222e1e..8e5a5ed9 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -26,6 +26,10 @@ Explicit anchors and full-parent percentage dimensions retain separate regressio Live player and horse portraits apply control opacity when Java composites their GUI textures. Color controls cover premultiplied RGB and alpha, transparent frames, and independent perspectives. The existing independent actor material-coverage path remains separate. +Fixed collection grids retain their full logical height while layout positions each instantiated cell by its original source index. +Regressions compare complete and windowed geometry, distant scroll positions, resize, final rows, and offscreen keyboard neighbors. +Overlapping windows retain animation clocks by source identity. +These controls do not establish identical native pooling or frame timing. Broader viewports, complete inventory pixels, entrance timing, and TrueType comparisons remain open. The [owning patch](0029-share-native-ui-scene-rendering.patch) retains implementation rationale and versioned evidence. From 165906e98bf68b2ab88b259240079e5276cca132 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 14:30:46 +0100 Subject: [PATCH 02/33] fix(network): wait for Java protocol acknowledgments Suppress synthetic keepalives while a modern Java client is between configuration and play protocols. Recheck the current states on the connection event loop so a queued task cannot send through a transition that began after scheduling. Preserve configuration keepalives for older Java clients. A focused regression covers acknowledgment, queued state changes, and legacy clients. The complete build passes, and the core and add-on stacks replay without cached ancestors. Lifeboat spawn mapping and authored-form fallback remain separate investigations. --- ...e-block-state-geometry-and-materials.patch | 131 +++++++++++++++++- ...e-block-state-geometry-and-materials.pr.md | 4 + 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch index a66c801d..ded14ac3 100644 --- a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch +++ b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch @@ -1,4 +1,4 @@ -From 1c6228f5d2e776e04e1fa10e7c53d5f3c2125367 Mon Sep 17 00:00:00 2001 +From dab41b4ae2e767e90f88af2ed251d19d63a63bd3 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(blocks): render Bedrock state geometry and materials @@ -363,6 +363,14 @@ must observe the same state snapshot even when no Java pack is loaded. Return isolated snapshots through the registered runtime-state owner, so equal serialized states and caller mutation cannot transfer component data. Keep recipe-specific interpretation in its crafting consumer patch. + +Wait for both Java protocol states to agree before synthetic keepalives +can enter configuration or play. During a resource-pack reconfiguration, +the direct client temporarily has no outbound codec; an early keepalive +then produces an EncoderException when it replies. Recheck states on the +connection event loop, and retain translated configuration keepalives for +Java versions without a configuration acknowledgment. A queued-transition +regression covers suppression, acknowledgment, and legacy compatibility. --- Data Asset Sources.md | 27 + .../chunk/blockstate/BlockItemDescriptor.java | 29 + @@ -400,6 +408,7 @@ Keep recipe-specific interpretation in its crafting consumer patch. .../protocol/storage/ResourcePackStorage.java | 44 +- .../protocol/storage/VoxelShapeRegistry.java | 125 ++ .../protocol/storage/WorldPacketBuffer.java | 75 ++ + .../protocol/task/KeepAliveTask.java | 10 +- .../data/bedrock/block_item_states.json | 1121 +++++++++++++++++ .../data/bedrock/block_property_values.json | 979 ++++++++++++++ .../data/bedrock/block_registry_aliases.json | 66 + @@ -427,7 +436,8 @@ Keep recipe-specific interpretation in its crafting consumer patch. .../storage/CustomBlockPackStorageTest.java | 31 + .../storage/VoxelShapeRegistryTest.java | 130 ++ .../storage/WorldPacketBufferTest.java | 230 ++++ - 63 files changed, 9525 insertions(+), 109 deletions(-) + .../protocol/task/KeepAliveTaskTest.java | 78 ++ + 65 files changed, 9611 insertions(+), 111 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemDescriptor.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemDescriptors.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemStates.java @@ -476,6 +486,7 @@ Keep recipe-specific interpretation in its crafting consumer patch. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CustomBlockPackStorageTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/VoxelShapeRegistryTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/WorldPacketBufferTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/task/KeepAliveTaskTest.java diff --git a/Data Asset Sources.md b/Data Asset Sources.md index 7912edc8..bd94d2dd 100644 @@ -4808,6 +4819,38 @@ index 00000000..3caaf771 + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/task/KeepAliveTask.java b/src/main/java/net/raphimc/viabedrock/protocol/task/KeepAliveTask.java +index 3f0dc0ae..c8b89e1d 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/task/KeepAliveTask.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/task/KeepAliveTask.java +@@ -21,6 +21,7 @@ import com.viaversion.viaversion.api.Via; + import com.viaversion.viaversion.api.connection.UserConnection; + import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; + import com.viaversion.viaversion.api.protocol.packet.State; ++import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; + import com.viaversion.viaversion.api.type.Types; + import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundConfigurationPackets26_3; + import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets26_3; +@@ -36,12 +37,17 @@ public class KeepAliveTask implements Runnable { + final State state = info.getProtocolInfo().getServerState(); + if ((state == State.PLAY || state == State.CONFIGURATION) && info.getProtocolInfo().getPipeline().contains(BedrockProtocol.class)) { + info.getChannel().eventLoop().submit(() -> { +- if (!info.getChannel().isActive()) { ++ final State currentState = info.getProtocolInfo().getServerState(); ++ if (!info.getChannel().isActive() ++ || (info.getProtocolInfo().protocolVersion().newerThanOrEqualTo(ProtocolVersion.v1_20_2) ++ && currentState != info.getProtocolInfo().getClientState()) ++ || (currentState != State.PLAY && currentState != State.CONFIGURATION) ++ || !info.getProtocolInfo().getPipeline().contains(BedrockProtocol.class)) { + return; + } + + try { +- final PacketWrapper keepAlive = PacketWrapper.create(info.getProtocolInfo().getServerState() == State.PLAY ? ClientboundPackets26_3.KEEP_ALIVE : ClientboundConfigurationPackets26_3.KEEP_ALIVE, info); ++ final PacketWrapper keepAlive = PacketWrapper.create(currentState == State.PLAY ? ClientboundPackets26_3.KEEP_ALIVE : ClientboundConfigurationPackets26_3.KEEP_ALIVE, info); + keepAlive.write(Types.LONG, ThreadLocalRandom.current().nextLong()); // id + keepAlive.send(BedrockProtocol.class); + } catch (final Throwable e) { diff --git a/src/main/resources/assets/viabedrock/data/bedrock/block_item_states.json b/src/main/resources/assets/viabedrock/data/bedrock/block_item_states.json new file mode 100644 index 00000000..450ca96e @@ -10940,5 +10983,89 @@ index 00000000..41138f38 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/task/KeepAliveTaskTest.java b/src/test/java/net/raphimc/viabedrock/protocol/task/KeepAliveTaskTest.java +new file mode 100644 +index 00000000..bdcf4c65 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/task/KeepAliveTaskTest.java +@@ -0,0 +1,78 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.task; ++ ++import com.viaversion.viaversion.api.Via; ++import com.viaversion.viaversion.api.ViaManager; ++import com.viaversion.viaversion.api.connection.UserConnection; ++import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; ++import com.viaversion.viaversion.api.protocol.packet.State; ++import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; ++import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundConfigurationPackets26_3; ++import io.netty.channel.Channel; ++import io.netty.channel.EventLoop; ++import net.raphimc.viabedrock.protocol.BedrockProtocol; ++import org.junit.jupiter.api.Test; ++ ++import java.util.ArrayDeque; ++import java.util.Queue; ++import java.util.Set; ++ ++import static org.mockito.Mockito.*; ++ ++class KeepAliveTaskTest { ++ ++ @Test ++ void sendsKeepAlivesOnlyAfterBothProtocolsAcknowledgeTheTransition() throws Exception { ++ final UserConnection user = mock(UserConnection.class, RETURNS_DEEP_STUBS); ++ final Channel channel = mock(Channel.class); ++ final EventLoop loop = mock(EventLoop.class); ++ final ViaManager manager = mock(ViaManager.class, RETURNS_DEEP_STUBS); ++ final PacketWrapper packet = mock(PacketWrapper.class); ++ final Queue pending = new ArrayDeque<>(); ++ when(user.getChannel()).thenReturn(channel); ++ when(user.getProtocolInfo().protocolVersion()).thenReturn(ProtocolVersion.v1_20_2); ++ when(channel.eventLoop()).thenReturn(loop); ++ when(channel.isActive()).thenReturn(true); ++ when(user.getProtocolInfo().getServerState()).thenReturn(State.CONFIGURATION); ++ when(user.getProtocolInfo().getClientState()).thenReturn(State.PLAY); ++ when(user.getProtocolInfo().getPipeline().contains(BedrockProtocol.class)).thenReturn(true); ++ when(manager.getConnectionManager().getConnections()).thenReturn(Set.of(user)); ++ doAnswer(call -> { ++ pending.add(call.getArgument(0)); ++ return null; ++ }).when(loop).submit(any(Runnable.class)); ++ try (var via = mockStatic(Via.class); ++ var packets = mockStatic(PacketWrapper.class)) { ++ via.when(Via::getManager).thenReturn(manager); ++ packets.when(() -> PacketWrapper.create(ClientboundConfigurationPackets26_3.KEEP_ALIVE, user)).thenReturn(packet); ++ final KeepAliveTask task = new KeepAliveTask(); ++ task.run(); ++ pending.remove().run(); ++ packets.verifyNoInteractions(); ++ ++ when(user.getProtocolInfo().getClientState()).thenReturn(State.CONFIGURATION); ++ task.run(); ++ pending.remove().run(); ++ verify(packet).send(BedrockProtocol.class); ++ ++ task.run(); ++ when(user.getProtocolInfo().getServerState()).thenReturn(State.PLAY); ++ pending.remove().run(); ++ verify(packet, times(1)).send(BedrockProtocol.class); ++ packets.verify(() -> PacketWrapper.create(ClientboundConfigurationPackets26_3.KEEP_ALIVE, user), times(1)); ++ packets.verifyNoMoreInteractions(); ++ ++ when(user.getProtocolInfo().protocolVersion()).thenReturn(ProtocolVersion.v1_20); ++ when(user.getProtocolInfo().getServerState()).thenReturn(State.CONFIGURATION); ++ when(user.getProtocolInfo().getClientState()).thenReturn(State.PLAY); ++ task.run(); ++ pending.remove().run(); ++ verify(packet, times(2)).send(BedrockProtocol.class); ++ } ++ } ++ ++} -- 2.56.0 diff --git a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md index 8679e1a8..e37d51d9 100644 --- a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md +++ b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md @@ -12,6 +12,10 @@ Resolve ordered permutations and preserve original native block identities. Assi Regression coverage includes `BlockItemDescriptorsTest`, `BlockItemStatesTest`, `BlockRegistryNamesTest`, `RuntimeBlockPaletteTest`. +`KeepAliveTaskTest` covers configuration acknowledgments, a transition after task scheduling, and clients without a configuration phase. +Synthetic keepalives wait for the direct client's outbound protocol to become ready during supplemental-pack reconfiguration. +The focused test passes. Installed Lifeboat retesting remains open. + Shape, state, culling, and conversion tests cover supported definitions. Carrier capacity, unsupported materials, and complete native appearance remain limits. The [owning patch](0054-render-native-block-state-geometry-and-materials.patch) retains implementation rationale and versioned evidence. From 5b650dd9a91810afcf11c09a0a3ee787c02677ee Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:17:43 +0100 Subject: [PATCH 03/33] fix(ui): accept Lifeboat authored form expressions Parse raw value tokens and redundant trailing closing groups without discarding the entire authored scene. Lifeboat uses both in its caption bindings, which previously forced the 23-button Navigator into Java dialog fallback. Preserve expression depth, output bounds, and original response identities. The complete build passes with 3152 tests and the installed add-on displays the authored Navigator. Extend private scene observations with the player block states and loaded palette identity to trace the separate spawn collision bug. --- ...are-native-ui-programs-and-resources.patch | 47 +++++++++++++------ ...are-native-ui-programs-and-resources.pr.md | 5 ++ .../replay/fabric/ReplaySceneDiagnostics.java | 40 ++++++++++++++++ 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch index cd5b148b..61865e74 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch @@ -1,4 +1,4 @@ -From 6f2e3e079e6260d4be5060fe2e4fe16a9b7c837e Mon Sep 17 00:00:00 2001 +From 801ba5dd8468bcd671796b1175fabc01ccf8ae32 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): share bounded native UI programs and resource @@ -371,6 +371,16 @@ rows with the same bindings and action identities. Reject windowed templates that redirect a cell root to another collection index, so viewport replacement cannot change the cell action identity. + +Accept raw value tokens and redundant closing groups in authored UI +binding expressions. The actual Lifeboat protocol-2225 Navigator request +has 23 buttons; its accepted UI pack uses unquoted formatting markers and +extra closing parentheses in caption predicates. Rejecting those inputs +aborts the whole scene and sends a Java dialog before the add-on renderer +receives it. Keep nested-group, input, string-output, and scene bounds. +The captured request resolves to a bounded scene with these grammar rules; +installed form presentation and full native equivalence remain separate +validation steps. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -413,7 +423,7 @@ index, so viewport replacement cannot change the cell action identity. .../resourcepack/ui/UiAsepriteMetadata.java | 95 + .../api/resourcepack/ui/UiBindings.java | 79 + .../api/resourcepack/ui/UiBitmapFonts.java | 282 +++ - .../api/resourcepack/ui/UiExpression.java | 308 +++ + .../api/resourcepack/ui/UiExpression.java | 306 +++ .../api/resourcepack/ui/UiFonts.java | 286 +++ .../api/resourcepack/ui/UiTextures.java | 94 + .../viabedrock/api/util/NativeSort.java | 168 ++ @@ -458,12 +468,12 @@ index, so viewport replacement cannot change the cell action identity. .../ui/UiAsepriteMetadataTest.java | 112 ++ .../resourcepack/ui/UiBitmapFontsTest.java | 285 +++ .../ui/UiCollectionWindowTest.java | 131 ++ - .../api/resourcepack/ui/UiExpressionTest.java | 68 + + .../api/resourcepack/ui/UiExpressionTest.java | 77 + .../api/resourcepack/ui/UiFontsTest.java | 192 ++ .../api/resourcepack/ui/UiTexturesTest.java | 175 ++ .../ui/native-inventory-item-filter.json | 311 ++++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 91 files changed, 14344 insertions(+), 60 deletions(-) + 91 files changed, 14351 insertions(+), 60 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerMessage.java @@ -7792,10 +7802,10 @@ index 00000000..f8051c4b +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpression.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpression.java new file mode 100644 -index 00000000..e243e4b7 +index 00000000..2cc280f6 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpression.java -@@ -0,0 +1,308 @@ +@@ -0,0 +1,306 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -7841,6 +7851,9 @@ index 00000000..e243e4b7 + final UiExpression parser = new UiExpression(input, values); + final JsonElement result = parser.expression(0); + parser.space(); ++ while (parser.take(')')) { ++ parser.space(); ++ } + if (parser.offset != input.length()) { + throw new IllegalArgumentException("Unexpected UI expression token"); + } @@ -7924,7 +7937,7 @@ index 00000000..e243e4b7 + final int start = this.offset; + while (this.offset < this.input.length()) { + final char character = this.input.charAt(this.offset); -+ if (!Character.isLetterOrDigit(character) && "_$#.:".indexOf(character) < 0) { ++ if (Character.isWhitespace(character) || "()=<>+-*/'".indexOf(character) >= 0) { + break; + } + this.offset++; @@ -7940,12 +7953,7 @@ index 00000000..e243e4b7 + final JsonElement value = this.values.apply(token); + return value == null ? JsonNull.INSTANCE : value; + } -+ try { -+ Double.parseDouble(token); -+ return generated(token); -+ } catch (final NumberFormatException invalid) { -+ throw new IllegalArgumentException("Unsupported UI expression value", invalid); -+ } ++ return generated(token); + } + + private String operator() { @@ -15466,10 +15474,10 @@ index 00000000..70718538 +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpressionTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpressionTest.java new file mode 100644 -index 00000000..f43ac37d +index 00000000..72cb7cd7 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpressionTest.java -@@ -0,0 +1,68 @@ +@@ -0,0 +1,77 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -15527,6 +15535,15 @@ index 00000000..f43ac37d + } + + @Test ++ void authoredCaptionPredicatesAcceptRawFormattingValuesAndTrailingGroups() { ++ final var values = Map.of("#caption", new JsonPrimitive("§a§rHeader")); ++ assertTrue(UiExpression.evaluate("(not((#caption - §a§r) = (#caption - ''))))", values).getAsBoolean()); ++ assertEquals("Header", UiExpression.evaluate("((#caption) - (§a§r))", values).getAsString()); ++ assertTrue(UiExpression.evaluate("(plain_value = plain_value)", values).getAsBoolean()); ++ assertFalse(UiExpression.evaluate("(plain_value = another_value)", values).getAsBoolean()); ++ } ++ ++ @Test + void boundsAndSyntaxCannotEscapeTheBindingEvaluator() { + assertThrows(IllegalArgumentException.class, () -> UiExpression.evaluate("(".repeat(65) + "1" + ")".repeat(65), Map.of())); + assertThrows(IllegalArgumentException.class, () -> UiExpression.evaluate("(1 / 0)", Map.of())); diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index 03974ba8..01c00eb4 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -41,3 +41,8 @@ The [owning patch](0095-share-native-ui-programs-and-resources.patch) retains im View bindings can read an authored `toggle_view_binding_name` alias. A regression checks section visibility when the destination has a different local toggle value. + +Authored bindings accept raw value tokens and redundant trailing closing groups. +The captured Lifeboat protocol-2225 Navigator request resolves with all 23 original response indices. +The add-on accepts that scene and shows the authored Navigator GUI in a live join. +The full build passes. Click routing, final typography, and complete native appearance remain separate validation steps. diff --git a/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/ReplaySceneDiagnostics.java b/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/ReplaySceneDiagnostics.java index e67de5f3..9ecd7ccc 100644 --- a/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/ReplaySceneDiagnostics.java +++ b/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/ReplaySceneDiagnostics.java @@ -1,6 +1,10 @@ package net.pistonmaster.stackanvil.replay.fabric; import com.viaversion.viaversion.libs.gson.Gson; +import com.viaversion.viaversion.api.Via; +import net.raphimc.viabedrock.api.model.BedrockBlockState; +import net.raphimc.viabedrock.protocol.BedrockProtocol; +import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; import java.nio.file.Files; import java.nio.file.Path; import net.pistonmaster.stackanvil.replay.PrivateFiles; @@ -83,6 +87,12 @@ static void sample(Object minecraft, Object player, Path directory, boolean iden Object below = world.getClass().getMethod("getBlockState", blockPosition).invoke(world, call(position, "below")); state.put("footIsAir", call(foot, "isAir")); state.put("belowIsAir", call(below, "isAir")); + state.put("footBlockState", foot.toString()); + state.put("belowBlockState", below.toString()); + Object above = call(position, "above"); + state.put("headBlockState", world.getClass().getMethod("getBlockState", blockPosition).invoke(world, above).toString()); + nativeBlocks(position, state); + nativePalette(state); int entities = 0; for (Object ignored : (Iterable) call(world, "entitiesForRendering")) entities++; state.put("loadedEntities", entities); @@ -100,6 +110,36 @@ private static Object call(Object target, String name) throws ReflectiveOperatio return target.getClass().getMethod(name).invoke(target); } + private static void nativeBlocks(Object position, Map state) throws ReflectiveOperationException { + Class blocks; + try { + blocks = Class.forName("com.viaversion.viafabricplus.bedrock.appearance.BedrockNativeBlocks"); + } catch (ClassNotFoundException ignored) { + return; + } + int x = ((Number) call(position, "getX")).intValue(); + int y = ((Number) call(position, "getY")).intValue(); + int z = ((Number) call(position, "getZ")).intValue(); + var lookup = blocks.getMethod("state", int.class, int.class, int.class, int.class); + state.put("nativeFootPrimary", String.valueOf(lookup.invoke(null, x, y, z, 0))); + state.put("nativeFootSecondary", String.valueOf(lookup.invoke(null, x, y, z, 1))); + state.put("nativeHeadPrimary", String.valueOf(lookup.invoke(null, x, y + 1, z, 0))); + } + + private static void nativePalette(Map state) { + for (var connection : Via.getManager().getConnectionManager().getConnections()) { + if (!connection.getChannel().isActive() + || !connection.getProtocolInfo().getPipeline().contains(BedrockProtocol.class)) continue; + BlockStateRewriter blocks = connection.get(BlockStateRewriter.class); + if (blocks == null) continue; + state.put("nativeAirRuntimeId", blocks.bedrockId(BedrockBlockState.AIR)); + state.put("nativeVanillaPaletteSize", BedrockProtocol.MAPPINGS.getBedrockBlockStates().size()); + state.put("coreCodeSource", BedrockProtocol.class.getProtectionDomain().getCodeSource().getLocation().toString()); + state.put("corePaletteResource", String.valueOf(BedrockProtocol.class.getResource("/assets/viabedrock/data/bedrock/block_palette.nbt"))); + break; + } + } + private static void nativeLightmap(Object renderer, Map state) throws ReflectiveOperationException { Class bridge; try { From b3f6cc310bda6ab80a4a531b2689c1bbe138bc1c Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:29:22 +0100 Subject: [PATCH 04/33] docs(ui): record verified Lifeboat form navigation --- .../0095-share-native-ui-programs-and-resources.pr.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index 01c00eb4..6fc5c88b 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -45,4 +45,6 @@ A regression checks section visibility when the destination has a different loca Authored bindings accept raw value tokens and redundant trailing closing groups. The captured Lifeboat protocol-2225 Navigator request resolves with all 23 original response indices. The add-on accepts that scene and shows the authored Navigator GUI in a live join. -The full build passes. Click routing, final typography, and complete native appearance remain separate validation steps. +A live add-on check opens Mini Game Selector, returns through Back, and closes through Escape. +The journal retains the original response indices 0 and 8 and the user-close reason. +The full build and fresh-clone replay checks pass. Final typography and complete native appearance remain unverified. From 9c9ad66bc188d9192ecb46ceb2c98d29281f5942 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:21:13 +0100 Subject: [PATCH 05/33] fix(inventory): keep Creative catalog cells active Use the current effective Creative mode for published cell permission while retaining recipe availability and captured search policies. Native 1.26.60.5 controls bypass recipe-disabled colors in Creative mode, and the snapshot regression restores recipe restrictions after returning to Survival. The full build and fresh core stack replay pass. Live icon backgrounds are corrected; successful catalog request dispatch remains under investigation. --- ...ain-bedrock-creative-catalog-entries.patch | 67 ++++++++++++++++--- ...ain-bedrock-creative-catalog-entries.pr.md | 9 +++ 2 files changed, 68 insertions(+), 8 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 b20abc78..002deefd 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 6e667d7fc79c738b7d4ccf52660435bee5fefb3e Mon Sep 17 00:00:00 2001 +From 072fe85252faeea27e4dee5ae5c4e285adf64941 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 @@ -226,6 +226,19 @@ subsequent ranges follow the real viewport rather than removing catalog rows or changing search results. Other callers keep full expansion. A 2,000-cell Creative source regression checks distant ranges, actual menu slots and exported-program replay without replacing the source model. + +Keep Creative catalog cells active independently of recipe ingredients. +Bedrock 1.26.60.5 background callback 14506bb30 bypasses the disabled +recipe gate in Creative mode. A matching protocol-2225 native capture +selects an unavailable trapped-chest recipe through CraftCreative, then +takes its stack to the cursor. Applying the collection's recipe-disabled +flag to published Creative cells colored every uncraftable entry red and +rejected the click before it could reach that request path. + +Use the current effective raw game mode for cell permission while leaving +retained availability, captured search policies, row identities and source +counts intact. A snapshot regression checks Creative cells with no recipe +ingredients and restores recipe gating after returning to Survival. --- .../model/NativePlayerInventoryAction.java | 88 ++++ .../model/NativePlayerInventoryMessage.java | 271 ++++++++++ @@ -251,12 +264,12 @@ slots and exported-program replay without replacing the source model. .../ui/PlayerInventoryUiTest.java | 126 +++++ .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++++++ .../protocol/packet/InventoryPacketsTest.java | 267 ++++++++-- - .../rewriter/PlayerRecipeModelTest.java | 412 +++++++++++++++ + .../rewriter/PlayerRecipeModelTest.java | 450 +++++++++++++++++ ...egisteredPlayerRecipeAvailabilityTest.java | 5 +- .../RegisteredPlayerRecipeSearchTest.java | 3 +- .../storage/CreativeInventoryActionTest.java | 214 ++++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ - 29 files changed, 4101 insertions(+), 170 deletions(-) + 29 files changed, 4139 insertions(+), 170 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 @@ -2659,7 +2672,7 @@ index 04ad1756..4a6f0c14 100644 private void updateInventoryOptions() { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java new file mode 100644 -index 00000000..255ff6db +index 00000000..94c66ad6 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java @@ -0,0 +1,471 @@ @@ -3026,6 +3039,7 @@ index 00000000..255ff6db + final var recipes = this.user().get(CraftingDataTracker.class); + final var resources = this.user().get(ResourcePackStorage.class); + final var catalog = items.creativeCatalog(); ++ final boolean creative = GameTypeRewriter.isCreativeMode(this.inputs.gameType, this.inputs.levelGameType); + final var counts = new ArrayList(); + for (final var view : List.of(CreativeInventoryModel.View.CONSTRUCTION, CreativeInventoryModel.View.EQUIPMENT, + CreativeInventoryModel.View.ITEMS, CreativeInventoryModel.View.NATURE)) { @@ -3055,7 +3069,7 @@ index 00000000..255ff6db + final int id = cells.size(); + final int modifier = row instanceof RecipeCollection.Header header ? header.expanded() ? 2 : 1 : 0; + final int source = entry != null && entry.catalogEntry() != null ? entry.catalogEntry().netId() : 0; -+ final boolean disabled = collection.disabled(id, new RecipeCollection.Eligibility() { ++ final boolean disabled = !creative && collection.disabled(id, new RecipeCollection.Eligibility() { + @Override + public int evaluate(final RecipeCollection.Row ignored) { + return state; @@ -3084,7 +3098,6 @@ index 00000000..255ff6db + "#container_item_background_texture", new JsonPrimitive(texture)))); + } + } -+ final boolean creative = GameTypeRewriter.isCreativeMode(this.inputs.gameType, this.inputs.levelGameType); + final boolean filtering = this.preferences == null || this.preferences.filtering(); + final String key = switch (tab) { + case 1 -> "craftingScreen.tab.construction"; @@ -4374,10 +4387,10 @@ index 19fe63ab..8cc84c93 100644 } diff --git a/src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java new file mode 100644 -index 00000000..90b84689 +index 00000000..c5f9da3a --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java -@@ -0,0 +1,412 @@ +@@ -0,0 +1,450 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -4397,6 +4410,8 @@ index 00000000..90b84689 +import net.raphimc.viabedrock.protocol.model.InventoryOptions; +import net.raphimc.viabedrock.protocol.storage.InventoryOptionsStorage; +import net.raphimc.viabedrock.protocol.storage.NativePlayerRecipeState; ++import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; ++import net.raphimc.viabedrock.api.resourcepack.ui.PlayerInventoryUi; +import net.raphimc.viabedrock.protocol.storage.PlayerInventoryScreenTracker; +import net.raphimc.viabedrock.api.resourcepack.ui.RecipeCollection; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.AbilitiesIndex; @@ -4462,6 +4477,42 @@ index 00000000..90b84689 + } + + @Test ++ void creativeCatalogCellsRemainActiveWithoutCraftingIngredients() throws Exception { ++ final var f = installed(true, false); ++ final var user = f.context.items.user(); ++ final var player = user.get(EntityTracker.class).getClientPlayer(); ++ final var session = mock(GameSessionStorage.class); ++ when(user.get(GameSessionStorage.class)).thenReturn(session); ++ when(session.getLevelGameType()).thenReturn(GameType.Survival); ++ when(player.gameType()).thenReturn(GameType.Creative); ++ final var resources = mock(ResourcePackStorage.class, RETURNS_DEEP_STUBS); ++ when(user.get(ResourcePackStorage.class)).thenReturn(resources); ++ when(resources.getTexts().translate(anyString())).thenAnswer(call -> call.getArgument(0)); ++ doReturn(javaItem()).when(f.context.items).javaItem(any(BedrockItem.class)); ++ user.get(CraftingDataTracker.class).updateCraftingDataList(List.of(), true); ++ f.context.inventory.inventory().getInventoryContainer().setItem(9, BedrockItem.empty()); ++ f.screens.request(new PlayerInventoryScreenRequest(1, true)); ++ for (int tick = 0; tick < 20; tick++) { ++ f.state.tick(); ++ } ++ final var snapshot = NativePlayerRecipeState.class.getDeclaredMethod("snapshot"); ++ snapshot.setAccessible(true); ++ final var presentation = snapshot.invoke(f.state); ++ final var state = presentation.getClass().getDeclaredMethod("state"); ++ state.setAccessible(true); ++ final var creative = (PlayerInventoryUi.State) state.invoke(presentation); ++ assertFalse(creative.cells().isEmpty()); ++ assertTrue(creative.cells().stream().allMatch(cell -> !cell.disabled())); ++ assertEquals("textures/ui/recipe_book_item_bg", creative.cells().getFirst().presentation() ++ .get("#container_item_background_texture").getAsString()); ++ when(player.gameType()).thenReturn(GameType.Survival); ++ f.state.tick(); ++ final var survival = (PlayerInventoryUi.State) state.invoke(snapshot.invoke(f.state)); ++ assertTrue(survival.cells().stream().allMatch(PlayerInventoryUi.Cell::disabled)); ++ assertEquals(creative.cells().getFirst().availability(), survival.cells().getFirst().availability()); ++ } ++ ++ @Test + void rawChangesClearOwnedSelectionAndImmediatelyRefreshOnlySearch() throws Exception { + final var context = context(false, false); + final var model = context.model; diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index 18b34ff2..d216752c 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -102,3 +102,12 @@ These controls do not execute the complete native cache wrapper or establish ins Five lifecycle regression cases exercise pending source changes, retained search policies, stale acknowledgments, profile/catalog updates, and same-object workbench reopening. These tests establish Java ownership and observable collection state; they do not establish complete native screen parity. + +Creative catalog cells use the current effective game mode for permission. +They remain active without recipe ingredients, while retained availability and captured search policies stay unchanged. +The Bedrock 1.26.60.5 background callback bypasses recipe-disabled colors in Creative. +A protocol-2225 native capture selects a trapped chest through CraftCreative and takes the resulting stack to the cursor. +Current native background controls cover explicit and inherited Creative modes. +A snapshot regression checks active Creative cells without ingredients and restores recipe restrictions in Survival. +The full build passes with 3,153 passing tests and 325 skipped tests. Fresh-clone replay passes for all 105 core patches. +A live add-on check removes the red disabled backgrounds. Catalog selection still sends no item request and needs further action-path work. From 82132ee6ae29757ba89cd96ef188517d13a85020 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:57:11 +0100 Subject: [PATCH 06/33] fix(blocks): honor server-defined vanilla registrations Exclude data-driven vanilla block types that StartGame does not advertise. Keeping their states unconditionally shifts Lifeboat runtime IDs and turns empty spawn cells into solid mushroom stems. Fold the fix into the owning block patch with versioned registry metadata and targeted regression coverage. Native captures and live registries match all 22,543 shared state IDs after filtering 1,417 unadvertised states. Installed add-on and ViaProxy checks render the lobby, walk through spawn, and open the authored Navigator. The full build passes with 3,156 passing tests and 325 skipped tests. A fresh replay applies all 105 core patches. --- ...e-block-state-geometry-and-materials.patch | 313 +++++++++++++++++- ...e-block-state-geometry-and-materials.pr.md | 9 +- ...rate-authored-native-ui-presentation.patch | 6 +- 3 files changed, 310 insertions(+), 18 deletions(-) diff --git a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch index ded14ac3..1a9bc354 100644 --- a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch +++ b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch @@ -1,4 +1,4 @@ -From dab41b4ae2e767e90f88af2ed251d19d63a63bd3 Mon Sep 17 00:00:00 2001 +From 274a231e0ae5d84e4b5772a8848f6c4824e619e7 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(blocks): render Bedrock state geometry and materials @@ -371,6 +371,22 @@ then produces an EncoderException when it replies. Recheck states on the connection event loop, and retain translated configuration keepalives for Java versions without a configuration acknowledgment. A queued-transition regression covers suppression, acknowledgment, and legacy compatibility. + +Register data-driven vanilla blocks only when StartGame advertises their +definitions. The complete 2225 palette contains 98 such block types with +1,417 states, but they are not unconditional builtin registrations. BDS +1.26.60.6 advertises all 98; Lifeboat advertises only its own 17 block types. +Keeping unadvertised states shifts air from native runtime ID 17713 to +18894 and maps empty spawn cells to solid mushroom stems. + +Compare official 1.26.60.5 (protocol 2225) traffic and live block registries +on both servers. All 22,543 shared state IDs match after removing exactly +those unadvertised types. Native and translated Lifeboat routes receive +byte-identical spawn chunks. Read-only Ghidra analysis of the matching +client traces StartGame, LevelChunk and registry enumeration; the live +registry comparison establishes the registration boundary. Preserve the +canonical state objects, hashes and order for advertised vanilla types. +Keep server-specific packets and process observations private. --- Data Asset Sources.md | 27 + .../chunk/blockstate/BlockItemDescriptor.java | 29 + @@ -378,6 +394,7 @@ regression covers suppression, acknowledgment, and legacy compatibility. .../api/chunk/blockstate/BlockItemStates.java | 134 ++ .../chunk/blockstate/BlockRegistryNames.java | 145 +++ .../chunk/blockstate/RuntimeBlockPalette.java | 68 + + .../chunk/blockstate/VanillaBlockStates.java | 60 + .../api/model/CustomBlockCulling.java | 206 +++ .../api/model/CustomBlockFaceLighting.java | 98 ++ .../api/model/CustomBlockLighting.java | 59 + @@ -397,7 +414,7 @@ regression covers suppression, acknowledgment, and legacy compatibility. .../protocol/packet/JoinPackets.java | 31 +- .../protocol/packet/ResourcePackPackets.java | 47 +- .../protocol/packet/VoxelShapePackets.java | 35 + - .../protocol/rewriter/BlockStateRewriter.java | 270 ++-- + .../protocol/rewriter/BlockStateRewriter.java | 273 ++-- .../rewriter/ResourcePackRewriter.java | 2 + .../CustomBlockCullingCompiler.java | 162 +++ .../resourcepack/CustomBlockModel.java | 587 +++++++++ @@ -413,10 +430,12 @@ regression covers suppression, acknowledgment, and legacy compatibility. .../data/bedrock/block_property_values.json | 979 ++++++++++++++ .../data/bedrock/block_registry_aliases.json | 66 + .../bedrock/block_restriction_groups.json | 549 ++++++++ + .../data/bedrock/server_defined_blocks.json | 105 ++ .../blockstate/BlockItemDescriptorsTest.java | 119 ++ .../chunk/blockstate/BlockItemStatesTest.java | 135 ++ .../blockstate/BlockRegistryNamesTest.java | 107 ++ .../blockstate/RuntimeBlockPaletteTest.java | 61 + + .../blockstate/VanillaBlockStatesTest.java | 78 ++ .../api/model/CustomBlockCullingTest.java | 54 + .../api/model/CustomBlockLightingTest.java | 171 +++ .../api/model/CustomBlockOffsetTest.java | 80 ++ @@ -437,12 +456,13 @@ regression covers suppression, acknowledgment, and legacy compatibility. .../storage/VoxelShapeRegistryTest.java | 130 ++ .../storage/WorldPacketBufferTest.java | 230 ++++ .../protocol/task/KeepAliveTaskTest.java | 78 ++ - 65 files changed, 9611 insertions(+), 111 deletions(-) + 68 files changed, 9856 insertions(+), 112 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemDescriptor.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemDescriptors.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemStates.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockRegistryNames.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/RuntimeBlockPalette.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStates.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/CustomBlockCulling.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/CustomBlockFaceLighting.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/CustomBlockLighting.java @@ -465,10 +485,12 @@ regression covers suppression, acknowledgment, and legacy compatibility. create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/block_property_values.json create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/block_registry_aliases.json create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/block_restriction_groups.json + create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/server_defined_blocks.json create mode 100644 src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemDescriptorsTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemStatesTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockRegistryNamesTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/RuntimeBlockPaletteTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStatesTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/CustomBlockCullingTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/CustomBlockLightingTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/CustomBlockOffsetTest.java @@ -1077,6 +1099,72 @@ index 00000000..72d7bd19 + public record Registration(int identifier, BedrockBlockState state) { } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStates.java b/src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStates.java +new file mode 100644 +index 00000000..0b101cc2 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStates.java +@@ -0,0 +1,60 @@ ++/* ++ * 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.chunk.blockstate; ++ ++import com.viaversion.viaversion.libs.gson.JsonParser; ++import net.raphimc.viabedrock.api.model.BedrockBlockState; ++import net.raphimc.viabedrock.protocol.model.BlockProperties; ++ ++import java.io.IOException; ++import java.io.InputStreamReader; ++import java.nio.charset.StandardCharsets; ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Locale; ++import java.util.Set; ++ ++public final class VanillaBlockStates { ++ ++ private static final Set SERVER_DEFINED = loadServerDefined(); ++ ++ private VanillaBlockStates() { } ++ ++ /** Data-driven vanilla blocks exist only when StartGame registers their definitions. */ ++ public static List forServer(final Collection canonical, final BlockProperties[] definitions) { ++ final Set registered = new HashSet<>(); ++ for (final BlockProperties definition : definitions) { ++ if (definition.legacyId() != null) { ++ registered.add(definition.name().toLowerCase(Locale.ROOT)); ++ } ++ } ++ final List states = new ArrayList<>(canonical.size()); ++ for (final BedrockBlockState state : canonical) { ++ if (!SERVER_DEFINED.contains(state.namespacedIdentifier()) || registered.contains(state.namespacedIdentifier())) { ++ states.add(state); ++ } ++ } ++ return states; ++ } ++ ++ private static Set loadServerDefined() { ++ final String path = "/assets/viabedrock/data/bedrock/server_defined_blocks.json"; ++ try (var input = VanillaBlockStates.class.getResourceAsStream(path)) { ++ if (input == null) { ++ throw new IllegalStateException("Missing server-defined vanilla block registry"); ++ } ++ final var json = JsonParser.parseReader(new InputStreamReader(input, StandardCharsets.UTF_8)).getAsJsonObject(); ++ final Set names = new HashSet<>(); ++ json.getAsJsonArray("blocks").forEach(name -> names.add(name.getAsString())); ++ return Set.copyOf(names); ++ } catch (final IOException error) { ++ throw new IllegalStateException("Failed to load server-defined vanilla block registry", error); ++ } ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/CustomBlockCulling.java b/src/main/java/net/raphimc/viabedrock/api/model/CustomBlockCulling.java new file mode 100644 index 00000000..f01a834b @@ -2713,10 +2801,10 @@ index 00000000..9c9d6637 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockStateRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockStateRewriter.java -index 10854c61..ac5ab7a0 100644 +index 10854c61..04549d49 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockStateRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockStateRewriter.java -@@ -17,23 +17,31 @@ +@@ -17,23 +17,32 @@ */ package net.raphimc.viabedrock.protocol.rewriter; @@ -2738,6 +2826,7 @@ index 10854c61..ac5ab7a0 100644 import net.raphimc.viabedrock.api.model.BlockState; +import net.raphimc.viabedrock.api.chunk.blockstate.BlockRegistryNames; +import net.raphimc.viabedrock.api.chunk.blockstate.RuntimeBlockPalette; ++import net.raphimc.viabedrock.api.chunk.blockstate.VanillaBlockStates; +import net.raphimc.viabedrock.api.model.CustomBlockVisual; +import net.raphimc.viabedrock.api.resourcepack.definition.CustomBlockDefinition; import net.raphimc.viabedrock.api.util.BlockStateHasher; @@ -2752,7 +2841,7 @@ index 10854c61..ac5ab7a0 100644 import java.util.*; import java.util.logging.Level; -@@ -43,12 +51,18 @@ public class BlockStateRewriter implements StorableObject { +@@ -43,23 +52,41 @@ public class BlockStateRewriter implements StorableObject { private final Int2IntMap blockStateIdMappings = new Int2IntOpenHashMap(); // Bedrock -> Java private final Int2IntMap legacyBlockStateIdMappings = new Int2IntOpenHashMap(); // Bedrock -> Bedrock @@ -2773,7 +2862,10 @@ index 10854c61..ac5ab7a0 100644 this.blockStateIdMappings.defaultReturnValue(-1); this.legacyBlockStateIdMappings.defaultReturnValue(-1); -@@ -58,8 +72,20 @@ public class BlockStateRewriter implements StorableObject { +- final List bedrockBlockStates = new ArrayList<>(BedrockProtocol.MAPPINGS.getBedrockBlockStates()); ++ final List bedrockBlockStates = VanillaBlockStates.forServer(BedrockProtocol.MAPPINGS.getBedrockBlockStates(), blockProperties); + final Map javaBlockStates = BedrockProtocol.MAPPINGS.getJavaBlockStates(); + final Map bedrockToJavaBlockStates = BedrockProtocol.MAPPINGS.getBedrockToJavaBlockStates(); final Map blockTags = BedrockProtocol.MAPPINGS.getBedrockCustomBlockTags(); final Set bedrockBlockIdentifiers = bedrockBlockStates.stream().map(BedrockBlockState::namespacedIdentifier).collect(Collectors.toSet()); final List customBlockStates = new ArrayList<>(); @@ -2795,7 +2887,7 @@ index 10854c61..ac5ab7a0 100644 for (BlockProperties blockProperty : blockProperties) { final String identifier = blockProperty.name().toLowerCase(Locale.ROOT); if (bedrockBlockIdentifiers.contains(identifier)) { -@@ -67,102 +93,106 @@ public class BlockStateRewriter implements StorableObject { +@@ -67,102 +94,106 @@ public class BlockStateRewriter implements StorableObject { } if (!effectiveBlockProperties.containsKey(identifier)) { @@ -2978,7 +3070,7 @@ index 10854c61..ac5ab7a0 100644 this.validBlockStates.computeIfAbsent(bedrockBlockState.namespacedIdentifier(), k -> new IntLinkedOpenHashSet()).add(bedrockId); if (blockTags.containsKey(bedrockBlockState.namespacedIdentifier())) { -@@ -172,6 +202,8 @@ public class BlockStateRewriter implements StorableObject { +@@ -172,6 +203,8 @@ public class BlockStateRewriter implements StorableObject { if (bedrockToJavaBlockStates.containsKey(bedrockBlockState)) { final int javaId = javaBlockStates.get(bedrockToJavaBlockStates.get(bedrockBlockState)); this.blockStateIdMappings.put(bedrockId, javaId); @@ -2987,7 +3079,7 @@ index 10854c61..ac5ab7a0 100644 } else { ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Missing bedrock -> java block state mapping: " + bedrockBlockState.toBlockStateString()); final int javaId = javaBlockStates.get(bedrockToJavaBlockStates.get(BedrockBlockState.INFO_UPDATE)); -@@ -186,10 +218,37 @@ public class BlockStateRewriter implements StorableObject { +@@ -186,10 +219,37 @@ public class BlockStateRewriter implements StorableObject { continue; // Dirty hack Mojang did in 1.12. Can be ignored safely as those values can't be used in chunk packets. } @@ -3026,7 +3118,7 @@ index 10854c61..ac5ab7a0 100644 } public int bedrockId(final CompoundTag bedrockBlockStateTag) { -@@ -206,11 +265,37 @@ public class BlockStateRewriter implements StorableObject { +@@ -206,11 +266,37 @@ public class BlockStateRewriter implements StorableObject { } public int bedrockId(final BlockState bedrockBlockState) { @@ -3066,7 +3158,7 @@ index 10854c61..ac5ab7a0 100644 } public int bedrockId(final int legacyBlockStateId) { -@@ -233,6 +318,15 @@ public class BlockStateRewriter implements StorableObject { +@@ -233,6 +319,15 @@ public class BlockStateRewriter implements StorableObject { }; } @@ -3082,7 +3174,7 @@ index 10854c61..ac5ab7a0 100644 public int waterlog(final int javaBlockStateId) { if (BedrockProtocol.MAPPINGS.getJavaPreWaterloggedBlockStates().contains(javaBlockStateId)) { return javaBlockStateId; -@@ -250,4 +344,8 @@ public class BlockStateRewriter implements StorableObject { +@@ -250,4 +345,8 @@ public class BlockStateRewriter implements StorableObject { return this.blockStateTags.get(bedrockBlockStateId); } @@ -7590,6 +7682,117 @@ index 00000000..0343c445 + "minecraft:piglin_head" + ] +} +diff --git a/src/main/resources/assets/viabedrock/data/bedrock/server_defined_blocks.json b/src/main/resources/assets/viabedrock/data/bedrock/server_defined_blocks.json +new file mode 100644 +index 00000000..c5825696 +--- /dev/null ++++ b/src/main/resources/assets/viabedrock/data/bedrock/server_defined_blocks.json +@@ -0,0 +1,105 @@ ++{ ++ "format": 1, ++ "protocol": 2225, ++ "gameVersion": "1.26.60.5", ++ "blocks": [ ++ "minecraft:black_concrete_double_slab", ++ "minecraft:black_concrete_slab", ++ "minecraft:black_concrete_stairs", ++ "minecraft:black_wool_double_slab", ++ "minecraft:black_wool_slab", ++ "minecraft:black_wool_stairs", ++ "minecraft:blue_concrete_double_slab", ++ "minecraft:blue_concrete_slab", ++ "minecraft:blue_concrete_stairs", ++ "minecraft:blue_wool_double_slab", ++ "minecraft:blue_wool_slab", ++ "minecraft:blue_wool_stairs", ++ "minecraft:brown_concrete_double_slab", ++ "minecraft:brown_concrete_slab", ++ "minecraft:brown_concrete_stairs", ++ "minecraft:brown_wool_double_slab", ++ "minecraft:brown_wool_slab", ++ "minecraft:brown_wool_stairs", ++ "minecraft:cyan_concrete_double_slab", ++ "minecraft:cyan_concrete_slab", ++ "minecraft:cyan_concrete_stairs", ++ "minecraft:cyan_wool_double_slab", ++ "minecraft:cyan_wool_slab", ++ "minecraft:cyan_wool_stairs", ++ "minecraft:gray_concrete_double_slab", ++ "minecraft:gray_concrete_slab", ++ "minecraft:gray_concrete_stairs", ++ "minecraft:gray_wool_double_slab", ++ "minecraft:gray_wool_slab", ++ "minecraft:gray_wool_stairs", ++ "minecraft:green_concrete_double_slab", ++ "minecraft:green_concrete_slab", ++ "minecraft:green_concrete_stairs", ++ "minecraft:green_wool_double_slab", ++ "minecraft:green_wool_slab", ++ "minecraft:green_wool_stairs", ++ "minecraft:light_blue_concrete_double_slab", ++ "minecraft:light_blue_concrete_slab", ++ "minecraft:light_blue_concrete_stairs", ++ "minecraft:light_blue_wool_double_slab", ++ "minecraft:light_blue_wool_slab", ++ "minecraft:light_blue_wool_stairs", ++ "minecraft:light_gray_concrete_double_slab", ++ "minecraft:light_gray_concrete_slab", ++ "minecraft:light_gray_concrete_stairs", ++ "minecraft:light_gray_wool_double_slab", ++ "minecraft:light_gray_wool_slab", ++ "minecraft:light_gray_wool_stairs", ++ "minecraft:lime_concrete_double_slab", ++ "minecraft:lime_concrete_slab", ++ "minecraft:lime_concrete_stairs", ++ "minecraft:lime_wool_double_slab", ++ "minecraft:lime_wool_slab", ++ "minecraft:lime_wool_stairs", ++ "minecraft:magenta_concrete_double_slab", ++ "minecraft:magenta_concrete_slab", ++ "minecraft:magenta_concrete_stairs", ++ "minecraft:magenta_wool_double_slab", ++ "minecraft:magenta_wool_slab", ++ "minecraft:magenta_wool_stairs", ++ "minecraft:orange_concrete_double_slab", ++ "minecraft:orange_concrete_slab", ++ "minecraft:orange_concrete_stairs", ++ "minecraft:orange_wool_double_slab", ++ "minecraft:orange_wool_slab", ++ "minecraft:orange_wool_stairs", ++ "minecraft:pink_concrete_double_slab", ++ "minecraft:pink_concrete_slab", ++ "minecraft:pink_concrete_stairs", ++ "minecraft:pink_wool_double_slab", ++ "minecraft:pink_wool_slab", ++ "minecraft:pink_wool_stairs", ++ "minecraft:purple_concrete_double_slab", ++ "minecraft:purple_concrete_slab", ++ "minecraft:purple_concrete_stairs", ++ "minecraft:purple_wool_double_slab", ++ "minecraft:purple_wool_slab", ++ "minecraft:purple_wool_stairs", ++ "minecraft:red_concrete_double_slab", ++ "minecraft:red_concrete_slab", ++ "minecraft:red_concrete_stairs", ++ "minecraft:red_shrub", ++ "minecraft:red_wool_double_slab", ++ "minecraft:red_wool_slab", ++ "minecraft:red_wool_stairs", ++ "minecraft:shelf_mushroom", ++ "minecraft:white_concrete_double_slab", ++ "minecraft:white_concrete_slab", ++ "minecraft:white_concrete_stairs", ++ "minecraft:white_wool_double_slab", ++ "minecraft:white_wool_slab", ++ "minecraft:white_wool_stairs", ++ "minecraft:yellow_concrete_double_slab", ++ "minecraft:yellow_concrete_slab", ++ "minecraft:yellow_concrete_stairs", ++ "minecraft:yellow_wool_double_slab", ++ "minecraft:yellow_wool_slab", ++ "minecraft:yellow_wool_stairs" ++ ] ++} diff --git a/src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemDescriptorsTest.java b/src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemDescriptorsTest.java new file mode 100644 index 00000000..56bc85da @@ -8036,6 +8239,90 @@ index 00000000..0ce3c2bd + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStatesTest.java b/src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStatesTest.java +new file mode 100644 +index 00000000..06e861fe +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStatesTest.java +@@ -0,0 +1,78 @@ ++/* ++ * 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.chunk.blockstate; ++ ++import com.viaversion.nbt.io.NBTIO; ++import com.viaversion.nbt.limiter.TagLimiter; ++import com.viaversion.nbt.tag.CompoundTag; ++import net.raphimc.viabedrock.api.model.BedrockBlockState; ++import net.raphimc.viabedrock.api.model.BlockState; ++import net.raphimc.viabedrock.protocol.model.BlockProperties; ++import org.junit.jupiter.api.Test; ++ ++import java.io.DataInputStream; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.zip.GZIPInputStream; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class VanillaBlockStatesTest { ++ ++ @Test ++ void omittedNetworkDefinitionsKeepLifeboatAirAndTerrainIdsAlignedWithNative() throws Exception { ++ final var canonical = canonical(); ++ final var selected = VanillaBlockStates.forServer(canonical, new BlockProperties[0]); ++ final var runtime = new RuntimeBlockPalette(selected, false); ++ assertEquals(22543, selected.size()); ++ assertEquals("minecraft:air", runtime.state(17713).namespacedIdentifier()); ++ assertEquals("minecraft:stone", runtime.state(3219).namespacedIdentifier()); ++ assertEquals(-1, runtime.identifier(BlockState.fromString("minecraft:red_shrub"))); ++ assertEquals(23960, canonical.size()); ++ } ++ ++ @Test ++ void advertisedVanillaDefinitionsRetainTheBdsPaletteAndHashedIdentity() throws Exception { ++ final var canonical = canonical(); ++ final BlockProperties[] definitions = canonical.stream().map(BedrockBlockState::namespacedIdentifier).distinct() ++ .map(name -> { ++ final CompoundTag properties = new CompoundTag(); ++ final CompoundTag vanilla = new CompoundTag(); ++ vanilla.putInt("block_id", 1); ++ properties.put("vanilla_block_data", vanilla); ++ return new BlockProperties(name, properties); ++ }).toArray(BlockProperties[]::new); ++ final var selected = VanillaBlockStates.forServer(canonical, definitions); ++ final var sequential = new RuntimeBlockPalette(selected, false); ++ assertEquals(canonical, selected); ++ assertEquals("minecraft:air", sequential.state(18894).namespacedIdentifier()); ++ assertEquals("minecraft:stone", sequential.state(3317).namespacedIdentifier()); ++ final var hashed = new RuntimeBlockPalette(selected, true); ++ final var filteredHashed = new RuntimeBlockPalette(VanillaBlockStates.forServer(canonical, new BlockProperties[0]), true); ++ final var air = sequential.state(18894); ++ assertEquals(hashed.registeredIdentifier(air), filteredHashed.registeredIdentifier(air)); ++ assertSame(air, filteredHashed.state(filteredHashed.registeredIdentifier(air))); ++ } ++ ++ @Test ++ void ignoredNetworkDefinitionsDoNotRegisterVanillaStates() throws Exception { ++ final var selected = VanillaBlockStates.forServer(canonical(), new BlockProperties[]{ ++ new BlockProperties("minecraft:red_shrub", new CompoundTag()) ++ }); ++ assertEquals("minecraft:air", new RuntimeBlockPalette(selected, false).state(17713).namespacedIdentifier()); ++ } ++ ++ private static List canonical() throws Exception { ++ try (var input = VanillaBlockStatesTest.class.getResourceAsStream("/assets/viabedrock/data/bedrock/block_palette.nbt"); ++ var data = new DataInputStream(new GZIPInputStream(input))) { ++ final var blocks = NBTIO.readTag(data, TagLimiter.noop(), true, CompoundTag.class).getListTag("blocks", CompoundTag.class); ++ final List states = new ArrayList<>(); ++ blocks.forEach(block -> states.add(BedrockBlockState.fromNbt(block))); ++ return states; ++ } ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/CustomBlockCullingTest.java b/src/test/java/net/raphimc/viabedrock/api/model/CustomBlockCullingTest.java new file mode 100644 index 00000000..d9e0da37 diff --git a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md index e37d51d9..b717268c 100644 --- a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md +++ b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md @@ -10,11 +10,16 @@ Resolve ordered permutations and preserve original native block identities. Assi ## Validation and remaining limits -Regression coverage includes `BlockItemDescriptorsTest`, `BlockItemStatesTest`, `BlockRegistryNamesTest`, `RuntimeBlockPaletteTest`. +Regression coverage includes `BlockItemDescriptorsTest`, `BlockItemStatesTest`, `BlockRegistryNamesTest`, `RuntimeBlockPaletteTest`, and `VanillaBlockStatesTest`. `KeepAliveTaskTest` covers configuration acknowledgments, a transition after task scheduling, and clients without a configuration phase. Synthetic keepalives wait for the direct client's outbound protocol to become ready during supplemental-pack reconfiguration. -The focused test passes. Installed Lifeboat retesting remains open. +The focused test passes. Installed Lifeboat testing passes without the outbound-protocol exception. + +Official Bedrock 1.26.60.5 captures identify 98 vanilla block types that require server definitions in `StartGame`. +BDS advertises these types; Lifeboat omits them. All 22,543 shared native state IDs match after excluding their 1,417 unadvertised states. +The palette now follows that registration boundary. Installed add-on and ViaProxy tests spawn in air and walk through the rendered lobby. +The authored Navigator opens on both routes. Full native appearance remains unverified. Shape, state, culling, and conversion tests cover supported definitions. Carrier capacity, unsupported materials, and complete native appearance remain limits. 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 9cf5afeb..9ad2ff55 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 f71d8a87098132d6e91b32a5f8be0033a9f71dff Mon Sep 17 00:00:00 2001 +From bb4bbfcb34c9f741206806019fc1f7e1ebb4832f 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 @@ -5459,7 +5459,7 @@ index 521161d5..a9fcc0c7 100644 writeRequest(wrapper, request); wrapper.sendToServer(BedrockProtocol.class); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockStateRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockStateRewriter.java -index ac5ab7a0..f8f3151b 100644 +index 04549d49..4554f290 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockStateRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/BlockStateRewriter.java @@ -28,6 +28,7 @@ import net.raphimc.viabedrock.api.chunk.blockstate.BlockStateSanitizer; @@ -5470,7 +5470,7 @@ index ac5ab7a0..f8f3151b 100644 import net.raphimc.viabedrock.api.model.BedrockBlockState; import net.raphimc.viabedrock.api.model.BlockState; import net.raphimc.viabedrock.api.chunk.blockstate.BlockRegistryNames; -@@ -290,6 +291,12 @@ public class BlockStateRewriter implements StorableObject { +@@ -291,6 +292,12 @@ public class BlockStateRewriter implements StorableObject { return this.blockItemStates.resolve(identifier, metadata); } From 0688428964c44d0e46f5c6760b1bf1b7a99f6308 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:51:27 +0100 Subject: [PATCH 07/33] fix(ui): preserve authored grid population and item limits Let explicit factories own their grid cells, resolve relative templates in the declaring namespace, and retain bound population limits. This removes the duplicated empty cells that clipped the Lifeboat Navigator into one column. The full stack build and fresh patch replays pass. Live Lifeboat checks show eight main controls in two columns and preserve World Games, Mini Games, SkyWars, and Solo selection responses. --- ...are-native-ui-programs-and-resources.patch | 87 +++++++++++++++---- ...are-native-ui-programs-and-resources.pr.md | 7 ++ 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch index 61865e74..2655dbfa 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch @@ -1,4 +1,4 @@ -From 801ba5dd8468bcd671796b1175fabc01ccf8ae32 Mon Sep 17 00:00:00 2001 +From f51d58dbbaba59cdf18d5bc000d5c701eb7e774f 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): share bounded native UI programs and resource @@ -381,6 +381,19 @@ receives it. Keep nested-group, input, string-output, and scene bounds. The captured request resolves to a bounded scene with these grammar rules; installed form presentation and full native equivalence remain separate validation steps. + +Let an authored grid factory own its collection population instead of +appending grid_item_template cells a second time. Resolve relative cell +templates in the defining namespace and honor the bound maximum item +count on both factory-backed and template-only grids. Native 1.26.60.5 +Lifeboat Navigator displays eight main controls in two columns. Its +protocol-2225 form has 23 button entries, and the authored grid binds a +maximum of eight. The old projection emits 23 real cells plus 23 empty +default-sized cells, forcing one column and clipping the main actions. +The reduced regression retains row text and authored dimensions without +duplicating the factory or exceeding its bound. Re-resolving the actual +captured pack produces eight cells instead of 46. Keep the raw pack and +traffic private; installed layout comparison remains a separate check. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -412,7 +425,7 @@ validation steps. .../api/resourcepack/ui/HudTitleUi.java | 28 + .../resourcepack/ui/InventoryItemFilter.java | 43 + .../resourcepack/ui/InventoryStackCount.java | 21 + - .../api/resourcepack/ui/JsonUiProgram.java | 1654 +++++++++++++++++ + .../api/resourcepack/ui/JsonUiProgram.java | 1671 +++++++++++++++++ .../resourcepack/ui/NativeRecipeSearch.java | 124 ++ .../ui/NativeSearchNormalization.java | 189 ++ .../api/resourcepack/ui/NpcDialogueUi.java | 316 ++++ @@ -456,7 +469,7 @@ validation steps. .../ui/FormCollectionActivationTest.java | 97 + .../api/resourcepack/ui/HudRootUiTest.java | 90 + .../ui/InventoryItemFilterTest.java | 33 + - .../resourcepack/ui/JsonUiProgramTest.java | 588 ++++++ + .../resourcepack/ui/JsonUiProgramTest.java | 615 ++++++ .../ui/NativeRecipeSearchTest.java | 110 ++ .../ui/NativeSearchNormalizationTest.java | 46 + .../resourcepack/ui/NpcDialogueUiTest.java | 108 ++ @@ -471,9 +484,9 @@ validation steps. .../api/resourcepack/ui/UiExpressionTest.java | 77 + .../api/resourcepack/ui/UiFontsTest.java | 192 ++ .../api/resourcepack/ui/UiTexturesTest.java | 175 ++ - .../ui/native-inventory-item-filter.json | 311 ++++ + .../ui/native-inventory-item-filter.json | 311 +++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 91 files changed, 14351 insertions(+), 60 deletions(-) + 91 files changed, 14395 insertions(+), 60 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerMessage.java @@ -3827,10 +3840,10 @@ index 00000000..5dfbb3f8 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java new file mode 100644 -index 00000000..acb78beb +index 00000000..61b577f1 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java -@@ -0,0 +1,1654 @@ +@@ -0,0 +1,1671 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -5000,13 +5013,22 @@ index 00000000..acb78beb + } else if (!childControls.isJsonNull()) { + throw new IllegalArgumentException("Invalid UI controls collection"); + } -+ if (control.has("factory") && control.get("factory").isJsonObject()) { -+ this.factory(control.getAsJsonObject("factory"), node, children, values, selectedScope, depth); ++ final boolean templateGrid = "grid".equals(string(control, "type")) && control.has("grid_item_template"); ++ final JsonObject factory = control.has("factory") && control.get("factory").isJsonObject() ? control.getAsJsonObject("factory") : null; ++ final boolean factoryOwnsCells = factory != null && (factory.has("control_name") ++ || factory.has("control_ids") && factory.get("control_ids").isJsonObject() && !factory.getAsJsonObject("control_ids").isEmpty()); ++ if (factory != null) { ++ if (templateGrid && factory.has("control_name")) { ++ node.addProperty("factory_name", UiExpression.text(resolveValue(factory.get("name"), values, 0))); ++ this.gridCollection(UiExpression.text(resolveValue(factory.get("control_name"), values, 0)), node, children, values, selectedScope, depth); ++ } else { ++ this.factory(factory, node, children, values, selectedScope, depth); ++ } + } + if ("factory".equals(string(control, "type")) && (control.has("control_ids") || control.has("control_name"))) { + this.factory(control, node, children, values, selectedScope, depth); + } -+ if ("grid".equals(string(control, "type")) && control.has("grid_item_template")) { ++ if (templateGrid && !factoryOwnsCells) { + this.gridCollection(UiExpression.text(resolveValue(control.get("grid_item_template"), values, 0)), node, children, values, selectedScope, depth); + } + node.add("children", children); @@ -5201,17 +5223,25 @@ index 00000000..acb78beb + this.collection(template, children, values, scope, collection, depth, null); + } + -+ private void gridCollection(final String template, final JsonObject node, final JsonArray children, ++ private void gridCollection(final String reference, final JsonObject node, final JsonArray children, + final Map values, final UiBindings.Scope scope, final int depth) { ++ final String namespace = UiExpression.text(values.get("$__namespace")); ++ final String template = reference.contains(".") || namespace.isEmpty() ? reference : namespace + "." + reference; + final String declaredCollection = string(node, "collection_name"); + final String collection = declaredCollection.isEmpty() ? this.bindings.defaultCollection() : declaredCollection; ++ Integer limit = node.has("collection_length") && !node.get("collection_length").isJsonNull() ++ ? collectionCount(node.get("collection_length")) : null; ++ if (node.has("maximum_grid_items") && !node.get("maximum_grid_items").isJsonNull()) { ++ limit = Math.min(limit == null ? this.collection(collection, scope).entries().size() : limit, ++ collectionCount(node.get("maximum_grid_items"))); ++ } + final String key = UiExpression.text(values.get("$__instance_path")); + final UiBindings.Window window = this.bindings.window(key, collection, this.controllerScope(scope)); + if (window == null) { -+ this.collection(template, children, values, scope, collection, depth); ++ this.collection(template, children, values, scope, collection, depth, limit); + return; + } -+ final List rows = this.collectionScopes(collection, scope, null); ++ final List rows = this.collectionScopes(collection, scope, limit); + if (rows.isEmpty()) { + return; + } @@ -12887,10 +12917,10 @@ index 00000000..bc575caa +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgramTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgramTest.java new file mode 100644 -index 00000000..707c97ff +index 00000000..c8beea74 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgramTest.java -@@ -0,0 +1,588 @@ +@@ -0,0 +1,615 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -12933,6 +12963,33 @@ index 00000000..707c97ff + } + + @Test ++ void aFactoryOwnsGridPopulationAndTheBoundMaximumLimitsItsRows() { ++ final var ui = program(""" ++ {"namespace":"example","root":{"type":"grid","collection_name":"first", ++ "grid_item_template":"cell","grid_rescaling_type":"horizontal", ++ "factory":{"name":"buttons","control_name":"example.cell"}, ++ "property_bag":{"#collection_length":2,"#maximum_grid_items":1}}, ++ "cell":{"type":"label","size":["50%-2px",34],"text":"#caption"}} ++ """); ++ final var rows = ui.resolve("example.root", new Bindings()).root().getAsJsonArray("children"); ++ assertEquals(1, rows.size()); ++ assertEquals("first-0", rows.get(0).getAsJsonObject().get("text").getAsString()); ++ assertEquals(JsonParser.parseString("[\"50%-2px\",34]"), rows.get(0).getAsJsonObject().get("size")); ++ } ++ ++ @Test ++ void templateGridsHonorTheBoundMaximumWithoutFactories() { ++ final var ui = program(""" ++ {"namespace":"example","root":{"type":"grid","collection_name":"first", ++ "grid_item_template":"cell","property_bag":{"#maximum_grid_items":1}}, ++ "cell":{"type":"label","text":"#caption"}} ++ """); ++ final var rows = ui.resolve("example.root", new Bindings()).root().getAsJsonArray("children"); ++ assertEquals(1, rows.size()); ++ assertEquals("first-0", rows.get(0).getAsJsonObject().get("text").getAsString()); ++ } ++ ++ @Test + void aViewBindingReadsTheNamedToggleAliasInsteadOfItsOwnPropertyBag() { + final var ui = program(""" + {"namespace":"example","root":{"type":"panel","controls":[ diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index 6fc5c88b..ac67044a 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -14,6 +14,13 @@ Prerequisites: ## Validation and remaining limits +Grid factories populate each collection once and respect the bound maximum item count. Relative cell templates use the defining namespace. +The captured Lifeboat Navigator now resolves eight main cells, matching native Bedrock 1.26.60.5, instead of 46 duplicated cells. +Reduced regressions cover factory ownership, template lookup, and item limits. +The rebuilt add-on shows two columns and four rows during a live join. +World Games, Mini Games, and SkyWars open through their authored controls. Selecting Solo reaches the waiting lobby. +The recorded replies retain the original button indices. Complete native appearance remains unverified. + Regression coverage includes `NativeContainerActionTest`, `NativeContainerDataTest`, `NativeContainerMessageTest`, `NativeCookingActionResultTest`. `InventoryStackCount` provides native count text separately from numeric binding quantities. From 8689ffffce8bde895b3e0d884bee4471f643ccb7 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:51:36 +0100 Subject: [PATCH 08/33] fix(inventory): admit the full creative cursor quantity Validate the requested cursor stack after applying its transfer count while keeping the unit craft result unchanged. The prior quantity check rejected a full stack against the one-item creative sample. Targeted regressions cover requests for one and 64 items. The installed add-on sends a 64-rail pick and hotbar placement that the Bedrock server accepts. --- ...ain-bedrock-creative-catalog-entries.patch | 105 ++++++++++++++++-- ...ain-bedrock-creative-catalog-entries.pr.md | 7 +- 2 files changed, 102 insertions(+), 10 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 002deefd..8beac5b1 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 072fe85252faeea27e4dee5ae5c4e285adf64941 Mon Sep 17 00:00:00 2001 +From 2c5cb530c5425af3e9621ed465a41b701c3fcffc 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 @@ -239,6 +239,17 @@ Use the current effective raw game mode for cell permission while leaving retained availability, captured search policies, row identities and source counts intact. A snapshot regression checks Creative cells with no recipe ingredients and restores recipe gating after returning to Survival. + +Admit Creative catalog picks with a cursor copy sized to the requested +stack before checking the destination. A unit catalog sample cannot +authorize a 64-item transfer because Container validates the whole source +quantity. Live protocol-2225 tracing reaches core with a current opening +and idle request tracker, then rejects precisely this count mismatch. +Keep the unit result for CraftCreative/CraftResults and use the separate +full stack only for destination admission and cursor prediction. This +preserves the official 1.26.60.5 captured unit-result/full-Take flow. +A request-boundary regression checks stack sizes one and 64, the packet +actions, cursor prediction and its pre-request restoration snapshot. --- .../model/NativePlayerInventoryAction.java | 88 ++++ .../model/NativePlayerInventoryMessage.java | 271 ++++++++++ @@ -256,7 +267,7 @@ ingredients and restores recipe gating after returning to Survival. .../protocol/storage/CraftingDataTracker.java | 129 ++++- .../storage/InventoryRequestTracker.java | 24 + .../protocol/storage/InventoryTracker.java | 21 + - .../storage/NativePlayerRecipeState.java | 471 ++++++++++++++++++ + .../storage/NativePlayerRecipeState.java | 475 ++++++++++++++++++ .../storage/PlayerInventoryScreenTracker.java | 40 +- .../NativePlayerInventoryMessageTest.java | 160 ++++++ .../container/NativeRecipeCounterTest.java | 27 + @@ -269,7 +280,8 @@ ingredients and restores recipe gating after returning to Survival. .../RegisteredPlayerRecipeSearchTest.java | 3 +- .../storage/CreativeInventoryActionTest.java | 214 ++++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ - 29 files changed, 4139 insertions(+), 170 deletions(-) + .../storage/NativePlayerCreativePickTest.java | 66 +++ + 30 files changed, 4209 insertions(+), 170 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 @@ -285,6 +297,7 @@ ingredients and restores recipe gating after returning to Survival. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CreativeInventoryActionTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java diff --git a/src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java b/src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java new file mode 100644 @@ -2672,10 +2685,10 @@ index 04ad1756..4a6f0c14 100644 private void updateInventoryOptions() { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java new file mode 100644 -index 00000000..94c66ad6 +index 00000000..6bdac2db --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java -@@ -0,0 +1,471 @@ +@@ -0,0 +1,475 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -3010,7 +3023,12 @@ index 00000000..94c66ad6 + } + final var result = entry.catalogEntry().bedrockItem().copy(); + final int count = Math.min(255, this.user().get(ItemRewriter.class).maxStackSize(result)); -+ if (count <= 0 || !hud.canPlaceItem(0, result, count)) { ++ if (count <= 0) { ++ return false; ++ } ++ final var cursor = result.copy(); ++ cursor.setAmount(count); ++ if (!hud.canPlaceItem(0, cursor, count)) { + return false; + } + final var requests = this.user().get(InventoryRequestTracker.class); @@ -3024,8 +3042,7 @@ index 00000000..94c66ad6 + requests.addRequest(new InventoryRequestStorage(request, 0, hud.copy(), List.of())); + requests.setPredictingClick(true); + try { -+ result.setAmount(count); -+ hud.setItem(0, result); ++ hud.setItem(0, cursor); + } finally { + requests.setPredictingClick(false); + } @@ -5206,5 +5223,77 @@ index b69f1868..037328f2 100644 @Test void successfulResponseUpdatesNameAndCountWithoutTreatingDurabilityCorrectionAsDamage() { final BedrockItem current = new BedrockItem(276); +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java +new file mode 100644 +index 00000000..746f0b54 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java +@@ -0,0 +1,66 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.storage; ++ ++import com.viaversion.viaversion.api.connection.UserConnection; ++import net.raphimc.viabedrock.api.model.container.player.HudContainer; ++import net.raphimc.viabedrock.api.util.PacketFactory; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.CreativeCatalog; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; ++import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.ValueSource; ++import org.mockito.ArgumentCaptor; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class NativePlayerCreativePickTest { ++ ++ @ParameterizedTest ++ @ValueSource(ints = {1, 64}) ++ void takesTheFullCreativeStackWhileKeepingTheCraftResultAtOne(final int maximum) throws Exception { ++ final var user = mock(UserConnection.class); ++ final var inventory = mock(InventoryTracker.class); ++ final var requests = mock(InventoryRequestTracker.class); ++ final var items = mock(ItemRewriter.class); ++ when(user.get(InventoryTracker.class)).thenReturn(inventory); ++ when(user.get(InventoryRequestTracker.class)).thenReturn(requests); ++ when(user.get(ItemRewriter.class)).thenReturn(items); ++ when(items.maxStackSize(any())).thenReturn(maximum); ++ when(requests.nextRequestId()).thenReturn(-1); ++ final var hud = new HudContainer(user); ++ when(inventory.getHudContainer()).thenReturn(hud); ++ final var sample = new BedrockItem(66, (short) 0, (byte) 1); ++ final var catalog = new CreativeCatalog.Entry(1808, 0, sample, null, false, true); ++ final var state = new NativePlayerRecipeState(user); ++ final var pick = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class); ++ pick.setAccessible(true); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ assertEquals(true, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog))); ++ final var saved = ArgumentCaptor.forClass(InventoryRequestStorage.class); ++ verify(requests).addRequest(saved.capture()); ++ final ItemStackRequestInfo request = saved.getValue().requestInfo(); ++ assertEquals(new ItemStackRequestAction.CraftCreativeAction(1808, 1), request.actions().get(0)); ++ final var result = assertInstanceOf(ItemStackRequestAction.CraftResultsDeprecatedAction.class, request.actions().get(1)); ++ assertEquals(1, result.timesCrafted()); ++ assertEquals(1, result.resultItems().getFirst().amount()); ++ final var take = assertInstanceOf(ItemStackRequestAction.TakeAction.class, request.actions().get(2)); ++ assertEquals(maximum, take.count()); ++ assertEquals(ContainerEnumName.CreatedOutputContainer, take.source().container().name()); ++ assertEquals(50, take.source().slot()); ++ assertEquals(ContainerEnumName.CursorContainer, take.destination().container().name()); ++ assertEquals(maximum, hud.getItem(0).amount()); ++ assertTrue(saved.getValue().prevCursorContainer().getItem(0).isEmpty()); ++ assertEquals(1, sample.amount()); ++ packets.verify(() -> PacketFactory.sendBedrockInventoryRequest(eq(user), any(ItemStackRequestInfo[].class))); ++ } ++ } ++ ++} -- 2.56.0 diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index d216752c..9e3b431d 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -109,5 +109,8 @@ The Bedrock 1.26.60.5 background callback bypasses recipe-disabled colors in Cre A protocol-2225 native capture selects a trapped chest through CraftCreative and takes the resulting stack to the cursor. Current native background controls cover explicit and inherited Creative modes. A snapshot regression checks active Creative cells without ingredients and restores recipe restrictions in Survival. -The full build passes with 3,153 passing tests and 325 skipped tests. Fresh-clone replay passes for all 105 core patches. -A live add-on check removes the red disabled backgrounds. Catalog selection still sends no item request and needs further action-path work. +A live add-on check removes the red disabled backgrounds. +The picker validates the full cursor stack before transfer admission while preserving the unit-sized native craft result. +Request regressions cover stack sizes one and 64, cursor prediction, and the restoration snapshot. +An installed add-on test selects 64 rails and places them in the hotbar; BDS accepts both requests. +Proxy validation of this quantity fix remains open. Cursor replacement, bulk actions, and complete native screen parity remain unfinished. From b78e168816d064a2108e989dd31cf0723f0f93ec Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:51:50 +0100 Subject: [PATCH 09/33] fix(servers): resolve hosted experience network destinations Read the opaque NetherNet network ID from Gatherings join responses instead of requiring an IP address and port. Keep socket destinations separate and pass JSON-RPC targets through the private recorder without an appended Java port. A current captured response passes the parser and a fresh SoulSteel browser join reaches pack negotiation. Transport loss before StartGame remains unresolved. Target validation also recognizes the current Hive domain so translated recordings retain their existing guard. --- docs/server-replay.md | 5 +- .../0015-add-featured-server-browser.patch | 110 ++++++++++++++---- .../0015-add-featured-server-browser.pr.md | 25 ++-- src/replay/cli.ts | 6 +- src/replay/targets.ts | 21 ++-- test/replay-targets.test.ts | 15 +++ 6 files changed, 134 insertions(+), 48 deletions(-) diff --git a/docs/server-replay.md b/docs/server-replay.md index eb0c8f86..0d923306 100644 --- a/docs/server-replay.md +++ b/docs/server-replay.md @@ -25,7 +25,10 @@ For another public server, use its current address from the in-game catalog or t bun run server-replay record public --target play.example.org:19132 --client addon --seconds 240 ``` -Public targets also accept `nethernet://host:port`. The private manifest records the selected endpoint. +Public targets also accept `nethernet://host:port`. +Hosted Xbox JSON-RPC experiences use `nethernet-xbox-json-rpc://network-id` with `--client addon` or `--client proxy`. +Resolve a fresh network ID through the catalog before connecting. These IDs have no port. +The private manifest records the selected endpoint. Catalog experiences can use temporary endpoints, so resolve the address again before a later run. Named targets retain their configured address. Local targets still require loopback. The Hive guard also applies to its hostnames supplied through `record public`. diff --git a/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.patch b/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.patch index 9b902173..1cf13d61 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.patch @@ -1,4 +1,4 @@ -From e56cea5adcaa3f058d32dd44d5bc0c552e0e4606 Mon Sep 17 00:00:00 2001 +From b6ec2ce43f0097e5758f63109118a619c952e7e5 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(bedrock): add featured server browser @@ -13,24 +13,36 @@ Friends remains separate because it also works while connected. Create browser screens only on activation and retain the caller's parent screen. This prepares the destinations for VFP's server-list popup without adding a dependency on its currently unavailable successful Jenkins artifact. + +Read hosted NetherNet destinations from the service netherNetId field. +Current protocol-2225 creator joins return NetherNet_JsonRpc and a network +ID without ipV4Address or port; requiring a socket address rejects those +valid responses before connection. Retain the address as a transport-bound +string so Xbox network IDs are never suffixed with a port. Socket joins +continue to use their advertised host and port. The captured current +Dimension Clash response passes the parser with its exact network ID. +Add boundary regressions for creator metadata, ordinary socket metadata, +and a missing network ID; keep raw service responses private. --- .../integration/MixinServerListsScreen.java | 18 +- .../mixin/core/integration/MixinVFPPopup.java | 2 +- .../screen/BedrockServerDetailsScreen.java | 211 +++++++++ .../bedrock/screen/BedrockServerList.java | 46 ++ .../bedrock/screen/BedrockServersScreen.java | 399 ++++++++++++++++++ - .../bedrock/servers/BedrockServerCatalog.java | 261 ++++++++++++ + .../bedrock/servers/BedrockServerCatalog.java | 271 ++++++++++++ .../servers/BedrockServerConnection.java | 53 +++ .../bedrock/servers/BedrockServerStatus.java | 116 +++++ .../bedrock/visual/BedrockImageCache.java | 3 +- .../viafabricplus-bedrock/lang/en_us.json | 20 + - 10 files changed, 1121 insertions(+), 8 deletions(-) + .../servers/BedrockServerCatalogTest.java | 42 ++ + 11 files changed, 1173 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/BedrockServerDetailsScreen.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/BedrockServerList.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/BedrockServersScreen.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalog.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerConnection.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerStatus.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalogTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinServerListsScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinServerListsScreen.java index caf35d9..f76e762 100644 @@ -760,10 +772,10 @@ index 0000000..4c799cb +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalog.java b/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalog.java new file mode 100644 -index 0000000..8be88e4 +index 0000000..bc810a2 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalog.java -@@ -0,0 +1,261 @@ +@@ -0,0 +1,271 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/florianreuth/viafabricplus-bedrock + * Copyright (C) 2023-2026 ViaVersion and contributors @@ -790,6 +802,7 @@ index 0000000..8be88e4 +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import net.raphimc.minecraftauth.bedrock.BedrockAuthManager; ++import net.raphimc.minecraftauth.extra.realms.model.RealmsJoinInformation; +import net.raphimc.viabedrock.protocol.data.ProtocolConstants; +import org.jetbrains.annotations.Nullable; + @@ -865,19 +878,33 @@ index 0000000..8be88e4 + .timeout(Duration.ofSeconds(20)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body.toString())).build()); -+ final JsonObject data = object(response, "result"); -+ final String host = string(data, "ipV4Address"); -+ final int port = data == null ? 0 : number(data, "port", 0); -+ if (host.isBlank() || port < 1 || port > 65535) { -+ throw new IOException("Bedrock experience returned no usable server address"); -+ } -+ return new JoinAddress(host, port, string(data, "networkProtocol")); ++ return parseJoin(response); + } catch (Exception exception) { + throw new IllegalStateException("Could not join Bedrock experience", exception); + } + }); + } + ++ static JoinAddress parseJoin(final JsonObject response) throws IOException { ++ final JsonObject data = object(response, "result"); ++ final String protocol = string(data, "networkProtocol"); ++ if (protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_NETHERNET) ++ || protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_NETHERNET_JSONRPC)) { ++ final String id = string(data, "netherNetId"); ++ if (id.isBlank()) throw new IOException("Bedrock experience returned no usable NetherNet ID"); ++ return new JoinAddress(id, protocol); ++ } ++ if (!protocol.isBlank() && !protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_DEFAULT)) { ++ throw new IOException("Bedrock experience returned an unsupported transport"); ++ } ++ final String host = string(data, "ipV4Address"); ++ final int port = data == null ? 0 : number(data, "port", 0); ++ if (host.isBlank() || port < 1 || port > 65535) { ++ throw new IOException("Bedrock experience returned no usable server address"); ++ } ++ return new JoinAddress(host + ':' + port, protocol); ++ } ++ + private static URI service() throws IOException, InterruptedException { + URI uri = serviceUri; + if (uri == null) { @@ -1017,17 +1044,12 @@ index 0000000..8be88e4 + public record Activity(String title, String subtitle, String description, String image) { + } + -+ public record JoinAddress(String host, int port, String networkProtocol) { -+ -+ public String address() { -+ return this.host + ':' + this.port; -+ } -+ } ++ public record JoinAddress(String address, String networkProtocol) { } + +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerConnection.java b/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerConnection.java new file mode 100644 -index 0000000..5d34485 +index 0000000..19ae194 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerConnection.java @@ -0,0 +1,53 @@ @@ -1059,7 +1081,7 @@ index 0000000..5d34485 + + public static CompletableFuture join(final BedrockAuthManager account, final Server server) { + final CompletableFuture destination = server.hasAddress() -+ ? CompletableFuture.completedFuture(new JoinAddress(server.host(), server.port(), RealmsJoinInformation.PROTOCOL_DEFAULT)) ++ ? CompletableFuture.completedFuture(new JoinAddress(server.address(), RealmsJoinInformation.PROTOCOL_DEFAULT)) + : BedrockServerCatalog.join(account, server); + return destination.thenCompose(address -> { + final CompletableFuture result = new CompletableFuture<>(); @@ -1251,5 +1273,53 @@ index fa7eff9..6e1452c 100644 "bedrock_lan.viafabricplus.button": "Bedrock LAN Worlds", "bedrock_lan.viafabricplus.join": "Join world", "bedrock_lan.viafabricplus.refresh": "Refresh", +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalogTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalogTest.java +new file mode 100644 +index 0000000..ba9702c +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalogTest.java +@@ -0,0 +1,42 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.servers; ++ ++import com.google.gson.JsonParser; ++import java.io.IOException; ++import org.junit.jupiter.api.Test; ++import static org.junit.jupiter.api.Assertions.*; ++ ++class BedrockServerCatalogTest { ++ ++ @Test ++ void creatorNetherNetJoinsKeepTheNetworkIdWithoutAnIpAddressOrPort() throws Exception { ++ final var response = JsonParser.parseString(""" ++ {"result":{"networkProtocol":"NetherNet_JsonRpc","netherNetId":"12345678-1234-1234-1234-123456789abc", ++ "destinationInfo":{"experienceName":"Example"}}} ++ """).getAsJsonObject(); ++ final var join = BedrockServerCatalog.parseJoin(response); ++ assertEquals("12345678-1234-1234-1234-123456789abc", join.address()); ++ assertEquals("NetherNet_JsonRpc", join.networkProtocol()); ++ } ++ ++ @Test ++ void socketJoinsRetainTheirAdvertisedPort() throws Exception { ++ final var response = JsonParser.parseString(""" ++ {"result":{"ipV4Address":"127.0.0.1","port":19132}} ++ """).getAsJsonObject(); ++ assertEquals("127.0.0.1:19132", BedrockServerCatalog.parseJoin(response).address()); ++ } ++ ++ @Test ++ void aNetherNetJoinCannotUseAnIpFallbackWhenTheNetworkIdIsMissing() { ++ final var response = JsonParser.parseString(""" ++ {"result":{"networkProtocol":"NetherNet_JsonRpc","ipV4Address":"127.0.0.1","port":19132}} ++ """).getAsJsonObject(); ++ assertThrows(IOException.class, () -> BedrockServerCatalog.parseJoin(response)); ++ } ++ ++} -- 2.56.0 diff --git a/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.pr.md b/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.pr.md index eb05f1e5..f7a12446 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.pr.md @@ -1,19 +1,12 @@ -# Featured Bedrock server browser +# Featured and creator server browser -Show the live Gatherings catalog with server details, artwork, status and hosted-experience address resolution. -Use the existing Bedrock connection path to join a selected experience. +Show the live Gatherings catalog with server details, artwork, status, and hosted experience resolution. +Use the existing Bedrock connection path for the advertised transport. -Keep Realms, LAN and featured-server launch policies in `BedrockServerList`. -The main footer uses these destinations and creates each browser only when its button is pressed. -Realms requires an account. All three browsers require a disconnected client. -Friends stays separate because it also works while connected. -Button order, labels, account tooltips and return navigation stay the same. +Hosted NetherNet joins read `netherNetId` instead of requiring an IP address and port. +Keep the network ID unchanged when selecting Xbox JSON-RPC signaling. Socket joins retain their advertised port. +The captured current Dimension Clash response passes this parser. Regression cases cover both destination types and missing network IDs. +A fresh browser join reaches SoulSteel pack negotiation over Xbox JSON-RPC. The transport then disconnects before StartGame. +This verifies catalog resolution and initial admission. Gameplay remains unverified. -The new upstream VFP server-list popup can host these destinations after a successful dependency build is available. -[VFP Jenkins build 2285](https://ci.viaversion.com/job/ViaFabricPlus/2285/) fails because Loom requires Java 25 and Jenkins uses Java 21. -Keep the verified VFP pin until the required Jenkins artifact and checksums can be verified. -The popup migration is still pending. - -All four projects build with 2,995 passing tests, 152 optional skips and no failures or errors. -All 38 add-on patches replay, and all 1,193 compiled client classes match the packaged JAR. -No live popup migration or new visual layout is claimed by this refactor. +Keep raw service responses, server endpoints, account data, and captures private. diff --git a/src/replay/cli.ts b/src/replay/cli.ts index a098a1bb..75489183 100644 --- a/src/replay/cli.ts +++ b/src/replay/cli.ts @@ -331,7 +331,7 @@ async function launchJava( await cp(join(source, file), join(instance, file)); const cfgPath = join(instance, "instance.cfg"); await rebindReplayJavaConfig(cfgPath, prismData, prism); - if (address.startsWith("nethernet://")) { + if (address.includes("://")) { // Prism's host/port launcher option appends a Java port to URI addresses. const pack = JSON.parse(await readFile(join(instance, "mmc-pack.json"), "utf8")) as { components: { uid: string; version: string }[]; @@ -461,7 +461,7 @@ async function launchJava( prism, "--launch", name, - ...(address.startsWith("nethernet://") ? [] : ["--server", address]), + ...(address.includes("://") ? [] : ["--server", address]), ], root, launcherLog, @@ -751,6 +751,8 @@ Live native recordings accept --native-ice-address for a specific hos } else if (mode === "record" && input === "public") { target = publicTarget(option("--target")); } else if (mode === "record" && !target) throw new Error("Unknown server."); + if (client === "native" && target?.startsWith("nethernet-xbox-json-rpc://")) + throw new Error("Xbox JSON-RPC recordings require --client addon or --client proxy."); const hive = mode === "record" && hiveTarget(target!); if (hive && client !== "native" && !args.includes("--allow-hive")) throw new Error( diff --git a/src/replay/targets.ts b/src/replay/targets.ts index 6ca1998d..f11e4ba7 100644 --- a/src/replay/targets.ts +++ b/src/replay/targets.ts @@ -1,28 +1,29 @@ /** Explicit public targets let the lab inspect catalog experiences with dynamic endpoints. */ export function publicTarget(address: string | undefined): string { if (!address) throw new Error("Public recordings require --target host:port."); - const netherNet = address.startsWith("nethernet://"); + const error = "Use host:port, nethernet://host:port, or nethernet-xbox-json-rpc://network-id."; let parsed: URL; try { - parsed = new URL(netherNet ? address : `raknet://${address}`); + parsed = new URL(address.includes("://") ? address : `raknet://${address}`); } catch { - throw new Error("Use host:port or nethernet://host:port for a public target."); + throw new Error(error); } + const networkId = parsed.protocol === "nethernet-xbox-json-rpc:"; if ( - parsed.protocol !== (netherNet ? "nethernet:" : "raknet:") || + !["raknet:", "nethernet:", "nethernet-xbox-json-rpc:"].includes(parsed.protocol) || !parsed.hostname || !/^[a-zA-Z0-9.[\]:-]+$/.test(parsed.hostname) || - !parsed.port || - Number(parsed.port) < 1 || + (networkId ? !!parsed.port : !parsed.port || Number(parsed.port) < 1) || parsed.username || parsed.password || parsed.pathname || parsed.search || parsed.hash ) { - throw new Error("Use host:port or nethernet://host:port for a public target."); + throw new Error(error); } - return `${netherNet ? "nethernet://" : ""}${parsed.hostname.toLowerCase()}:${parsed.port}`; + if (networkId) return `nethernet-xbox-json-rpc://${parsed.hostname}`; + return `${parsed.protocol === "nethernet:" ? "nethernet://" : ""}${parsed.hostname.toLowerCase()}:${parsed.port}`; } export function hiveTarget(address: string): boolean { @@ -33,7 +34,9 @@ export function hiveTarget(address: string): boolean { host === "hivebedrock.cloud" || host.endsWith(".hivebedrock.cloud") || host === "hivebedrock.network" || - host.endsWith(".hivebedrock.network") + host.endsWith(".hivebedrock.network") || + host === "playhive.com" || + host.endsWith(".playhive.com") ); } diff --git a/test/replay-targets.test.ts b/test/replay-targets.test.ts index 4b7b85d5..df796b1a 100644 --- a/test/replay-targets.test.ts +++ b/test/replay-targets.test.ts @@ -27,16 +27,31 @@ test("recognizes Hive targets without matching unrelated suffixes", () => { "geo.hivebedrock.cloud:19132", "GEO.HIVEBEDROCK.CLOUD:19132", "nethernet://hivebedrock.network:443", + "nethernet.playhive.com:19132", ]) expect(hiveTarget(publicTarget(address))).toBe(true); for (const address of [ "play.cubecraft.net:19132", "hivebedrock.cloud.example.org:19132", "nothivebedrock.cloud:19132", + "notplayhive.com:19132", ]) expect(hiveTarget(publicTarget(address))).toBe(false); }); +test("keeps hosted experience network IDs separate from socket endpoints", () => { + const target = "nethernet-xbox-json-rpc://12345678-1234-1234-1234-123456789abc"; + expect(publicTarget(target)).toBe(target); + expect(viaProxyTarget(publicTarget(target))).toBe(target); + for (const invalid of [ + `${target}:19132`, + `${target}/path`, + `${target}?token=secret`, + "nethernet-xbox-json-rpc://user:secret@network-id", + ]) + expect(() => publicTarget(invalid)).toThrow(); +}); + test("passes NetherNet hosts and ports to ViaProxy as socket addresses", () => { expect(viaProxyTarget(publicTarget("nethernet://127.0.0.1:5701"))).toBe("127.0.0.1:5701"); expect(viaProxyTarget(publicTarget("nethernet://[::1]:5701"))).toBe("[::1]:5701"); From 7d4008ad7ea0705c9c91914bb6e71380c6209d1b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 18:33:25 +0100 Subject: [PATCH 10/33] fix(servers): preserve hosted join layouts and actor bindings Resolve current catalog destinations through Gatherings and distinguish HTTP NetherNet signaling from hosted Xbox network IDs. Decode the single presence override string so current hosted StartGame packets preserve their telemetry fields. Fold reachable actor model and texture selection into the appearance owner. This removes the Cartesian furniture-preview expansion that exhausted a 4 GiB heap on OneBlockOnline while retaining independent selectors and conservative fallbacks. Full builds, standalone protocol checks and fresh patch replays pass. Mineville reaches its lobby over HTTP NetherNet. A private conversion fixture processes all 867 captured actors in 2 GiB; intermittent hosted transport losses still prevent a complete live parity claim. --- ...erver-authoritative-bedrock-requests.patch | 6 +- ...edrock-recipes-and-place-ingredients.patch | 6 +- .../0001-update-bedrock-to-1-26-60.patch | 148 ++++++++- .../0001-update-bedrock-to-1-26-60.pr.md | 7 +- ...rse-properties-and-composed-metadata.patch | 4 +- ...13-translate-remote-player-locations.patch | 6 +- ...lize-and-preserve-bedrock-clock-time.patch | 10 +- ...slate-command-block-edits-to-bedrock.patch | 6 +- ...-encode-current-bedrock-skin-packets.patch | 6 +- ...e-block-state-geometry-and-materials.patch | 12 +- .../0061-expose-native-player-emotes.patch | 6 +- ...0064-evaluate-named-actor-properties.patch | 6 +- ...ve-complete-native-appearance-assets.patch | 281 ++++++++++++++++-- ...ve-complete-native-appearance-assets.pr.md | 9 + ...-transport-native-player-permissions.patch | 6 +- .../0081-retain-server-hud-visibility.patch | 6 +- ...-synchronize-native-armor-attributes.patch | 6 +- ...transport-native-camera-instructions.patch | 6 +- ...unacknowledged-chunk-radius-requests.patch | 6 +- ...n-completed-client-prediction-frames.patch | 8 +- ...gin-before-resource-pack-negotiation.patch | 4 +- ...rate-authored-native-ui-presentation.patch | 12 +- ...bridge-native-actor-picking-hitboxes.patch | 6 +- ...tive-language-parsing-and-formatting.patch | 6 +- ...transport-resolved-native-fog-stacks.patch | 6 +- .../0015-add-featured-server-browser.patch | 56 ++-- .../0015-add-featured-server-browser.pr.md | 11 +- 27 files changed, 533 insertions(+), 119 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 04a0e2ae..dea1d861 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 a7279c0cb32b48674a147e954ac82480377f35d9 Mon Sep 17 00:00:00 2001 +From b7c8e6c022f5192e9f9eeddb569482433b6df174 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 @@ -11342,10 +11342,10 @@ index d22d7616..20e14855 100644 private ContainerUi.Title presentation() { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 677047ac..91f2662a 100644 +index b7fb312e..e2b41cce 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -475,7 +475,7 @@ public final class JoinPackets { +@@ -446,7 +446,7 @@ public final class JoinPackets { itemDefinitions.remove(entry.identifier()); } } 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 ee948b79..0f1e5b80 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 99504877696882cc572173a75ac3d84c1eaeeee0 Mon Sep 17 00:00:00 2001 +From 7176feda38eaae0f109ccddeea64d2dd8952a285 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 @@ -4115,10 +4115,10 @@ index 20e14855..0672f7a7 100644 buffer.release(); } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 91f2662a..7d828823 100644 +index e2b41cce..1904a56d 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -597,6 +597,7 @@ public final class JoinPackets { +@@ -568,6 +568,7 @@ public final class JoinPackets { user.get(EntityTracker.class).sendNativeCustomEntitySnapshot(); user.get(PlayerInputStorage.class).markJoined(); user.get(PlayerInputStorage.class).sendSnapshot(); diff --git a/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.patch b/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.patch index 815cacaf..ae30e76a 100644 --- a/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.patch +++ b/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.patch @@ -1,4 +1,4 @@ -From 790b42ad4a12ed7a67fa2918c7c9aae7a49cde0d Mon Sep 17 00:00:00 2001 +From b9abcca849ef45026fa63238149f14e56b19c166 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:53:57 +0100 Subject: [PATCH] fix(protocol): complete Bedrock 1.26.60 compatibility @@ -23,6 +23,16 @@ including tests for the passenger codec now supplied by upstream. Sources: Mojang/bedrock-protocol-docs v1.26.60 (protocol 2225), the matching 1.26.60.5 GDK archive, and the native BDS 1.26.60.6 inventory fixture. Data Asset Sources records the archive hashes and experimental-data limits. + +Decode the optional server presence configuration as one richPresenceId +string. Mojang protocol-2225 presenceConfig metadata has one optional +field. Fresh current Dimension Clash and Treasure Hunt StartGame packets +include that configuration; reading a second string consumes the first +telemetry field and then overruns the packet at ownerId. A boundary +regression preserves the four following telemetry strings with presence +both absent and present. The captured Dimension Clash payload consumes +exactly when decoded with the single presence string. Raw captures remain +private. --- Data Asset Sources.md | 19 ++++ build.gradle | 11 ++ @@ -39,7 +49,7 @@ Data Asset Sources records the archive hashes and experimental-data limits. .../data/generated/java/RegistryKeys.java | 3 + .../protocol/model/DimensionDefinition.java | 33 ++++++ .../protocol/packet/ClientPlayerPackets.java | 8 +- - .../protocol/packet/JoinPackets.java | 27 +++-- + .../protocol/packet/JoinPackets.java | 90 ++++++++-------- .../packet/ResourcePackClientResponse.java | 52 ++++++++++ .../protocol/packet/ResourcePackPackets.java | 18 ++-- .../protocol/packet/UnhandledPackets.java | 2 + @@ -56,12 +66,13 @@ Data Asset Sources records the archive hashes and experimental-data limits. .../data/JavaDimensionRegistryTest.java | 55 ++++++++++ .../packet/DimensionDataPacketTest.java | 96 ++++++++++++++++++ .../ResourcePackClientResponseTest.java | 74 ++++++++++++++ + .../StartGameServerConfigurationTest.java | 48 +++++++++ .../types/entitydata/EntityDataTypeTest.java | 55 ++++++++++ .../entitydata/PassengerOfBlocksTypeTest.java | 69 +++++++++++++ .../model/FullContainerNameTypeTest.java | 49 +++++++++ .../BedrockDataEnumGeneratorTest.java | 73 +++++++++++++ .../generator/BedrockDataEnumGenerator.java | 55 +++++++++- - 37 files changed, 1017 insertions(+), 64 deletions(-) + 38 files changed, 1098 insertions(+), 94 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/ConnectionBlockStateUpgradeSchema.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/ActorSwingPacketFactory.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/DimensionDefinition.java @@ -73,6 +84,7 @@ Data Asset Sources records the archive hashes and experimental-data limits. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/data/JavaDimensionRegistryTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/DimensionDataPacketTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/ResourcePackClientResponseTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/StartGameServerConfigurationTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/types/entitydata/EntityDataTypeTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/types/entitydata/PassengerOfBlocksTypeTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/types/model/FullContainerNameTypeTest.java @@ -488,7 +500,7 @@ index 49145e9d..d7c69522 100644 if (clientPlayer.blockBreakingInfo() != null) { if (!gameSession.isBlockBreakingServerAuthoritative()) { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 4f9cc6a8..2e456cd7 100644 +index 4f9cc6a8..25599875 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -31,7 +31,6 @@ import com.viaversion.viaversion.api.protocol.remapper.PacketHandler; @@ -507,7 +519,45 @@ index 4f9cc6a8..2e456cd7 100644 import net.raphimc.viabedrock.protocol.model.*; import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; -@@ -434,16 +434,9 @@ public final class JoinPackets { +@@ -293,36 +293,7 @@ public final class JoinPackets { + wrapper.read(Types.BOOLEAN); // client side generation + final boolean hashedRuntimeBlockIds = wrapper.read(Types.BOOLEAN); // use hashed block runtime ids + wrapper.read(Types.BOOLEAN); // server authoritative sounds +- if (wrapper.read(Types.BOOLEAN)) { // has server join information +- if (wrapper.read(Types.BOOLEAN)) { // has gathering join information +- wrapper.read(BedrockTypes.UUID); // experience id +- wrapper.read(BedrockTypes.STRING); // experience name +- if (wrapper.read(Types.BOOLEAN)) { // has experience world id +- wrapper.read(BedrockTypes.UUID); // experience world id +- } +- if (wrapper.read(Types.BOOLEAN)) { // has experience world name +- wrapper.read(BedrockTypes.STRING); // experience world name +- } +- wrapper.read(BedrockTypes.STRING); // creator id +- if (wrapper.read(Types.BOOLEAN)) { // has target id +- wrapper.read(BedrockTypes.UUID); // target id +- } +- if (wrapper.read(Types.BOOLEAN)) { +- wrapper.read(BedrockTypes.STRING); // scenario id +- } +- if (wrapper.read(Types.BOOLEAN)) { // has server id +- wrapper.read(BedrockTypes.STRING); // server id +- } +- } +- if (wrapper.read(Types.BOOLEAN)) { // has store entry point info +- wrapper.read(BedrockTypes.STRING); // store id +- wrapper.read(BedrockTypes.STRING); // store name +- } +- if (wrapper.read(Types.BOOLEAN)) { // has presence info +- wrapper.read(BedrockTypes.STRING); // experience name +- wrapper.read(BedrockTypes.STRING); // world name +- } +- } ++ readServerConfiguration(wrapper); + wrapper.read(BedrockTypes.STRING); // server id + wrapper.read(BedrockTypes.STRING); // scenario id + wrapper.read(BedrockTypes.STRING); // world id +@@ -434,16 +405,9 @@ public final class JoinPackets { final int count = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // entry count for (int i = 0; i < count; i++) { final String dimensionIdentifier = wrapper.read(BedrockTypes.STRING); // dimension identifier @@ -527,10 +577,42 @@ index 4f9cc6a8..2e456cd7 100644 } } }, State.PLAY, (PacketHandler) PacketWrapper::cancel // Bedrock client ignores dimension data after start game -@@ -636,6 +629,18 @@ public final class JoinPackets { +@@ -636,6 +600,50 @@ public final class JoinPackets { setTime.send(BedrockProtocol.class, false); } ++ static void readServerConfiguration(final PacketWrapper wrapper) { ++ if (wrapper.read(Types.BOOLEAN)) { // has server join information ++ if (wrapper.read(Types.BOOLEAN)) { // has gathering join information ++ wrapper.read(BedrockTypes.UUID); // experience id ++ wrapper.read(BedrockTypes.STRING); // experience name ++ if (wrapper.read(Types.BOOLEAN)) { // has experience world id ++ wrapper.read(BedrockTypes.UUID); // experience world id ++ } ++ if (wrapper.read(Types.BOOLEAN)) { // has experience world name ++ wrapper.read(BedrockTypes.STRING); // experience world name ++ } ++ wrapper.read(BedrockTypes.STRING); // creator id ++ if (wrapper.read(Types.BOOLEAN)) { // has target id ++ wrapper.read(BedrockTypes.UUID); // target id ++ } ++ if (wrapper.read(Types.BOOLEAN)) { ++ wrapper.read(BedrockTypes.STRING); // scenario id ++ } ++ if (wrapper.read(Types.BOOLEAN)) { // has server id ++ wrapper.read(BedrockTypes.STRING); // server id ++ } ++ } ++ if (wrapper.read(Types.BOOLEAN)) { // has store entry point info ++ wrapper.read(BedrockTypes.STRING); // store id ++ wrapper.read(BedrockTypes.STRING); // store name ++ } ++ if (wrapper.read(Types.BOOLEAN)) { // has presence info ++ wrapper.read(BedrockTypes.STRING); // rich presence id ++ } ++ } ++ } ++ + static DimensionDefinition readDimensionDefinition(final PacketWrapper wrapper) { + final int minimumY = wrapper.read(BedrockTypes.VAR_INT); + final int heightRange = wrapper.read(BedrockTypes.VAR_INT); @@ -1774,6 +1856,60 @@ index 00000000..e00a7083 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/StartGameServerConfigurationTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/StartGameServerConfigurationTest.java +new file mode 100644 +index 00000000..98233f20 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/StartGameServerConfigurationTest.java +@@ -0,0 +1,48 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.packet; ++ ++import com.viaversion.viaversion.protocol.packet.PacketWrapperImpl; ++import io.netty.buffer.ByteBuf; ++import io.netty.buffer.Unpooled; ++import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; ++import net.raphimc.viabedrock.protocol.types.BedrockTypes; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.ValueSource; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class StartGameServerConfigurationTest { ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void optionalPresenceLeavesTheFollowingTelemetryFieldsAligned(final boolean presence) { ++ final ByteBuf buffer = Unpooled.buffer(); ++ try { ++ buffer.writeBoolean(true); // server configuration ++ buffer.writeBoolean(false); // gathering ++ buffer.writeBoolean(false); // store entry point ++ buffer.writeBoolean(presence); ++ if (presence) { ++ BedrockTypes.STRING.write(buffer, "override"); ++ } ++ final String[] telemetry = {"server", "scenario", "world", "owner"}; ++ for (final String field : telemetry) { ++ BedrockTypes.STRING.write(buffer, field); ++ } ++ ++ final var wrapper = new PacketWrapperImpl(ClientboundBedrockPackets.START_GAME, buffer, null); ++ JoinPackets.readServerConfiguration(wrapper); ++ for (final String field : telemetry) { ++ assertEquals(field, wrapper.read(BedrockTypes.STRING)); ++ } ++ assertFalse(buffer.isReadable()); ++ } finally { ++ buffer.release(); ++ } ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/types/entitydata/EntityDataTypeTest.java b/src/test/java/net/raphimc/viabedrock/protocol/types/entitydata/EntityDataTypeTest.java new file mode 100644 index 00000000..fa10411d diff --git a/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.pr.md b/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.pr.md index 2e53ec34..095ccfc7 100644 --- a/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.pr.md +++ b/patches/viabedrock/upstreamable/0001-update-bedrock-to-1-26-60.pr.md @@ -12,7 +12,12 @@ Dimension bounds also use the earlier native Hive fixture. The native BDS 1.26.60.6 inventory fixture establishes the 57-entry capacity. Experimental fallbacks do not establish complete native parity. +The optional server presence configuration contains one `richPresenceId` string. +Current Dimension Clash and Treasure Hunt StartGame captures expose an overread from the former two-string decoder. +A boundary regression retains the following telemetry fields with presence both absent and present. +The captured Dimension Clash payload consumes exactly with the corrected layout. Live gameplay remains a separate check. + The first patch applies independently to the pinned upstream base without StackAnvil setup or deferred inventory patches. -Its standalone build, Checkstyle tasks, and all 22 packet-boundary and compatibility tests pass. +Its standalone build, Checkstyle tasks, and the packet-boundary and compatibility tests pass. The complete four-project build and modpack bundle also pass against the rebased pins. diff --git a/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch b/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch index 69f81888..daf2b71e 100644 --- a/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch +++ b/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch @@ -1,4 +1,4 @@ -From 0da647c7928d3eb4bd28d6b702835e8cbcbfa10d Mon Sep 17 00:00:00 2001 +From d1cc1cf3f521b2459b17d6ed53fb3aed6f59b4ab 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(metadata): preserve sparse properties and composed actor @@ -701,7 +701,7 @@ index b8ba20f5..fd0640c4 100644 private static void writeTickedEntityMotion(final PacketWrapper wrapper, final long runtimeId, final Position3f motion, final long tick) { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index e334cbe0..5c54c4e1 100644 +index 0fa95fb6..25355ba0 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -293,7 +293,7 @@ public final class JoinPackets { diff --git a/patches/viabedrock/upstreamable/0013-translate-remote-player-locations.patch b/patches/viabedrock/upstreamable/0013-translate-remote-player-locations.patch index 238b0e06..245938e5 100644 --- a/patches/viabedrock/upstreamable/0013-translate-remote-player-locations.patch +++ b/patches/viabedrock/upstreamable/0013-translate-remote-player-locations.patch @@ -1,4 +1,4 @@ -From 38a853542deaba25e9d8ed07d8ce2ac44a6c4194 Mon Sep 17 00:00:00 2001 +From 306d27d95b002198ba9e4b2d2c64294abb0c5885 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:34 +0200 Subject: [PATCH] feat(hud): preserve native Bedrock locator updates @@ -353,10 +353,10 @@ index 46d77ccc..7be21380 100644 final Pair scoreboardEntry = scoreboardTracker.getEntryForPlayer(entry.key()); if (scoreboardEntry != null) { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 2e456cd7..65176e12 100644 +index 25599875..d6fd114a 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -566,6 +566,7 @@ public final class JoinPackets { +@@ -537,6 +537,7 @@ public final class JoinPackets { joinGame.write(Types.BOOLEAN, false); // online mode joinGame.write(Types.BOOLEAN, false); // enforces secure chat joinGame.send(BedrockProtocol.class); diff --git a/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.patch b/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.patch index 047a0a26..ef2e1d4f 100644 --- a/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.patch +++ b/patches/viabedrock/upstreamable/0014-initialize-and-preserve-bedrock-clock-time.patch @@ -1,4 +1,4 @@ -From df72146b8c1310a6d0cf7ed082b81ecba2f42449 Mon Sep 17 00:00:00 2001 +From 562ea4fb7abc4e46ccfeb28f8fec7bac7be0a6aa Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:34 +0200 Subject: [PATCH] fix(world): preserve native Bedrock world clock semantics @@ -83,7 +83,7 @@ index 4dcc5b7d..8a8f60fb 100644 user.put(new BlobCache(user)); user.put(new PacketSyncStorage(user)); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 65176e12..da0f2171 100644 +index d6fd114a..443bc8ca 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -230,8 +230,8 @@ public final class JoinPackets { @@ -97,7 +97,7 @@ index 65176e12..da0f2171 100644 wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); // education edition offers wrapper.read(Types.BOOLEAN); // education features enabled wrapper.read(BedrockTypes.STRING); // education product id -@@ -382,7 +382,7 @@ public final class JoinPackets { +@@ -353,7 +353,7 @@ public final class JoinPackets { clientPlayer.setGameType(playerGameType); clientPlayer.setName(wrapper.user().getProtocolInfo().getUsername()); @@ -106,7 +106,7 @@ index 65176e12..da0f2171 100644 wrapper.user().put(new GameRulesStorage(wrapper.user(), gameRules)); wrapper.user().put(new BlockStateRewriter(blockProperties, hashedRuntimeBlockIds)); wrapper.user().put(new ItemRewriter(wrapper.user(), new ItemEntry[0])); -@@ -625,9 +625,15 @@ public final class JoinPackets { +@@ -596,9 +596,15 @@ public final class JoinPackets { } } @@ -124,7 +124,7 @@ index 65176e12..da0f2171 100644 + return dayCycleStopTime >= 0 ? dayCycleStopTime : levelTime; } - static DimensionDefinition readDimensionDefinition(final PacketWrapper wrapper) { + static void readServerConfiguration(final PacketWrapper wrapper) { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/WorldPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/WorldPackets.java index 5f991ba9..5f2db346 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/WorldPackets.java diff --git a/patches/viabedrock/upstreamable/0042-translate-command-block-edits-to-bedrock.patch b/patches/viabedrock/upstreamable/0042-translate-command-block-edits-to-bedrock.patch index b61a0151..2d3e6b73 100644 --- a/patches/viabedrock/upstreamable/0042-translate-command-block-edits-to-bedrock.patch +++ b/patches/viabedrock/upstreamable/0042-translate-command-block-edits-to-bedrock.patch @@ -1,4 +1,4 @@ -From e73e79d4b0b9571737befc0a30fd48c3edcbdebd Mon Sep 17 00:00:00 2001 +From 984111a10bc6111a4fd81fd6a54a1045b651d08f Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] fix(world): translate command block edits to Bedrock @@ -197,10 +197,10 @@ index 4b233b07..9be115b5 100644 if (container == null) { wrapper.cancel(); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index da0f2171..2d5ae5b4 100644 +index 443bc8ca..15123622 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -568,6 +568,8 @@ public final class JoinPackets { +@@ -539,6 +539,8 @@ public final class JoinPackets { joinGame.send(BedrockProtocol.class); user.get(PlayerWaypointTracker.class).markJoined(); diff --git a/patches/viabedrock/upstreamable/0047-encode-current-bedrock-skin-packets.patch b/patches/viabedrock/upstreamable/0047-encode-current-bedrock-skin-packets.patch index a3bc02a0..62ec0051 100644 --- a/patches/viabedrock/upstreamable/0047-encode-current-bedrock-skin-packets.patch +++ b/patches/viabedrock/upstreamable/0047-encode-current-bedrock-skin-packets.patch @@ -1,4 +1,4 @@ -From 99360629d1ea77d8265d9587eed9192b0daf2a4d Mon Sep 17 00:00:00 2001 +From 2ccc531996fc35a8c063631104afb7cc17139579 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] fix(skins): encode current Bedrock skin packets @@ -460,10 +460,10 @@ index bac34b8f..a69ed176 100644 uuidsToRemoveSafe.add(uuid); } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 2d5ae5b4..bfb30d38 100644 +index 15123622..e1a58abd 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -567,8 +567,10 @@ public final class JoinPackets { +@@ -538,8 +538,10 @@ public final class JoinPackets { joinGame.write(Types.BOOLEAN, false); // enforces secure chat joinGame.send(BedrockProtocol.class); user.get(PlayerWaypointTracker.class).markJoined(); diff --git a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch index 1a9bc354..d69d4a0b 100644 --- a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch +++ b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch @@ -1,4 +1,4 @@ -From 274a231e0ae5d84e4b5772a8848f6c4824e619e7 Mon Sep 17 00:00:00 2001 +From 48a516a162681ecaf30e90a7a478b95c31a27a17 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(blocks): render Bedrock state geometry and materials @@ -2617,10 +2617,10 @@ index 00000000..ee311d9b + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 727ae985..3f829ae4 100644 +index d97ef829..7dd7f708 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -384,7 +384,8 @@ public final class JoinPackets { +@@ -355,7 +355,8 @@ public final class JoinPackets { wrapper.user().put(new JoinGameStorage(levelName, difficulty, rainLevel, lightningLevel, dayCycleStopTime, chunkTickRange)); wrapper.user().put(new GameRulesStorage(wrapper.user(), gameRules)); @@ -2630,7 +2630,7 @@ index 727ae985..3f829ae4 100644 wrapper.user().put(new ItemRewriter(wrapper.user(), new ItemEntry[0])); wrapper.user().put(new ChunkTracker(wrapper.user(), dimension)); final EntityTracker entityTracker = new EntityTracker(wrapper.user()); -@@ -403,13 +404,17 @@ public final class JoinPackets { +@@ -374,13 +375,17 @@ public final class JoinPackets { updateEnabledFeatures.send(BedrockProtocol.class); } @@ -2655,7 +2655,7 @@ index 727ae985..3f829ae4 100644 }, State.PLAY, (PacketHandler) PacketWrapper::cancel // Bedrock client ignores multiple start game packets ); protocol.registerClientboundTransition(ClientboundBedrockPackets.BIOME_DEFINITION_LIST, -@@ -501,6 +506,15 @@ public final class JoinPackets { +@@ -472,6 +477,15 @@ public final class JoinPackets { } } @@ -2671,7 +2671,7 @@ index 727ae985..3f829ae4 100644 private static void handleJavaClientGameJoin(final UserConnection user) { final JoinGameStorage joinGameStorage = user.get(JoinGameStorage.class); final GameSessionStorage gameSession = user.get(GameSessionStorage.class); -@@ -636,6 +650,7 @@ public final class JoinPackets { +@@ -607,6 +621,7 @@ public final class JoinPackets { initialDayTime(joinGameStorage.dayCycleStopTime(), gameSession.getLevelTime()), user.get(GameRulesStorage.class).getGameRule("doDayLightCycle")); setTime.send(BedrockProtocol.class); diff --git a/patches/viabedrock/upstreamable/0061-expose-native-player-emotes.patch b/patches/viabedrock/upstreamable/0061-expose-native-player-emotes.patch index 64effc2d..93c114ec 100644 --- a/patches/viabedrock/upstreamable/0061-expose-native-player-emotes.patch +++ b/patches/viabedrock/upstreamable/0061-expose-native-player-emotes.patch @@ -1,4 +1,4 @@ -From 0cfbcd4ee858b226afd86bd0ca9b91f709fa1da5 Mon Sep 17 00:00:00 2001 +From 139058352c987d9b068a97d624d1e437f939644d Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(players): expose native emotes to client integrations @@ -312,7 +312,7 @@ index 3d4f4a41..4d0d38fe 100644 for (PlayerAuthInputData inputData : PlayerAuthInputData.values()) { if (clientPlayer.authInputData().contains(inputData)) { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 3f829ae4..66775c1f 100644 +index 7dd7f708..330abbf5 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -260,7 +260,7 @@ public final class JoinPackets { @@ -324,7 +324,7 @@ index 3f829ae4..66775c1f 100644 final String vanillaVersion = wrapper.read(BedrockTypes.STRING); // vanilla version wrapper.read(BedrockTypes.INT_LE); // limited world width wrapper.read(BedrockTypes.INT_LE); // limited world height -@@ -370,6 +370,7 @@ public final class JoinPackets { +@@ -341,6 +341,7 @@ public final class JoinPackets { gameSession.setLevelTime(levelTime); gameSession.setHardcoreMode(hardcore); gameSession.setChatRestrictionLevel(chatRestrictionLevel); diff --git a/patches/viabedrock/upstreamable/0064-evaluate-named-actor-properties.patch b/patches/viabedrock/upstreamable/0064-evaluate-named-actor-properties.patch index 2df2f885..82f2375b 100644 --- a/patches/viabedrock/upstreamable/0064-evaluate-named-actor-properties.patch +++ b/patches/viabedrock/upstreamable/0064-evaluate-named-actor-properties.patch @@ -1,4 +1,4 @@ -From f93aa224e51ede973adb9cc81d0e280c2e1be7c7 Mon Sep 17 00:00:00 2001 +From b7efde7bfa272de8ab093a4b2e780a427cc2c85a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(entities): evaluate named actor properties and @@ -1672,10 +1672,10 @@ index 767c62d6..c1d364d2 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index bfb30d38..727ae985 100644 +index e1a58abd..d97ef829 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -568,6 +568,8 @@ public final class JoinPackets { +@@ -539,6 +539,8 @@ public final class JoinPackets { joinGame.send(BedrockProtocol.class); user.get(PlayerWaypointTracker.class).markJoined(); user.get(NativeSkinStorage.class).markJoined(); diff --git a/patches/viabedrock/upstreamable/0066-preserve-complete-native-appearance-assets.patch b/patches/viabedrock/upstreamable/0066-preserve-complete-native-appearance-assets.patch index de87d64a..fd206a6d 100644 --- a/patches/viabedrock/upstreamable/0066-preserve-complete-native-appearance-assets.patch +++ b/patches/viabedrock/upstreamable/0066-preserve-complete-native-appearance-assets.patch @@ -1,4 +1,4 @@ -From 45de91564ec7d9b22c07ad52c786ca8e4e77f656 Mon Sep 17 00:00:00 2001 +From 31342db079f065bf10d1bd43811017b3ac965a88 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] fix(models): export complete asset pairs and empty item @@ -97,19 +97,40 @@ is actually selected. The rendering gate recognizes that warning, so an actively missing custom icon remains a failed rendering control. Use the existing empty-definition schema when no variant resolves; no substitute camera pixels or camera-specific exclusion is introduced. + +Compile actor model/texture pairs reachable from declared render controllers +instead of every geometry alias crossed with every texture alias. Keep +independent selectors independent. Equal stable query reads over arrays +of equal length select paired entries; random or unsupported expressions +and unresolved controllers retain conservative combinations. Preserve the +existing model keys and all referenced source geometry and textures. + +A current protocol-2225 OneBlockOnline capture exhausts a 4 GiB heap during +actor resource conversion. Four furniture preview actors each declare 720 +geometry and texture aliases. Their three controllers select corresponding +240-entry arrays with the same named property. Each actor needs 720 pairs, +not 518,400. The actual captured controllers produce that bounded set with +this projection. Regressions cover correlated properties, independent +queries, repeated random expressions, and unresolved-controller fallback. +Raw server packs and packet captures remain private. This reduces unreachable +conversion work; it does not establish full native actor rendering parity. --- .../api/model/entity/CustomEntity.java | 9 + .../definition/AttachableDefinitions.java | 41 ++- .../protocol/rewriter/ItemRewriter.java | 8 +- .../CustomAttachableResourceRewriter.java | 77 +++-- - .../CustomEntityResourceRewriter.java | 37 +-- + .../CustomEntityResourceRewriter.java | 43 ++- .../CustomItemTextureResourceRewriter.java | 20 +- + .../resourcepack/EntityModelBindings.java | 138 +++++++++ .../ItemModelResourceRewriter.java | 127 +++++++- ...CustomItemTextureResourceRewriterTest.java | 112 +++++++ + .../resourcepack/EntityModelBindingsTest.java | 68 ++++ .../ItemModelResourceRewriterTest.java | 123 ++++++++ .../resourcepack/ModelResourceAssetsTest.java | 290 ++++++++++++++++++ - 10 files changed, 776 insertions(+), 68 deletions(-) + 12 files changed, 985 insertions(+), 71 deletions(-) + create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/EntityModelBindings.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/CustomItemTextureResourceRewriterTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/EntityModelBindingsTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/ItemModelResourceRewriterTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/ModelResourceAssetsTest.java @@ -373,7 +394,7 @@ index 40083952..fcc26eee 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/CustomEntityResourceRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/CustomEntityResourceRewriter.java -index 60070350..e68258a9 100644 +index 60070350..9c15e9f5 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/CustomEntityResourceRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/CustomEntityResourceRewriter.java @@ -19,16 +19,15 @@ package net.raphimc.viabedrock.protocol.rewriter.resourcepack; @@ -395,7 +416,7 @@ index 60070350..e68258a9 100644 import java.util.function.Consumer; import java.util.function.Supplier; -@@ -46,31 +45,29 @@ public class CustomEntityResourceRewriter extends ItemModelResourceRewriter { +@@ -46,32 +45,30 @@ public class CustomEntityResourceRewriter extends ItemModelResourceRewriter { @Override public void submitTasks(final ResourcePackStorage resourcePackStorage, final Consumer> submitter) { @@ -419,29 +440,33 @@ index 60070350..e68258a9 100644 - } - } - } -+ final Set textures = this.copyTextures(resourcePackStorage, entityDefinition.entityData().getTextures().values(), javaContent); - for (Map.Entry modelEntry : entityDefinition.entityData().getGeometries().entrySet()) { - final BedrockGeometryModel bedrockGeometry = resourcePackStorage.getModels().entityModels().get(modelEntry.getValue()); +- for (Map.Entry modelEntry : entityDefinition.entityData().getGeometries().entrySet()) { +- final BedrockGeometryModel bedrockGeometry = resourcePackStorage.getModels().entityModels().get(modelEntry.getValue()); - if (bedrockGeometry != null) { - for (Map.Entry textureEntry : entityDefinition.entityData().getTextures().entrySet()) { - final String modelKey = modelEntry.getKey() + "_" + textureEntry.getKey(); - final JavaItemModel itemModelData = bedrockGeometry.toJavaItemModel("viabedrock:" + this.getJavaTexturePath(textureEntry.getValue()), RotationType.POST_1_21_11); - javaContent.putModel(modelKey, itemModelData.compile()); - resourcePackStorage.getConverterData().put("ce_" + entityDefinition.identifier() + '_' + modelKey + "_scale", itemModelData.getScale()); -+ for (Map.Entry textureEntry : entityDefinition.entityData().getTextures().entrySet()) { -+ final String modelKey = modelEntry.getKey() + "_" + textureEntry.getKey(); -+ final boolean hasTexture = textures.contains(textureEntry.getValue()); -+ this.recordMissingAssets(resourcePackStorage, entityDefinition.identifier(), modelKey, modelEntry.getValue(), textureEntry.getValue(), bedrockGeometry != null, hasTexture); -+ if (bedrockGeometry == null || !hasTexture) { -+ continue; - } -+ final CompiledGeometry geometry = geometries.computeIfAbsent(bedrockGeometry, CompiledGeometry::compile); -+ javaContent.putGeometry(geometry); -+ javaContent.putModel(modelKey, geometry.withTexture("viabedrock:" + this.getJavaTexturePath(textureEntry.getValue()))); -+ resourcePackStorage.getConverterData().put("ce_" + entityDefinition.identifier() + '_' + modelKey + "_scale", geometry.scale()); +- } ++ final Set textures = this.copyTextures(resourcePackStorage, entityDefinition.entityData().getTextures().values(), javaContent); ++ for (final var binding : EntityModelBindings.collect(entityDefinition.entityData(), resourcePackStorage.getRenderControllers()::get)) { ++ final String geometryName = entityDefinition.entityData().getGeometries().get(binding.geometry()); ++ final String textureName = entityDefinition.entityData().getTextures().get(binding.texture()); ++ final BedrockGeometryModel bedrockGeometry = resourcePackStorage.getModels().entityModels().get(geometryName); ++ final String modelKey = binding.geometry() + "_" + binding.texture(); ++ final boolean hasTexture = textures.contains(textureName); ++ this.recordMissingAssets(resourcePackStorage, entityDefinition.identifier(), modelKey, geometryName, textureName, bedrockGeometry != null, hasTexture); ++ if (bedrockGeometry == null || !hasTexture) { ++ continue; } ++ final CompiledGeometry geometry = geometries.computeIfAbsent(bedrockGeometry, CompiledGeometry::compile); ++ javaContent.putGeometry(geometry); ++ javaContent.putModel(modelKey, geometry.withTexture("viabedrock:" + this.getJavaTexturePath(textureName))); ++ resourcePackStorage.getConverterData().put("ce_" + entityDefinition.identifier() + '_' + modelKey + "_scale", geometry.scale()); } javaContent.generateItemDefinition(); + return javaContent; diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/CustomItemTextureResourceRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/CustomItemTextureResourceRewriter.java index a1e27de2..474640e6 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/CustomItemTextureResourceRewriter.java @@ -489,6 +514,150 @@ index a1e27de2..474640e6 100644 } final JsonObject itemModel = new JsonObject(); itemModel.addProperty("parent", "minecraft:item/generated"); +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/EntityModelBindings.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/EntityModelBindings.java +new file mode 100644 +index 00000000..ddda2ab6 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/EntityModelBindings.java +@@ -0,0 +1,138 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.rewriter.resourcepack; ++ ++import org.cube.converter.data.bedrock.BedrockEntityData; ++import org.cube.converter.data.bedrock.controller.BedrockRenderController; ++ ++import java.util.ArrayList; ++import java.util.LinkedHashSet; ++import java.util.List; ++import java.util.Locale; ++import java.util.Map; ++import java.util.Set; ++import java.util.function.Function; ++import java.util.regex.Pattern; ++ ++/** Conservative model and texture pairs reachable through an actor's render controllers. */ ++final class EntityModelBindings { ++ ++ private static final Pattern ARRAY = Pattern.compile("(?i)(array\\.[a-z0-9_.]+)\\[(.+)]"); ++ private static final Pattern QUERY = Pattern.compile("(?i)(?:query|q)\\.(variant|skin_id|mark_variant)"); ++ private static final Pattern PROPERTY = Pattern.compile("(?i)(?:query|q)\\.property\\((['\"])([a-z0-9_:.-]+)\\1\\)"); ++ ++ static Set collect(final BedrockEntityData entity, final Function controllers) { ++ final Set result = new LinkedHashSet<>(); ++ if (entity.getControllers().isEmpty()) { ++ return all(entity); ++ } ++ for (final var reference : entity.getControllers()) { ++ final BedrockRenderController controller = controllers.apply(reference.identifier()); ++ if (controller == null || controller.textureExpressions().isEmpty()) { ++ return all(entity); ++ } ++ final Selection geometry = select(controller.geometryExpression(), "geometry.", controller.geometries(), entity.getGeometries()); ++ if (geometry == null) { ++ return all(entity); ++ } ++ for (final String expression : controller.textureExpressions()) { ++ final Selection texture = select(expression, "texture.", controller.textures(), entity.getTextures()); ++ if (texture == null) { ++ return all(entity); ++ } ++ if (geometry.query != null && geometry.query.equals(texture.query) && geometry.aliases.size() == texture.aliases.size()) { ++ // Only stable query reads correlate. Equal random or stateful expressions need independent combinations. ++ for (int index = 0; index < geometry.aliases.size(); index++) { ++ result.add(new Binding(geometry.aliases.get(index), texture.aliases.get(index))); ++ } ++ } else { ++ addAll(result, geometry.aliases, texture.aliases); ++ } ++ } ++ } ++ return result; ++ } ++ ++ private static Selection select(final String expression, final String prefix, final List arrays, ++ final Map aliases) { ++ final String value = expression.trim(); ++ final String direct = alias(value, prefix, aliases); ++ if (direct != null) { ++ return new Selection(List.of(direct), null); ++ } ++ final var match = ARRAY.matcher(value); ++ if (!match.matches()) { ++ return null; ++ } ++ for (final var array : arrays) { ++ if (!array.name().equalsIgnoreCase(match.group(1)) || array.values().isEmpty()) { ++ continue; ++ } ++ final List selected = new ArrayList<>(); ++ for (final String element : array.values()) { ++ final String name = alias(element.trim(), prefix, aliases); ++ if (name == null) { ++ return null; ++ } ++ selected.add(name); ++ } ++ return new Selection(selected, stableQuery(match.group(2))); ++ } ++ return null; ++ } ++ ++ private static String alias(final String value, final String prefix, final Map aliases) { ++ if (!value.regionMatches(true, 0, prefix, 0, prefix.length())) { ++ return null; ++ } ++ final String name = value.substring(prefix.length()); ++ String selected = null; ++ for (final String key : aliases.keySet()) { ++ if (key.equalsIgnoreCase(name)) { ++ if (selected != null) { ++ return null; ++ } ++ selected = key; ++ } ++ } ++ return selected; ++ } ++ ++ private static String stableQuery(final String expression) { ++ final String value = expression.replaceAll("\\s+", ""); ++ final var query = QUERY.matcher(value); ++ if (query.matches()) { ++ return query.group(1).toLowerCase(Locale.ROOT); ++ } ++ final var property = PROPERTY.matcher(value); ++ // Preserve the property name's case, even though Molang identifiers are case-insensitive. ++ return property.matches() ? "property:" + property.group(2) : null; ++ } ++ ++ private static Set all(final BedrockEntityData entity) { ++ final Set result = new LinkedHashSet<>(); ++ addAll(result, entity.getGeometries().keySet(), entity.getTextures().keySet()); ++ return result; ++ } ++ ++ private static void addAll(final Set result, final Iterable geometries, final Iterable textures) { ++ for (final String geometry : geometries) { ++ for (final String texture : textures) { ++ result.add(new Binding(geometry, texture)); ++ } ++ } ++ } ++ ++ private EntityModelBindings() { ++ } ++ ++ record Binding(String geometry, String texture) { ++ } ++ ++ private record Selection(List aliases, String query) { ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/ItemModelResourceRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/ItemModelResourceRewriter.java index 799413e5..4deef2b9 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/ItemModelResourceRewriter.java @@ -779,6 +948,80 @@ index 00000000..a488f7d4 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/EntityModelBindingsTest.java b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/EntityModelBindingsTest.java +new file mode 100644 +index 00000000..16d9d113 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/EntityModelBindingsTest.java +@@ -0,0 +1,68 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.rewriter.resourcepack; ++ ++import org.cube.converter.data.bedrock.BedrockEntityData; ++import org.cube.converter.data.bedrock.controller.BedrockRenderController; ++import org.cube.converter.parser.bedrock.controller.BedrockControllerParser; ++import org.junit.jupiter.api.Test; ++ ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class EntityModelBindingsTest { ++ ++ @Test ++ void matchingPropertySelectorsRetainOnlyTheirPairedModelsAndTextures() { ++ final var controller = controller("query.property('example:index')", "q.property('example:index')"); ++ final var bindings = EntityModelBindings.collect(entity(), _ -> controller); ++ assertEquals(Set.of(new EntityModelBindings.Binding("small", "red"), ++ new EntityModelBindings.Binding("large", "blue")), bindings); ++ } ++ ++ @Test ++ void independentQueriesPreserveCrossedModelAndTextureSelections() { ++ final var controller = controller("query.variant", "query.mark_variant"); ++ final var bindings = EntityModelBindings.collect(entity(), _ -> controller); ++ assertEquals(4, bindings.size()); ++ assertTrue(bindings.contains(new EntityModelBindings.Binding("small", "blue"))); ++ assertTrue(bindings.contains(new EntityModelBindings.Binding("large", "red"))); ++ } ++ ++ @Test ++ void equalRandomExpressionsStillPermitIndependentSelections() { ++ final var controller = controller("math.random(0,1)", "math.random(0,1)"); ++ assertEquals(4, EntityModelBindings.collect(entity(), _ -> controller).size()); ++ } ++ ++ @Test ++ void anUnresolvedControllerPreservesTheConservativeAliasCombinations() { ++ final var bindings = EntityModelBindings.collect(entity(), _ -> null); ++ assertEquals(9, bindings.size()); ++ assertTrue(bindings.contains(new EntityModelBindings.Binding("unused", "unused"))); ++ } ++ ++ private static BedrockEntityData entity() { ++ return new BedrockEntityData("example:actor", null, ++ List.of(new BedrockEntityData.RenderController("controller.render.example", "")), Map.of(), Map.of(), ++ Map.of("red", "textures/red", "blue", "textures/blue", "unused", "textures/unused"), ++ Map.of("small", "geometry.small", "large", "geometry.large", "unused", "geometry.unused")); ++ } ++ ++ private static BedrockRenderController controller(final String geometryIndex, final String textureIndex) { ++ return BedrockControllerParser.parse(""" ++ {"render_controllers":{"controller.render.example":{ ++ "geometry":"Array.geometries[%s]","textures":["Array.textures[%s]"], ++ "arrays":{"geometries":{"Array.geometries":["Geometry.small","Geometry.large"]}, ++ "textures":{"Array.textures":["Texture.red","Texture.blue"]}} ++ }}} ++ """.formatted(geometryIndex, textureIndex)).getFirst(); ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/ItemModelResourceRewriterTest.java b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/ItemModelResourceRewriterTest.java new file mode 100644 index 00000000..84c8e7df diff --git a/patches/viabedrock/upstreamable/0066-preserve-complete-native-appearance-assets.pr.md b/patches/viabedrock/upstreamable/0066-preserve-complete-native-appearance-assets.pr.md index 7fd1e0e8..b1c355ef 100644 --- a/patches/viabedrock/upstreamable/0066-preserve-complete-native-appearance-assets.pr.md +++ b/patches/viabedrock/upstreamable/0066-preserve-complete-native-appearance-assets.pr.md @@ -15,3 +15,12 @@ Regression coverage includes `CustomItemTextureResourceRewriterTest`, `ItemModel Semantic conversion and recorded equipment controls cover supported variants. Additional binding forms, equipment lifecycles, and complete appearance remain unverified. The [owning patch](0066-preserve-complete-native-appearance-assets.patch) retains implementation rationale and versioned evidence. + +Actor conversion retains pairs reachable through declared render controllers. +Equal stable query reads over equally sized arrays select corresponding geometry and texture entries. +Independent and random selectors retain independent combinations. Unknown controllers and unsupported expressions retain the conservative fallback. +The four captured OneBlockOnline furniture-preview actors each retain 720 pairs instead of 518,400. +Reduced tests cover paired properties, independent queries, random expressions, and unresolved controllers. +A private conversion fixture processes all 867 captured actors within a 2 GiB heap. +It emits 4,068 model selections. Each preview emits 719 models and reports one unresolved source pair from the captured packs. +The rebuilt live retry loses NetherNet transport before conversion. Live admission and complete native actor appearance remain unverified. diff --git a/patches/viabedrock/upstreamable/0080-retain-and-transport-native-player-permissions.patch b/patches/viabedrock/upstreamable/0080-retain-and-transport-native-player-permissions.patch index 54e6029e..ad74ee90 100644 --- a/patches/viabedrock/upstreamable/0080-retain-and-transport-native-player-permissions.patch +++ b/patches/viabedrock/upstreamable/0080-retain-and-transport-native-player-permissions.patch @@ -1,4 +1,4 @@ -From c80419cad710199bac479d3adbdec3d33f4b9dec Mon Sep 17 00:00:00 2001 +From d94d92e4a22440a5ca29729939a95b4c2a93d746 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(input): retain and transport native player permissions @@ -367,10 +367,10 @@ index 00000000..71927cf1 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 66775c1f..592a104d 100644 +index 330abbf5..b545a18f 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -585,6 +585,8 @@ public final class JoinPackets { +@@ -556,6 +556,8 @@ public final class JoinPackets { user.get(NativeSkinStorage.class).markJoined(); user.get(EntityTracker.class).markNativeActorsJoined(); user.get(EntityTracker.class).sendNativeCustomEntitySnapshot(); diff --git a/patches/viabedrock/upstreamable/0081-retain-server-hud-visibility.patch b/patches/viabedrock/upstreamable/0081-retain-server-hud-visibility.patch index 9803df87..9e41cf23 100644 --- a/patches/viabedrock/upstreamable/0081-retain-server-hud-visibility.patch +++ b/patches/viabedrock/upstreamable/0081-retain-server-hud-visibility.patch @@ -1,4 +1,4 @@ -From 12323890fa1d3932885180a7219ef233d5041919 Mon Sep 17 00:00:00 2001 +From 6738965c6572404ec13dbe605faa7d3468e05f64 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(hud): retain server visibility restrictions @@ -224,10 +224,10 @@ index 00000000..c4caba14 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 592a104d..7e6d9f53 100644 +index b545a18f..dc1d68b8 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -587,6 +587,8 @@ public final class JoinPackets { +@@ -558,6 +558,8 @@ public final class JoinPackets { user.get(EntityTracker.class).sendNativeCustomEntitySnapshot(); user.get(PlayerInputStorage.class).markJoined(); user.get(PlayerInputStorage.class).sendSnapshot(); diff --git a/patches/viabedrock/upstreamable/0083-synchronize-native-armor-attributes.patch b/patches/viabedrock/upstreamable/0083-synchronize-native-armor-attributes.patch index ce6bc16b..8ed488d6 100644 --- a/patches/viabedrock/upstreamable/0083-synchronize-native-armor-attributes.patch +++ b/patches/viabedrock/upstreamable/0083-synchronize-native-armor-attributes.patch @@ -1,4 +1,4 @@ -From 654389dce62acdcd641fbe0744675ce11146060f Mon Sep 17 00:00:00 2001 +From bc8ba37809625adfe902dc002c64110a3e1e6ce8 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(equipment): synchronize native armor attributes @@ -220,10 +220,10 @@ index 3097c85f..21d20d88 100644 clientPlayer.setAbilities(clientPlayer.abilities()); // Java client always resets abilities on respawn. Resend them PacketFactory.sendJavaGameEvent(wrapper.user(), GameEventType.LEVEL_CHUNKS_LOAD_START, 0F); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 7e6d9f53..df09392a 100644 +index dc1d68b8..f6c8e18f 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -596,6 +596,7 @@ public final class JoinPackets { +@@ -567,6 +567,7 @@ public final class JoinPackets { clientPlayer.createTeam(); clientPlayer.sendInitialEntityData(); clientPlayer.updateAttributes(clientPlayer.attributes().values().toArray(new EntityAttribute[0])); diff --git a/patches/viabedrock/upstreamable/0089-resolve-and-transport-native-camera-instructions.patch b/patches/viabedrock/upstreamable/0089-resolve-and-transport-native-camera-instructions.patch index d1eb25ee..ab0e2d59 100644 --- a/patches/viabedrock/upstreamable/0089-resolve-and-transport-native-camera-instructions.patch +++ b/patches/viabedrock/upstreamable/0089-resolve-and-transport-native-camera-instructions.patch @@ -1,4 +1,4 @@ -From ea62a8b000e67be6f84f635035803c9004b3efc7 Mon Sep 17 00:00:00 2001 +From 1054956c42a146a26e00d27bc9d8e3ca27647313 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(camera): resolve server presets and camera movement @@ -6836,10 +6836,10 @@ index 00000000..9feb3f9e + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index df09392a..01389255 100644 +index f6c8e18f..e93b7a41 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -589,6 +589,8 @@ public final class JoinPackets { +@@ -560,6 +560,8 @@ public final class JoinPackets { user.get(PlayerInputStorage.class).sendSnapshot(); user.get(HudVisibilityStorage.class).markJoined(); user.get(HudVisibilityStorage.class).sendSnapshot(); diff --git a/patches/viabedrock/upstreamable/0091-recover-unacknowledged-chunk-radius-requests.patch b/patches/viabedrock/upstreamable/0091-recover-unacknowledged-chunk-radius-requests.patch index 3fd55572..161c9d3a 100644 --- a/patches/viabedrock/upstreamable/0091-recover-unacknowledged-chunk-radius-requests.patch +++ b/patches/viabedrock/upstreamable/0091-recover-unacknowledged-chunk-radius-requests.patch @@ -1,4 +1,4 @@ -From 700d11a5e12cca08b456b7fb24bb056c08a54e37 Mon Sep 17 00:00:00 2001 +From b3eaaf0f634f729ffc3b1d5f82523c47ef0de934 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] fix(chunks): recover unacknowledged join radius requests @@ -39,7 +39,7 @@ index 33bf0711..13872293 100644 user.put(new WorldClockTracker()); user.put(new VoxelShapeRegistry()); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 01389255..7a8861d0 100644 +index e93b7a41..64d13b71 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -47,7 +47,6 @@ import net.raphimc.viabedrock.protocol.BedrockProtocol; @@ -58,7 +58,7 @@ index 01389255..7a8861d0 100644 PacketFactory.sendBedrockLoadingScreen(wrapper.user(), ServerboundLoadingScreenPacketType.EndLoadingScreen, null); final PacketWrapper setLocalPlayerAsInitialized = PacketWrapper.create(ServerboundBedrockPackets.SET_LOCAL_PLAYER_AS_INITIALIZED, wrapper.user()); -@@ -509,10 +509,7 @@ public final class JoinPackets { +@@ -480,10 +480,7 @@ public final class JoinPackets { private static void finishJoin(final UserConnection user) { handleJavaClientGameJoin(user); diff --git a/patches/viabedrock/upstreamable/0092-retain-completed-client-prediction-frames.patch b/patches/viabedrock/upstreamable/0092-retain-completed-client-prediction-frames.patch index 9249093e..19deb2fe 100644 --- a/patches/viabedrock/upstreamable/0092-retain-completed-client-prediction-frames.patch +++ b/patches/viabedrock/upstreamable/0092-retain-completed-client-prediction-frames.patch @@ -1,4 +1,4 @@ -From 9467704a42df921568b8afb474859789f5e7ef72 Mon Sep 17 00:00:00 2001 +From 7f4cb1fa29482819ef434e70e8aefc5a67a76d87 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(movement): retain completed client prediction frames @@ -4802,10 +4802,10 @@ index c6ea78df..423c0c09 100644 final Entity entity = wrapper.user().get(EntityTracker.class).getEntityByRid(runtimeId); if (entity == null) { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 7a8861d0..f3b0cfeb 100644 +index 64d13b71..847efa7c 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -366,6 +366,7 @@ public final class JoinPackets { +@@ -337,6 +337,7 @@ public final class JoinPackets { gameSession.setBedrockVanillaVersion(version); gameSession.setFlatGenerator(generatorType == GeneratorType.Flat); gameSession.setMovementRewindHistorySize(rewindHistorySize); @@ -4813,7 +4813,7 @@ index 7a8861d0..f3b0cfeb 100644 gameSession.setLevelGameType(levelGameType); gameSession.setLevelTime(levelTime); gameSession.setHardcoreMode(hardcore); -@@ -594,6 +595,7 @@ public final class JoinPackets { +@@ -565,6 +566,7 @@ public final class JoinPackets { clientPlayer.createTeam(); clientPlayer.sendInitialEntityData(); diff --git a/patches/viabedrock/upstreamable/0093-complete-login-before-resource-pack-negotiation.patch b/patches/viabedrock/upstreamable/0093-complete-login-before-resource-pack-negotiation.patch index 125a457d..c72291e4 100644 --- a/patches/viabedrock/upstreamable/0093-complete-login-before-resource-pack-negotiation.patch +++ b/patches/viabedrock/upstreamable/0093-complete-login-before-resource-pack-negotiation.patch @@ -1,4 +1,4 @@ -From e78a8442cb0a09e343b82c483d6dc3ee69736963 Mon Sep 17 00:00:00 2001 +From d4890416a98517c77c4144398c6c0fd9bd12021e 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(login): complete login before resource pack negotiation @@ -13,7 +13,7 @@ Advance only the server protocol state after the first success. Leave the Java c create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/LoginTransitionTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index f3b0cfeb..f1f65d01 100644 +index 847efa7c..8f8b7567 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -136,6 +136,11 @@ public final class JoinPackets { 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 9ad2ff55..875c6f1e 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 bb4bbfcb34c9f741206806019fc1f7e1ebb4832f Mon Sep 17 00:00:00 2001 +From f30328f70b25d66cd894c6d984636be3d68ba970 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 @@ -4836,7 +4836,7 @@ index 39b8a7e4..d22d7616 100644 + } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index f1f65d01..e334cbe0 100644 +index 8f8b7567..0fa95fb6 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -267,6 +267,7 @@ public final class JoinPackets { @@ -4847,7 +4847,7 @@ index f1f65d01..e334cbe0 100644 wrapper.read(BedrockTypes.INT_LE); // limited world width wrapper.read(BedrockTypes.INT_LE); // limited world height wrapper.read(Types.BOOLEAN); // nether type -@@ -365,7 +366,7 @@ public final class JoinPackets { +@@ -336,7 +337,7 @@ public final class JoinPackets { } if (!inventoryServerAuthoritative) { @@ -4856,7 +4856,7 @@ index f1f65d01..e334cbe0 100644 } gameSession.setBedrockVanillaVersion(version); -@@ -455,7 +456,8 @@ public final class JoinPackets { +@@ -426,7 +427,8 @@ public final class JoinPackets { ); protocol.registerClientbound(ClientboundBedrockPackets.ITEM_REGISTRY, null, wrapper -> { wrapper.cancel(); @@ -4866,7 +4866,7 @@ index f1f65d01..e334cbe0 100644 ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Received ITEM_REGISTRY after item rewriter was already initialized"); return; } -@@ -463,11 +465,14 @@ public final class JoinPackets { +@@ -434,11 +436,14 @@ public final class JoinPackets { final ItemEntry[] itemEntries = wrapper.read(BedrockTypes.ITEM_ENTRY_ARRAY); // items final ItemRewriter itemRewriter = new ItemRewriter(wrapper.user(), itemEntries); wrapper.user().put(itemRewriter); @@ -4883,7 +4883,7 @@ index f1f65d01..e334cbe0 100644 } for (ItemEntry itemEntry : itemEntries) { if (itemEntry.componentData() != null && itemEntry.version() == ItemVersion.DataDriven && itemRewriter.getComponentItems().contains(itemEntry.identifier())) { -@@ -590,6 +595,7 @@ public final class JoinPackets { +@@ -561,6 +566,7 @@ public final class JoinPackets { user.get(EntityTracker.class).sendNativeCustomEntitySnapshot(); user.get(PlayerInputStorage.class).markJoined(); user.get(PlayerInputStorage.class).sendSnapshot(); diff --git a/patches/viabedrock/upstreamable/0099-bridge-native-actor-picking-hitboxes.patch b/patches/viabedrock/upstreamable/0099-bridge-native-actor-picking-hitboxes.patch index d45b8870..9f864d69 100644 --- a/patches/viabedrock/upstreamable/0099-bridge-native-actor-picking-hitboxes.patch +++ b/patches/viabedrock/upstreamable/0099-bridge-native-actor-picking-hitboxes.patch @@ -1,4 +1,4 @@ -From 4802a495a7a5357544ec2ca60c0cee4484e75c61 Mon Sep 17 00:00:00 2001 +From f369213f195d38650eeea176dc885c20d06e3069 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(entities): bridge authoritative native picking hitboxes @@ -573,10 +573,10 @@ index 718884c8..841a7cb8 100644 }); protocol.registerServerbound(ServerboundPackets26_3.MOVE_PLAYER_STATUS_ONLY, null, wrapper -> { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 5c54c4e1..f73214cc 100644 +index 25355ba0..e816a984 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -607,6 +607,7 @@ public final class JoinPackets { +@@ -578,6 +578,7 @@ public final class JoinPackets { clientPlayer.createTeam(); clientPlayer.sendInitialEntityData(); clientPlayer.sendStepHeight(); diff --git a/patches/viabedrock/upstreamable/0102-match-native-language-parsing-and-formatting.patch b/patches/viabedrock/upstreamable/0102-match-native-language-parsing-and-formatting.patch index 519d73a4..a36077b9 100644 --- a/patches/viabedrock/upstreamable/0102-match-native-language-parsing-and-formatting.patch +++ b/patches/viabedrock/upstreamable/0102-match-native-language-parsing-and-formatting.patch @@ -1,4 +1,4 @@ -From 56e3a14a736e6de776777bb6e6ba33aac9aadb51 Mon Sep 17 00:00:00 2001 +From 993542a82281e41669f0ea85aa688ebbc9ccf3fe Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:11:46 +0200 Subject: [PATCH] fix(text): match native language parsing and formatting @@ -612,7 +612,7 @@ index 95d2d5b3..fc7688fb 100644 + } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index f73214cc..4d295481 100644 +index e816a984..0b9f70f8 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -38,6 +38,7 @@ import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets @@ -623,7 +623,7 @@ index f73214cc..4d295481 100644 import net.raphimc.viabedrock.api.util.BitSets; import net.raphimc.viabedrock.api.util.PacketFactory; import net.raphimc.viabedrock.api.util.StringUtil; -@@ -503,7 +504,8 @@ public final class JoinPackets { +@@ -474,7 +475,8 @@ public final class JoinPackets { } private static void writePlayStatusKickMessage(final PacketWrapper wrapper, final PlayStatus status) { diff --git a/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.patch b/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.patch index 8c659407..cb2574ca 100644 --- a/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.patch +++ b/patches/viabedrock/upstreamable/0104-transport-resolved-native-fog-stacks.patch @@ -1,4 +1,4 @@ -From 7d8da80a21e2b85ecc7d925dfd9e40f5a8671aab Mon Sep 17 00:00:00 2001 +From c37b0cb2b03906c92236e3cb3312c85762893e65 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:05:15 +0100 Subject: [PATCH] feat(fog): transport resolved native fog settings @@ -373,10 +373,10 @@ index 00000000..c0d18523 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -index 4d295481..677047ac 100644 +index 0b9f70f8..b7fb312e 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -598,6 +598,8 @@ public final class JoinPackets { +@@ -569,6 +569,8 @@ public final class JoinPackets { user.get(PlayerInputStorage.class).markJoined(); user.get(PlayerInputStorage.class).sendSnapshot(); HudPackets.sendNativeRoot(user); diff --git a/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.patch b/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.patch index 1cf13d61..9a6d80cc 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.patch @@ -1,4 +1,4 @@ -From b6ec2ce43f0097e5758f63109118a619c952e7e5 Mon Sep 17 00:00:00 2001 +From 2c433b2d3bf45713a802c3aba3e9694505db5bbe Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(bedrock): add featured server browser @@ -23,6 +23,16 @@ continue to use their advertised host and port. The captured current Dimension Clash response passes the parser with its exact network ID. Add boundary regressions for creator metadata, ordinary socket metadata, and a missing network ID; keep raw service responses private. + +Resolve every catalog selection through Gatherings, including entries with +static display addresses. Current 1.26.60 Mineville and CubeCraft join +responses select NetherNet with an HTTP signaling host and port, while +hosted SoulSteel uses NetherNet_JsonRpc with an opaque network ID. Keep +these destinations separate. A catalog display address does not establish +that the selected experience still accepts RakNet. The Mineville display +endpoint times out over RakNet before any game packet. HTTP destinations +use the existing explicit NetherNet URI connection path. This is transport +selection evidence, not a claim of complete server gameplay parity. --- .../integration/MixinServerListsScreen.java | 18 +- .../mixin/core/integration/MixinVFPPopup.java | 2 +- @@ -30,12 +40,12 @@ and a missing network ID; keep raw service responses private. .../bedrock/screen/BedrockServerList.java | 46 ++ .../bedrock/screen/BedrockServersScreen.java | 399 ++++++++++++++++++ .../bedrock/servers/BedrockServerCatalog.java | 271 ++++++++++++ - .../servers/BedrockServerConnection.java | 53 +++ + .../servers/BedrockServerConnection.java | 49 +++ .../bedrock/servers/BedrockServerStatus.java | 116 +++++ .../bedrock/visual/BedrockImageCache.java | 3 +- .../viafabricplus-bedrock/lang/en_us.json | 20 + - .../servers/BedrockServerCatalogTest.java | 42 ++ - 11 files changed, 1173 insertions(+), 8 deletions(-) + .../servers/BedrockServerCatalogTest.java | 52 +++ + 11 files changed, 1179 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/BedrockServerDetailsScreen.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/BedrockServerList.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/BedrockServersScreen.java @@ -772,7 +782,7 @@ index 0000000..4c799cb +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalog.java b/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalog.java new file mode 100644 -index 0000000..bc810a2 +index 0000000..5320244 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalog.java @@ -0,0 +1,271 @@ @@ -888,13 +898,13 @@ index 0000000..bc810a2 + static JoinAddress parseJoin(final JsonObject response) throws IOException { + final JsonObject data = object(response, "result"); + final String protocol = string(data, "networkProtocol"); -+ if (protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_NETHERNET) -+ || protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_NETHERNET_JSONRPC)) { ++ if (protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_NETHERNET_JSONRPC)) { + final String id = string(data, "netherNetId"); + if (id.isBlank()) throw new IOException("Bedrock experience returned no usable NetherNet ID"); + return new JoinAddress(id, protocol); + } -+ if (!protocol.isBlank() && !protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_DEFAULT)) { ++ final boolean http = protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_NETHERNET); ++ if (!protocol.isBlank() && !protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_DEFAULT) && !http) { + throw new IOException("Bedrock experience returned an unsupported transport"); + } + final String host = string(data, "ipV4Address"); @@ -902,7 +912,7 @@ index 0000000..bc810a2 + if (host.isBlank() || port < 1 || port > 65535) { + throw new IOException("Bedrock experience returned no usable server address"); + } -+ return new JoinAddress(host + ':' + port, protocol); ++ return new JoinAddress((http ? "nethernet://" : "") + host + ':' + port, protocol); + } + + private static URI service() throws IOException, InterruptedException { @@ -1049,10 +1059,10 @@ index 0000000..bc810a2 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerConnection.java b/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerConnection.java new file mode 100644 -index 0000000..19ae194 +index 0000000..50ab35b --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerConnection.java -@@ -0,0 +1,53 @@ +@@ -0,0 +1,49 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/florianreuth/viafabricplus-bedrock + * Copyright (C) 2023-2026 ViaVersion and contributors @@ -1071,7 +1081,6 @@ index 0000000..19ae194 +import net.raphimc.minecraftauth.bedrock.BedrockAuthManager; +import net.raphimc.minecraftauth.extra.realms.model.RealmsJoinInformation; +import net.raphimc.viabedrock.api.BedrockProtocolVersion; -+import org.cloudburstmc.netty.channel.nethernet.config.NetherNetAddress; + +/** Resolves hosted experiences and connects through the transport requested by Gatherings. */ +public final class BedrockServerConnection { @@ -1080,18 +1089,15 @@ index 0000000..19ae194 + } + + public static CompletableFuture join(final BedrockAuthManager account, final Server server) { -+ final CompletableFuture destination = server.hasAddress() -+ ? CompletableFuture.completedFuture(new JoinAddress(server.address(), RealmsJoinInformation.PROTOCOL_DEFAULT)) -+ : BedrockServerCatalog.join(account, server); ++ final CompletableFuture destination = BedrockServerCatalog.join(account, server); + return destination.thenCompose(address -> { + final CompletableFuture result = new CompletableFuture<>(); + Minecraft.getInstance().execute(() -> { + try { + final String protocol = address.networkProtocol(); -+ if (protocol.isBlank() || protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_DEFAULT)) { ++ if (protocol.isBlank() || protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_DEFAULT) ++ || protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_NETHERNET)) { + ConnectionUtil.connect(address.address(), BedrockProtocolVersion.BEDROCK_LATEST); -+ } else if (protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_NETHERNET)) { -+ BedrockConnectionUtil.connectNetherNet(new NetherNetAddress(address.address())); + } else if (protocol.equalsIgnoreCase(RealmsJoinInformation.PROTOCOL_NETHERNET_JSONRPC)) { + BedrockConnectionUtil.connectNetherNet(new NetherNetJsonRpcAddress(address.address())); + } else { @@ -1275,10 +1281,10 @@ index fa7eff9..6e1452c 100644 "bedrock_lan.viafabricplus.refresh": "Refresh", diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalogTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalogTest.java new file mode 100644 -index 0000000..ba9702c +index 0000000..051a151 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/servers/BedrockServerCatalogTest.java -@@ -0,0 +1,42 @@ +@@ -0,0 +1,52 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1305,6 +1311,16 @@ index 0000000..ba9702c + } + + @Test ++ void httpNetherNetJoinsUseTheSignalingHostAndPort() throws Exception { ++ final var response = JsonParser.parseString(""" ++ {"result":{"networkProtocol":"NetherNet","ipV4Address":"example.test","port":19132}} ++ """).getAsJsonObject(); ++ final var join = BedrockServerCatalog.parseJoin(response); ++ assertEquals("nethernet://example.test:19132", join.address()); ++ assertEquals("NetherNet", join.networkProtocol()); ++ } ++ ++ @Test + void socketJoinsRetainTheirAdvertisedPort() throws Exception { + final var response = JsonParser.parseString(""" + {"result":{"ipV4Address":"127.0.0.1","port":19132}} diff --git a/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.pr.md b/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.pr.md index f7a12446..15fa05e5 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0015-add-featured-server-browser.pr.md @@ -3,10 +3,15 @@ Show the live Gatherings catalog with server details, artwork, status, and hosted experience resolution. Use the existing Bedrock connection path for the advertised transport. -Hosted NetherNet joins read `netherNetId` instead of requiring an IP address and port. -Keep the network ID unchanged when selecting Xbox JSON-RPC signaling. Socket joins retain their advertised port. +Hosted Xbox JSON-RPC joins read `netherNetId` instead of requiring an IP address and port. +Keep the network ID unchanged when selecting Xbox JSON-RPC signaling. +HTTP NetherNet joins use the signaling host and port. Socket joins retain their advertised port. +Every catalog selection resolves its current destination through Gatherings, including entries with static display addresses. The captured current Dimension Clash response passes this parser. Regression cases cover both destination types and missing network IDs. A fresh browser join reaches SoulSteel pack negotiation over Xbox JSON-RPC. The transport then disconnects before StartGame. -This verifies catalog resolution and initial admission. Gameplay remains unverified. +This verifies catalog resolution and initial admission. SoulSteel gameplay remains unverified. +The current Mineville response selects HTTP NetherNet. Its RakNet endpoint times out before any game packet. +A live add-on recording through that HTTP destination reaches a visible lobby and completes its scene. +The browser transport choice has targeted parser coverage. Full native appearance remains unverified. Keep raw service responses, server endpoints, account data, and captures private. From fa0943df66ddc15280f34dadbb646acba4bba6fa Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:33:31 +0100 Subject: [PATCH 11/33] fix(ui): restore authored hosted-server menus Preserve printable resource names, optional image policy, and native frame strips within bounded image decoding. These resource failures replaced Enchanted menus with ordinary controls. Retain sibling extents during stack fill and exact fractional grid capacity so authored cards keep their geometry and response identities. Fold the fixes and native comparison evidence into the owning UI patches. --- ...are-native-ui-programs-and-resources.patch | 137 +++++- ...are-native-ui-programs-and-resources.pr.md | 12 + ...0029-share-native-ui-scene-rendering.patch | 421 ++++++++++++------ ...0029-share-native-ui-scene-rendering.pr.md | 13 + 4 files changed, 425 insertions(+), 158 deletions(-) diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch index 2655dbfa..5f4e6736 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch @@ -1,4 +1,4 @@ -From f51d58dbbaba59cdf18d5bc000d5c701eb7e774f Mon Sep 17 00:00:00 2001 +From cb0004f758535d7a16ab4555d115192c73b8c045 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): share bounded native UI programs and resource @@ -394,6 +394,32 @@ The reduced regression retains row text and authored dimensions without duplicating the factory or exceeding its bound. Re-resolving the actual captured pack produces eight cells instead of 46. Keep the raw pack and traffic private; installed layout comparison remains a separate check. + +Preserve printable UTF-8 names and spaces in bounded relative UI resource +paths. Converted Java identifiers already hash the original resource name, +so an ASCII-only source filter rejects valid native references without +providing additional identifier safety. Continue rejecting absolute paths, +traversal, drive/URI syntax, backslash separators, control characters and +unpaired UTF-16 surrogates. + +A native Bedrock 1.26.60.5 Enchanted capture opens the authored initial hub +menu and sends original button index 2 for OP SMP. Its exported 58-file +pack matches the add-on's pack byte for byte. A generated decorative icon +reference contains a formatting character and spaces; the previous filter +rejects that reference and replaces the whole menu with a generic dialog. +The corrected program resolves the captured menu and retains all 11 response +identities. Regressions preserve bound texture pixels and metadata through +conversion and retain a valid form action when a printable texture is absent. +Raw packs, traffic and native screenshots remain private. The native capture +stops after the transfer selection, so it is not a completed gameplay scene. + +Retain tall or wide UI frame strips within the previous 16-Mipixel decoded +image budget. The matching Enchanted native pack includes a 963x5916 logo +atlas and a 5880x480 disconnect background. The former square 4096-per-side +limit silently discards both despite their bounded pixel counts, leaving +required converted references unavailable. Share the revised dimension +policy with client decoding and retain a small encoded-strip regression +that checks both endpoint pixels after conversion. Keep source byte limits. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -425,7 +451,7 @@ traffic private; installed layout comparison remains a separate check. .../api/resourcepack/ui/HudTitleUi.java | 28 + .../resourcepack/ui/InventoryItemFilter.java | 43 + .../resourcepack/ui/InventoryStackCount.java | 21 + - .../api/resourcepack/ui/JsonUiProgram.java | 1671 +++++++++++++++++ + .../api/resourcepack/ui/JsonUiProgram.java | 1676 +++++++++++++++++ .../resourcepack/ui/NativeRecipeSearch.java | 124 ++ .../ui/NativeSearchNormalization.java | 189 ++ .../api/resourcepack/ui/NpcDialogueUi.java | 316 ++++ @@ -438,6 +464,7 @@ traffic private; installed layout comparison remains a separate check. .../api/resourcepack/ui/UiBitmapFonts.java | 282 +++ .../api/resourcepack/ui/UiExpression.java | 306 +++ .../api/resourcepack/ui/UiFonts.java | 286 +++ + .../api/resourcepack/ui/UiTextureLimits.java | 22 + .../api/resourcepack/ui/UiTextures.java | 94 + .../viabedrock/api/util/NativeSort.java | 168 ++ .../rewriter/ResourcePackRewriter.java | 2 + @@ -473,7 +500,7 @@ traffic private; installed layout comparison remains a separate check. .../ui/NativeRecipeSearchTest.java | 110 ++ .../ui/NativeSearchNormalizationTest.java | 46 + .../resourcepack/ui/NpcDialogueUiTest.java | 108 ++ - .../resourcepack/ui/NpcEditorStateTest.java | 209 +++ + .../resourcepack/ui/NpcEditorStateTest.java | 209 ++ .../resourcepack/ui/RecipeCollectionTest.java | 227 +++ .../ui/ServerCustomFormUiTest.java | 146 ++ .../ui/ServerFormUiSliderStateTest.java | 126 ++ @@ -483,10 +510,10 @@ traffic private; installed layout comparison remains a separate check. .../ui/UiCollectionWindowTest.java | 131 ++ .../api/resourcepack/ui/UiExpressionTest.java | 77 + .../api/resourcepack/ui/UiFontsTest.java | 192 ++ - .../api/resourcepack/ui/UiTexturesTest.java | 175 ++ + .../api/resourcepack/ui/UiTexturesTest.java | 213 +++ .../ui/native-inventory-item-filter.json | 311 +++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 91 files changed, 14395 insertions(+), 60 deletions(-) + 92 files changed, 14460 insertions(+), 60 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerMessage.java @@ -528,6 +555,7 @@ traffic private; installed layout comparison remains a separate check. create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiBitmapFonts.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiExpression.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiFonts.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextureLimits.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextures.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/util/NativeSort.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/JsonUiResourceRewriter.java @@ -3840,10 +3868,10 @@ index 00000000..5dfbb3f8 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java new file mode 100644 -index 00000000..61b577f1 +index 00000000..3bac1d97 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java -@@ -0,0 +1,1671 @@ +@@ -0,0 +1,1676 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -4666,7 +4694,12 @@ index 00000000..61b577f1 + } + + static boolean safePath(final String path) { -+ return path.matches("[A-Za-z0-9_./~-]{1,256}") && !path.contains("..") && !path.startsWith("/"); ++ if (path.isEmpty() || path.length() > 256 || path.contains("..") || path.startsWith("/") ++ || path.indexOf('\\') >= 0 || path.indexOf(':') >= 0) { ++ return false; ++ } ++ // Native resource names are UTF-8. Converted identifiers hash the original name. ++ return path.codePoints().noneMatch(value -> Character.isISOControl(value) || Character.getType(value) == Character.SURROGATE); + } + + static String string(final JsonObject object, final String key) { @@ -8434,9 +8467,37 @@ index 00000000..8715b892 + record Metrics(double size, double baseline, double lineHeight) { } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextureLimits.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextureLimits.java +new file mode 100644 +index 00000000..b2fcd650 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextureLimits.java +@@ -0,0 +1,22 @@ ++/* ++ * 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; ++ ++/** Shared bounds for converted pack textures and their decoded client images. */ ++public final class UiTextureLimits { ++ ++ private static final int MAX_DIMENSION = 16384; ++ private static final long MAX_PIXELS = 16L * 1024 * 1024; ++ ++ public static boolean supportsDimensions(final int width, final int height) { ++ return width > 0 && height > 0 && width <= MAX_DIMENSION && height <= MAX_DIMENSION ++ && (long) width * height <= MAX_PIXELS; ++ } ++ ++ private UiTextureLimits() { ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextures.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextures.java new file mode 100644 -index 00000000..e597c112 +index 00000000..cec70899 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextures.java @@ -0,0 +1,94 @@ @@ -8528,7 +8589,7 @@ index 00000000..e597c112 + } + + private static void validateDimensions(final int width, final int height) throws IOException { -+ if (width <= 0 || height <= 0 || width > 4096 || height > 4096) { ++ if (!UiTextureLimits.supportsDimensions(width, height)) { + throw new IOException("UI texture exceeds dimension limits"); + } + } @@ -15812,10 +15873,10 @@ index 00000000..b83ce92b +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTexturesTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTexturesTest.java new file mode 100644 -index 00000000..29be2f0c +index 00000000..d918257f --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTexturesTest.java -@@ -0,0 +1,175 @@ +@@ -0,0 +1,213 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -15831,12 +15892,15 @@ index 00000000..29be2f0c +import java.util.HashMap; +import java.util.List; +import java.util.Map; ++import java.util.UUID; +import net.raphimc.viabedrock.api.resourcepack.ResourcePack; +import net.raphimc.viabedrock.api.resourcepack.content.InMemoryContent; +import net.raphimc.viabedrock.protocol.rewriter.resourcepack.JsonUiResourceRewriter; +import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; +import javax.imageio.ImageIO; +import org.junit.jupiter.api.Test; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.ValueSource; +import static org.junit.jupiter.api.Assertions.*; + +class UiTexturesTest { @@ -15848,7 +15912,7 @@ index 00000000..29be2f0c + final var lower = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + lower.setRGB(0, 0, 0xFFAB5612); + final var content = new InMemoryContent(); -+ content.putString("manifest.json", "{\"format_version\":2,\"header\":{\"uuid\":\"" + java.util.UUID.randomUUID() ++ content.putString("manifest.json", "{\"format_version\":2,\"header\":{\"uuid\":\"" + UUID.randomUUID() + + "\",\"version\":[1,0,0],\"name\":\"test\"}}"); + content.put("textures/ui/Banners.png", encode(upper, "png")); + content.put("textures/ui/banners.png", encode(lower, "png")); @@ -15863,12 +15927,15 @@ index 00000000..29be2f0c + } + assertEquals(upper.getRGB(0, 0), converted.getImage("assets/viabedrock/" + upperId.substring(11)).getImage().getRGB(0, 0)); + assertEquals(lower.getRGB(0, 0), converted.getImage("assets/viabedrock/" + lowerId.substring(11)).getImage().getRGB(0, 0)); -+ assertThrows(IllegalArgumentException.class, () -> JsonUiProgram.textureId("textures/ui/../Banners")); ++ for (final String invalid : List.of("textures/ui/../Banners", "/textures/ui/Banners", "C:\\textures\\Banners", ++ "textures/ui/line\nfeed", "textures/ui/nul" + '\0' + "name")) { ++ assertThrows(IllegalArgumentException.class, () -> JsonUiProgram.textureId(invalid)); ++ } + } + -+ @Test -+ void boundTexturesOutsideTheUiDirectoryRetainLayerPixelsAndMetadata() throws Exception { -+ final String path = "textures/profile/backgrounds/default"; ++ @ParameterizedTest ++ @ValueSource(strings = {"textures/profile/backgrounds/default", "textures/ui/icons/§lCHECK OUT"}) ++ void boundNativeTextureNamesRetainLayerPixelsAndMetadata(final String path) throws Exception { + final var lowerImage = new BufferedImage(2, 1, BufferedImage.TYPE_INT_ARGB); + lowerImage.setRGB(0, 0, 0xFF1256AB); + final byte[] lower = encode(lowerImage, "png"); @@ -15902,6 +15969,38 @@ index 00000000..29be2f0c + } + + @Test ++ void aMissingPrintableTextureDoesNotDiscardTheFormsValidAction() { ++ final var ui = new ServerFormUi(List.of(Map.of("ui/server_form.json", """ ++ {"namespace":"server_form","main_screen_content":{"type":"button","collection_index":0, ++ "$pressed_button_name":"button.form_button_click","controls":[{"tag":{"type":"image", ++ "texture":"#title_text","allow_debug_missing_texture":false,"bindings":[{"binding_name":"#title_text"}]}}]}} ++ """.getBytes(StandardCharsets.UTF_8)))); ++ final var form = JsonParser.parseString(""" ++ {"type":"form","title":"textures/ui/icons/§lCHECK OUT","elements":[{"type":"button","text":"Continue"}]} ++ """).getAsJsonObject(); ++ final var scene = ui.resolve(form); ++ assertNotNull(scene); ++ final var root = scene.getAsJsonObject("root"); ++ assertEquals(0, root.get("action").getAsInt()); ++ final var tag = root.getAsJsonArray("children").get(0).getAsJsonObject(); ++ assertEquals(JsonUiProgram.textureId(form.get("title").getAsString()), tag.get("texture").getAsString()); ++ } ++ ++ @Test ++ void tallAnimationStripsRetainTheirDimensionsAndEndPixels() throws Exception { ++ final var strip = new BufferedImage(1, 6000, BufferedImage.TYPE_INT_ARGB); ++ strip.setRGB(0, 0, 0xFF1256AB); ++ strip.setRGB(0, 5999, 0xFFAB5612); ++ final Map textures = new HashMap<>(); ++ UiTextures.load(Map.of("textures/ui/strip.png", encode(strip, "png")), textures); ++ final var decoded = ImageIO.read(new ByteArrayInputStream(textures.get("textures/ui/strip.png"))); ++ assertEquals(1, decoded.getWidth()); ++ assertEquals(6000, decoded.getHeight()); ++ assertEquals(strip.getRGB(0, 0), decoded.getRGB(0, 0)); ++ assertEquals(strip.getRGB(0, 5999), decoded.getRGB(0, 5999)); ++ } ++ ++ @Test + void jpegConversionPreservesDecodedPixelsAndLayerPriority() throws Exception { + final var image = new BufferedImage(3, 2, BufferedImage.TYPE_INT_RGB); + image.setRGB(1, 0, 0xBD3716); @@ -15954,7 +16053,7 @@ index 00000000..29be2f0c + @Test + void malformedAndOversizedImagesLeaveUnrelatedTexturesUsable() throws Exception { + final byte[] png = encode(new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB), "png"); -+ final byte[] oversized = encode(new BufferedImage(4097, 1, BufferedImage.TYPE_INT_RGB), "jpg"); ++ final byte[] oversized = encode(new BufferedImage(16385, 1, BufferedImage.TYPE_INT_RGB), "jpg"); + final var result = new HashMap(); + UiTextures.load(Map.of("textures/ui/retained.png", png, "textures/ui/broken.jpg", new byte[]{1, 2}, + "textures/ui/oversized.jpg", oversized, "textures/ui/../../foreign.jpg", oversized), result); @@ -15963,7 +16062,7 @@ index 00000000..29be2f0c + UiTextures.load(Map.of("textures/ui/retained.jpg", new byte[]{1, 2}), result); + assertArrayEquals(png, result.get("textures/ui/retained.png")); + UiTextures.load(Map.of("textures/ui/retained.png", new byte[]{1, 2}, -+ "textures/ui/oversized.png", encode(new BufferedImage(4097, 1, BufferedImage.TYPE_INT_RGB), "png")), result); ++ "textures/ui/oversized.png", encode(new BufferedImage(16385, 1, BufferedImage.TYPE_INT_RGB), "png")), result); + assertEquals(1, result.size()); + assertArrayEquals(png, result.get("textures/ui/retained.png")); + } diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index ac67044a..062bd71e 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -55,3 +55,15 @@ The add-on accepts that scene and shows the authored Navigator GUI in a live joi A live add-on check opens Mini Game Selector, returns through Back, and closes through Escape. The journal retains the original response indices 0 and 8 and the user-close reason. The full build and fresh-clone replay checks pass. Final typography and complete native appearance remain unverified. + +Bounded relative UI paths preserve printable UTF-8 names and spaces. Converted identifiers still hash the original resource name. +Traversal, absolute paths, drive or URI syntax, backslash separators, control characters, and unpaired surrogates remain rejected. +A native Bedrock 1.26.60.5 capture shows Enchanted's authored hub menu and sends original response index 2 for OP SMP. +All 58 exported pack files match the add-on capture. The native run stops after transfer selection and does not establish gameplay after transfer. +The captured menu now resolves and passes visible-control admission with all 11 original responses. +Regressions retain bound image pixels and metadata and preserve form actions when a printable decorative texture is absent. +Frame strips can exceed 4,096 pixels on one side while preserving the previous 16-Mipixel total budget and source byte limits. +The current native pack uses a 963 × 5,916 logo atlas and a 5,880 × 480 background. +A shared dimension policy bounds conversion and client decoding. An encoded-strip regression preserves its endpoint pixels. +The rebuilt add-on opens the authored hub menu with all five trending cards and the three lower game cards. +A physical OP SMP selection sends the native original response index 2. The destination connection times out after Transfer; gameplay there remains unverified. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index d3d5653b..6ff56a48 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From b541289db9117312f094b9d59faf5fbd3a5d066a Mon Sep 17 00:00:00 2001 +From e9f6d54aa627ff9b5f38d702bacfef33319ab4c5 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(ui): share native scene rendering and GUI actors @@ -613,122 +613,166 @@ not by its current position in the instantiated list. This keeps focus and adjacent navigation targets available after a distant wheel scroll or column reflow. A focus regression compares the next row against the full grid while the viewport is hundreds of rows away. + +Honor allow_debug_missing_texture=false when a local image resource is +absent. Skip that image without failing the form or its image status. +Keep required images and malformed resources on the existing error path. + +Native Bedrock 1.26.60.5 opens the Enchanted initial hub menu with generated +decorative icon references that do not always name an existing resource. +The pack explicitly disables the missing-texture debug image on these +controls. All 58 exported native pack files match the add-on capture. +After the shared compiler accepts the printable resource names, the prior +client renderer still throws for an absent decoration and replaces the +whole authored menu with ordinary controls. Retain the image's authored +missing-texture policy at resource lookup instead. The regression checks +that this omission leaves the image status clear while required image +failure and source-reload recovery keep their existing behavior. + +Use the shared decoded-image dimension policy for tall and wide pack UI +frame strips. The current native Enchanted logo atlas is 963x5916 and the +disconnect background is 5880x480, both below the previous 16-Mipixel +budget. Rejecting each side above 4096 prevents rendering these valid +assets even after conversion retains them. Keep a per-side bound and the +original total-pixel budget. Regression cases accept the actual atlas +shape through metadata eviction and reload, reject an excessive square +and an overlong side, and release rejected decoded image buffers. + +Preserve sibling-derived dimensions when a stack distributes remaining +space along its other axis. The captured Enchanted hub uses a remaining- +width panel whose height matches the adjacent large card. Remeasuring +that panel with zero sibling metrics collapses all four right-hand cards +and their hit targets, despite accepting the authored screen. Carry the +sibling extents into the fill measurement. A reduced regression fails +before this correction and retains visible child bounds in both stack +orientations afterward. Native screenshots establish the authored layout; +complete visual and animation parity remain separate validation. + +Retain exact fractional grid capacity when percentage arithmetic rounds a +fit one float ULP below an integer. Enchanted sizes each Other Games cell +to one quarter of the grid and offsets its first eight entries above the +viewport. Dropping to three columns exposes two heading entries as cards. +The pinned native screenshot retains four columns and only three lower +game cards. Correct the capacity boundary on either rescaling axis while +keeping measurably larger cells on the next row. The reduced fractional +fixture fails before the correction. This reconstructs the observed layout +constraint and does not claim the native internal rounding algorithm. --- - .../bedrock/ViaFabricPlusBedrock.java | 2 + - .../appearance/BedrockBoneAnimation.java | 3 + - .../appearance/BedrockEmotePlayback.java | 19 + - .../appearance/BedrockGuiActorAppearance.java | 94 ++ - .../appearance/BedrockResourceLibrary.java | 45 +- - .../access/IBedrockGuiActorRenderer.java | 12 + - .../features/input/MixinNativeUiInput.java | 69 ++ - .../render/MixinAvatarAnimationRenderer.java | 2 + - .../render/MixinBedrockActorSubmission.java | 6 + - .../render/MixinBedrockBitmapFont.java | 40 + - .../MixinBedrockBitmapGlyphRenderState.java | 27 + - .../MixinBedrockRenderingResources.java | 2 + - .../render/MixinEntityRenderDispatcher.java | 21 +- - .../render/MixinGuiEntityRenderer.java | 53 + - .../render/MixinNativeHudOverlays.java | 34 + - .../render/MixinNativeUiExtraction.java | 40 + - .../render/MixinNativeUiPictures.java | 72 ++ - .../render/MixinNativeUiViewport.java | 25 + - .../render/MixinPictureInPictureRenderer.java | 41 + - .../BedrockActorMaterialDefinitions.java | 4 +- - .../render/BedrockActorRenderTypes.java | 64 +- - .../render/BedrockAnimatedEntityState.java | 16 + - .../bedrock/render/BedrockEntityRenderer.java | 6 +- - .../bedrock/render/BedrockGeometry.java | 4 +- - .../render/BedrockGuiActorContext.java | 181 ++++ - .../render/BedrockGuiActorRenderer.java | 140 +++ - .../render/BedrockPlayerAttachables.java | 17 +- - .../bedrock/render/BedrockPlayerSkins.java | 44 +- - .../render/font/BedrockBitmapFont.java | 251 +++++ - .../render/font/BedrockBitmapGlyph.java | 105 ++ - .../render/font/BedrockBitmapPipeline.java | 49 + - .../bedrock/screen/ui/BedrockHudOverlays.java | 129 +++ - .../screen/ui/NativeHudOverlayState.java | 66 ++ - .../bedrock/screen/ui/NativeHudPayload.java | 36 + - .../bedrock/screen/ui/NativeHudResources.java | 21 + - .../screen/ui/NativeUiActorCamera.java | 60 ++ - .../screen/ui/NativeUiActorPortraits.java | 258 +++++ - .../screen/ui/NativeUiAnimationLifecycle.java | 131 +++ - .../bedrock/screen/ui/NativeUiAnimations.java | 382 +++++++ - .../bedrock/screen/ui/NativeUiAseprite.java | 54 + - .../bedrock/screen/ui/NativeUiButton.java | 167 +++ - .../screen/ui/NativeUiCollectionWindow.java | 92 ++ - .../bedrock/screen/ui/NativeUiColor.java | 55 + - .../bedrock/screen/ui/NativeUiController.java | 37 + - .../bedrock/screen/ui/NativeUiDimension.java | 93 ++ - .../bedrock/screen/ui/NativeUiFlipbook.java | 47 + - .../bedrock/screen/ui/NativeUiFocus.java | 329 ++++++ - .../bedrock/screen/ui/NativeUiFont.java | 58 ++ - .../bedrock/screen/ui/NativeUiGraphics.java | 47 + - .../bedrock/screen/ui/NativeUiImageCache.java | 229 +++++ - .../screen/ui/NativeUiImageRegions.java | 132 +++ - .../bedrock/screen/ui/NativeUiImages.java | 171 ++++ - .../bedrock/screen/ui/NativeUiInput.java | 248 +++++ - .../screen/ui/NativeUiInputRouter.java | 72 ++ - .../bedrock/screen/ui/NativeUiLayout.java | 551 ++++++++++ - .../screen/ui/NativeUiMultilineInput.java | 267 +++++ - .../screen/ui/NativeUiPlayerCamera.java | 72 ++ - .../screen/ui/NativeUiPresentation.java | 162 +++ - .../bedrock/screen/ui/NativeUiRenderer.java | 153 +++ - .../bedrock/screen/ui/NativeUiScene.java | 107 ++ - .../screen/ui/NativeUiSceneScreen.java | 823 +++++++++++++++ - .../bedrock/screen/ui/NativeUiScreen.java | 11 + - .../bedrock/screen/ui/NativeUiScroll.java | 69 ++ - .../bedrock/screen/ui/NativeUiSlider.java | 123 +++ - .../bedrock/screen/ui/NativeUiText.java | 64 ++ - .../bedrock/screen/ui/NativeUiToggle.java | 94 ++ - .../bedrock/screen/ui/NativeUiViewport.java | 32 + - .../bedrock/settings/base/NativeUiScale.java | 27 + - .../settings/impl/BedrockSettings.java | 7 + - .../viafabricplus-bedrock/lang/en_us.json | 2 + - .../shaders/core/bedrock_actor.fsh | 8 + - .../shaders/core/bedrock_bitmap_font.fsh | 26 + - .../shaders/core/bedrock_bitmap_font.vsh | 29 + - .../viafabricplus-bedrock.accesswidener | 1 + - .../viafabricplus-bedrock.mixins.json | 11 +- - .../BedrockBitmapFontResourcesTest.java | 67 ++ - .../BedrockGuiActorAppearanceTest.java | 92 ++ - .../BedrockGuiActorContextTest.java | 140 +++ - .../BedrockResourceLibraryTest.java | 25 + - ...icPlusBuiltinResourcePackProviderTest.java | 4 + - .../render/BedrockActorMaterialsGpuTest.java | 38 +- - .../render/BedrockShaderPipelinesTest.java | 24 + - .../render/font/BedrockBitmapFontTest.java | 239 +++++ - .../render/font/BedrockBitmapGlyphTest.java | 71 ++ - .../font/BedrockBitmapShaderGpuTest.java | 291 ++++++ - .../bedrock/screen/ui/AnimationPrograms.java | 23 + - .../screen/ui/NativeHudOverlayStateTest.java | 153 +++ - .../screen/ui/NativeUiActorCameraTest.java | 102 ++ - .../screen/ui/NativeUiActorPortraitsTest.java | 164 +++ - .../ui/NativeUiAnimationLifecycleTest.java | 155 +++ - .../screen/ui/NativeUiAnimationsTest.java | 264 +++++ - .../screen/ui/NativeUiAsepriteTest.java | 108 ++ - .../bedrock/screen/ui/NativeUiButtonTest.java | 127 +++ - .../ui/NativeUiCollectionWindowTest.java | 144 +++ - .../screen/ui/NativeUiColorAnimationTest.java | 114 +++ - .../bedrock/screen/ui/NativeUiFocusTest.java | 124 +++ - .../bedrock/screen/ui/NativeUiFontTest.java | 148 +++ - .../screen/ui/NativeUiImageCacheTest.java | 142 +++ - .../screen/ui/NativeUiImageRegionsTest.java | 217 ++++ - .../bedrock/screen/ui/NativeUiImagesTest.java | 100 ++ - .../screen/ui/NativeUiInputRouterTest.java | 125 +++ - .../bedrock/screen/ui/NativeUiInputTest.java | 200 ++++ - .../bedrock/screen/ui/NativeUiLayoutTest.java | 960 ++++++++++++++++++ - .../screen/ui/NativeUiMultilineInputTest.java | 210 ++++ - .../screen/ui/NativeUiPlayerCameraTest.java | 132 +++ - .../screen/ui/NativeUiPresentationTest.java | 258 +++++ - .../screen/ui/NativeUiRendererTest.java | 71 ++ - .../NativeUiSceneScreenInteractionTest.java | 359 +++++++ - .../bedrock/screen/ui/NativeUiSceneTest.java | 40 + - .../bedrock/screen/ui/NativeUiSliderTest.java | 120 +++ - .../bedrock/screen/ui/NativeUiTextTest.java | 56 + - .../ui/NativeUiTextureAnimationTest.java | 108 ++ - .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ - .../screen/ui/NativeUiViewportTest.java | 95 ++ - 114 files changed, 12901 insertions(+), 36 deletions(-) + .../bedrock/ViaFabricPlusBedrock.java | 2 + + .../appearance/BedrockBoneAnimation.java | 3 + + .../appearance/BedrockEmotePlayback.java | 19 + + .../appearance/BedrockGuiActorAppearance.java | 94 ++ + .../appearance/BedrockResourceLibrary.java | 45 +- + .../access/IBedrockGuiActorRenderer.java | 12 + + .../features/input/MixinNativeUiInput.java | 69 ++ + .../render/MixinAvatarAnimationRenderer.java | 2 + + .../render/MixinBedrockActorSubmission.java | 6 + + .../render/MixinBedrockBitmapFont.java | 40 + + .../MixinBedrockBitmapGlyphRenderState.java | 27 + + .../MixinBedrockRenderingResources.java | 2 + + .../render/MixinEntityRenderDispatcher.java | 21 +- + .../render/MixinGuiEntityRenderer.java | 53 + + .../render/MixinNativeHudOverlays.java | 34 + + .../render/MixinNativeUiExtraction.java | 40 + + .../render/MixinNativeUiPictures.java | 72 ++ + .../render/MixinNativeUiViewport.java | 25 + + .../render/MixinPictureInPictureRenderer.java | 41 + + .../BedrockActorMaterialDefinitions.java | 4 +- + .../render/BedrockActorRenderTypes.java | 64 +- + .../render/BedrockAnimatedEntityState.java | 16 + + .../bedrock/render/BedrockEntityRenderer.java | 6 +- + .../bedrock/render/BedrockGeometry.java | 4 +- + .../render/BedrockGuiActorContext.java | 181 +++ + .../render/BedrockGuiActorRenderer.java | 140 +++ + .../render/BedrockPlayerAttachables.java | 17 +- + .../bedrock/render/BedrockPlayerSkins.java | 44 +- + .../render/font/BedrockBitmapFont.java | 251 +++++ + .../render/font/BedrockBitmapGlyph.java | 105 ++ + .../render/font/BedrockBitmapPipeline.java | 49 + + .../bedrock/screen/ui/BedrockHudOverlays.java | 129 +++ + .../screen/ui/NativeHudOverlayState.java | 66 ++ + .../bedrock/screen/ui/NativeHudPayload.java | 36 + + .../bedrock/screen/ui/NativeHudResources.java | 21 + + .../screen/ui/NativeUiActorCamera.java | 60 + + .../screen/ui/NativeUiActorPortraits.java | 258 +++++ + .../screen/ui/NativeUiAnimationLifecycle.java | 131 +++ + .../bedrock/screen/ui/NativeUiAnimations.java | 382 +++++++ + .../bedrock/screen/ui/NativeUiAseprite.java | 54 + + .../bedrock/screen/ui/NativeUiButton.java | 167 +++ + .../screen/ui/NativeUiCollectionWindow.java | 92 ++ + .../bedrock/screen/ui/NativeUiColor.java | 55 + + .../bedrock/screen/ui/NativeUiController.java | 37 + + .../bedrock/screen/ui/NativeUiDimension.java | 93 ++ + .../bedrock/screen/ui/NativeUiFlipbook.java | 47 + + .../bedrock/screen/ui/NativeUiFocus.java | 329 ++++++ + .../bedrock/screen/ui/NativeUiFont.java | 58 + + .../bedrock/screen/ui/NativeUiGraphics.java | 47 + + .../bedrock/screen/ui/NativeUiImageCache.java | 229 ++++ + .../screen/ui/NativeUiImageRegions.java | 132 +++ + .../bedrock/screen/ui/NativeUiImages.java | 177 +++ + .../bedrock/screen/ui/NativeUiInput.java | 248 ++++ + .../screen/ui/NativeUiInputRouter.java | 72 ++ + .../bedrock/screen/ui/NativeUiLayout.java | 563 +++++++++ + .../screen/ui/NativeUiMultilineInput.java | 267 +++++ + .../screen/ui/NativeUiPlayerCamera.java | 72 ++ + .../screen/ui/NativeUiPresentation.java | 162 +++ + .../bedrock/screen/ui/NativeUiRenderer.java | 153 +++ + .../bedrock/screen/ui/NativeUiScene.java | 107 ++ + .../screen/ui/NativeUiSceneScreen.java | 823 ++++++++++++++ + .../bedrock/screen/ui/NativeUiScreen.java | 11 + + .../bedrock/screen/ui/NativeUiScroll.java | 69 ++ + .../bedrock/screen/ui/NativeUiSlider.java | 123 ++ + .../bedrock/screen/ui/NativeUiText.java | 64 ++ + .../bedrock/screen/ui/NativeUiToggle.java | 94 ++ + .../bedrock/screen/ui/NativeUiViewport.java | 32 + + .../bedrock/settings/base/NativeUiScale.java | 27 + + .../settings/impl/BedrockSettings.java | 7 + + .../viafabricplus-bedrock/lang/en_us.json | 2 + + .../shaders/core/bedrock_actor.fsh | 8 + + .../shaders/core/bedrock_bitmap_font.fsh | 26 + + .../shaders/core/bedrock_bitmap_font.vsh | 29 + + .../viafabricplus-bedrock.accesswidener | 1 + + .../viafabricplus-bedrock.mixins.json | 11 +- + .../BedrockBitmapFontResourcesTest.java | 67 ++ + .../BedrockGuiActorAppearanceTest.java | 92 ++ + .../BedrockGuiActorContextTest.java | 140 +++ + .../BedrockResourceLibraryTest.java | 25 + + ...icPlusBuiltinResourcePackProviderTest.java | 4 + + .../render/BedrockActorMaterialsGpuTest.java | 38 +- + .../render/BedrockShaderPipelinesTest.java | 24 + + .../render/font/BedrockBitmapFontTest.java | 239 ++++ + .../render/font/BedrockBitmapGlyphTest.java | 71 ++ + .../font/BedrockBitmapShaderGpuTest.java | 291 +++++ + .../bedrock/screen/ui/AnimationPrograms.java | 23 + + .../screen/ui/NativeHudOverlayStateTest.java | 153 +++ + .../screen/ui/NativeUiActorCameraTest.java | 102 ++ + .../screen/ui/NativeUiActorPortraitsTest.java | 164 +++ + .../ui/NativeUiAnimationLifecycleTest.java | 155 +++ + .../screen/ui/NativeUiAnimationsTest.java | 264 +++++ + .../screen/ui/NativeUiAsepriteTest.java | 108 ++ + .../bedrock/screen/ui/NativeUiButtonTest.java | 127 +++ + .../ui/NativeUiCollectionWindowTest.java | 144 +++ + .../screen/ui/NativeUiColorAnimationTest.java | 114 ++ + .../bedrock/screen/ui/NativeUiFocusTest.java | 124 ++ + .../bedrock/screen/ui/NativeUiFontTest.java | 148 +++ + .../screen/ui/NativeUiImageCacheTest.java | 142 +++ + .../screen/ui/NativeUiImageRegionsTest.java | 217 ++++ + .../bedrock/screen/ui/NativeUiImagesTest.java | 137 +++ + .../screen/ui/NativeUiInputRouterTest.java | 125 ++ + .../bedrock/screen/ui/NativeUiInputTest.java | 200 ++++ + .../bedrock/screen/ui/NativeUiLayoutTest.java | 1004 +++++++++++++++++ + .../screen/ui/NativeUiMultilineInputTest.java | 210 ++++ + .../screen/ui/NativeUiPlayerCameraTest.java | 132 +++ + .../screen/ui/NativeUiPresentationTest.java | 258 +++++ + .../screen/ui/NativeUiRendererTest.java | 71 ++ + .../NativeUiSceneScreenInteractionTest.java | 359 ++++++ + .../bedrock/screen/ui/NativeUiSceneTest.java | 40 + + .../bedrock/screen/ui/NativeUiSliderTest.java | 120 ++ + .../bedrock/screen/ui/NativeUiTextTest.java | 56 + + .../ui/NativeUiTextureAnimationTest.java | 108 ++ + .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ + .../screen/ui/NativeUiViewportTest.java | 95 ++ + 114 files changed, 13000 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearance.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiActorRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinNativeUiInput.java @@ -5337,10 +5381,10 @@ index 0000000..3f3e908 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImages.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImages.java new file mode 100644 -index 0000000..8475ec8 +index 0000000..d77f29a --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImages.java -@@ -0,0 +1,171 @@ +@@ -0,0 +1,177 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5367,6 +5411,7 @@ index 0000000..8475ec8 +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.Identifier; ++import net.raphimc.viabedrock.api.resourcepack.ui.UiTextureLimits; +import net.raphimc.viabedrock.protocol.data.ProtocolConstants; + +/** Draws accepted pack textures and bounded public HTTPS images. */ @@ -5395,7 +5440,7 @@ index 0000000..8475ec8 + + public Image image(final NativeUiScene.Node node) { + return node.string("texture_file_system", "path").equals("url") -+ ? this.remote(node.string("texture", "")) : this.image(node.string("texture", "")); ++ ? this.remote(node.string("texture", "")) : this.image(node.string("texture", ""), !node.flag("allow_debug_missing_texture", true)); + } + + private Image remote(final String url) { @@ -5461,11 +5506,16 @@ index 0000000..8475ec8 + } + + public Image image(final String texture) { ++ return this.image(texture, false); ++ } ++ ++ private Image image(final String texture, final boolean allowMissing) { + final Identifier id = Identifier.tryParse(texture); + if (id == null || !id.getNamespace().equals("viabedrock") || !id.getPath().startsWith("textures/")) + throw new IllegalArgumentException("Invalid native UI texture"); + final var resource = Minecraft.getInstance().getResourceManager().getResource(id).orElse(null); + if (resource == null) { ++ if (allowMissing) return null; + this.failed = true; + throw new IllegalArgumentException("Missing native UI texture: " + id); + } @@ -5474,7 +5524,7 @@ index 0000000..8475ec8 + final Image cached = this.images.get(id); + if (cached != null && cached.source == resource.source()) return cached; + try (final var input = resource.open(); final NativeImage pixels = NativeImage.read(input)) { -+ if (pixels.getWidth() > 4096 || pixels.getHeight() > 4096) ++ if (!UiTextureLimits.supportsDimensions(pixels.getWidth(), pixels.getHeight())) + throw new IOException("Native UI texture exceeds size limits"); + final Image image = new Image(id, pixels.getWidth(), pixels.getHeight(), resource.source()); + this.images.put(id, image); @@ -5846,10 +5896,10 @@ index 0000000..8c0c6f6 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayout.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayout.java new file mode 100644 -index 0000000..2af142c +index 0000000..7e4d4f0 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayout.java -@@ -0,0 +1,551 @@ +@@ -0,0 +1,563 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -6024,8 +6074,19 @@ index 0000000..2af142c + } + if (fills > 0) { + final float remainder = Math.max(0, (vertical ? height : width) - used) / fills; -+ for (int i = 0; i < result.size(); i++) if (this.visibleNodes.getOrDefault(node.children().get(i), true) && fill(node.children().get(i), axis)) -+ result.set(i, this.measure(node.children().get(i), vertical ? availableWidth : remainder, vertical ? remainder : availableHeight)); ++ for (int i = 0; i < result.size(); i++) { ++ final var child = node.children().get(i); ++ if (!this.visibleNodes.getOrDefault(child, true) || !fill(child, axis)) continue; ++ float siblingWidth = 0, siblingHeight = 0; ++ if (siblingDimension(child)) for (int other = 0; other < result.size(); other++) if (other != i) { ++ siblingWidth = Math.max(siblingWidth, result.get(other).width); ++ siblingHeight = Math.max(siblingHeight, result.get(other).height); ++ } ++ final float fillWidth = vertical ? availableWidth : remainder; ++ final float fillHeight = vertical ? remainder : availableHeight; ++ result.set(i, this.measure(child, new Measurement(fillWidth, fillHeight, ++ siblingWidth, siblingHeight, fillWidth, fillHeight))); ++ } + } + } + return result; @@ -6056,8 +6117,8 @@ index 0000000..2af142c + throw new IllegalArgumentException("Native collection cell geometry changed"); + return new Size(grid.columns * window.cellWidth(), grid.rows * window.cellHeight()); + } -+ final Map widths = new java.util.HashMap<>(); -+ final Map heights = new java.util.HashMap<>(); ++ final Map widths = new HashMap<>(); ++ final Map heights = new HashMap<>(); + for (int i = 0; i < children.size(); i++) { + final int[] position = grid.position(node.children().get(i), i); + final int column = position[0], row = position[1]; @@ -6296,11 +6357,12 @@ index 0000000..2af142c + cellWidth = Math.max(cellWidth, size.width); + cellHeight = Math.max(cellHeight, size.height); + } ++ // Percentage sizing can round an exact capacity down by one float ULP. + if (rescaling.equals("horizontal")) { -+ columns = cellWidth > 0 ? Math.clamp((int) Math.floor(width / cellWidth), 1, 128) : 1; ++ columns = cellWidth > 0 ? Math.clamp((int) Math.floor(Math.nextUp(width / cellWidth)), 1, 128) : 1; + rows = Math.max(1, (int) Math.ceilDiv((long) count, columns)); + } else { -+ rows = cellHeight > 0 ? Math.clamp((int) Math.floor(height / cellHeight), 1, 4096) : 1; ++ rows = cellHeight > 0 ? Math.clamp((int) Math.floor(Math.nextUp(height / cellHeight)), 1, 4096) : 1; + columns = Math.max(1, (int) Math.ceilDiv((long) count, rows)); + } + } @@ -11855,10 +11917,10 @@ index 0000000..d0b8045 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImagesTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImagesTest.java new file mode 100644 -index 0000000..0d360fd +index 0000000..d4e9931 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImagesTest.java -@@ -0,0 +1,100 @@ +@@ -0,0 +1,137 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -11866,24 +11928,29 @@ index 0000000..0d360fd + */ +package com.viaversion.viafabricplus.bedrock.screen.ui; + ++import com.google.gson.JsonObject; +import com.mojang.blaze3d.platform.NativeImage; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.IOException; ++import java.util.List; +import java.util.Optional; +import net.minecraft.client.Minecraft; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.junit.jupiter.api.Test; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +final class NativeUiImagesTest { -+ @Test -+ void evictsOldMetadataWithoutFailingLaterIconsAndRefreshesChangedPackSources() throws Exception { ++ @ParameterizedTest ++ @CsvSource({"16,12", "963,5916"}) ++ void retainsFrameStripsAcrossMetadataEvictionAndSourceReload(final int width, final int height) throws Exception { + final var game = mock(Minecraft.class); + final var resources = mock(ResourceManager.class); + final var resource = mock(Resource.class); @@ -11893,8 +11960,8 @@ index 0000000..0d360fd + when(resource.source()).thenReturn(originalSource); + when(resource.open()).thenAnswer(_ -> new ByteArrayInputStream(new byte[0])); + final var pixels = mock(NativeImage.class); -+ when(pixels.getWidth()).thenReturn(16); -+ when(pixels.getHeight()).thenReturn(12); ++ when(pixels.getWidth()).thenReturn(width); ++ when(pixels.getHeight()).thenReturn(height); + try (final var singleton = mockStatic(Minecraft.class); + final var decoder = mockStatic(NativeImage.class); + final var images = new NativeUiImages()) { @@ -11915,14 +11982,14 @@ index 0000000..0d360fd + final var replacement = images.image(texture(0)); + assertNotSame(first, replacement); + assertSame(replacementSource, replacement.source()); -+ assertEquals(16, replacement.width()); -+ assertEquals(12, replacement.height()); ++ assertEquals(width, replacement.width()); ++ assertEquals(height, replacement.height()); + verify(resource, times(259)).open(); + } + } + + @Test -+ void rejectsUnavailablePackImagesAndRecoversWhenTheirSourceChanges() throws Exception { ++ void hidesAuthoredMissingDecorationsAndRecoversRequiredImagesWhenTheirSourceChanges() throws Exception { + final var game = mock(Minecraft.class); + final var resources = mock(ResourceManager.class); + when(game.getResourceManager()).thenReturn(resources); @@ -11931,6 +11998,14 @@ index 0000000..0d360fd + final var decoder = mockStatic(NativeImage.class); + final var images = new NativeUiImages()) { + singleton.when(Minecraft::getInstance).thenReturn(game); ++ final JsonObject properties = new JsonObject(); ++ properties.addProperty("texture", texture(0)); ++ properties.addProperty("allow_debug_missing_texture", false); ++ final var decoration = new NativeUiScene.Node("decoration", "image", properties, List.of(), Integer.MIN_VALUE); ++ assertNull(images.image(decoration)); ++ assertFalse(images.failed()); ++ properties.remove("allow_debug_missing_texture"); ++ assertThrows(IllegalArgumentException.class, () -> images.image(decoration)); + assertThrows(IllegalArgumentException.class, () -> images.image(texture(0))); + assertTrue(images.failed()); + @@ -11955,6 +12030,30 @@ index 0000000..0d360fd + } + } + ++ @ParameterizedTest ++ @CsvSource({"8192,8192", "16385,1"}) ++ void decodedImagesKeepThePixelBudgetAndDimensionBound(final int width, final int height) throws Exception { ++ final var game = mock(Minecraft.class); ++ final var resources = mock(ResourceManager.class); ++ final var resource = mock(Resource.class); ++ when(game.getResourceManager()).thenReturn(resources); ++ when(resources.getResource(any())).thenReturn(Optional.of(resource)); ++ when(resource.source()).thenReturn(mock(PackResources.class)); ++ when(resource.open()).thenAnswer(_ -> new ByteArrayInputStream(new byte[0])); ++ final var pixels = mock(NativeImage.class); ++ when(pixels.getWidth()).thenReturn(width); ++ when(pixels.getHeight()).thenReturn(height); ++ try (final var singleton = mockStatic(Minecraft.class); ++ final var decoder = mockStatic(NativeImage.class); ++ final var images = new NativeUiImages()) { ++ singleton.when(Minecraft::getInstance).thenReturn(game); ++ decoder.when(() -> NativeImage.read(any(InputStream.class))).thenReturn(pixels); ++ assertThrows(IllegalArgumentException.class, () -> images.image(texture(0))); ++ assertTrue(images.failed()); ++ verify(pixels).close(); ++ } ++ } ++ + private static String texture(final int index) { + return "viabedrock:textures/server_ui/icon_" + index + ".png"; + } @@ -12298,10 +12397,10 @@ index 0000000..bec654e +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayoutTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayoutTest.java new file mode 100644 -index 0000000..cc678e4 +index 0000000..a754f44 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayoutTest.java -@@ -0,0 +1,960 @@ +@@ -0,0 +1,1004 @@ +package com.viaversion.viafabricplus.bedrock.screen.ui; + +import com.google.gson.JsonArray; @@ -12380,6 +12479,27 @@ index 0000000..cc678e4 + } + + @Test ++ void retainsSiblingExtentWhenFillingTheOtherStackAxis() { ++ for (final boolean vertical : new boolean[]{false, true}) { ++ final JsonObject root = node("stack_panel", 200, 160); ++ root.addProperty("orientation", vertical ? "vertical" : "horizontal"); ++ final JsonObject fixed = node("panel", vertical ? 70 : 80, vertical ? 60 : 90); ++ final JsonObject filling = node("panel", vertical ? "100%sm" : "fill", vertical ? "fill" : "100%sm"); ++ final JsonObject button = node("button", "100%", "100%"); ++ button.addProperty("action", 2); ++ children(filling, button); ++ children(root, fixed, filling); ++ ++ final var box = layout(scene(root), 200, 160).children().get(1); ++ assertEquals(vertical ? 70 : 120, box.bounds().width()); ++ assertEquals(vertical ? 100 : 90, box.bounds().height()); ++ assertEquals(vertical ? 0 : 80, box.bounds().x()); ++ assertEquals(vertical ? 60 : 0, box.bounds().y()); ++ assertEquals(box.bounds(), box.children().getFirst().visibleBounds()); ++ } ++ } ++ ++ @Test + void placesStackChildrenWithoutAnchorsUnlessExplicitlyEnabled() { + for (final boolean vertical : new boolean[] {false, true}) { + for (final boolean anchored : new boolean[] {false, true}) { @@ -12653,6 +12773,29 @@ index 0000000..cc678e4 + } + + @Test ++ void retainsExactFractionalGridCapacityWithoutFittingLargerCells() { ++ for (final boolean vertical : new boolean[]{false, true}) { ++ final JsonObject root = node("grid", vertical ? 100 : 421.99F, vertical ? 421.99F : 100); ++ root.addProperty("grid_item_template", "shared.cell"); ++ root.addProperty("grid_rescaling_type", vertical ? "vertical" : "horizontal"); ++ root.addProperty("grid_fill_direction", vertical ? "vertical" : "horizontal"); ++ final JsonObject[] cells = new JsonObject[5]; ++ for (int i = 0; i < cells.length; i++) cells[i] = node("button", vertical ? 20 : "25%", vertical ? "25%" : 20); ++ children(root, cells); ++ var box = layout(scene(root), 500, 500); ++ final var fourth = box.children().get(3).bounds(); ++ assertEquals(vertical ? box.bounds().x() : box.bounds().y(), vertical ? fourth.x() : fourth.y(), .0001F); ++ final var fifth = box.children().get(4).bounds(); ++ assertTrue(vertical ? fifth.x() > fourth.x() : fifth.y() > fourth.y()); ++ ++ for (final JsonObject cell : cells) cell.add("size", pair(vertical ? 20 : "25% + 0.01px", vertical ? "25% + 0.01px" : 20)); ++ box = layout(scene(root), 500, 500); ++ final var largerFourth = box.children().get(3).bounds(); ++ assertTrue(vertical ? largerFourth.x() > box.bounds().x() : largerFourth.y() > box.bounds().y()); ++ } ++ } ++ ++ @Test + void retainsGridRowsAndSparseFormButtonIndices() { + final JsonObject root = node("grid", 180, "100%c"); + root.addProperty("grid_item_template", "shared.cell"); diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index 8e5a5ed9..4940922f 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -33,3 +33,16 @@ These controls do not establish identical native pooling or frame timing. Broader viewports, complete inventory pixels, entrance timing, and TrueType comparisons remain open. The [owning patch](0029-share-native-ui-scene-rendering.patch) retains implementation rationale and versioned evidence. + +Local image controls honor `allow_debug_missing_texture: false` when the resource is absent. +The renderer omits that decoration and preserves the rest of the authored scene. Required and malformed images keep their existing error behavior. +Native Bedrock 1.26.60.5 shows Enchanted's hub menu with these optional decorations. All 58 exported native pack files match the add-on capture. +The image regression checks omission, required-image failure, and recovery after a source change. Client decoding shares the converted-pack dimension policy and retains the previous 16-Mipixel budget for tall or wide frame strips. +Regression cases accept the native atlas shape through eviction and reload and release buffers rejected by the pixel or side bounds. +Stack fill allocation preserves sibling-derived dimensions on the other axis. +A reduced regression fails before the correction and retains visible child controls in both orientations. +Enchanted's native hub uses this layout for its four smaller trending cards. +Fractional grid capacity tolerates a one-ULP round-down at an exact fit on either axis. Larger cells still wrap. +The rebuilt add-on opens Enchanted's authored hub with all five trending cards and the three lower game cards. +A physical OP SMP selection sends original response index 2 and receives Transfer, matching native. The destination connection times out. +Complete gameplay after transfer, final typography, screen overlays, and animation parity remain unverified. From f4efdd53a9d0e2b7da13929bdbaa9782689b42ca Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:13:19 +0100 Subject: [PATCH 12/33] fix(inventory): restore backdrop opacity and reuse scene geometry Honor the native entrance animation's initial-alpha scale so opening the Survival inventory keeps the authored translucent world backdrop. Version-matched native factory and updater code establish the scale, and a fade regression fails before the correction. Retain stable hit regions, input ancestor geometry and collection windows. Invalidate them when layout, focus or control visibility changes. Reuse the shared painter order and separate authored layers into Java GUI strata to bound repeated overlap searches during rendering. Validate the full dependency and add-on build, clean patch replay, installed backdrop rendering, and server-authoritative pickup and drag distribution. Broader modpack timing, menu overlap and shield parity remain open. --- ...9-present-authored-native-containers.patch | 143 ++++++++++++++---- ...9-present-authored-native-containers.pr.md | 12 +- ...0029-share-native-ui-scene-rendering.patch | 75 +++++++-- ...0029-share-native-ui-scene-rendering.pr.md | 6 + 4 files changed, 193 insertions(+), 43 deletions(-) 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 6e44891f..e40de54b 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 fd96c68c8fe2aa3322774497e0db78b08f56fa31 Mon Sep 17 00:00:00 2001 +From ffa0baba5f2a9936927377fe51ae7b74577120df 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 @@ -548,6 +548,41 @@ closed catalog can render without authorizing destruction. Both mapped host commands and Java quick-move on those authored hotbar slots restore ordinary controls before changing items or sending a request. Test both recipe-book and Creative layouts, including the refused destruction path. + +Retain input ancestor geometry while the immutable layout root is unchanged. +Invalidate it when the scene is prepared again, and continue updating the +current draw visibility and viewport on every input-layout call. Collection +windows likewise need rescanning only after layout or keyboard focus changes. +Render the shared presentation's stable layer order directly instead of +allocating and sorting a second list for each frame. + +A Survival inventory JFR profile identifies collection metadata and repeated +input traversal among the render-thread costs. Existing pointer, focus, +scrolling, relayout and screen lifecycle regressions validate these cache +boundaries; hardware performance and native visual parity need live checks. + +Reuse parsed container hit regions while draw boxes, painter order, +enablement and the positive-opacity boundary remain unchanged. Box +identity covers immutable layout bounds, clips and scene action mappings. +Scene refreshes and relayouts create new boxes and rebuild validation; +hover branches and zero-opacity transitions invalidate the cached regions. +This removes per-frame action-map parsing from stable inventories without +retaining input targets after they become hidden, disabled or moved. +A regression covers active fades, hidden and disabled slots, restored +controls, layout movement and removal from the draw list. + +Start a Java GUI stratum at each cumulative authored layer boundary. +The shared presentation already orders these layers, so the renderer need +not search earlier layers again to recover overlap order for every quad. +Equal-layer controls retain authored order and ordinary overlap handling. +This bounds Java's GUI ordering work without changing native hit regions +or inventory packet ownership. + +A controlled replacement of this method in the same isolated Survival +session reduces render-thread overlap-search samples. Static recipe, +slot-grid and crafting-background pixels remain identical before and +after the replacement. Software-renderer timing is diagnostic evidence, +not a performance guarantee for player hardware or native pixel parity. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -560,7 +595,7 @@ recipe-book and Creative layouts, including the refused destruction path. .../render/MixinNativeGuiItemState.java | 20 + .../render/MixinNativeGuiOversizedItem.java | 31 + .../container/BedrockFurnacePreferences.java | 60 + - .../BedrockNativeContainerScreen.java | 1242 +++++++++++++++ + .../BedrockNativeContainerScreen.java | 1254 +++++++++++++++ .../container/BedrockNativeContainers.java | 278 ++++ .../container/BedrockNativeCooking.java | 145 ++ .../BedrockNativePlayerInventory.java | 155 ++ @@ -569,8 +604,8 @@ recipe-book and Creative layouts, including the refused destruction path. .../NativeContainerCurrentPayload.java | 30 + .../container/NativeContainerDataPayload.java | 30 + .../NativeContainerFamiliesPayload.java | 30 + - .../container/NativeContainerGeometry.java | 258 +++ - .../container/NativeContainerInput.java | 501 ++++++ + .../container/NativeContainerGeometry.java | 272 ++++ + .../container/NativeContainerInput.java | 506 ++++++ .../container/NativeContainerPayload.java | 22 + .../container/NativeContainerPending.java | 65 + .../NativeContainerPresentation.java | 44 + @@ -606,7 +641,7 @@ recipe-book and Creative layouts, including the refused destruction path. .../viafabricplus-bedrock.mixins.json | 8 +- .../BedrockNativeContainerScreenTest.java | 605 +++++++ .../container/NativeContainerButtonTest.java | 280 ++++ - .../NativeContainerGeometryTest.java | 366 +++++ + .../NativeContainerGeometryTest.java | 389 +++++ .../container/NativeContainerInputTest.java | 542 +++++++ ...veContainerPresentationControllerTest.java | 91 ++ .../container/NativeContainerScrollTest.java | 51 + @@ -624,7 +659,7 @@ recipe-book and Creative layouts, including the refused destruction path. .../NativePlayerPresentationTest.java | 153 ++ .../container/NativePlayerScreenTest.java | 269 ++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 75 files changed, 10097 insertions(+), 62 deletions(-) + 75 files changed, 10151 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 @@ -1129,10 +1164,10 @@ index 0000000..1583ce0 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java new file mode 100644 -index 0000000..d5ea53e +index 0000000..c7fbef6 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java -@@ -0,0 +1,1242 @@ +@@ -0,0 +1,1254 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1156,7 +1191,6 @@ index 0000000..d5ea53e +import com.viaversion.viafabricplus.injection.access.core.IConnection; +import java.util.ArrayList; +import java.util.Collections; -+import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; @@ -1232,6 +1266,8 @@ index 0000000..d5ea53e + private long layoutRevision; + private long nativeInputRevision; + private NativeUiLayout.Box root; ++ private NativeUiLayout.Box windowRoot; ++ private NativeUiScene.Node windowFocus; + private NativeContainerGeometry geometry; + private List draws = List.of(); + private long previousPresentationRevision = -1; @@ -1390,8 +1426,12 @@ index 0000000..d5ea53e + } + + private boolean updateWindows() { -+ return this.presentation instanceof NativePlayerPresentation player && !player.helping() -+ && player.windows(NativeUiCollectionWindow.visible(this.root, this.nativeInput.focused())); ++ if (!(this.presentation instanceof NativePlayerPresentation player) || player.helping()) return false; ++ final var focused = this.nativeInput.focused(); ++ if (this.windowRoot == this.root && this.windowFocus == focused) return false; ++ this.windowRoot = this.root; ++ this.windowFocus = focused; ++ return player.windows(NativeUiCollectionWindow.visible(this.root, focused)); + } + + private void refreshScene(final List slots) { @@ -1456,9 +1496,7 @@ index 0000000..d5ea53e + this.root = new NativeUiLayout(this.renderer, animations).layout(this.scene, this.width, this.height, this.scroll.offsets(this.scene)); + this.draws = NativeUiPresentation.draws(this.root, box -> box.node() == this.nativeInput.focused() || box.visibleBounds().contains(pointerX, pointerY), node -> animations.opacityAt(node, now), this.nativeInput::hasButtonState); + if (this.authoredController()) this.nativeInput.layout(this.root, this.draws, this.width, this.height); -+ this.geometry = new NativeContainerGeometry(this.draws, this.menu.slots.size(), this.recipeActions, -+ this.presentation instanceof NativeCookingPresentation, this.presentation instanceof NativePlayerPresentation); -+ this.requireCoverage(); ++ this.updateGeometry(); + this.reconcileSearch(); + this.vanillaCursor.clear(); + this.cursor(this.root, false); @@ -1612,6 +1650,13 @@ index 0000000..d5ea53e + return handled; + } + ++ private void updateGeometry() { ++ if (this.geometry != null && this.geometry.reusableFor(this.draws)) return; ++ this.geometry = new NativeContainerGeometry(this.draws, this.menu.slots.size(), this.recipeActions, ++ this.presentation instanceof NativeCookingPresentation, this.presentation instanceof NativePlayerPresentation); ++ this.requireCoverage(); ++ } ++ + private void requireCoverage() { + if (this.presentation instanceof NativePlayerPresentation) return; + if (this.presentation instanceof NativeCookingPresentation cooking @@ -1759,18 +1804,20 @@ index 0000000..d5ea53e + this.draws = NativeUiPresentation.draws(this.root, box -> box.visibleBounds().contains(mouseX, mouseY), + node -> this.lifecycle.animations().opacityAt(node, now), this.nativeInput::hasButtonState); + if (this.authoredController()) this.nativeInput.layout(this.root, this.draws, this.width, this.height); -+ this.geometry = new NativeContainerGeometry(this.draws, this.menu.slots.size(), this.recipeActions, -+ this.presentation instanceof NativeCookingPresentation, this.presentation instanceof NativePlayerPresentation); -+ this.requireCoverage(); ++ this.updateGeometry(); + this.hoveredSlot = this.nativeHoveredSlot(mouseX, mouseY); + if (this.hoveredSlot != null) NativeInventoryItemPresentation.requireTooltip(this.hoveredSlot.getItem()); -+ final List ordered = new ArrayList<>(this.draws); -+ ordered.sort(Comparator.comparingDouble(draw -> draw.box().layer())); + this.previews.clear(); -+ for (final var draw : ordered) { ++ float layer = Float.NaN; ++ for (final var draw : this.draws) { + final var box = draw.box(); + if (this.vanillaCursor.contains(box.node())) continue; + if (draw.alpha() <= 0 || box.visibleBounds().width() <= 0 || box.visibleBounds().height() <= 0) continue; ++ // Authored layers already establish painter order. Bound Java's overlap search to each layer. ++ if (layer != box.layer()) { ++ graphics.nextStratum(); ++ layer = box.layer(); ++ } + final var clip = + box.node().type().equals("label") ? box.visibleBounds() : box.clip(); + graphics.enableScissor(Math.round(clip.x()), Math.round(clip.y()), @@ -3215,10 +3262,10 @@ index 0000000..3387649 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometry.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometry.java new file mode 100644 -index 0000000..e0257f3 +index 0000000..3c762e5 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometry.java -@@ -0,0 +1,258 @@ +@@ -0,0 +1,272 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3246,6 +3293,7 @@ index 0000000..e0257f3 + static final Set RECIPE_AUXILIARY_ACTIONS = Set.of("button.drop_one", "button.drop_all", "button.shape_drawing"); + private static final Set RECIPE_MAPPING_FIELDS = Set.of("from_button_id", "to_button_id", "mapping_type", "ignored", "consume_event"); + private final List controls; ++ private final List draws; + private final Set registered = new HashSet<>(); + private boolean navigation; + @@ -3265,6 +3313,7 @@ index 0000000..e0257f3 + + NativeContainerGeometry(final List draws, final int slotCount, + final Map> recipeActions, final boolean cooking, final boolean player) { ++ this.draws = List.copyOf(draws); + final Set reinjected = new HashSet<>(); + if (cooking) for (final var draw : draws) { + final var input = NativeUiInput.parse(draw.box().node().properties()); @@ -3303,6 +3352,18 @@ index 0000000..e0257f3 + this.controls = List.copyOf(frontToBack); + } + ++ /** Box identity retains bounds, clipping, layers and the prepared scene's action mappings. */ ++ boolean reusableFor(final List draws) { ++ if (this.draws.size() != draws.size()) return false; ++ for (int index = 0; index < draws.size(); index++) { ++ final var previous = this.draws.get(index); ++ final var current = draws.get(index); ++ if (previous.box() != current.box() || previous.enabled() != current.enabled() ++ || (previous.alpha() > 0) != (current.alpha() > 0)) return false; ++ } ++ return true; ++ } ++ + private static Set closeInputs(final NativeUiScene.Node node) { + if (!(node.string("inventory_command", "").equals("close") || node.flag("help_close", false)) || !node.properties().has("button_mappings")) return Set.of(); + final Set inputs = new HashSet<>(); @@ -3479,10 +3540,10 @@ index 0000000..e0257f3 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerInput.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerInput.java new file mode 100644 -index 0000000..96ca210 +index 0000000..12b7fec --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerInput.java -@@ -0,0 +1,501 @@ +@@ -0,0 +1,506 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3521,6 +3582,7 @@ index 0000000..96ca210 + private Map replacements = Map.of(); + private List frames = List.of(); + private NativeUiLayout.Box root; ++ private NativeUiLayout.Box geometryRoot; + private NativeUiScene.Node focused; + private float pointerX = -1, pointerY = -1; + private float inputX = -1, inputY = -1; @@ -3547,6 +3609,7 @@ index 0000000..96ca210 + this.nodes = nodes; + this.entries = current; + this.indexed = indexed; ++ this.geometryRoot = null; + this.replacements = replacements; + // Every component exists before the deferred name resolver can invoke a child callback. + this.updateComponents(root, true, true); @@ -3558,7 +3621,10 @@ index 0000000..96ca210 + this.viewportWidth = viewportWidth; + this.viewportHeight = viewportHeight; + this.root = root; -+ this.geometry(root, List.of()); ++ if (this.geometryRoot != root) { ++ this.geometry(root, List.of()); ++ this.geometryRoot = root; ++ } + if (!this.replacements.isEmpty()) { + this.history.remap(this.replacements, this.targets()); + this.replacements = Map.of(); @@ -7083,10 +7149,10 @@ index 0000000..5a97c2d +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometryTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometryTest.java new file mode 100644 -index 0000000..8d089b7 +index 0000000..e120ebc --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometryTest.java -@@ -0,0 +1,366 @@ +@@ -0,0 +1,389 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -7113,6 +7179,29 @@ index 0000000..8d089b7 + +final class NativeContainerGeometryTest { + @Test ++ void retainedHitRegionsFollowVisibilityEnablementAndLayoutChanges() { ++ final var slot = draw(0, true, 1, 0, 0, 18, new NativeUiLayout.Rect(0, 0, 100, 100), 1); ++ var geometry = new NativeContainerGeometry(List.of(slot), 1); ++ assertEquals(0, geometry.hovered(1, 1)); ++ final var fading = new NativeUiPresentation.Draw(slot.box(), .4F, 1, true); ++ assertTrue(geometry.reusableFor(List.of(fading))); ++ assertEquals(0, geometry.hovered(1, 1)); ++ for (final var hidden : List.of(new NativeUiPresentation.Draw(slot.box(), 0, 1, true), ++ new NativeUiPresentation.Draw(slot.box(), 1, 1, false))) { ++ assertFalse(geometry.reusableFor(List.of(hidden))); ++ final var inactive = new NativeContainerGeometry(List.of(hidden), 1); ++ assertEquals(-1, inactive.hovered(1, 1)); ++ assertFalse(inactive.reusableFor(List.of(slot))); ++ } ++ final var moved = draw(0, true, 1, 40, 0, 18, new NativeUiLayout.Rect(0, 0, 100, 100), 1); ++ assertFalse(geometry.reusableFor(List.of(moved))); ++ geometry = new NativeContainerGeometry(List.of(moved), 1); ++ assertEquals(-1, geometry.hovered(1, 1)); ++ assertEquals(0, geometry.hovered(41, 1)); ++ assertFalse(geometry.reusableFor(List.of())); ++ } ++ ++ @Test + void layoutPreferencesPreserveTheirNativeFamilyAndRejectForcedProfiles() { + final var toggle = preference(); + toggle.box().node().properties().addProperty("toggle_name", "layout_toggle"); 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 86d844d0..cc54798e 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 @@ -46,6 +46,16 @@ They cover distant scrolling, resize, original action IDs, and keyboard navigati Creative hotbar destruction restores ordinary controls before item changes or requests. Passive hover notifications do not trigger the fallback for unsupported recipe actions. Full Creative catalog expansion, initial bundle insertion, bulk recipe actions, and Creative cursor replacement remain unresolved. -Complete cooking integration, live transfers, filled-bundle pixels, and the original E-key background report also remain unresolved. +Complete cooking integration, live transfers, and filled-bundle pixels also remain unresolved. + +Stable frames reuse parsed hit regions and input ancestor geometry. +Relayout, changed painter order, enablement, or a zero-opacity transition rebuilds the affected regions. +Collection windows rescan after layout or keyboard focus changes, and rendering uses the shared draw order directly. +Input, scrolling, focus, and screen regressions cover these boundaries. A hit-region regression checks hidden, disabled, moved, and removed controls. +Cumulative authored layers use separate Java GUI strata to bound overlap-ordering work. +A same-session check preserves static recipe, slot-grid, and crafting-background pixels across this change. +The installed Survival screen retains the translucent backdrop after its entrance animation. +Live direct checks cover pickup, drag distribution, and return to the hotbar. The client and Bedrock server retain matching item totals. +Broader modpack timing, the reported menu overlap, and shield presentation still need verification. The [owning patch](0029-present-authored-native-containers.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index 6ff56a48..8726bd61 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From e9f6d54aa627ff9b5f38d702bacfef33319ab4c5 Mon Sep 17 00:00:00 2001 +From 1d462dae8614109151bc563b54ee79503e0af80e 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(ui): share native scene rendering and GUI actors @@ -657,6 +657,28 @@ game cards. Correct the capacity boundary on either rescaling axis while keeping measurably larger cells on the next row. The reduced fractional fixture fails before the correction. This reconstructs the observed layout constraint and does not claim the native internal rounding algorithm. + +Skip child-state selection for leaf controls after recording their own +opacity and enabled draw state. Leaf controls have no named children to +exclude. This avoids repeated JSON lookups and empty-set allocation in +the inventory's per-frame traversal while preserving authored output. +A current Survival inventory profile identifies this traversal as a hot +render path; shared presentation and layout regressions remain unchanged. + +Honor scale_from_starting_alpha when creating alpha animation tracks. +Capture the control's authored alpha once, retain it with the animation +across scene refreshes, and multiply the sampled value after interpolation. +Apply that scale to the potential-opacity bound as well. Absolute tracks +keep their existing behavior. + +The default 1.26.60.5 Survival backdrop authors alpha 0.4 and requests this +scaling on its entrance fade. Ignoring the flag replaces 0.4 with 1.0 and +hides the entire world. The pinned native factory at 141c62bd0 captures +ImageComponent alpha into the alpha track, and updater 141c5e330 multiplies +the interpolation result by that captured value. The native clone retains +the scale. A reduced fade and scene-refresh regression fails before this +correction and covers scaled bounds, the settled backdrop and absolute +animation controls afterward. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../appearance/BedrockBoneAnimation.java | 3 + @@ -696,7 +718,7 @@ constraint and does not claim the native internal rounding algorithm. .../screen/ui/NativeUiActorCamera.java | 60 + .../screen/ui/NativeUiActorPortraits.java | 258 +++++ .../screen/ui/NativeUiAnimationLifecycle.java | 131 +++ - .../bedrock/screen/ui/NativeUiAnimations.java | 382 +++++++ + .../bedrock/screen/ui/NativeUiAnimations.java | 383 +++++++ .../bedrock/screen/ui/NativeUiAseprite.java | 54 + .../bedrock/screen/ui/NativeUiButton.java | 167 +++ .../screen/ui/NativeUiCollectionWindow.java | 92 ++ @@ -748,7 +770,7 @@ constraint and does not claim the native internal rounding algorithm. .../screen/ui/NativeUiActorCameraTest.java | 102 ++ .../screen/ui/NativeUiActorPortraitsTest.java | 164 +++ .../ui/NativeUiAnimationLifecycleTest.java | 155 +++ - .../screen/ui/NativeUiAnimationsTest.java | 264 +++++ + .../screen/ui/NativeUiAnimationsTest.java | 286 +++++ .../screen/ui/NativeUiAsepriteTest.java | 108 ++ .../bedrock/screen/ui/NativeUiButtonTest.java | 127 +++ .../ui/NativeUiCollectionWindowTest.java | 144 +++ @@ -772,7 +794,7 @@ constraint and does not claim the native internal rounding algorithm. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ - 114 files changed, 13000 insertions(+), 36 deletions(-) + 114 files changed, 13023 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearance.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiActorRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinNativeUiInput.java @@ -3581,10 +3603,10 @@ index 0000000..687730c +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java new file mode 100644 -index 0000000..6bc0cbe +index 0000000..057c0e9 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java -@@ -0,0 +1,382 @@ +@@ -0,0 +1,383 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3715,9 +3737,9 @@ index 0000000..6bc0cbe + final JsonObject program = node.properties().getAsJsonObject("animation_program"); + if (program != null) { + for (final var value : program.getAsJsonArray("tracks")) { -+ final Definition definition = new Definition(value.getAsJsonObject()); ++ final Definition definition = new Definition(value.getAsJsonObject(), node.number("alpha", 1)); + this.definitions.add(definition); -+ if (definition.alpha) this.potential = Math.max(this.potential, Math.max(definition.track.from, definition.track.to)); ++ if (definition.alpha) this.potential = Math.max(this.potential, Math.max(definition.track.from, definition.track.to) * definition.alphaScale); + } + for (final var root : program.getAsJsonArray("roots")) this.active.add(root.getAsInt()); + } @@ -3846,7 +3868,7 @@ index 0000000..6bc0cbe + this.u = new TextureCoordinate(0, 1, definition.uvFrom.x + (definition.uvTo.x - definition.uvFrom.x) * factor); + this.v = new TextureCoordinate(0, 1, definition.uvFrom.y + (definition.uvTo.y - definition.uvFrom.y) * factor); + } -+ if (definition.alpha) this.current = reset ? definition.track.from : definition.track.value(elapsed); ++ if (definition.alpha) this.current = (reset ? definition.track.from : definition.track.value(elapsed)) * definition.alphaScale; + if (definition.colorFrom != null && this.node.type().equals("image")) + this.color = reset ? definition.colorFrom : definition.colorFrom.interpolate(definition.colorTo, definition.track.factor(elapsed)); + if (definition.from == null) return; @@ -3873,16 +3895,17 @@ index 0000000..6bc0cbe + final Vector from, to; + final NativeUiColor colorFrom, colorTo; + final boolean alpha, image, flipbook, aseprite, looping, reversible, vertical, resettable; -+ final float fps; ++ final float fps, alphaScale; + final int frames; + final Uv uvFrom, uvTo; + final boolean waitForRender; + final int next; + final String play, reset, end, destroy; + -+ Definition(final JsonObject value) { ++ Definition(final JsonObject value, final float startingAlpha) { + this.type = NativeUiScene.string(value, "anim_type", ""); + this.alpha = this.type.equals("alpha"); ++ this.alphaScale = this.alpha && flag(value, "scale_from_starting_alpha", false) ? startingAlpha : 1; + this.flipbook = this.type.equals("flip_book"); + this.aseprite = this.type.equals("aseprite_flip_book"); + this.image = this.aseprite || this.flipbook || this.type.equals("uv") || this.type.equals("color"); @@ -6816,7 +6839,7 @@ index 0000000..81c52c3 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiPresentation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiPresentation.java new file mode 100644 -index 0000000..1279cdc +index 0000000..99d79fd --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiPresentation.java @@ -0,0 +1,162 @@ @@ -6939,7 +6962,7 @@ index 0000000..1279cdc + // Native image opacity is local unless the owner explicitly enables propagation. + // Dialogs use transparent image owners whose labels and buttons remain opaque. + final Opacity childAlpha = node.flag("propagate_alpha", false) ? alpha : inheritedAlpha; -+ if (childAlpha.potential == 0) return; ++ if (childAlpha.potential == 0 || box.children().isEmpty()) return; + final boolean isHovered = node.interactive() && hovered.test(box); + final Set excluded = new HashSet<>(); + if (!node.type().equals("slider") && !node.type().equals("slider_box") && !retainedButtons.test(node)) { @@ -10475,10 +10498,10 @@ index 0000000..e407bbc +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationsTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationsTest.java new file mode 100644 -index 0000000..7b0e4d0 +index 0000000..90d8eea --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationsTest.java -@@ -0,0 +1,264 @@ +@@ -0,0 +1,286 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -10489,12 +10512,34 @@ index 0000000..7b0e4d0 +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.List; ++import java.util.Map; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class NativeUiAnimationsTest { + @Test ++ void entranceFadeRetainsTheAuthoredBackdropOpacityAcrossSceneRefreshes() { ++ final var fade = track("alpha", 1, 0, 1); ++ fade.addProperty("scale_from_starting_alpha", true); ++ final var node = animated(fade); ++ node.properties().addProperty("alpha", .4F); ++ final var animations = new NativeUiAnimations(); ++ assertEquals(0, animations.opacityAt(node, 0).current()); ++ assertEquals(.4F, animations.opacityAt(node, 0).potential()); ++ assertEquals(.2F, animations.opacityAt(node, 500_000_000).current(), .000001F); ++ final var replacement = new NativeUiScene.Node(node.name(), node.type(), node.properties().deepCopy(), ++ node.children(), node.action()); ++ animations.reconcile(Map.of(node, replacement), 500_000_000); ++ assertEquals(.4F, animations.opacityAt(replacement, 1_000_000_000).current(), .000001F); ++ ++ fade.addProperty("scale_from_starting_alpha", false); ++ final var absolute = animated(fade); ++ absolute.properties().addProperty("alpha", .4F); ++ assertEquals(.5F, new NativeUiAnimations().opacityAt(absolute, 500_000_000).current(), .000001F); ++ } ++ ++ @Test + void treatsUnsetAnimationBindingsAsOmittedProperties() { + final var explicit = track("alpha", 2, 1, 1); + final var unset = explicit.deepCopy(); diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index 4940922f..84263e50 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -32,6 +32,12 @@ Overlapping windows retain animation clocks by source identity. These controls do not establish identical native pooling or frame timing. Broader viewports, complete inventory pixels, entrance timing, and TrueType comparisons remain open. +Alpha tracks honor `scale_from_starting_alpha` and retain their initial scale across scene refreshes. +Native Bedrock 1.26.60.5 captures ImageComponent alpha when creating the track and multiplies the interpolated value by that scale. +The default Survival backdrop uses this flag with opacity 0.4. Ignoring it makes the settled backdrop opaque. +A regression fails before the correction and covers the fade, potential-opacity bound, retained clock, and absolute-track behavior. +Leaf controls skip child-state selection after contributing their own draw state. + The [owning patch](0029-share-native-ui-scene-rendering.patch) retains implementation rationale and versioned evidence. Local image controls honor `allow_debug_missing_texture: false` when the resource is absent. From 7740f62c916c7b6683e03af6124eef7ccdcefdb8 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:44:04 +0100 Subject: [PATCH 13/33] fix(effects): tolerate absent native effect removals MegaSMP can clear effect IDs through experimental Freezing even though that effect has no translated Java mapping. Cancel absent-state removals after consuming the payload while preserving unsupported-addition errors and known-effect packet projection. A pinned native control preserves Speed across removal of absent ID 38, then removes Speed normally. Registered-handler tests reproduce the old failure, and the original captured clear sequence replays without it. Record the packet fixture's shared test-infrastructure prerequisites and keep this upstream-sized change before deferred patches. Full builds and fresh replay of the complete stack pass. The live server's converted pack still exceeds Java's download cap, so rendering and movement parity remain separate work. --- ...edrock-actor-events-to-java-statuses.patch | 6 +- patches/viabedrock/series.json | 16 ++ ...removals-for-absent-unmapped-effects.patch | 176 ++++++++++++++++++ ...removals-for-absent-unmapped-effects.pr.md | 20 ++ 4 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.patch create mode 100644 patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.pr.md diff --git a/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch b/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch index 56999a82..3d829a30 100644 --- a/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch +++ b/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch @@ -1,4 +1,4 @@ -From 041c5023c3ff55f037122573e94cfafcddf932c9 Mon Sep 17 00:00:00 2001 +From a98a158c61f1561706f3ca962b5903f15c6681d4 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(entity-events): translate Bedrock actor events to Java @@ -327,7 +327,7 @@ index b47f5471..88ce252d 100644 levelSoundEvent.send(BedrockProtocol.class, false); } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java -index fd0640c4..ddb84e53 100644 +index 619ae050..53f67c81 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java @@ -39,6 +39,7 @@ import net.raphimc.viabedrock.api.model.NativeActorMessage; @@ -434,7 +434,7 @@ index fd0640c4..ddb84e53 100644 } } }); -@@ -777,6 +826,34 @@ public final class EntityPackets { +@@ -782,6 +831,34 @@ public final class EntityPackets { return entity; } diff --git a/patches/viabedrock/series.json b/patches/viabedrock/series.json index 1c76f7d0..860a3316 100644 --- a/patches/viabedrock/series.json +++ b/patches/viabedrock/series.json @@ -540,6 +540,22 @@ "reason": "Retain the shared join and registration snapshot hooks before adding the independent fog snapshot." } ] + }, + { + "file": "0105-ignore-removals-for-absent-unmapped-effects.patch", + "title": "fix(effects): ignore removals for absent unmapped effects", + "prerequisites": [ + { + "project": "viabedrock", + "patch": "0001-update-bedrock-to-1-26-60.patch", + "reason": "Land the shared ViaVersion, Guava, Netty and JUnit test dependencies before compiling the packet regression." + }, + { + "project": "viabedrock", + "patch": "0015-transport-native-resources-and-sound-commands.patch", + "reason": "Land Mockito and the shared JVM packet-test suite configuration used to invoke the registered effect handler." + } + ] } ], "deferred": [ diff --git a/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.patch b/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.patch new file mode 100644 index 00000000..4608a2a4 --- /dev/null +++ b/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.patch @@ -0,0 +1,176 @@ +From 3da144d089da71bc3ac62d5aed7bf594e89dc4b6 Mon Sep 17 00:00:00 2001 +From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> +Date: Sat, 31 Oct 2026 21:25:36 +0100 +Subject: [PATCH] fix(effects): ignore removals for absent unmapped effects + +Consume the full MobEffect payload and cancel removal of an effect that +has no Bedrock mapping. Such an effect cannot exist in the translated +entity state because unsupported Add and Update packets still fail. +Keep known-effect removal and its Java packet projection unchanged. + +MegaSMP clears IDs 0 through 38 during login. The pinned Bedrock 1.26.60.5 +factory registers experimental Freezing as ID 38, while the translated +registry covers IDs 1 through 37. Treating its removal as an attempted +addition disconnects the client before lobby initialization completes. +Do not invent a Java effect mapping for the experimental effect. + +An isolated official-client control adds Speed, removes absent ID 38, +then removes Speed. The Speed icon survives the first removal, disappears +after its own removal, and the full 120-second recording remains playable. +The target executable hash and complete packet framing are retained in +private evidence. Packet regressions preserve the known active effect, +consume the payload, project known removal, and reject unsupported adds +and updates. + +Discarding other unmapped removals is a robustness policy for absent +state. The native control establishes ID 38 only; mixed invalid-ID native +probes can crash and do not establish behavior for arbitrary integers. +--- + .../protocol/packet/EntityPackets.java | 7 +- + .../protocol/packet/MobEffectPacketTest.java | 117 ++++++++++++++++++ + 2 files changed, 123 insertions(+), 1 deletion(-) + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/MobEffectPacketTest.java + +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +index fd0640c4..619ae050 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +@@ -609,7 +609,12 @@ public final class EntityPackets { + } + + final String bedrockIdentifier = BedrockProtocol.MAPPINGS.getBedrockEffects().inverse().get(effectId); +- if (bedrockIdentifier == null) { // Bedrock client crashes ++ if (bedrockIdentifier == null) { ++ // An unmapped effect cannot exist in the translated entity's retained state. ++ if (event == MobEffectPacketPayload_Event.Remove) { ++ wrapper.cancel(); ++ return; ++ } + throw new IllegalStateException("Unknown bedrock effect: " + effectId); + } + final EntityEffect effect = new EntityEffect(bedrockIdentifier, amplifier, duration, showParticles, ambient); +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/MobEffectPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/MobEffectPacketTest.java +new file mode 100644 +index 00000000..5bec25ba +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/MobEffectPacketTest.java +@@ -0,0 +1,117 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.packet; ++ ++import com.google.common.collect.HashBiMap; ++import com.viaversion.viaversion.api.connection.UserConnection; ++import com.viaversion.viaversion.api.minecraft.entities.EntityTypes26_3; ++import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; ++import com.viaversion.viaversion.api.protocol.remapper.PacketHandler; ++import com.viaversion.viaversion.api.type.Types; ++import com.viaversion.viaversion.protocol.packet.PacketWrapperImpl; ++import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets26_3; ++import io.netty.buffer.Unpooled; ++import net.raphimc.viabedrock.api.model.entity.LivingEntity; ++import net.raphimc.viabedrock.protocol.BedrockProtocol; ++import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; ++import net.raphimc.viabedrock.protocol.storage.EntityTracker; ++import net.raphimc.viabedrock.protocol.types.BedrockTypes; ++import org.junit.jupiter.api.Test; ++import org.mockito.ArgumentCaptor; ++ ++import java.lang.reflect.Field; ++import java.util.LinkedHashMap; ++import java.util.Map; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class MobEffectPacketTest { ++ ++ @Test ++ void unmappedRemovalPreservesTheActiveEffectUntilItsOwnRemoval() throws Exception { ++ try (var fixture = new Fixture()) { ++ final var added = fixture.receive(1, 1); ++ assertFalse(added.isCancelled()); ++ final var speed = fixture.entity.effects().get("minecraft:speed"); ++ assertNotNull(speed); ++ assertTrue(fixture.receive(3, 38).isCancelled()); ++ assertSame(speed, fixture.entity.effects().get("minecraft:speed")); ++ final var removed = fixture.receive(3, 1); ++ assertEquals(ClientboundPackets26_3.REMOVE_MOB_EFFECT, removed.getPacketType()); ++ assertEquals(3, removed.get(Types.VAR_INT, 0)); ++ assertEquals(1, removed.get(Types.VAR_INT, 1)); ++ assertTrue(fixture.entity.effects().isEmpty()); ++ } ++ } ++ ++ @Test ++ void unmappedAdditionsAndUpdatesStillRejectUnsupportedEffects() throws Exception { ++ try (var fixture = new Fixture()) { ++ for (final int event : new int[]{1, 2}) { ++ assertThrows(IllegalStateException.class, () -> fixture.receive(event, 38)); ++ assertTrue(fixture.entity.effects().isEmpty()); ++ } ++ } ++ } ++ ++ private static final class Fixture implements AutoCloseable { ++ private final Map original = new LinkedHashMap<>(); ++ private final UserConnection user = mock(UserConnection.class); ++ private final LivingEntity entity = new LivingEntity(this.user, 1, 2, "minecraft:player", 3, null, EntityTypes26_3.PLAYER); ++ private final PacketHandler handler; ++ ++ private Fixture() throws Exception { ++ this.mapping("bedrockEffects", HashBiMap.create(Map.of("minecraft:speed", 1))); ++ this.mapping("javaEffects", HashBiMap.create(Map.of("minecraft:speed", 1))); ++ this.mapping("bedrockToJavaEffects", Map.of("minecraft:speed", "minecraft:speed")); ++ final var tracker = mock(EntityTracker.class); ++ when(this.user.get(EntityTracker.class)).thenReturn(tracker); ++ when(tracker.getEntityByRid(2)).thenReturn(this.entity); ++ final var protocol = mock(BedrockProtocol.class); ++ EntityPackets.register(protocol); ++ final var handler = ArgumentCaptor.forClass(PacketHandler.class); ++ verify(protocol).registerClientbound(eq(ClientboundBedrockPackets.MOB_EFFECT), ++ eq(ClientboundPackets26_3.UPDATE_MOB_EFFECT), handler.capture()); ++ this.handler = handler.getValue(); ++ } ++ ++ private void mapping(final String name, final Object value) throws ReflectiveOperationException { ++ final var field = BedrockProtocol.MAPPINGS.getClass().getDeclaredField(name); ++ field.setAccessible(true); ++ this.original.put(field, field.get(BedrockProtocol.MAPPINGS)); ++ field.set(BedrockProtocol.MAPPINGS, value); ++ } ++ ++ private PacketWrapper receive(final int event, final int effect) throws Exception { ++ final var input = Unpooled.buffer(); ++ try { ++ BedrockTypes.UNSIGNED_VAR_LONG.writePrimitive(input, 2); ++ input.writeByte(event); ++ BedrockTypes.VAR_INT.writePrimitive(input, effect); ++ BedrockTypes.VAR_INT.writePrimitive(input, 1); ++ input.writeBoolean(false); ++ BedrockTypes.VAR_INT.writePrimitive(input, 1200); ++ BedrockTypes.UNSIGNED_VAR_LONG.writePrimitive(input, 0); ++ input.writeBoolean(false); ++ final var packet = new PacketWrapperImpl(ClientboundBedrockPackets.MOB_EFFECT, input, this.user); ++ this.handler.handle(packet); ++ assertFalse(input.isReadable()); ++ return packet; ++ } finally { ++ input.release(); ++ } ++ } ++ ++ @Override ++ public void close() throws IllegalAccessException { ++ for (final var entry : this.original.entrySet()) { ++ entry.getKey().set(BedrockProtocol.MAPPINGS, entry.getValue()); ++ } ++ } ++ } ++ ++} +-- +2.56.0 diff --git a/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.pr.md b/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.pr.md new file mode 100644 index 00000000..e2395f28 --- /dev/null +++ b/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.pr.md @@ -0,0 +1,20 @@ +# fix(effects): ignore removals for absent unmapped effects + +Cancel removal of an unmapped effect after consuming the complete MobEffect payload. +Unsupported additions and updates still fail. Known effects keep their existing state and Java packet projection. + +MegaSMP clears effect IDs through 38 during login. +Native Bedrock 1.26.60.5 registers experimental Freezing as ID 38, which has no translated Java effect mapping. +An isolated native control keeps Speed active after removing absent ID 38, removes Speed on its own request, and completes the recording. +The control uses an owned loopback server and explicitly injected effect packets. It does not represent unmodified public-server traffic. + +Packet regressions cover retained known state, complete payload consumption, known removal, and rejected unsupported additions and updates. +Discarding other unmapped removals is an absent-state robustness policy. The native control establishes ID 38 only. +The implementation applies alone to pinned upstream. Its packet fixture requires the shared test dependencies and JVM test-suite configuration recorded in `series.json`. +The ordinary standalone build cannot compile that fixture until those prerequisites land. +A fresh direct capture reaches a visible MegaSMP lobby, but it contains no MobEffect packets. +The earlier failing recording replays all 39 removals without the unknown-effect error and preserves the complete scene payloads. +Its rendering check fails because Java rejects the converted resource pack for exceeding its download cap. The fresh live run has the same rejection. +Horizontal input changes position, but repeated vertical prediction corrections remain. Resource-pack loading, complete gameplay, and native movement parity need further work. + +The [owning patch](0105-ignore-removals-for-absent-unmapped-effects.patch) retains the implementation rationale and versioned evidence. From c96532e20dd1a7fa3f245eee300c2c8e05acd563 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:37:11 +0100 Subject: [PATCH 14/33] fix(ui): preserve native JPEG resources through pack conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep validated JPEG bytes in converted UI packs and transport the selected format through program export. Decode them with a bounded reloadable client texture so resource replacement and GPU ownership follow the normal texture lifecycle. This prevents MegaSMP image expansion from exceeding Java’s download cap. Full builds, clean patch replay, decoded-pixel controls and an installed resource-reload check pass. A fresh join consumes all effect removals and walks grounded; custom block mappings and the attempted menu input remain unresolved. --- ...are-native-ui-programs-and-resources.patch | 146 ++++++++--- ...are-native-ui-programs-and-resources.pr.md | 6 + ...removals-for-absent-unmapped-effects.pr.md | 7 +- ...0029-share-native-ui-scene-rendering.patch | 237 +++++++++++++++++- ...0029-share-native-ui-scene-rendering.pr.md | 6 + 5 files changed, 355 insertions(+), 47 deletions(-) diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch index 5f4e6736..e17962dd 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch @@ -1,4 +1,4 @@ -From cb0004f758535d7a16ab4555d115192c73b8c045 Mon Sep 17 00:00:00 2001 +From b9653dadb51b0c19ac1ae17e12bc8e9d61294bf4 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): share bounded native UI programs and resource @@ -420,6 +420,28 @@ limit silently discards both despite their bounded pixel counts, leaving required converted references unavailable. Share the revised dimension policy with client decoding and retain a small encoded-strip regression that checks both endpoint pixels after conversion. Keep source byte limits. + +Retain validated JPEG encoding in native UI resources instead of expanding +every source image to PNG during pack conversion. Keep PNG preference +within each pack and replace the previous layer's selected format by its +shared basename. TGA images still normalize to PNG. Publish selected JPEG +basenames in the bounded UI program so client-side resolution selects the +same encoded resource after transport. Keep extension-sensitive resource +identifiers and invalidate the prior converted-pack cache identity. + +MegaSMP's captured native packs include JPEG store and bundle images. +Normalizing matched JPEG sources expands about 12 MB of encoded images to +about 88 MB of PNG data. The complete converted pack exceeds Java's normal +download budget. Retaining original JPEG bytes avoids that expansion and +preserves their existing decoded pixels without another lossy encode. +The add-on supplies a bounded JPEG texture consumer; the shared encoded +byte and pixel limits remain unchanged. + +Regressions retain exact JPEG bytes through resource export, preserve +within-pack and cross-pack priority in both directions, and resolve the +same image and metadata after UI program transport. Pinned native source +assets establish JPEG use; actual rendering and full installed pack +admission remain end-to-end checks. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -434,7 +456,7 @@ that checks both endpoint pixels after conversion. Keep source byte limits. .../api/model/NativeCookingStatus.java | 41 + .../api/model/NativeFormMessage.java | 56 + .../api/model/NativeHudMessage.java | 60 + - .../api/model/NativeItemHoverText.java | 106 ++ + .../api/model/NativeItemHoverText.java | 106 + .../viabedrock/api/model/NativeItemNames.java | 69 + .../api/model/NativeNpcDialogueMessage.java | 60 + .../http/ConvertedResourcePackCache.java | 6 +- @@ -451,10 +473,10 @@ that checks both endpoint pixels after conversion. Keep source byte limits. .../api/resourcepack/ui/HudTitleUi.java | 28 + .../resourcepack/ui/InventoryItemFilter.java | 43 + .../resourcepack/ui/InventoryStackCount.java | 21 + - .../api/resourcepack/ui/JsonUiProgram.java | 1676 +++++++++++++++++ + .../api/resourcepack/ui/JsonUiProgram.java | 1703 +++++++++++++++++ .../resourcepack/ui/NativeRecipeSearch.java | 124 ++ .../ui/NativeSearchNormalization.java | 189 ++ - .../api/resourcepack/ui/NpcDialogueUi.java | 316 ++++ + .../api/resourcepack/ui/NpcDialogueUi.java | 316 +++ .../resourcepack/ui/NpcEditorBindings.java | 329 ++++ .../api/resourcepack/ui/NpcEditorState.java | 322 ++++ .../api/resourcepack/ui/RecipeCollection.java | 266 +++ @@ -464,8 +486,8 @@ that checks both endpoint pixels after conversion. Keep source byte limits. .../api/resourcepack/ui/UiBitmapFonts.java | 282 +++ .../api/resourcepack/ui/UiExpression.java | 306 +++ .../api/resourcepack/ui/UiFonts.java | 286 +++ - .../api/resourcepack/ui/UiTextureLimits.java | 22 + - .../api/resourcepack/ui/UiTextures.java | 94 + + .../api/resourcepack/ui/UiTextureLimits.java | 24 + + .../api/resourcepack/ui/UiTextures.java | 101 + .../viabedrock/api/util/NativeSort.java | 168 ++ .../rewriter/ResourcePackRewriter.java | 2 + .../GlyphSheetResourceRewriter.java | 64 +- @@ -479,7 +501,7 @@ that checks both endpoint pixels after conversion. Keep source byte limits. .../model/NativeCookingActionResultTest.java | 63 + .../api/model/NativeCookingActionTest.java | 170 ++ .../api/model/NativeCookingControlTest.java | 99 + - .../api/model/NativeCookingMessageTest.java | 424 +++++ + .../api/model/NativeCookingMessageTest.java | 424 ++++ .../api/model/NativeCookingProgressTest.java | 38 + .../api/model/NativeFormMessageTest.java | 51 + .../api/model/NativeHudMessageTest.java | 38 + @@ -510,10 +532,10 @@ that checks both endpoint pixels after conversion. Keep source byte limits. .../ui/UiCollectionWindowTest.java | 131 ++ .../api/resourcepack/ui/UiExpressionTest.java | 77 + .../api/resourcepack/ui/UiFontsTest.java | 192 ++ - .../api/resourcepack/ui/UiTexturesTest.java | 213 +++ + .../api/resourcepack/ui/UiTexturesTest.java | 231 +++ .../ui/native-inventory-item-filter.json | 311 +++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 92 files changed, 14460 insertions(+), 60 deletions(-) + 92 files changed, 14514 insertions(+), 60 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerMessage.java @@ -3868,10 +3890,10 @@ index 00000000..5dfbb3f8 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java new file mode 100644 -index 00000000..3bac1d97 +index 00000000..5a5b7d16 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java -@@ -0,0 +1,1676 @@ +@@ -0,0 +1,1703 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -3925,6 +3947,7 @@ index 00000000..3bac1d97 + private volatile Map nativeTemplates; + private final Map globals = new HashMap<>(); + private final Map textureFiles = new LinkedHashMap<>(); ++ private final Set jpegTextures = new LinkedHashSet<>(); + private final UiFonts fonts = new UiFonts(); + private final Set customizedFiles = new HashSet<>(); + private final UnaryOperator translator; @@ -4050,6 +4073,20 @@ index 00000000..3bac1d97 + }); + } + ++ if (program.has("jpeg_textures")) { ++ final JsonArray textures = program.getAsJsonArray("jpeg_textures"); ++ if (textures.size() > UiTextures.MAX_FILES) { ++ throw new IllegalArgumentException("Too many UI JPEG textures"); ++ } ++ for (final JsonElement texture : textures) { ++ if (!texture.isJsonPrimitive() || !texture.getAsJsonPrimitive().isString() ++ || !texture.getAsString().startsWith("textures/") || !safePath(texture.getAsString())) { ++ throw new IllegalArgumentException("Invalid UI JPEG texture"); ++ } ++ ui.jpegTextures.add(texture.getAsString()); ++ } ++ } ++ + if (program.has("bitmap_font_characters")) { + ui.fonts.importBitmapCharacters(program.get("bitmap_font_characters").getAsString()); + } @@ -4314,6 +4351,12 @@ index 00000000..3bac1d97 + } + }); + result.add("texture_metadata", metadata); ++ final Set jpegTextures = new LinkedHashSet<>(this.jpegTextures); ++ this.textureFiles.keySet().stream().filter(path -> path.endsWith(".jpg")) ++ .map(JsonUiProgram::textureBasename).forEach(jpegTextures::add); ++ final JsonArray jpeg = new JsonArray(); ++ jpegTextures.forEach(jpeg::add); ++ result.add("jpeg_textures", jpeg); + final JsonObject fonts = new JsonObject(); + this.fonts.ids().forEach(fonts::addProperty); + result.add("font_ids", fonts); @@ -4617,7 +4660,7 @@ index 00000000..3bac1d97 + + public static String builtinIdentity() { + // Include image and font conversion revisions in the converted-pack cache identity. -+ return "7:" + Builtins.IDENTITY; ++ return "8:" + Builtins.IDENTITY; + } + + public static String textureId(final String path) { @@ -4628,12 +4671,18 @@ index 00000000..3bac1d97 + // Native paths are case-sensitive and permit characters Java identifiers reject. + // Hash the normalized source name, keeping references and converted bytes identical. + return "viabedrock:textures/server_ui/" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") -+ .digest(textureBasename(path).getBytes(StandardCharsets.UTF_8))) + ".png"; ++ .digest(textureBasename(path).getBytes(StandardCharsets.UTF_8))) + (path.endsWith(".jpg") ? ".jpg" : ".png"); + } catch (final NoSuchAlgorithmException impossible) { + throw new IllegalStateException(impossible); + } + } + ++ private String resolvedTextureId(final String path) { ++ final String basename = textureBasename(path); ++ final boolean jpeg = this.jpegTextures.contains(basename) || this.textureFiles.containsKey(basename + ".jpg"); ++ return textureId(basename + (jpeg ? ".jpg" : ".png")); ++ } ++ + private static String textureBasename(final String path) { + return path.replaceAll("\\.(png|jpg|tga)$", ""); + } @@ -4952,7 +5001,7 @@ index 00000000..3bac1d97 + throw new IllegalArgumentException("Unsupported UI texture URL"); + } + } else if (!texture.isEmpty() && safePath(texture)) { -+ node.addProperty("texture", textureId(texture)); ++ node.addProperty("texture", JsonUiProgram.this.resolvedTextureId(texture)); + final byte[] metadata = JsonUiProgram.this.textureFiles.get(textureBasename(texture) + ".json"); + if (metadata != null) { + final JsonElement parsed = JsonParser.parseString(new String(metadata, StandardCharsets.UTF_8)); @@ -8469,10 +8518,10 @@ index 00000000..8715b892 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextureLimits.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextureLimits.java new file mode 100644 -index 00000000..b2fcd650 +index 00000000..cc526d4c --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextureLimits.java -@@ -0,0 +1,22 @@ +@@ -0,0 +1,24 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -8483,6 +8532,8 @@ index 00000000..b2fcd650 +/** Shared bounds for converted pack textures and their decoded client images. */ +public final class UiTextureLimits { + ++ public static final int MAX_ENCODED_BYTES = 16 * 1024 * 1024; ++ + private static final int MAX_DIMENSION = 16384; + private static final long MAX_PIXELS = 16L * 1024 * 1024; + @@ -8497,10 +8548,10 @@ index 00000000..b2fcd650 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextures.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextures.java new file mode 100644 -index 00000000..cec70899 +index 00000000..b30b3a2f --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTextures.java -@@ -0,0 +1,94 @@ +@@ -0,0 +1,101 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -8517,7 +8568,7 @@ index 00000000..cec70899 +import javax.imageio.ImageIO; +import net.raphimc.viabedrock.api.resourcepack.content.Content; + -+/** Normalizes a pack layer using the same image format priority as Bedrock content lookups. */ ++/** Normalizes a pack layer using the same image format priority as Bedrock content lookups, retaining JPEG encoding. */ +final class UiTextures { + + static final int MAX_FILES = 16384; @@ -8530,7 +8581,7 @@ index 00000000..cec70899 + for (final var entry : files.entrySet()) { + final String path = entry.getKey(); + if (!path.startsWith("textures/") || !JsonUiProgram.safePath(path) -+ || entry.getValue().length > 16 * 1024 * 1024) { ++ || entry.getValue().length > UiTextureLimits.MAX_ENCODED_BYTES) { + continue; + } + if (path.endsWith(".json")) { @@ -8542,19 +8593,26 @@ index 00000000..cec70899 + } + } + for (final String path : images) { -+ if (result.size() >= MAX_FILES && !result.containsKey(path + ".png")) { ++ if (result.size() >= MAX_FILES && !result.containsKey(path + ".png") && !result.containsKey(path + ".jpg")) { + continue; + } + for (final String format : List.of("png", "jpg", "tga")) { + final byte[] bytes = files.get(path + "." + format); -+ if (bytes == null || bytes.length > 16 * 1024 * 1024) { ++ if (bytes == null || bytes.length > UiTextureLimits.MAX_ENCODED_BYTES) { + continue; + } + try { + validateDimensions(bytes, format); -+ final byte[] png = new Content.LazyImage(bytes, format).getPngBytes(); -+ if (png.length <= 16 * 1024 * 1024) { -+ result.put(path + ".png", png); ++ final Content.LazyImage image = new Content.LazyImage(bytes, format); ++ final boolean jpeg = format.equals("jpg"); ++ if (jpeg && image.getImage() == null) { ++ throw new IOException("Invalid UI JPEG"); ++ } ++ final byte[] encoded = jpeg ? bytes : image.getPngBytes(); ++ if (encoded.length <= UiTextureLimits.MAX_ENCODED_BYTES) { ++ result.remove(path + ".png"); ++ result.remove(path + ".jpg"); ++ result.put(path + (jpeg ? ".jpg" : ".png"), encoded); + } + } catch (final IOException | RuntimeException malformed) { + // An unusable image cannot prevent unrelated controls or textures from loading. @@ -8877,7 +8935,7 @@ index a1c8999f..12598aaf 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/JsonUiResourceRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/JsonUiResourceRewriter.java new file mode 100644 -index 00000000..f984c478 +index 00000000..cf15ee7a --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/resourcepack/JsonUiResourceRewriter.java @@ -0,0 +1,36 @@ @@ -8907,7 +8965,7 @@ index 00000000..f984c478 + public static Content convert(final ResourcePackStorage storage) { + final Content result = new InMemoryContent(); + for (final var entry : storage.getJsonUiProgram().textures().entrySet()) { -+ if (entry.getKey().endsWith(".png")) { ++ if (entry.getKey().endsWith(".png") || entry.getKey().endsWith(".jpg")) { + final String identifier = JsonUiProgram.textureId(entry.getKey()); + result.put("assets/viabedrock/" + identifier.substring("viabedrock:".length()), entry.getValue()); + } @@ -15873,10 +15931,10 @@ index 00000000..b83ce92b +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTexturesTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTexturesTest.java new file mode 100644 -index 00000000..d918257f +index 00000000..b0cca4f9 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/UiTexturesTest.java -@@ -0,0 +1,213 @@ +@@ -0,0 +1,231 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -15892,6 +15950,7 @@ index 00000000..d918257f +import java.util.HashMap; +import java.util.List; +import java.util.Map; ++import java.util.Set; +import java.util.UUID; +import net.raphimc.viabedrock.api.resourcepack.ResourcePack; +import net.raphimc.viabedrock.api.resourcepack.content.InMemoryContent; @@ -15922,7 +15981,7 @@ index 00000000..d918257f + final String lowerId = JsonUiProgram.textureId("textures/ui/banners"); + assertTrue(upperId.matches("[a-z0-9_.-]+:[a-z0-9/._-]+")); + assertNotEquals(upperId, lowerId); -+ for (final String extension : List.of("png", "jpg", "tga")) { ++ for (final String extension : List.of("png", "tga")) { + assertEquals(upperId, JsonUiProgram.textureId("textures/ui/Banners." + extension)); + } + assertEquals(upper.getRGB(0, 0), converted.getImage("assets/viabedrock/" + upperId.substring(11)).getImage().getRGB(0, 0)); @@ -15956,10 +16015,10 @@ index 00000000..d918257f + final var form = JsonParser.parseString("{\"type\":\"form\",\"title\":\"" + path + + "\",\"elements\":[{\"type\":\"button\",\"text\":\"\"}]}").getAsJsonObject(); + final var root = new ServerFormUi(ui).resolve(form).getAsJsonObject("root"); -+ assertEquals(JsonUiProgram.textureId(path), root.get("texture").getAsString()); ++ assertEquals(JsonUiProgram.textureId(path + ".jpg"), root.get("texture").getAsString()); + assertEquals(1, root.get("nineslice_size").getAsInt()); + final var expected = ImageIO.read(new ByteArrayInputStream(upper)); -+ final var actual = ImageIO.read(new ByteArrayInputStream(ui.textures().get(path + ".png"))); ++ final var actual = ImageIO.read(new ByteArrayInputStream(ui.textures().get(path + ".jpg"))); + assertArrayEquals(expected.getRGB(0, 0, 2, 1, null, 0, 2), + actual.getRGB(0, 0, 2, 1, null, 0, 2)); + final var rejected = new HashMap(); @@ -16001,7 +16060,7 @@ index 00000000..d918257f + } + + @Test -+ void jpegConversionPreservesDecodedPixelsAndLayerPriority() throws Exception { ++ void jpegBytesAndLayerSelectionSurviveProgramTransport() throws Exception { + final var image = new BufferedImage(3, 2, BufferedImage.TYPE_INT_RGB); + image.setRGB(1, 0, 0xBD3716); + final byte[] jpeg = encode(image, "jpg"); @@ -16018,14 +16077,31 @@ index 00000000..d918257f + "ui/server_form.json", definition.getBytes(StandardCharsets.UTF_8)))); + final var form = JsonParser.parseString("{\"type\":\"form\",\"title\":\"\",\"elements\":[{\"type\":\"button\",\"text\":\"\"}]}").getAsJsonObject(); + final var root = new ServerFormUi(upper).resolve(form).getAsJsonObject("root"); -+ assertEquals(JsonUiProgram.textureId("textures/ui/item"), root.get("texture").getAsString()); ++ assertEquals(JsonUiProgram.textureId("textures/ui/item.jpg"), root.get("texture").getAsString()); ++ final var transported = ServerFormUi.fromProgram(upper.export(Set.of("server_form.main_screen_content"), form)); ++ assertEquals(root, transported.resolve(form).getAsJsonObject("root")); ++ assertArrayEquals(jpeg, upper.textures().get("textures/ui/item.jpg")); ++ assertFalse(upper.textures().containsKey("textures/ui/item.png")); + assertEquals(2, root.get("nineslice_size").getAsInt()); + final var decoded = ImageIO.read(new ByteArrayInputStream(jpeg)); -+ final var normalized = ImageIO.read(new ByteArrayInputStream(upper.textures().get("textures/ui/item.png"))); ++ final var normalized = ImageIO.read(new ByteArrayInputStream(upper.textures().get("textures/ui/item.jpg"))); + assertEquals(decoded.getWidth(), normalized.getWidth()); + assertEquals(decoded.getHeight(), normalized.getHeight()); + assertArrayEquals(decoded.getRGB(0, 0, 3, 2, null, 0, 3), normalized.getRGB(0, 0, 3, 2, null, 0, 3)); + assertEquals(2, upper.textures().size()); ++ final var pngOverride = new JsonUiProgram(List.of(upper.textures(), Map.of("textures/ui/item.png", png, ++ "ui/server_form.json", definition.getBytes(StandardCharsets.UTF_8)))); ++ assertArrayEquals(png, pngOverride.textures().get("textures/ui/item.png")); ++ assertFalse(pngOverride.textures().containsKey("textures/ui/item.jpg")); ++ assertEquals(JsonUiProgram.textureId("textures/ui/item.png"), ++ new ServerFormUi(pngOverride).resolve(form).getAsJsonObject("root").get("texture").getAsString()); ++ final var content = new InMemoryContent(); ++ content.putString("manifest.json", "{\"format_version\":2,\"header\":{\"uuid\":\"" + UUID.randomUUID() ++ + "\",\"version\":[1,0,0],\"name\":\"test\"}}"); ++ content.put("textures/ui/item.jpg", jpeg); ++ final var converted = JsonUiResourceRewriter.convert(new ResourcePackStorage(List.of(new ResourcePack(content)))); ++ final String identifier = JsonUiProgram.textureId("textures/ui/item.jpg"); ++ assertArrayEquals(jpeg, converted.get("assets/viabedrock/" + identifier.substring(11))); + } + + @Test diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index 062bd71e..57a51c3a 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -67,3 +67,9 @@ The current native pack uses a 963 × 5,916 logo atlas and a 5,880 × 480 backgr A shared dimension policy bounds conversion and client decoding. An encoded-strip regression preserves its endpoint pixels. The rebuilt add-on opens the authored hub menu with all five trending cards and the three lower game cards. A physical OP SMP selection sends the native original response index 2. The destination connection times out after Transfer; gameplay there remains unverified. + +Validated native UI JPEGs retain their original encoding in converted packs. +Program export preserves the selected format and pack-layer precedence. PNG preference within a pack and TGA conversion remain unchanged. +Regressions cover byte preservation, both override directions, program round trips, metadata, and resource export. +The rebuilt add-on accepts MegaSMP's converted pack under Java's existing download cap and reaches a visible lobby. +A fresh walking control remains grounded. The attempted hotbar menu input opens no screen; complete gameplay remains unverified. diff --git a/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.pr.md b/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.pr.md index e2395f28..bb046907 100644 --- a/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.pr.md +++ b/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.pr.md @@ -12,9 +12,10 @@ Packet regressions cover retained known state, complete payload consumption, kno Discarding other unmapped removals is an absent-state robustness policy. The native control establishes ID 38 only. The implementation applies alone to pinned upstream. Its packet fixture requires the shared test dependencies and JVM test-suite configuration recorded in `series.json`. The ordinary standalone build cannot compile that fixture until those prerequisites land. -A fresh direct capture reaches a visible MegaSMP lobby, but it contains no MobEffect packets. The earlier failing recording replays all 39 removals without the unknown-effect error and preserves the complete scene payloads. -Its rendering check fails because Java rejects the converted resource pack for exceeding its download cap. The fresh live run has the same rejection. -Horizontal input changes position, but repeated vertical prediction corrections remain. Resource-pack loading, complete gameplay, and native movement parity need further work. +The [shared UI resource patch](0095-share-native-ui-programs-and-resources.pr.md) fixes the separate converted-pack size failure. +The earlier recording now passes pack admission but fails the rendering check on missing custom block mappings. +A current fresh run receives all 39 removals with complete payload framing, loads the pack, reaches a visible lobby, and remains grounded during walking. +Complete gameplay, custom block rendering, and native movement parity need further work. The [owning patch](0105-ignore-removals-for-absent-unmapped-effects.patch) retains the implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index 8726bd61..f4bbd55b 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From 1d462dae8614109151bc563b54ee79503e0af80e Mon Sep 17 00:00:00 2001 +From 9e2b335ef3106e348f060f23455a8749f1318e7f 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(ui): share native scene rendering and GUI actors @@ -679,6 +679,28 @@ the interpolation result by that captured value. The native clone retains the scale. A reduced fade and scene-refresh regression fails before this correction and covers scaled bounds, the settled backdrop and absolute animation controls afterward. + +Load native UI JPEG resources through a bounded reloadable texture instead +of requiring their conversion to PNG in the transported pack. Decode the +selected image before native pixel allocation, retain the existing encoded +byte and pixel budgets, and copy its ARGB rows directly into NativeImage. +The ordinary texture manager owns upload, replacement, resource reload and +retirement. Cache misses prepare the image once; reloads read the current +resource rather than retaining stale prepared pixels. PNG and remote-cache +paths retain their existing behavior. + +Minecraft 26.3's NativeImage reader validates a PNG header before invoking +its decoder. Preserved native JPEGs therefore need this explicit consumer. +The default texture manager cannot load them directly. Native pack JPEG +images previously expanded to PNG in the core UI exporter and caused +MegaSMP's converted pack to exceed Java's download cap. + +Tests compare decoded pixels with the previous JPEG-to-PNG path, retain +replacement content across resource reload, and reject oversized headers +before image decoding. A private corpus probe checks all 126 matched JPEGs +from the captured MegaSMP packs, covering 53,564,211 pixels with no decoded +pixel differences. These CPU controls do not establish complete native or +GPU visual parity; installed pack admission and rendering remain live gates. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../appearance/BedrockBoneAnimation.java | 3 + @@ -731,9 +753,10 @@ animation controls afterward. .../bedrock/screen/ui/NativeUiGraphics.java | 47 + .../bedrock/screen/ui/NativeUiImageCache.java | 229 ++++ .../screen/ui/NativeUiImageRegions.java | 132 +++ - .../bedrock/screen/ui/NativeUiImages.java | 177 +++ + .../bedrock/screen/ui/NativeUiImages.java | 192 ++++ .../bedrock/screen/ui/NativeUiInput.java | 248 ++++ .../screen/ui/NativeUiInputRouter.java | 72 ++ + .../screen/ui/NativeUiJpegTexture.java | 83 ++ .../bedrock/screen/ui/NativeUiLayout.java | 563 +++++++++ .../screen/ui/NativeUiMultilineInput.java | 267 +++++ .../screen/ui/NativeUiPlayerCamera.java | 72 ++ @@ -782,6 +805,7 @@ animation controls afterward. .../bedrock/screen/ui/NativeUiImagesTest.java | 137 +++ .../screen/ui/NativeUiInputRouterTest.java | 125 ++ .../bedrock/screen/ui/NativeUiInputTest.java | 200 ++++ + .../screen/ui/NativeUiJpegTextureTest.java | 83 ++ .../bedrock/screen/ui/NativeUiLayoutTest.java | 1004 +++++++++++++++++ .../screen/ui/NativeUiMultilineInputTest.java | 210 ++++ .../screen/ui/NativeUiPlayerCameraTest.java | 132 +++ @@ -794,7 +818,7 @@ animation controls afterward. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ - 114 files changed, 13023 insertions(+), 36 deletions(-) + 116 files changed, 13204 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearance.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiActorRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinNativeUiInput.java @@ -835,6 +859,7 @@ animation controls afterward. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImages.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiInput.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiInputRouter.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiJpegTexture.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayout.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiMultilineInput.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiPlayerCamera.java @@ -874,6 +899,7 @@ animation controls afterward. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImagesTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiInputRouterTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiInputTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiJpegTextureTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayoutTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiMultilineInputTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiPlayerCameraTest.java @@ -5404,10 +5430,10 @@ index 0000000..3f3e908 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImages.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImages.java new file mode 100644 -index 0000000..d77f29a +index 0000000..0712aea --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImages.java -@@ -0,0 +1,177 @@ +@@ -0,0 +1,192 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -5546,10 +5572,25 @@ index 0000000..d77f29a + throw new IllegalArgumentException("Invalid native UI texture: " + id); + final Image cached = this.images.get(id); + if (cached != null && cached.source == resource.source()) return cached; -+ try (final var input = resource.open(); final NativeImage pixels = NativeImage.read(input)) { -+ if (!UiTextureLimits.supportsDimensions(pixels.getWidth(), pixels.getHeight())) -+ throw new IOException("Native UI texture exceeds size limits"); -+ final Image image = new Image(id, pixels.getWidth(), pixels.getHeight(), resource.source()); ++ try (final var input = resource.open()) { ++ final Image image; ++ if (id.getPath().endsWith(".jpg")) { ++ final var decoded = NativeUiJpegTexture.decode(input); ++ final var textureResource = new NativeUiJpegTexture(id, decoded); ++ try { ++ Minecraft.getInstance().getTextureManager().registerAndLoad(id, textureResource); ++ } catch (final RuntimeException | Error failure) { ++ textureResource.close(); ++ throw failure; ++ } ++ image = new Image(id, decoded.getWidth(), decoded.getHeight(), resource.source()); ++ } else { ++ try (final NativeImage pixels = NativeImage.read(input)) { ++ if (!UiTextureLimits.supportsDimensions(pixels.getWidth(), pixels.getHeight())) ++ throw new IOException("Native UI texture exceeds size limits"); ++ image = new Image(id, pixels.getWidth(), pixels.getHeight(), resource.source()); ++ } ++ } + this.images.put(id, image); + if (this.images.size() > MAX_PACK_IMAGES) this.images.pollFirstEntry(); + this.failedImages.remove(id); @@ -5917,6 +5958,95 @@ index 0000000..8c0c6f6 + public record Event(K key, NativeUiInput.ButtonEvent button) {} + public record Result(boolean consumed, List> events) {} +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiJpegTexture.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiJpegTexture.java +new file mode 100644 +index 0000000..a0dfa44 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiJpegTexture.java +@@ -0,0 +1,83 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.screen.ui; ++ ++import com.mojang.blaze3d.platform.NativeImage; ++import java.awt.image.BufferedImage; ++import java.io.ByteArrayInputStream; ++import java.io.IOException; ++import java.io.InputStream; ++import javax.imageio.ImageIO; ++import javax.imageio.stream.MemoryCacheImageInputStream; ++import net.minecraft.client.renderer.texture.ReloadableTexture; ++import net.minecraft.client.renderer.texture.TextureContents; ++import net.minecraft.resources.Identifier; ++import net.minecraft.server.packs.resources.ResourceManager; ++import net.raphimc.viabedrock.api.resourcepack.ui.UiTextureLimits; ++ ++/** Native JPEG resources keep their encoding in the pack and decode only when displayed. */ ++final class NativeUiJpegTexture extends ReloadableTexture { ++ private BufferedImage prepared; ++ ++ NativeUiJpegTexture(final Identifier id, final BufferedImage prepared) { ++ super(id); ++ this.prepared = prepared; ++ } ++ ++ @Override ++ public TextureContents loadContents(final ResourceManager resources) throws IOException { ++ BufferedImage image = this.prepared; ++ this.prepared = null; ++ if (image == null) { ++ try (final var input = resources.getResourceOrThrow(this.resourceId()).open()) { ++ image = decode(input); ++ } ++ } ++ return new TextureContents(pixels(image), null); ++ } ++ ++ @Override ++ public void close() { ++ this.prepared = null; ++ super.close(); ++ } ++ ++ static BufferedImage decode(final InputStream source) throws IOException { ++ final byte[] bytes = source.readNBytes(UiTextureLimits.MAX_ENCODED_BYTES + 1); ++ if (bytes.length > UiTextureLimits.MAX_ENCODED_BYTES) throw new IOException("Native UI texture exceeds byte limits"); ++ try (final var input = new MemoryCacheImageInputStream(new ByteArrayInputStream(bytes))) { ++ final var readers = ImageIO.getImageReaders(input); ++ if (!readers.hasNext()) throw new IOException("Unsupported native UI image"); ++ final var reader = readers.next(); ++ try { ++ reader.setInput(input, true, true); ++ if (!UiTextureLimits.supportsDimensions(reader.getWidth(0), reader.getHeight(0))) ++ throw new IOException("Native UI texture exceeds dimension limits"); ++ final BufferedImage image = reader.read(0); ++ if (image == null || !UiTextureLimits.supportsDimensions(image.getWidth(), image.getHeight())) ++ throw new IOException("Invalid native UI image"); ++ return image; ++ } finally { ++ reader.dispose(); ++ } ++ } ++ } ++ ++ static NativeImage pixels(final BufferedImage image) { ++ final NativeImage pixels = new NativeImage(image.getWidth(), image.getHeight(), false); ++ try { ++ final int[] row = new int[image.getWidth()]; ++ for (int y = 0; y < image.getHeight(); y++) { ++ image.getRGB(0, y, image.getWidth(), 1, row, 0, image.getWidth()); ++ for (int x = 0; x < image.getWidth(); x++) pixels.setPixel(x, y, row[x]); ++ } ++ return pixels; ++ } catch (final RuntimeException | Error failure) { ++ pixels.close(); ++ throw failure; ++ } ++ } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayout.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayout.java new file mode 100644 index 0000000..7e4d4f0 @@ -12440,6 +12570,95 @@ index 0000000..bec654e + return new NativeUiInput.Context(0x7c | (hovered ? 2 : 0), mode, x, y, hovered, true, false, false, nanos, 0); + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiJpegTextureTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiJpegTextureTest.java +new file mode 100644 +index 0000000..4405dd6 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiJpegTextureTest.java +@@ -0,0 +1,83 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.screen.ui; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++import com.mojang.blaze3d.systems.RenderSystem; ++import java.awt.image.BufferedImage; ++import java.io.ByteArrayInputStream; ++import java.io.ByteArrayOutputStream; ++import java.io.IOException; ++import java.util.List; ++import javax.imageio.ImageIO; ++import javax.imageio.ImageReader; ++import net.minecraft.resources.Identifier; ++import net.minecraft.server.packs.resources.Resource; ++import net.minecraft.server.packs.resources.ResourceManager; ++import net.raphimc.viabedrock.api.resourcepack.content.Content; ++import org.junit.jupiter.api.Test; ++ ++final class NativeUiJpegTextureTest { ++ @Test ++ void jpegPixelsMatchThePreviousPngTransportWithoutAnotherLossyEncoding() throws Exception { ++ final byte[] jpeg = jpeg(0xBD3716); ++ final var previous = ImageIO.read(new ByteArrayInputStream(new Content.LazyImage(jpeg, "jpg").getPngBytes())); ++ final var decoded = NativeUiJpegTexture.decode(new ByteArrayInputStream(jpeg)); ++ try (final var pixels = NativeUiJpegTexture.pixels(decoded)) { ++ assertEquals(previous.getWidth(), pixels.getWidth()); ++ assertEquals(previous.getHeight(), pixels.getHeight()); ++ assertArrayEquals(previous.getRGB(0, 0, 3, 2, null, 0, 3), pixels.getPixels()); ++ } ++ } ++ ++ @Test ++ void resourceReloadReadsTheCurrentImageInsteadOfRetainingPreparedPixels() throws Exception { ++ final var id = Identifier.parse("viabedrock:textures/server_ui/reload.jpg"); ++ final var resources = mock(ResourceManager.class); ++ final var resource = mock(Resource.class); ++ when(resources.getResourceOrThrow(id)).thenReturn(resource); ++ when(resource.open()).thenAnswer(_ -> new ByteArrayInputStream(jpeg(0x16AB37))); ++ final var prepared = NativeUiJpegTexture.decode(new ByteArrayInputStream(jpeg(0xBD3716))); ++ try (final var render = mockStatic(RenderSystem.class, RETURNS_DEEP_STUBS); ++ final var texture = new NativeUiJpegTexture(id, prepared)) { ++ try (final var initial = texture.loadContents(resources)) { ++ assertArrayEquals(prepared.getRGB(0, 0, 3, 2, null, 0, 3), initial.image().getPixels()); ++ } ++ verifyNoInteractions(resources); ++ final var replacement = ImageIO.read(new ByteArrayInputStream(jpeg(0x16AB37))); ++ try (final var reloaded = texture.loadContents(resources)) { ++ assertArrayEquals(replacement.getRGB(0, 0, 3, 2, null, 0, 3), reloaded.image().getPixels()); ++ assertFalse(reloaded.blur()); ++ assertFalse(reloaded.clamp()); ++ } ++ verify(resource).open(); ++ } ++ } ++ ++ @Test ++ void oversizedHeadersAreRejectedBeforeAllocatingDecodedPixels() throws Exception { ++ final var reader = mock(ImageReader.class); ++ when(reader.getWidth(0)).thenReturn(4097); ++ when(reader.getHeight(0)).thenReturn(4097); ++ try (final var images = mockStatic(ImageIO.class)) { ++ images.when(() -> ImageIO.getImageReaders(any())).thenAnswer(_ -> List.of(reader).iterator()); ++ assertThrows(IOException.class, () -> NativeUiJpegTexture.decode(new ByteArrayInputStream(new byte[1]))); ++ verify(reader, never()).read(0); ++ verify(reader).dispose(); ++ } ++ } ++ ++ private static byte[] jpeg(final int color) throws IOException { ++ final var image = new BufferedImage(3, 2, BufferedImage.TYPE_INT_RGB); ++ image.setRGB(0, 0, color); ++ image.setRGB(1, 1, 0xE59732); ++ final var bytes = new ByteArrayOutputStream(); ++ assertTrue(ImageIO.write(image, "jpg", bytes)); ++ return bytes.toByteArray(); ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayoutTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiLayoutTest.java new file mode 100644 index 0000000..a754f44 diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index 84263e50..b012d4b4 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -52,3 +52,9 @@ Fractional grid capacity tolerates a one-ULP round-down at an exact fit on eithe The rebuilt add-on opens Enchanted's authored hub with all five trending cards and the three lower game cards. A physical OP SMP selection sends original response index 2 and receives Transfer, matching native. The destination connection times out. Complete gameplay after transfer, final typography, screen overlays, and animation parity remain unverified. + +Local native UI JPEGs use a bounded reloadable texture. The texture manager owns upload, replacement, reload, and retirement. +Tests compare decoded pixels with the previous PNG conversion, check replacement resources after reload, and reject oversized headers before decoding. +A private control checks all 126 matched JPEGs in the captured MegaSMP packs without a decoded pixel difference. +An installed-client GPU control renders the captured bundle banner with identical pixels through the JPEG and previous PNG paths, including after resource reload. +The full four-project build passes. These controls do not establish complete native screen appearance or frame timing. From d928b8c02202d3043c1c4bb185a145fe2d079cd2 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 23:46:29 +0100 Subject: [PATCH 15/33] fix(animations): retain playback after invalid timeline scripts Keep a zero-valued timeline entry for an individual Molang parser syntax error so it cannot discard the entire actor graph. Structural validation and expression limits still reject malformed or oversized declarations. A version-matched native control preserves the captured MegaSMP script and subsequent pose updates. The regression fails before the correction and passes afterward. Full builds, clean patch replay and offline checks pass. The installed complete MegaSMP replay retains both affected graphs and poses; unresolved mappings still fail the broader rendering gate. --- ...re-native-actor-and-resource-runtime.patch | 72 ++++++++++++++++--- ...re-native-actor-and-resource-runtime.pr.md | 12 ++++ ...0029-share-native-ui-scene-rendering.patch | 6 +- 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.patch b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.patch index 948775eb..8d22810d 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.patch @@ -1,4 +1,4 @@ -From 65fd417812f73089edfca69b6be8fb581be983b9 Mon Sep 17 00:00:00 2001 +From cda49860833316900d23783748ee0d3afb25d64f Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(runtime): share native actor animation and resource @@ -419,6 +419,30 @@ of the hit surface. Stone and fence controls share that boundary, and a diagonal pair rejects an axis-based or fixed face-distance shortcut. Gate center filtering on the v2 attack negotiation so older proxy cores retain their existing picker. Keep block occlusion and camera ownership. + +Retain the animation and other timeline entries when an individual Molang +script has a syntax error. Keep a zero-valued entry at its authored time, +so the invalid script neither evaluates nor changes track duration or +ordering. Catch only parser syntax failures. Malformed declarations and +the existing expression, node, time and collection limits still reject. +Do not reinterpret unsupported strings as actor or server commands. + +MegaSMP includes mm:Pet_Step_SFX; in actor walk timelines. Rejecting that +string discarded the complete holy-knight and mecha-oondasta graphs. +A private native Bedrock 1.26.60.5/protocol-2225 local-world control keeps +the captured invalid string between valid assignments and still executes +the following script and a later timeline entry. The trial and otherwise +identical control render their cubes at the same raised position. +The installed executable's loaded PE sections match the pinned Ghidra +reference, including its complete instruction section; signing/header +data differs. The network attempts did not reach gameplay and are not +used as animation evidence. + +A reduced playback regression uses the captured string and retains the +valid pose updates and held final pose. It fails on the complete stack +before the correction with the same parser error as the live actor log. +The native control establishes this captured failure, not every malformed +expression's recovery or complete public-server actor visual parity. --- .gitignore | 4 +- build.gradle.kts | 10 + @@ -430,7 +454,7 @@ retain their existing picker. Keep block occlusion and camera ownership. .../appearance/BedrockAnimationParticle.java | 70 ++ .../appearance/BedrockAnimationSound.java | 67 + .../appearance/BedrockAppearanceStore.java | 2 +- - .../appearance/BedrockBoneAnimation.java | 535 ++++++++ + .../appearance/BedrockBoneAnimation.java | 544 +++++++++ .../appearance/BedrockBonePattern.java | 31 + .../appearance/BedrockBuiltinAssets.java | 314 +++++ .../appearance/BedrockClassicModel.java | 1 + @@ -560,7 +584,7 @@ retain their existing picker. Keep block occlusion and camera ownership. .../BedrockAnimationCommandsTest.java | 180 +++ .../BedrockAnimationParticleTest.java | 104 ++ .../appearance/BedrockAnimationSoundTest.java | 146 +++ - .../appearance/BedrockBoneAnimationTest.java | 450 +++++++ + .../appearance/BedrockBoneAnimationTest.java | 469 +++++++ .../bedrock/appearance/BedrockBrArchive.java | 73 ++ .../BedrockControllerAnimationTest.java | 386 ++++++ .../BedrockControllerSoundTest.java | 150 +++ @@ -630,7 +654,7 @@ retain their existing picker. Keep block occlusion and camera ownership. .../render/BedrockViewBobbingTest.java | 93 ++ .../render/BedrockWalkingAnimationTest.java | 65 + .../render/NativeActorColorPayloadTest.java | 50 + - 210 files changed, 25037 insertions(+), 16 deletions(-) + 210 files changed, 25065 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockActorAnimation.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockActorQueries.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockActorScale.java @@ -2469,10 +2493,10 @@ index 5ed89d0..85d5028 100644 List.of(), List.of(), false, "False", ""); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBoneAnimation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBoneAnimation.java new file mode 100644 -index 0000000..71efbd8 +index 0000000..6a53b73 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBoneAnimation.java -@@ -0,0 +1,535 @@ +@@ -0,0 +1,544 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -2490,6 +2514,7 @@ index 0000000..71efbd8 +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; ++import org.redlance.mocha.parser.ParseException; + +/** Immutable animation tracks with a separate Molang environment for each playback. */ +public final class BedrockBoneAnimation { @@ -2643,7 +2668,15 @@ index 0000000..71efbd8 + throw new IOException("This animation needs named actor event playback"); + if (!script.isJsonPrimitive() || !script.getAsJsonPrimitive().isString()) + throw new IOException("Invalid animation timeline script"); -+ entries.add(new TimelineEntry(time, BedrockMolang.Program.parse(script, 0))); ++ final BedrockMolang.Program program; ++ try { ++ program = BedrockMolang.Program.parse(script, 0); ++ } catch (final ParseException malformed) { ++ // Native keeps the animation and other timeline scripts after a syntax error. ++ entries.add(new TimelineEntry(time, new BedrockMolang.Program(0, null))); ++ return; ++ } ++ entries.add(new TimelineEntry(time, program)); + } + } + @@ -16487,10 +16520,10 @@ index 0000000..3c74e01 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBoneAnimationTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBoneAnimationTest.java new file mode 100644 -index 0000000..6585027 +index 0000000..56b6009 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBoneAnimationTest.java -@@ -0,0 +1,450 @@ +@@ -0,0 +1,469 @@ +package com.viaversion.viafabricplus.bedrock.appearance; + +import static org.junit.jupiter.api.Assertions.*; @@ -16813,12 +16846,29 @@ index 0000000..6585027 + } + + @Test ++ void retainsThePoseAndLaterTimelineScriptsAfterAnInvalidExpression() throws Exception { ++ final var playback = ++ animation( ++ """ ++ {"animation_length":1,"loop":"hold_on_last_frame", ++ "timeline":{"0":["v.height=8;","mm:Pet_Step_SFX;","v.height=32;"], ++ "0.1":"v.height=48;"}, ++ "bones":{"root":{"position":[0,"v.height??0",0]}}} ++ """) ++ .playback(); ++ assertEquals(0, playback.sample(0, Map.of()).get("root").position().y()); ++ assertEquals(32, playback.sample(0.1F, Map.of()).get("root").position().y()); ++ assertEquals(48, playback.sample(0.2F, Map.of()).get("root").position().y()); ++ assertEquals(48, playback.sample(2, Map.of()).get("root").position().y()); ++ } ++ ++ @Test + void rejectsInvalidAndOversizedTimelinesBeforePlayback() { + for (final String timeline : + new String[] { + "[]", "{\"NaN\":\"v.a=1;\"}", "{\"-1\":\"v.a=1;\"}", + "{\"601\":\"v.a=1;\"}", "{\"0\":null}", "{\"0\":[1]}", -+ "{\"0\":{\"event\":\"attack\"}}", "{\"0\":\"v.a=);\"}" ++ "{\"0\":{\"event\":\"attack\"}}" + }) + assertThrows(IOException.class, () -> animation("{\"timeline\":" + timeline + "}")); + final var source = new JsonObject(); @@ -16828,6 +16878,8 @@ index 0000000..6585027 + timeline.add("0", scripts); + source.add("timeline", timeline); + assertThrows(IOException.class, () -> BedrockBoneAnimation.parse(source)); ++ timeline.addProperty("0", "v.a=1;".repeat(600)); ++ assertThrows(IOException.class, () -> BedrockBoneAnimation.parse(source)); + } + + @Test diff --git a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md index 7ad5638f..885d8906 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md @@ -37,4 +37,16 @@ Block controls cover stone, fence, diagonal approach, Survival, and Creative, wi Small differences in facing and eye height remain. These outcomes do not establish equal ray coordinates or complete picking parity. Other actor shapes and multipart target ordering remain open. +Animation timelines retain the remaining animation when an individual Molang string has a syntax error. +The failed script keeps a zero-valued entry at its authored time. Structural validation and resource limits still reject invalid input. +A native Bedrock 1.26.60.5 local-world control places MegaSMP's captured invalid string between valid assignments. +The trial and control cubes reach the same raised position, including the later timeline update. +The installed executable's loaded PE sections match the pinned reference. Its headers and signing data differ. +The playback regression fails before the correction with the live actor's parser error and passes afterward. +The full four-project build and fresh patch replay pass. +An installed complete MegaSMP packet replay preserves all scene payloads, loads the pack, and acknowledges gameplay. +Both `mvmodels:holy_knight` and `mvmodels:mecha_oondasta` retain loaded graphs and non-empty poses. +The overall rendering check still fails on unresolved mappings, including the custom block gaps. +This control does not establish every malformed expression's recovery or complete public-server actor appearance. + The [owning patch](0008-share-native-actor-and-resource-runtime.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index f4bbd55b..803aaa3c 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From 9e2b335ef3106e348f060f23455a8749f1318e7f Mon Sep 17 00:00:00 2001 +From 48912212b3ae8cf4fcdd9908bc098ca659f4dbeb 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(ui): share native scene rendering and GUI actors @@ -934,10 +934,10 @@ index 7fa3316..1017163 100644 // Capture the listener identity at INIT. Fabric Context.player() returns the current client player. ClientPlayConnectionEvents.INIT.register((listener, client) -> diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBoneAnimation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBoneAnimation.java -index 71efbd8..486f389 100644 +index 6a53b73..f82fd5a 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBoneAnimation.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockBoneAnimation.java -@@ -246,6 +246,9 @@ public final class BedrockBoneAnimation { +@@ -255,6 +255,9 @@ public final class BedrockBoneAnimation { this.molang = molang; } From bc62ab825ca64861b0848407e761565e4241c663 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:24:24 +0100 Subject: [PATCH 16/33] fix(ui): avoid per-frame static animation traversal Index controls with authored animation tracks for each resolved subtree. Rebuild the index after root changes or binding reconciliation, while preserving hidden tracks, event handling and independent control clocks. The installed Survival inventory repeatedly walks static control children in its render-thread profile. The new traversal preserves its slot and cursor state, backdrop and scene replay checks. Targeted animation tests and a fresh full-stack apply cover clock scope and patch export. --- ...0029-share-native-ui-scene-rendering.patch | 65 ++++++++++++++++--- ...0029-share-native-ui-scene-rendering.pr.md | 2 + 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index 803aaa3c..cedd707a 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From 48912212b3ae8cf4fcdd9908bc098ca659f4dbeb Mon Sep 17 00:00:00 2001 +From f524a4ca7cc76d9ca5ae529cac7a00151a959f35 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(ui): share native scene rendering and GUI actors @@ -701,6 +701,21 @@ before image decoding. A private corpus probe checks all 126 matched JPEGs from the captured MegaSMP packs, covering 53,564,211 pixels with no decoded pixel differences. These CPU controls do not establish complete native or GPU visual parity; installed pack admission and rendering remain live gates. + +Index each resolved subtree's track-bearing animation managers once rather +than visiting every static control during every lifecycle update. Keep +preorder manager execution, advance hidden tracks and retain the complete +entry map for independent events and opacity sampling. Rebuild the index +when its root changes or binding reconciliation replaces control entries. + +An installed Survival inventory profile spends render-thread time walking +child iterators and looking up animation entries, although only a small +fraction of its controls have tracks. Removing that repeated tree walk +keeps scene output and animation behavior intact. A scoped-clock regression +ensures an update cannot advance another registered subtree. Existing +native alpha, event, replacement and destruction controls remain the +behavioral contract; this does not establish complete UI frame-time parity +on other hardware or resolve the separate menu-overlap and shield reports. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../appearance/BedrockBoneAnimation.java | 3 + @@ -740,7 +755,7 @@ GPU visual parity; installed pack admission and rendering remain live gates. .../screen/ui/NativeUiActorCamera.java | 60 + .../screen/ui/NativeUiActorPortraits.java | 258 +++++ .../screen/ui/NativeUiAnimationLifecycle.java | 131 +++ - .../bedrock/screen/ui/NativeUiAnimations.java | 383 +++++++ + .../bedrock/screen/ui/NativeUiAnimations.java | 398 +++++++ .../bedrock/screen/ui/NativeUiAseprite.java | 54 + .../bedrock/screen/ui/NativeUiButton.java | 167 +++ .../screen/ui/NativeUiCollectionWindow.java | 92 ++ @@ -793,7 +808,7 @@ GPU visual parity; installed pack admission and rendering remain live gates. .../screen/ui/NativeUiActorCameraTest.java | 102 ++ .../screen/ui/NativeUiActorPortraitsTest.java | 164 +++ .../ui/NativeUiAnimationLifecycleTest.java | 155 +++ - .../screen/ui/NativeUiAnimationsTest.java | 286 +++++ + .../screen/ui/NativeUiAnimationsTest.java | 301 +++++ .../screen/ui/NativeUiAsepriteTest.java | 108 ++ .../bedrock/screen/ui/NativeUiButtonTest.java | 127 +++ .../ui/NativeUiCollectionWindowTest.java | 144 +++ @@ -818,7 +833,7 @@ GPU visual parity; installed pack admission and rendering remain live gates. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ - 116 files changed, 13204 insertions(+), 36 deletions(-) + 116 files changed, 13234 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearance.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiActorRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinNativeUiInput.java @@ -3629,10 +3644,10 @@ index 0000000..687730c +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java new file mode 100644 -index 0000000..057c0e9 +index 0000000..6279edb --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java -@@ -0,0 +1,383 @@ +@@ -0,0 +1,398 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3654,6 +3669,8 @@ index 0000000..057c0e9 +public final class NativeUiAnimations { + private final Map entries = new IdentityHashMap<>(); + private final BiPredicate destroy; ++ private NativeUiScene.Node indexedRoot; ++ private List managers = List.of(); + private long layoutRevision; + + public long layoutRevision() { return this.layoutRevision; } @@ -3676,6 +3693,8 @@ index 0000000..057c0e9 + } + this.entries.clear(); + this.entries.putAll(retained); ++ this.indexedRoot = null; ++ this.managers = List.of(); + this.layoutRevision++; + } + @@ -3727,8 +3746,19 @@ index 0000000..057c0e9 + } + + void advance(final NativeUiScene.Node node, final long elapsed) { -+ this.entries.computeIfAbsent(node, value -> new Entry(value, 0)).advance(elapsed); -+ for (final var child : node.children()) advance(child, elapsed); ++ if (this.indexedRoot != node) { ++ final List managers = new ArrayList<>(); ++ this.collectManagers(node, managers); ++ this.managers = List.copyOf(managers); ++ this.indexedRoot = node; ++ } ++ for (final var manager : this.managers) manager.advance(elapsed); ++ } ++ ++ private void collectManagers(final NativeUiScene.Node node, final List managers) { ++ final Entry entry = this.entries.computeIfAbsent(node, value -> new Entry(value, 0)); ++ if (!entry.definitions.isEmpty()) managers.add(entry); ++ for (final var child : node.children()) this.collectManagers(child, managers); + } + + /** Events affect the receiving control and its descendants, not unrelated scrolling panels. */ @@ -10628,10 +10658,10 @@ index 0000000..e407bbc +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationsTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationsTest.java new file mode 100644 -index 0000000..90d8eea +index 0000000..81427b5 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationsTest.java -@@ -0,0 +1,286 @@ +@@ -0,0 +1,301 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -10649,6 +10679,21 @@ index 0000000..90d8eea + +final class NativeUiAnimationsTest { + @Test ++ void advancingOneSubtreeDoesNotAdvanceAnotherRegisteredControl() { ++ final var first = animated(track("alpha", 1, 0, 1)); ++ final var second = animated(track("alpha", 1, 0, 1)); ++ final var animations = new NativeUiAnimations(); ++ animations.opacityAt(second, 0); ++ animations.advance(first, 500_000_000); ++ assertEquals(.5F, animations.opacityAt(first, 500_000_000).current()); ++ assertEquals(.25F, animations.opacityAt(second, 250_000_000).current()); ++ animations.advance(second, 750_000_000); ++ animations.advance(first, 750_000_000); ++ assertEquals(.75F, animations.opacityAt(first, 750_000_000).current()); ++ assertEquals(.75F, animations.opacityAt(second, 750_000_000).current()); ++ } ++ ++ @Test + void entranceFadeRetainsTheAuthoredBackdropOpacityAcrossSceneRefreshes() { + final var fade = track("alpha", 1, 0, 1); + fade.addProperty("scale_from_starting_alpha", true); diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index b012d4b4..2793bf98 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -37,6 +37,8 @@ Native Bedrock 1.26.60.5 captures ImageComponent alpha when creating the track a The default Survival backdrop uses this flag with opacity 0.4. Ignoring it makes the settled backdrop opaque. A regression fails before the correction and covers the fade, potential-opacity bound, retained clock, and absolute-track behavior. Leaf controls skip child-state selection after contributing their own draw state. +Animation updates reuse an index of controls with authored tracks. Root replacement and binding refreshes rebuild that index. +The scoped-clock regression checks that an update leaves unrelated registered controls unchanged. The [owning patch](0029-share-native-ui-scene-rendering.patch) retains implementation rationale and versioned evidence. From 9260d11529945e33270a73708ad2fda603aa4cfa Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:25:29 +0100 Subject: [PATCH 17/33] fix(inventory): hide gameplay HUD behind native containers Official Bedrock 1.26.60.5 keeps held items visible behind the Survival inventory while hiding the gameplay HUD. Skip HUD extraction while the current screen is an authored native container to match that behavior. Keep local HUD settings and server visibility restrictions unchanged so closing the inventory immediately restores normal rendering. First-person item rendering and the separate shield-pose report remain independent. --- ...9-present-authored-native-containers.patch | 61 +++++++++++++++++-- ...9-present-authored-native-containers.pr.md | 3 + ...orted-native-world-actor-definitions.patch | 6 +- ...1-edit-native-command-block-settings.patch | 6 +- ...2-reconcile-native-block-break-audio.patch | 6 +- ...-animate-native-moving-piston-blocks.patch | 6 +- .../0035-forward-bedrock-fluid-pickup.patch | 6 +- ...0038-predict-held-trapdoor-placement.patch | 6 +- ...ply-native-command-stacks-to-dry-air.patch | 6 +- 9 files changed, 79 insertions(+), 27 deletions(-) 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 e40de54b..8ccb0e3a 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 ffa0baba5f2a9936927377fe51ae7b74577120df Mon Sep 17 00:00:00 2001 +From 7205493233fb38b23c228e0d4b96cfe69fa4e22a 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 @@ -583,12 +583,25 @@ session reduces render-thread overlap-search samples. Static recipe, slot-grid and crafting-background pixels remain identical before and after the replacement. Software-renderer timing is diagnostic evidence, not a performance guarantee for player hardware or native pixel parity. + +Suppress gameplay HUD extraction while an authored native container is the +current screen. Official Bedrock 1.26.60.5 (protocol 2225), with a log in +the main hand and shield in the offhand, keeps both held items behind the +Survival inventory but hides the gameplay hotbar, health and hunger. +The installed add-on control incorrectly draws those HUD elements there. + +Gate the HUD call by the current native-container screen instead of +changing the local HUD setting or server visibility state. Closing or +replacing the screen restores normal HUD extraction immediately. Keep +world and first-person item rendering independent of this screen rule. +This does not establish shield-pose or complete inventory pixel parity. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + .../MixinNativeContainerGeometry.java | 46 + .../MixinNativeContainerScreenAdmission.java | 28 + .../features/misc/MixinInventoryScreen.java | 61 - + .../render/MixinNativeContainerHud.java | 28 + .../MixinNativeContainerPresentation.java | 26 + .../render/MixinNativeGuiItemAtlas.java | 28 + .../render/MixinNativeGuiItemExtraction.java | 32 + @@ -638,7 +651,7 @@ not a performance guarantee for player hardware or native pixel parity. .../PlayerInventoryScreenClosePayload.java | 22 + .../PlayerInventoryScreenLifecycle.java | 74 + .../PlayerInventoryScreenRequestPayload.java | 22 + - .../viafabricplus-bedrock.mixins.json | 8 +- + .../viafabricplus-bedrock.mixins.json | 9 +- .../BedrockNativeContainerScreenTest.java | 605 +++++++ .../container/NativeContainerButtonTest.java | 280 ++++ .../NativeContainerGeometryTest.java | 389 +++++ @@ -659,11 +672,12 @@ not a performance guarantee for player hardware or native pixel parity. .../NativePlayerPresentationTest.java | 153 ++ .../container/NativePlayerScreenTest.java | 269 ++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 75 files changed, 10151 insertions(+), 62 deletions(-) + 76 files changed, 10180 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 delete mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/misc/MixinInventoryScreen.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerHud.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerPresentation.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeGuiItemAtlas.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeGuiItemExtraction.java @@ -929,6 +943,40 @@ index 1349cd6..0000000 - } - -} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerHud.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerHud.java +new file mode 100644 +index 0000000..1933bb3 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerHud.java +@@ -0,0 +1,28 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.injection.mixin.features.render; ++ ++import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; ++import com.viaversion.viafabricplus.bedrock.screen.container.BedrockNativeContainerScreen; ++import net.minecraft.client.DeltaTracker; ++import net.minecraft.client.gui.Gui; ++import net.minecraft.client.gui.GuiGraphicsExtractor; ++import net.minecraft.client.gui.Hud; ++import net.minecraft.client.gui.screens.Screen; ++import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.Shadow; ++import org.spongepowered.asm.mixin.injection.At; ++ ++@Mixin(Gui.class) ++public abstract class MixinNativeContainerHud { ++ @Shadow public abstract Screen screen(); ++ ++ @WrapWithCondition(method = "extractRenderState", at = @At(value = "INVOKE", target = ++ "Lnet/minecraft/client/gui/Hud;extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V")) ++ private boolean showGameplayHud(final Hud hud, final GuiGraphicsExtractor graphics, final DeltaTracker delta) { ++ return !(this.screen() instanceof BedrockNativeContainerScreen); ++ } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerPresentation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerPresentation.java new file mode 100644 index 0000000..19e2421 @@ -6219,14 +6267,15 @@ index 0000000..a1d1f14 + public Type type() { return TYPE; } +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index fef5b3f..99d0531 100644 +index fef5b3f..241b180 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -14,6 +14,11 @@ +@@ -14,6 +14,12 @@ "features.render.MixinNativeChunkFluid", "features.render.MixinNativeRegionFluid", "compat.sodium.MixinSodiumNativeFluid", + "features.render.MixinNativeContainerPresentation", ++ "features.render.MixinNativeContainerHud", + "features.render.MixinNativeGuiItemState", + "features.render.MixinNativeGuiItemExtraction", + "features.render.MixinNativeGuiItemAtlas", @@ -6234,7 +6283,7 @@ index fef5b3f..99d0531 100644 "compat.iris.MixinIrisPipelines", "compat.iris.IrisProgramSourceConstructor", "compat.iris.MixinIrisEmissionPrograms", -@@ -89,13 +94,14 @@ +@@ -89,13 +95,14 @@ "features.creative.MixinCreativeModeInventoryScreen", "features.creative.MixinCreativeModeTab_ItemDisplayBuilder", "features.creative.MixinCreativeModeTabs", 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 cc54798e..a72cd398 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 @@ -56,6 +56,9 @@ Cumulative authored layers use separate Java GUI strata to bound overlap-orderin A same-session check preserves static recipe, slot-grid, and crafting-background pixels across this change. The installed Survival screen retains the translucent backdrop after its entrance animation. Live direct checks cover pickup, drag distribution, and return to the hotbar. The client and Bedrock server retain matching item totals. +Authored containers suppress gameplay HUD extraction while they are the current screen. +Official Bedrock 1.26.60.5 keeps the held log and shield visible behind the Survival inventory but hides the gameplay HUD. +An installed control checks open, close, and reopen. The HUD returns on close, the local visibility setting stays unchanged, and every slot remains intact. Broader modpack timing, the reported menu overlap, and shield presentation still need verification. The [owning patch](0029-present-authored-native-containers.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0030-use-supported-native-world-actor-definitions.patch b/patches/viafabricplus-bedrock/upstreamable/0030-use-supported-native-world-actor-definitions.patch index baeebd68..12755fbb 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0030-use-supported-native-world-actor-definitions.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0030-use-supported-native-world-actor-definitions.patch @@ -1,4 +1,4 @@ -From 3d299f46270c3f9ff366fa99051a8df2d67d6ba8 Mon Sep 17 00:00:00 2001 +From f0cd8e09b56162090e8bacad3e6899ae8f70ae27 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(render): use supported native world actor definitions @@ -1031,10 +1031,10 @@ index 0000000..a7d9469 + } +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 99d0531..494f549 100644 +index 241b180..5be1c7d 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -116,6 +116,7 @@ +@@ -117,6 +117,7 @@ "features.render.MixinBedrockActorDynamicData", "features.render.MixinBedrockActorPreparedRenderType", "features.render.MixinBedrockActorSubmission", diff --git a/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch b/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch index c2ac38ab..6b8749fc 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch @@ -1,4 +1,4 @@ -From 1020b9ccba3d9d7a7e5f3ae60c673677e296757f Mon Sep 17 00:00:00 2001 +From 56ae3be1fc46d4fb5cbdb8ae096637b9715f64f0 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(command): edit native command block settings @@ -341,10 +341,10 @@ index d0fac3c..d106c6b 100644 "base.viafabricplus.code": "Code", "base.viafabricplus.copy_link": "Copy link", diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 494f549..0025e56 100644 +index 5be1c7d..8dbe1c4 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -200,6 +200,8 @@ +@@ -201,6 +201,8 @@ "features.movement.MixinLocalPlayer", "features.movement.MixinPlayerCorrectionPacketListener", "features.movement.MixinPlayer", diff --git a/patches/viafabricplus-bedrock/upstreamable/0032-reconcile-native-block-break-audio.patch b/patches/viafabricplus-bedrock/upstreamable/0032-reconcile-native-block-break-audio.patch index d4bd22f9..cf5b62ff 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0032-reconcile-native-block-break-audio.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0032-reconcile-native-block-break-audio.patch @@ -1,4 +1,4 @@ -From 847a192c29f705b0cf075460b435a20f5ee6e3f3 Mon Sep 17 00:00:00 2001 +From ff3d849ff0fdf7ab73c04b3a31d08fbec3d47412 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] fix(sound): reconcile native block break audio @@ -71,10 +71,10 @@ index 0000000..9f24afb + } +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 0025e56..f8cdc86 100644 +index 8dbe1c4..cc85e7a 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -200,6 +200,7 @@ +@@ -201,6 +201,7 @@ "features.movement.MixinLocalPlayer", "features.movement.MixinPlayerCorrectionPacketListener", "features.movement.MixinPlayer", diff --git a/patches/viafabricplus-bedrock/upstreamable/0034-animate-native-moving-piston-blocks.patch b/patches/viafabricplus-bedrock/upstreamable/0034-animate-native-moving-piston-blocks.patch index eeba913c..9d0b8b8b 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0034-animate-native-moving-piston-blocks.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0034-animate-native-moving-piston-blocks.patch @@ -1,4 +1,4 @@ -From ecf72621b68d4c865053eb9968c19049ebc19998 Mon Sep 17 00:00:00 2001 +From 028bdf035da9f5e6732e0233910acc2ee241bc5e 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] fix(render): animate native moving piston blocks @@ -155,10 +155,10 @@ index 0000000..f23da98 + } +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index f8cdc86..558fbb5 100644 +index cc85e7a..b91bdb8 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -203,6 +203,8 @@ +@@ -204,6 +204,8 @@ "features.sound.MixinBedrockMiningAudio", "features.block.MixinBedrockCommandBlockSettings", "features.inventory.MixinBedrockCommandBlockScreen", diff --git a/patches/viafabricplus-bedrock/upstreamable/0035-forward-bedrock-fluid-pickup.patch b/patches/viafabricplus-bedrock/upstreamable/0035-forward-bedrock-fluid-pickup.patch index a2bfbfd6..3bfe88ec 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0035-forward-bedrock-fluid-pickup.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0035-forward-bedrock-fluid-pickup.patch @@ -1,4 +1,4 @@ -From 3e7e7b83a0fe5d48bdbce9ae5b8beabdcaa9bcdd Mon Sep 17 00:00:00 2001 +From fd9df6e89342b2402c470e25145ad459be952555 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] fix(interaction): forward Bedrock fluid pickup @@ -113,10 +113,10 @@ index 0000000..d3c7c2d + } +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 558fbb5..6282247 100644 +index b91bdb8..f7c3bf4 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -96,6 +96,7 @@ +@@ -97,6 +97,7 @@ "features.creative.MixinCreativeModeTabs", "features.inventory.MixinNativeContainerGeometry", "features.inventory.MixinNativeContainerScreenAdmission", diff --git a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch index 64275c2f..e6569202 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch @@ -1,4 +1,4 @@ -From 49aa42d180018d4bbcaab987b8e038cef5300cf0 Mon Sep 17 00:00:00 2001 +From f2a72e8adc2fcc0d6286ac752b54e55acd6348fc Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:36:18 +0100 Subject: [PATCH] feat(interaction): predict held trapdoor placement @@ -608,10 +608,10 @@ index 0000000..7a2e793 + } +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 6282247..5466229 100644 +index f7c3bf4..4bdcb1f 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -91,6 +91,9 @@ +@@ -92,6 +92,9 @@ "features.block.MixinRedstoneWireBlock", "features.block.MixinSeaPickleBlock", "features.block.MixinTrapDoorBlock", diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch b/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch index 13cb3714..75655bd2 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch @@ -1,4 +1,4 @@ -From 4650aa04cd4977cc516287ea49f006e5e59e5663 Mon Sep 17 00:00:00 2001 +From 1d4599b435664116174a6a190c6b8509ae7bddae Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:28:02 +0100 Subject: [PATCH] feat(fog): apply native biome and command settings @@ -591,10 +591,10 @@ index 0000000..afd7b44 + texCoord0 = UV0; +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 5466229..8f44d45 100644 +index 4bdcb1f..ff5a82c 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -139,6 +139,9 @@ +@@ -140,6 +140,9 @@ "features.render.MixinBedrockLightmap", "features.render.MixinBedrockLightmapPipelines", "features.render.MixinBedrockVisionFog", From 50de47af581fac5da55afc8434c783949dd5631d Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:46:35 +0100 Subject: [PATCH 18/33] docs(render): record native shield state controls Record native and installed local-server checks for resting, blocking, release and inventory transitions. Keep the reported extreme pose and broader shield rendering limits open because these controls do not reproduce them. --- .../0008-share-native-actor-and-resource-runtime.pr.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md index 885d8906..c9540b13 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md @@ -37,6 +37,14 @@ Block controls cover stone, fence, diagonal approach, Survival, and Creative, wi Small differences in facing and eye height remain. These outcomes do not establish equal ray coordinates or complete picking parity. Other actor shapes and multipart target ordering remain open. +A native Bedrock 1.26.60.5 control covers a plain offhand shield with an oak log in the main hand. +Crouching raises and tilts the shield. Releasing crouch restores the resting pose. +The installed add-on retains these transitions on local BDS 1.26.60.6 through NetherNet HTTP, using crouch and Use inputs. +Read-only controller snapshots show that blocking clears and the resting transform returns after either input. +Opening and closing the Survival inventory leaves the resting shield transform unchanged in early and later samples. +These controls do not reproduce the reported extreme shield pose. +Movement, hit reactions, patterned shields, and third-party mod combinations remain open. + Animation timelines retain the remaining animation when an individual Molang string has a syntax error. The failed script keeps a zero-valued entry at its authored time. Structural validation and resource limits still reject invalid input. A native Bedrock 1.26.60.5 local-world control places MegaSMP's captured invalid string between valid assignments. From e7eb16dd4ce4769cedeebfcf89438ff9755a28c1 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:08:16 +0100 Subject: [PATCH 19/33] feat(capture): record hotbar context in movement audits Position and item quantity alone cannot distinguish a missing movement impulse from a server hotbar selection change. Record the held item, selected slot and wall-clock timestamp at the existing observation boundary so captured movement can be correlated with input markers and packet ordering. --- docs/server-replay.md | 5 +++++ .../stackanvil/replay/fabric/MovementAudit.java | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/server-replay.md b/docs/server-replay.md index 0d923306..6a25cc14 100644 --- a/docs/server-replay.md +++ b/docs/server-replay.md @@ -172,6 +172,11 @@ Private `camera-audit.jsonl` observations include frame time, world clocks, play Private `held-item-light-audit.jsonl` observations record actual first-person submissions. They include packed light, quad normals, tint layers, material emission, and the selected pipeline. Observations do not change the item or camera. Compare them with the screenshot timestamp; startup light values can change while chunks load. +Private `movement-audit.jsonl` observations record position, velocity, item use, held item, and selected hotbar slot at each observed movement phase. +The `epochMillis` timestamp aligns those observations with external input markers. +Use the item and slot fields to distinguish a changed selection from a missing movement impulse. +Check the packet journal for the actual teleport, correction, and motion commands before judging a combined-mechanics trial. + The native Hive recording sends `SetTime` 12445, then `GameRulesChanged` with `dodaylightcycle=false`. Java previously stored that rule without updating its running clock rate. The clock patch now sends the pause to Java. This fixes the lobby's drift into night. Legacy `SetTime` still advances while daylight is enabled. Use `--client proxy --transport-only` to check the separate Java → ViaProxy route. An accepted converted server pack carries versioned Bedrock rendering metadata. The add-on uses this metadata to activate the native lightmap and disable Java's low-light vignette on the proxy route. Local packs and unsupported metadata cannot activate this context. Resource reload and disconnect clear it. Stale reload completion cannot restore it. diff --git a/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/MovementAudit.java b/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/MovementAudit.java index cf87f3cc..e3f834ca 100644 --- a/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/MovementAudit.java +++ b/src/replay/fabric/net/pistonmaster/stackanvil/replay/fabric/MovementAudit.java @@ -29,6 +29,7 @@ public static synchronized void sample(Object player, String phase) { Runtime.getRuntime().addShutdownHook(new Thread(MovementAudit::close, "movement-audit-close")); } Map row = new LinkedHashMap<>(); + row.put("epochMillis", System.currentTimeMillis()); row.put("phase", phase); row.put("tick", player.getClass().getField("tickCount").getInt(player)); row.put("position", Map.of("x", call(player, "getX"), "y", call(player, "getY"), "z", call(player, "getZ"))); @@ -54,7 +55,10 @@ public static synchronized void sample(Object player, String phase) { Object usedItem = call(player, "getUseItem"); row.put("usedItem", call(call(usedItem, "getItem"), "getDescriptionId")); row.put("usedItemCount", call(usedItem, "getCount")); - row.put("heldItemCount", call(call(player, "getMainHandItem"), "getCount")); + Object heldItem = call(player, "getMainHandItem"); + row.put("heldItem", call(call(heldItem, "getItem"), "getDescriptionId")); + row.put("heldItemCount", call(heldItem, "getCount")); + row.put("selectedHotbarSlot", call(call(player, "getInventory"), "getSelectedSlot")); output.write(JSON.toJson(row)); output.newLine(); if (++samples % 40 == 0) output.flush(); From 8bb9f783f4bcd09d046a46fae05c01515da62b72 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:57:17 +0100 Subject: [PATCH 20/33] fix(interaction): preserve throwable selection and consumption Match the native Survival air-use envelope for pearls and wind charges. Without its declared debit, a delayed BDS hotbar selection can restore the pearl slot after the player selects a wind charge. Advance predicted core quantities after sending the request so silent acceptance cannot leave the next throw using a stale count. Preserve local stack network identities. Fold the shared behavior into the existing deferred air-use patch. Native 1.26.60.5 protocol 2225 captures establish the request fields and equipment ordering. Focused regressions cover slot switching, repeated debits, the final item and send-before-commit ordering. This does not close the separate reported repeated movement rollback. --- ...ronize-glide-rockets-with-auth-input.patch | 306 +++++++++++++----- patches/viabedrock/series.json | 2 +- 2 files changed, 233 insertions(+), 75 deletions(-) diff --git a/patches/viabedrock/deferred/0016-synchronize-glide-rockets-with-auth-input.patch b/patches/viabedrock/deferred/0016-synchronize-glide-rockets-with-auth-input.patch index 940a7f19..fec637fd 100644 --- a/patches/viabedrock/deferred/0016-synchronize-glide-rockets-with-auth-input.patch +++ b/patches/viabedrock/deferred/0016-synchronize-glide-rockets-with-auth-input.patch @@ -1,7 +1,8 @@ -From 4b4a56e87439927d2918e927e4cbc44d0e3d05ce Mon Sep 17 00:00:00 2001 +From 80c9a15b9a85b4359317ad87a210c062a18ef530 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(movement): synchronize glide rockets with auth input +Subject: [PATCH] fix(interaction): preserve native air-use prediction and + timing Apply corrected Bedrock velocity to Java while gliding so movement corrections do not preserve stale local motion. Keep protocol 2193 packed @@ -20,7 +21,7 @@ Allocate legacy IDs separately from the odd item-stack-request sequence. Static analysis of the matching 1.26.51.1 client confirms an independent counter that starts at -4 and decreases by two. -Keep the local inventory and its network IDs unchanged until the server +Keep the flight-rocket inventory and its network IDs unchanged until the server confirms the result. The captured Geyser route answers this legacy use with an authoritative InventorySlot update, without ItemStackResponse. Targeted tests cover input ordering, legacy metadata serialization, @@ -29,16 +30,48 @@ counter isolation, descriptor copies, and the final-rocket debit. Allocate legacy even request IDs from the inventory owner shared with native block placement. Keep stack-request IDs independent and verify interleaved placement/rocket allocation without a second legacy counter. + +Extend the same legacy consumption envelope to pearl and wind-charge air +use. Native Bedrock 1.26.60.5 protocol 2225 captures against local BDS 1.26.60.6 +carry a negative even request ID, inventory slot declaration and one-item +debit for both. Held and action descriptors omit network IDs. Retain the +Unknown trigger, Failure prediction and Off cooldown fields from native. + +The prior unpredicted throwable transaction causes BDS to send a selecting +PlayerHotbar packet for the used slot. A delayed capture shows that command +returning the client to the pearl slot after it selected a wind charge, +preventing the intended second use. Preserve ordinary throwable packet +timing and authoritative server inventory ownership; only flight rockets +wait for their establishing movement frame. Do not predict a Creative +item debit or alter continuous-use consumption. + +A regression checks consecutive native debit requests across a slot +switch, descriptor isolation and an empty final stack. It fails before +the correction with request ID 0 instead of -4. This does not establish +complete native inventory or movement parity or close the separate +reported repeated-rollback issue. + +Commit an immediate throwable debit to predicted core inventory only after +sending its transaction. BDS can accept the declared change without a +slot acknowledgement, so waiting for a response leaves the next use with +a stale quantity. Preserve the original stack network ID on the local +remainder; server inventory updates continue to replace predictions. The +resulting equipment packet follows the transaction, matching native order. + +A repeated-use control with descriptor prediction alone sent 64-to-63 twice +and the server accepted only two of three attempted throws. The packet-to- +inventory regression now checks repeated debits, preserved local identity, +final-stack removal and send-before-commit ordering. --- .../api/model/entity/ClientPlayerEntity.java | 5 + - .../protocol/packet/ClientPlayerPackets.java | 21 ++- - .../protocol/packet/InteractionPackets.java | 93 +++++++---- + .../protocol/packet/ClientPlayerPackets.java | 21 +- + .../protocol/packet/InteractionPackets.java | 115 ++++++--- .../InventoryTransactionRewriter.java | 10 +- - .../InventoryTransactionPacketType.java | 50 +++--- - .../packet/ClientPlayerPacketsTest.java | 90 ++++++++++ - .../packet/InteractionPacketsTest.java | 156 ++++++++++++++++++ - .../InventoryTransactionPacketTypeTest.java | 143 ++++++++++++++++ - 8 files changed, 514 insertions(+), 54 deletions(-) + .../InventoryTransactionPacketType.java | 50 ++-- + .../packet/ClientPlayerPacketsTest.java | 90 +++++++ + .../packet/InteractionPacketsTest.java | 236 ++++++++++++++++++ + .../InventoryTransactionPacketTypeTest.java | 143 +++++++++++ + 8 files changed, 615 insertions(+), 55 deletions(-) create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/types/inventory/InventoryTransactionPacketTypeTest.java diff --git a/src/main/java/net/raphimc/viabedrock/api/model/entity/ClientPlayerEntity.java b/src/main/java/net/raphimc/viabedrock/api/model/entity/ClientPlayerEntity.java @@ -118,10 +151,28 @@ index 531a753c..eed50df5 100644 protocol.registerServerbound(ServerboundPackets26_3.PLAYER_ABILITIES, null, wrapper -> { wrapper.cancel(); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java -index 1a911c7b..a4586779 100644 +index 1a911c7b..a0b0a0ab 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java -@@ -291,7 +291,8 @@ public final class InteractionPackets { +@@ -21,6 +21,7 @@ import com.viaversion.viaversion.api.minecraft.BlockFace; + import com.viaversion.viaversion.api.minecraft.BlockPosition; + import com.viaversion.viaversion.api.connection.UserConnection; + import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; ++import com.viaversion.viaversion.api.type.Type; + import com.viaversion.viaversion.api.type.Types; + import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets26_3; + import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ServerboundPackets26_3; +@@ -66,6 +67,9 @@ import java.util.logging.Level; + + public final class InteractionPackets { + ++ private static final Set CONSUMING_AIR_USE_ITEMS = Set.of( ++ "minecraft:ender_pearl", "minecraft:wind_charge"); ++ + private static final Set CONTINUOUS_USE_ITEMS = Set.of( + "minecraft:bow", "minecraft:crossbow", "minecraft:trident", + "minecraft:potion", "minecraft:milk_bucket", "minecraft:ominous_bottle", +@@ -291,7 +295,8 @@ public final class InteractionPackets { wrapper.cancel(); return; } @@ -131,7 +182,16 @@ index 1a911c7b..a4586779 100644 inventoryContainer.getSelectedHotbarSlot(), usedItem, itemIdentifier, itemIdentifier == null ? Set.of() : BedrockProtocol.MAPPINGS.getBedrockItemTags().getOrDefault(itemIdentifier, Set.of())); -@@ -326,22 +327,20 @@ public final class InteractionPackets { +@@ -302,7 +307,7 @@ public final class InteractionPackets { + } + return; + } +- wrapper.write(inventoryTransactionRewriter.getInventoryTransactionType(), transaction); ++ writeItemUse(wrapper, inventoryTransactionRewriter.getInventoryTransactionType(), transaction, inventoryContainer); + }); + + protocol.registerServerbound(ServerboundPackets26_3.USE_ITEM_ON, null, wrapper -> { +@@ -326,22 +331,20 @@ public final class InteractionPackets { PacketFactory.sendJavaBlockChangedAck(wrapper.user(), sequence); return; } @@ -157,36 +217,67 @@ index 1a911c7b..a4586779 100644 final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); final BedrockItem usedItem = inventoryTracker.getInventoryContainer().getSelectedHotbarItem(); if (usedItem.hasBlockState()) { -@@ -418,10 +417,66 @@ public final class InteractionPackets { +@@ -418,31 +421,23 @@ public final class InteractionPackets { return hunger == null || food.allowsUse(hunger.computeClampedValue(), hunger.computeMaxValue(), player.javaGameMode() == GameMode.CREATIVE); } -+ static BedrockInventoryTransaction beginItemUse(final ClientPlayerEntity player, +- static boolean isContinuousUseItem(final String identifier, final Set tags) { +- return identifier != null && (tags.contains("minecraft:is_food") || CONTINUOUS_USE_ITEMS.contains(identifier)); +- } +- +- static void sendPassengerUpdates(final PacketWrapper source, final EntityTracker entityTracker) { +- for (Entity vehicle : entityTracker.drainPassengerUpdates()) { +- final PacketWrapper passengersPacket = PacketWrapper.create(ClientboundPackets26_3.SET_PASSENGERS, +- source.user()); +- writeJavaPassengers(passengersPacket, entityTracker, vehicle); +- passengersPacket.send(BedrockProtocol.class); +- } +- } +- +- private static void writeJavaPassengers(final PacketWrapper wrapper, +- final EntityTracker entityTracker, final Entity vehicle) { +- final List passengers = entityTracker.getTrackedPassengers(vehicle); +- wrapper.write(Types.VAR_INT, vehicle.javaId()); // vehicle +- wrapper.write(Types.VAR_INT, passengers.size()); // number of passengers +- for (Entity passenger : passengers) { +- wrapper.write(Types.VAR_INT, passenger.javaId()); // passenger id ++ static void writeItemUse(final PacketWrapper wrapper, final Type transactionType, ++ final BedrockInventoryTransaction transaction, final InventoryContainer inventory) { ++ wrapper.write(transactionType, transaction); ++ if (transaction.actions() == null || transaction.actions().isEmpty()) { ++ return; + } ++ // Native commits its hotbar debit after the request; the resulting equipment update follows it. ++ wrapper.sendToServer(BedrockProtocol.class); ++ wrapper.cancel(); ++ final var debit = transaction.actions().getFirst(); ++ final BedrockItem remainder = inventory.getItem(debit.slot()).copy(); ++ remainder.setAmount(debit.toItem().amount()); ++ inventory.setItem(debit.slot(), remainder.isEmpty() ? BedrockItem.empty() : remainder); + } + + static BedrockInventoryTransaction beginItemUse(final ClientPlayerEntity player, +- final InteractionHand hand, final int hotbarSlot, final BedrockItem item, + final InventoryTracker inventory, final InteractionHand hand, final int hotbarSlot, final BedrockItem item, -+ final String identifier, final Set tags) { -+ if (item.isEmpty()) { -+ return null; -+ } -+ if ("minecraft:shield".equals(identifier)) { -+ player.setShieldBlockingHand(hand); -+ return null; -+ } -+ if (hand == InteractionHand.OFF_HAND) { -+ return null; -+ } -+ player.setShieldBlockingHand(null); -+ if (isContinuousUseItem(identifier, tags)) { -+ player.addAuthInputData(PlayerAuthInputData.StartUsingItem); -+ } + final String identifier, final Set tags) { + if (item.isEmpty()) { + return null; +@@ -458,13 +453,67 @@ public final class InteractionPackets { + if (isContinuousUseItem(identifier, tags)) { + player.addAuthInputData(PlayerAuthInputData.StartUsingItem); + } +- return new BedrockInventoryTransaction( +- 0, null, null, ComplexInventoryTransaction_Type.ItemUseTransaction, + final boolean flightRocket = isFlightRocket(identifier, player.isGliding()); ++ final boolean predictsConsumption = flightRocket || (identifier != null && CONSUMING_AIR_USE_ITEMS.contains(identifier)); + int legacyRequestId = 0; + List legacySlots = null; + List actions = null; + final BedrockItem heldItem = item.copy(); -+ if (flightRocket) { ++ if (predictsConsumption) { + heldItem.setNetId(null); + } -+ if (flightRocket && player.javaGameMode() != GameMode.CREATIVE) { ++ if (predictsConsumption && player.javaGameMode() != GameMode.CREATIVE) { + final BedrockItem remaining = heldItem.copy(); + remaining.setAmount(remaining.amount() - 1); + legacyRequestId = inventory.nextLegacyRequestId(); @@ -199,11 +290,14 @@ index 1a911c7b..a4586779 100644 + } + final BedrockInventoryTransaction transaction = new BedrockInventoryTransaction( + legacyRequestId, legacySlots, actions, ComplexInventoryTransaction_Type.ItemUseTransaction, -+ new InventoryTransactionData.UseItemTransactionData( + new InventoryTransactionData.UseItemTransactionData( +- ItemUseActionType.Use, ItemUseTriggerType.Unknown, +- new BlockPosition(0, 0, 0), 255, hotbarSlot, HandSlot.Mainhand, item, + ItemUseActionType.Use, + ItemUseTriggerType.Unknown, + new BlockPosition(0, 0, 0), 255, hotbarSlot, HandSlot.Mainhand, heldItem, -+ player.position(), Position3f.ZERO, 0, + player.position(), Position3f.ZERO, 0, +- ItemUsePredictedResult.Failure, ItemUseClientCooldownState.Off)); + ItemUsePredictedResult.Failure, + ItemUseClientCooldownState.Off)); + if (flightRocket) { @@ -213,50 +307,34 @@ index 1a911c7b..a4586779 100644 + return transaction; + } + - static boolean isContinuousUseItem(final String identifier, final Set tags) { - return identifier != null && (tags.contains("minecraft:is_food") || CONTINUOUS_USE_ITEMS.contains(identifier)); - } - ++ static boolean isContinuousUseItem(final String identifier, final Set tags) { ++ return identifier != null && (tags.contains("minecraft:is_food") || CONTINUOUS_USE_ITEMS.contains(identifier)); ++ } ++ + static boolean isFlightRocket(final String identifier, final boolean gliding) { + return gliding && "minecraft:firework_rocket".equals(identifier); + } + - static void sendPassengerUpdates(final PacketWrapper source, final EntityTracker entityTracker) { - for (Entity vehicle : entityTracker.drainPassengerUpdates()) { - final PacketWrapper passengersPacket = PacketWrapper.create(ClientboundPackets26_3.SET_PASSENGERS, -@@ -441,32 +496,6 @@ public final class InteractionPackets { - } ++ static void sendPassengerUpdates(final PacketWrapper source, final EntityTracker entityTracker) { ++ for (Entity vehicle : entityTracker.drainPassengerUpdates()) { ++ final PacketWrapper passengersPacket = PacketWrapper.create(ClientboundPackets26_3.SET_PASSENGERS, ++ source.user()); ++ writeJavaPassengers(passengersPacket, entityTracker, vehicle); ++ passengersPacket.send(BedrockProtocol.class); ++ } ++ } ++ ++ private static void writeJavaPassengers(final PacketWrapper wrapper, ++ final EntityTracker entityTracker, final Entity vehicle) { ++ final List passengers = entityTracker.getTrackedPassengers(vehicle); ++ wrapper.write(Types.VAR_INT, vehicle.javaId()); // vehicle ++ wrapper.write(Types.VAR_INT, passengers.size()); // number of passengers ++ for (Entity passenger : passengers) { ++ wrapper.write(Types.VAR_INT, passenger.javaId()); // passenger id ++ } } -- static BedrockInventoryTransaction beginItemUse(final ClientPlayerEntity player, -- final InteractionHand hand, final int hotbarSlot, final BedrockItem item, -- final String identifier, final Set tags) { -- if (item.isEmpty()) { -- return null; -- } -- if ("minecraft:shield".equals(identifier)) { -- player.setShieldBlockingHand(hand); -- return null; -- } -- if (hand == InteractionHand.OFF_HAND) { -- return null; -- } -- player.setShieldBlockingHand(null); -- if (isContinuousUseItem(identifier, tags)) { -- player.addAuthInputData(PlayerAuthInputData.StartUsingItem); -- } -- return new BedrockInventoryTransaction( -- 0, null, null, ComplexInventoryTransaction_Type.ItemUseTransaction, -- new InventoryTransactionData.UseItemTransactionData( -- ItemUseActionType.Use, ItemUseTriggerType.Unknown, -- new BlockPosition(0, 0, 0), 255, hotbarSlot, HandSlot.Mainhand, item, -- player.position(), Position3f.ZERO, 0, -- ItemUsePredictedResult.Failure, ItemUseClientCooldownState.Off)); -- } -- record BlockPlacementPrediction(int requestId, List slots, - List actions, BedrockItem remainder) { } - diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/InventoryTransactionRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/InventoryTransactionRewriter.java index 5034ca22..72131417 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/InventoryTransactionRewriter.java @@ -570,14 +648,20 @@ index 28f67a41..cde0169a 100644 + } diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/InteractionPacketsTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/InteractionPacketsTest.java -index 0bf1b705..9613d7ff 100644 +index 0bf1b705..4a770d23 100644 --- a/src/test/java/net/raphimc/viabedrock/protocol/packet/InteractionPacketsTest.java +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/InteractionPacketsTest.java -@@ -9,11 +9,33 @@ +@@ -9,11 +9,39 @@ */ package net.raphimc.viabedrock.protocol.packet; +import com.viaversion.viaversion.api.connection.UserConnection; ++import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; ++import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; ++import net.raphimc.viabedrock.protocol.model.inventory.BedrockInventoryTransaction; ++import net.raphimc.viabedrock.protocol.types.inventory.InventoryTransactionPacketType; ++import java.util.ArrayList; ++import java.util.List; +import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.HandSlot; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemUseTriggerType; @@ -607,7 +691,7 @@ index 0bf1b705..9613d7ff 100644 import static org.junit.jupiter.api.Assertions.assertTrue; class InteractionPacketsTest { -@@ -28,4 +50,138 @@ class InteractionPacketsTest { +@@ -28,4 +56,212 @@ class InteractionPacketsTest { assertFalse(InteractionPackets.isContinuousUseItem(null, Set.of())); } @@ -734,6 +818,80 @@ index 0bf1b705..9613d7ff 100644 + assertEquals(3, item.amount()); + } + ++ @Test ++ void throwableUsePreservesNativeConsumptionWhenSwitchingHotbarSlots() { ++ final ClientPlayerEntity player = player(); ++ final InventoryTracker inventory = new InventoryTracker(user()); ++ final InventoryContainer contents = new InventoryContainer(user()) { ++ @Override ++ protected void onSlotChanged(final int slot, final BedrockItem previous, final BedrockItem current) { ++ // Observe local quantities without the fixture emitting equipment packets. ++ } ++ }; ++ final List amountsAtSend = new ArrayList<>(); ++ final BedrockInventoryTransaction[] written = new BedrockInventoryTransaction[1]; ++ final PacketWrapper packet = (PacketWrapper) Proxy.newProxyInstance(PacketWrapper.class.getClassLoader(), ++ new Class[]{PacketWrapper.class}, (proxy, method, args) -> { ++ if (method.getName().equals("write")) { ++ written[0] = (BedrockInventoryTransaction) args[1]; ++ } ++ if (method.getName().equals("sendToServer")) { ++ final var use = (InventoryTransactionData.UseItemTransactionData) written[0].transactionData(); ++ amountsAtSend.add(contents.getItem(use.hotbarSlot()).amount()); ++ } ++ return null; ++ }); ++ final var type = new InventoryTransactionPacketType(() -> null, null); ++ final BedrockItem pearl = new BedrockItem(425); ++ pearl.setAmount(16); ++ pearl.setNetId(27); ++ contents.setItem(0, pearl); ++ final var first = InteractionPackets.beginItemUse(player, inventory, InteractionHand.MAIN_HAND, ++ 0, pearl, "minecraft:ender_pearl", Set.of()); ++ assertEquals(-4, first.legacyRequestId()); ++ assertEquals(ContainerEnumName.InventoryContainer, first.legacySlots().getFirst().container()); ++ assertArrayEquals(new byte[]{0}, first.legacySlots().getFirst().slots()); ++ assertEquals(0, first.actions().getFirst().slot()); ++ assertEquals(16, first.actions().getFirst().fromItem().amount()); ++ assertEquals(15, first.actions().getFirst().toItem().amount()); ++ assertNull(first.actions().getFirst().fromItem().netId()); ++ assertNull(first.actions().getFirst().toItem().netId()); ++ assertNull(((InventoryTransactionData.UseItemTransactionData) first.transactionData()).itemInHand().netId()); ++ ++ InteractionPackets.writeItemUse(packet, type, first, contents); ++ assertEquals(15, contents.getItem(0).amount()); ++ assertEquals(27, contents.getItem(0).netId()); ++ ++ final BedrockItem windCharge = new BedrockItem(882); ++ windCharge.setNetId(28); ++ contents.setItem(1, windCharge); ++ final var second = InteractionPackets.beginItemUse(player, inventory, InteractionHand.MAIN_HAND, ++ 1, windCharge, "minecraft:wind_charge", Set.of()); ++ assertEquals(-6, second.legacyRequestId()); ++ assertArrayEquals(new byte[]{1}, second.legacySlots().getFirst().slots()); ++ assertEquals(1, second.actions().getFirst().slot()); ++ assertEquals(1, second.actions().getFirst().fromItem().amount()); ++ assertTrue(second.actions().getFirst().toItem().isEmpty()); ++ assertEquals(1, ((InventoryTransactionData.UseItemTransactionData) second.transactionData()).hotbarSlot()); ++ InteractionPackets.writeItemUse(packet, type, second, contents); ++ assertTrue(contents.getItem(1).isEmpty()); ++ final var repeated = InteractionPackets.beginItemUse(player, inventory, InteractionHand.MAIN_HAND, ++ 0, contents.getItem(0), "minecraft:ender_pearl", Set.of()); ++ assertEquals(-8, repeated.legacyRequestId()); ++ assertEquals(15, repeated.actions().getFirst().fromItem().amount()); ++ assertEquals(14, repeated.actions().getFirst().toItem().amount()); ++ InteractionPackets.writeItemUse(packet, type, repeated, contents); ++ assertEquals(14, contents.getItem(0).amount()); ++ assertEquals(27, contents.getItem(0).netId()); ++ assertEquals(List.of(16, 1, 15), amountsAtSend); ++ assertTrue(player.pendingFlightItemUses().isEmpty()); ++ assertTrue(player.authInputData().isEmpty()); ++ assertEquals(16, pearl.amount()); ++ assertEquals(27, pearl.netId()); ++ assertEquals(1, windCharge.amount()); ++ assertEquals(28, windCharge.netId()); ++ } ++ + private static UserConnection user() { + return (UserConnection) Proxy.newProxyInstance( + UserConnection.class.getClassLoader(), new Class[]{UserConnection.class}, diff --git a/patches/viabedrock/series.json b/patches/viabedrock/series.json index 860a3316..88dc6774 100644 --- a/patches/viabedrock/series.json +++ b/patches/viabedrock/series.json @@ -585,7 +585,7 @@ }, { "file": "0016-synchronize-glide-rockets-with-auth-input.patch", - "reason": "Glide rocket item use intersects the server-authoritative inventory translation in [ViaBedrock #276](https://github.com/ViaVersionAddons/ViaBedrock/pull/276)." + "reason": "Air-use consumption prediction and glide rocket timing intersect the server-authoritative inventory translation in [ViaBedrock #276](https://github.com/ViaVersionAddons/ViaBedrock/pull/276)." }, { "file": "0021-confirm-survival-offhand-transfers.patch", From 36a69a1c3949bad4dc6534d9cc32d4a690d89b5c Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 02:21:12 +0100 Subject: [PATCH 21/33] perf(inventory): retain stable native draw lists Reuse painter traversal and input frames while the container layout, pointer, component state and opacity stay unchanged. Installed inventory profiles show repeated property-map lookups during otherwise stable frames. Items, portraits and image animations still render every frame. Expose alpha changes through the shared animation runtime and include toggle changes in input visual revisions. Regressions retain fades, reset events and same-pointer checked-state changes. Fold both changes into their owning patches and preserve their existing dependency order. --- ...9-present-authored-native-containers.patch | 168 ++++++++++++++++-- ...9-present-authored-native-containers.pr.md | 14 +- ...0029-share-native-ui-scene-rendering.patch | 25 ++- ...0029-share-native-ui-scene-rendering.pr.md | 4 + 4 files changed, 188 insertions(+), 23 deletions(-) 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 8ccb0e3a..2fe803a0 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 7205493233fb38b23c228e0d4b96cfe69fa4e22a Mon Sep 17 00:00:00 2001 +From f559594942938ba211a4089ca61d4de13c1ae1e1 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 @@ -595,6 +595,20 @@ changing the local HUD setting or server visibility state. Closing or replacing the screen restores normal HUD extraction immediately. Keep world and first-person item rendering independent of this screen rule. This does not establish shield-pose or complete inventory pixel parity. + +Retain container painter traversal while the layout, pointer, input +visual state and sampled opacity stay unchanged. The installed Survival +inventory profile spends time in repeated property-map lookups during +NativeUiPresentation.collect, even after static animation-manager walks +were removed. Continue rendering items, portraits, image color and texture +animations every frame; only reuse the accepted draw list and input frames. + +Toggle changes advance the input visual revision, including keyboard tab +selection, so unchanged pointer coordinates cannot retain an old checked +branch. Alpha sampling supplies a separate revision for fades and reset +events. Replaced layout roots invalidate geometry and presentation together. +Targeted controls preserve fades and a same-pointer toggle transition. +This does not establish full modpack frame-time or native GPU parity. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -608,7 +622,7 @@ This does not establish shield-pose or complete inventory pixel parity. .../render/MixinNativeGuiItemState.java | 20 + .../render/MixinNativeGuiOversizedItem.java | 31 + .../container/BedrockFurnacePreferences.java | 60 + - .../BedrockNativeContainerScreen.java | 1254 +++++++++++++++ + .../BedrockNativeContainerScreen.java | 1257 +++++++++++++++ .../container/BedrockNativeContainers.java | 278 ++++ .../container/BedrockNativeCooking.java | 145 ++ .../BedrockNativePlayerInventory.java | 155 ++ @@ -616,9 +630,10 @@ This does not establish shield-pose or complete inventory pixel parity. .../NativeContainerActionPayload.java | 30 + .../NativeContainerCurrentPayload.java | 30 + .../container/NativeContainerDataPayload.java | 30 + + .../container/NativeContainerDraws.java | 36 + .../NativeContainerFamiliesPayload.java | 30 + .../container/NativeContainerGeometry.java | 272 ++++ - .../container/NativeContainerInput.java | 506 ++++++ + .../container/NativeContainerInput.java | 509 ++++++ .../container/NativeContainerPayload.java | 22 + .../container/NativeContainerPending.java | 65 + .../NativeContainerPresentation.java | 44 + @@ -654,6 +669,7 @@ This does not establish shield-pose or complete inventory pixel parity. .../viafabricplus-bedrock.mixins.json | 9 +- .../BedrockNativeContainerScreenTest.java | 605 +++++++ .../container/NativeContainerButtonTest.java | 280 ++++ + .../container/NativeContainerDrawsTest.java | 70 + .../NativeContainerGeometryTest.java | 389 +++++ .../container/NativeContainerInputTest.java | 542 +++++++ ...veContainerPresentationControllerTest.java | 91 ++ @@ -672,7 +688,7 @@ This does not establish shield-pose or complete inventory pixel parity. .../NativePlayerPresentationTest.java | 153 ++ .../container/NativePlayerScreenTest.java | 269 ++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 76 files changed, 10180 insertions(+), 62 deletions(-) + 78 files changed, 10292 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 @@ -692,6 +708,7 @@ This does not establish shield-pose or complete inventory pixel parity. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerActionPayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerCurrentPayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerDataPayload.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerDraws.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerFamiliesPayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometry.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerInput.java @@ -729,6 +746,7 @@ This does not establish shield-pose or complete inventory pixel parity. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenRequestPayload.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreenTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerButtonTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerDrawsTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometryTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerInputTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerPresentationControllerTest.java @@ -1212,10 +1230,10 @@ index 0000000..1583ce0 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java new file mode 100644 -index 0000000..c7fbef6 +index 0000000..0ffea5d --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java -@@ -0,0 +1,1254 @@ +@@ -0,0 +1,1257 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1318,6 +1336,7 @@ index 0000000..c7fbef6 + private NativeUiScene.Node windowFocus; + private NativeContainerGeometry geometry; + private List draws = List.of(); ++ private final NativeContainerDraws presentationDraws = new NativeContainerDraws(); + private long previousPresentationRevision = -1; + private Map recipePreviews = Map.of(); + private Map> recipeActions = Map.of(); @@ -1849,10 +1868,12 @@ index 0000000..c7fbef6 + this.layout(now, mouseX, mouseY); + } + if (this.updateWindows()) this.refresh(); -+ this.draws = NativeUiPresentation.draws(this.root, box -> box.visibleBounds().contains(mouseX, mouseY), -+ node -> this.lifecycle.animations().opacityAt(node, now), this.nativeInput::hasButtonState); -+ if (this.authoredController()) this.nativeInput.layout(this.root, this.draws, this.width, this.height); -+ this.updateGeometry(); ++ final var previousDraws = this.draws; ++ this.draws = this.presentationDraws.update(this.root, mouseX, mouseY, this.lifecycle.animations(), this.nativeInput, now); ++ if (this.draws != previousDraws) { ++ if (this.authoredController()) this.nativeInput.layout(this.root, this.draws, this.width, this.height); ++ this.updateGeometry(); ++ } + this.hoveredSlot = this.nativeHoveredSlot(mouseX, mouseY); + if (this.hoveredSlot != null) NativeInventoryItemPresentation.requireTooltip(this.hoveredSlot.getItem()); + this.previews.clear(); @@ -3272,6 +3293,48 @@ index 0000000..7773e13 + return TYPE; + } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerDraws.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerDraws.java +new file mode 100644 +index 0000000..0c36c70 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerDraws.java +@@ -0,0 +1,36 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.screen.container; ++ ++import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiAnimations; ++import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiLayout; ++import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiPresentation; ++import java.util.List; ++ ++/** Retains painter traversal until layout, component state, pointer or opacity changes. */ ++final class NativeContainerDraws { ++ private NativeUiLayout.Box root; ++ private double pointerX, pointerY; ++ private long inputRevision, opacityRevision; ++ private List draws = List.of(); ++ ++ List update(final NativeUiLayout.Box root, final double pointerX, ++ final double pointerY, final NativeUiAnimations animations, final NativeContainerInput input, final long now) { ++ final long inputRevision = input.visualRevision(); ++ if (this.root != root || this.pointerX != pointerX || this.pointerY != pointerY ++ || this.inputRevision != inputRevision || this.opacityRevision != animations.opacityRevision()) { ++ this.draws = NativeUiPresentation.draws(root, box -> box.visibleBounds().contains(pointerX, pointerY), ++ node -> animations.opacityAt(node, now), input::hasButtonState); ++ this.root = root; ++ this.pointerX = pointerX; ++ this.pointerY = pointerY; ++ this.inputRevision = inputRevision; ++ // First-render tracks can start while collecting this frame. ++ this.opacityRevision = animations.opacityRevision(); ++ } ++ return this.draws; ++ } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerFamiliesPayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerFamiliesPayload.java new file mode 100644 index 0000000..3387649 @@ -3588,10 +3651,10 @@ index 0000000..3c762e5 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerInput.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerInput.java new file mode 100644 -index 0000000..12b7fec +index 0000000..ed79a3b --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerInput.java -@@ -0,0 +1,506 @@ +@@ -0,0 +1,509 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3636,6 +3699,7 @@ index 0000000..12b7fec + private float inputX = -1, inputY = -1; + private float viewportWidth, viewportHeight; + private boolean initialized; ++ private long toggleRevision; + + NativeContainerInput(final Consumer revealFocus) { + this.revealFocus = java.util.Objects.requireNonNull(revealFocus); @@ -3789,7 +3853,7 @@ index 0000000..12b7fec + } + + long visualRevision() { -+ long revision = 0; ++ long revision = this.toggleRevision; + for (final var entry : this.entries.values()) if (entry.button != null) revision += entry.button.revision(); + return revision; + } @@ -3879,6 +3943,7 @@ index 0000000..12b7fec + + private void change(final Entry entry, final NativeUiToggle.Change change, final List events) { + if (change == null) return; ++ this.toggleRevision++; + for (final var sibling : this.entries.values()) { + if (sibling.toggle == null) continue; + sibling.toggle.sibling(change); @@ -3974,6 +4039,7 @@ index 0000000..12b7fec + selected = right ? minimum : maximum; + } + if (selected == -1) return List.of(); ++ this.toggleRevision++; + Entry previous = null, target = null; + for (final var entry : tabs) { + final boolean checked = entry.toggleDefinition.index(0) == selected; @@ -7196,6 +7262,82 @@ index 0000000..5a97c2d + return properties; + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerDrawsTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerDrawsTest.java +new file mode 100644 +index 0000000..f0c9c26 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerDrawsTest.java +@@ -0,0 +1,70 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.screen.container; ++ ++import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiAnimationLifecycle; ++import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiLayout; ++import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiScene; ++import java.util.Map; ++import java.util.Set; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++final class NativeContainerDrawsTest { ++ @Test ++ void aStationaryPointerDoesNotFreezeFadesOrEventResets() { ++ final var scene = NativeUiScene.parse(""" ++ {"format":1,"root":{"name":"backdrop","type":"image","alpha":0.4,"size":[100,100], ++ "animation_program":{"roots":[0],"tracks":[ ++ {"anim_type":"alpha","from":0,"to":1,"duration":1,"easing":"linear", ++ "scale_from_starting_alpha":true,"play_event":"fade","reset_event":"reset"}]}}} ++ """, Set.of()).root(); ++ final var root = layout(scene); ++ final var lifecycle = new NativeUiAnimationLifecycle(scene); ++ final var input = new NativeContainerInput(_ -> { }); ++ input.prepare(scene); ++ final var draws = new NativeContainerDraws(); ++ lifecycle.animations().event(scene, "fade", 0); ++ lifecycle.advance(500_000_000); ++ assertEquals(.2F, draws.update(root, 50, 50, lifecycle.animations(), input, 500_000_000).getFirst().alpha(), .000001F); ++ lifecycle.animations().event(scene, "reset", 500_000_000); ++ assertEquals(0, draws.update(root, 50, 50, lifecycle.animations(), input, 500_000_000).getFirst().alpha()); ++ lifecycle.animations().event(scene, "fade", 500_000_000); ++ lifecycle.advance(1_500_000_000); ++ assertEquals(.4F, draws.update(root, 50, 50, lifecycle.animations(), input, 1_500_000_000).getFirst().alpha(), .000001F); ++ lifecycle.advance(2_000_000_000); ++ assertEquals(.4F, draws.update(root, 50, 50, lifecycle.animations(), input, 2_000_000_000).getFirst().alpha(), .000001F); ++ } ++ ++ @Test ++ void togglingWithoutPointerMovementReplacesTheVisibleState() { ++ final var scene = NativeUiScene.parse(""" ++ {"format":1,"root":{"name":"filter","type":"toggle","size":[100,100], ++ "toggle_state":false,"toggle_name":"toggle.enable_filtering", ++ "checked_control":"checked","unchecked_control":"unchecked", ++ "button_mappings":[{"from_button_id":"button.menu_select", ++ "to_button_id":"button.menu_select","mapping_type":"pressed"}], ++ "children":[{"name":"checked","type":"image","size":[100,100]}, ++ {"name":"unchecked","type":"image","size":[100,100]}]}} ++ """, Set.of()).root(); ++ final var root = layout(scene); ++ final var lifecycle = new NativeUiAnimationLifecycle(scene); ++ final var input = new NativeContainerInput(_ -> { }); ++ input.prepare(scene); ++ final var draws = new NativeContainerDraws(); ++ final var initial = draws.update(root, 50, 50, lifecycle.animations(), input, 0); ++ assertSame(scene.children().getLast(), initial.getLast().box().node()); ++ input.layout(root, initial, 100, 100); ++ input.pointer(50, 50, 1, 0, true); ++ assertTrue(input.button("button.menu_select", true, 1, 0, true).events().getFirst().change().checked()); ++ assertSame(scene.children().getFirst(), draws.update(root, 50, 50, lifecycle.animations(), input, 0).getLast().box().node()); ++ } ++ ++ private static NativeUiLayout.Box layout(final NativeUiScene.Node scene) { ++ return new NativeUiLayout(_ -> new NativeUiLayout.Size(0, 0)).layout(scene, 100, 100, Map.of()); ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometryTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometryTest.java new file mode 100644 index 0000000..e120ebc 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 a72cd398..4b746206 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 @@ -14,7 +14,7 @@ Prerequisites: ## Validation and remaining limits -Regression coverage includes `BedrockNativeContainerScreenTest`, `NativeContainerButtonTest`, `NativeContainerGeometryTest`, `NativeContainerInputTest`. +Regression coverage includes `BedrockNativeContainerScreenTest`, `NativeContainerButtonTest`, `NativeContainerGeometryTest`, `NativeContainerInputTest`, and `NativeContainerDrawsTest`. Authored slot labels use [shared native count text](../../viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md). The consumer regression keeps raw binding quantities and genuine quickcraft preview text independent of display capping. @@ -48,14 +48,20 @@ Passive hover notifications do not trigger the fallback for unsupported recipe a Full Creative catalog expansion, initial bundle insertion, bulk recipe actions, and Creative cursor replacement remain unresolved. Complete cooking integration, live transfers, and filled-bundle pixels also remain unresolved. -Stable frames reuse parsed hit regions and input ancestor geometry. +Stable frames reuse the draw list, parsed hit regions, and input ancestor geometry. +Changed pointer coordinates, component state, or animated opacity refresh the draw list. +Toggle changes include keyboard tab selection. +Regressions preserve fades, alpha resets, and a toggle change with a stationary pointer. Relayout, changed painter order, enablement, or a zero-opacity transition rebuilds the affected regions. Collection windows rescan after layout or keyboard focus changes, and rendering uses the shared draw order directly. Input, scrolling, focus, and screen regressions cover these boundaries. A hit-region regression checks hidden, disabled, moved, and removed controls. Cumulative authored layers use separate Java GUI strata to bound overlap-ordering work. -A same-session check preserves static recipe, slot-grid, and crafting-background pixels across this change. +Matched installed scene profiles preserve static recipe, slot-grid, and crafting-background pixels. +The optimized profile no longer samples repeated draw-list traversal. +These profiles do not establish stable FPS or native frame-time parity. The installed Survival screen retains the translucent backdrop after its entrance animation. -Live direct checks cover pickup, drag distribution, and return to the hotbar. The client and Bedrock server retain matching item totals. +Live direct checks cover pickup, drag distribution, return to the hotbar, and reopening. +Bedrock accepts every observed stack request, and the client and server retain matching item totals. Authored containers suppress gameplay HUD extraction while they are the current screen. Official Bedrock 1.26.60.5 keeps the held log and shield visible behind the Survival inventory but hides the gameplay HUD. An installed control checks open, close, and reopen. The HUD returns on close, the local visibility setting stays unchanged, and every slot remains intact. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index cedd707a..6ba39e01 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From f524a4ca7cc76d9ca5ae529cac7a00151a959f35 Mon Sep 17 00:00:00 2001 +From 55346089df868c4570df108b271c0e477e671ca2 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(ui): share native scene rendering and GUI actors @@ -716,6 +716,11 @@ ensures an update cannot advance another registered subtree. Existing native alpha, event, replacement and destruction controls remain the behavioral contract; this does not establish complete UI frame-time parity on other hardware or resolve the separate menu-overlap and shield reports. + +Expose an opacity revision that advances when an alpha sample changes. +Retained scene consumers can reuse painter traversal between visual +changes while preserving fades and event-driven alpha resets. Layout +changes and control replacement retain their separate invalidation. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../appearance/BedrockBoneAnimation.java | 3 + @@ -755,7 +760,7 @@ on other hardware or resolve the separate menu-overlap and shield reports. .../screen/ui/NativeUiActorCamera.java | 60 + .../screen/ui/NativeUiActorPortraits.java | 258 +++++ .../screen/ui/NativeUiAnimationLifecycle.java | 131 +++ - .../bedrock/screen/ui/NativeUiAnimations.java | 398 +++++++ + .../bedrock/screen/ui/NativeUiAnimations.java | 406 +++++++ .../bedrock/screen/ui/NativeUiAseprite.java | 54 + .../bedrock/screen/ui/NativeUiButton.java | 167 +++ .../screen/ui/NativeUiCollectionWindow.java | 92 ++ @@ -833,7 +838,7 @@ on other hardware or resolve the separate menu-overlap and shield reports. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ - 116 files changed, 13234 insertions(+), 36 deletions(-) + 116 files changed, 13242 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearance.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiActorRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinNativeUiInput.java @@ -3644,10 +3649,10 @@ index 0000000..687730c +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java new file mode 100644 -index 0000000..6279edb +index 0000000..0c2c738 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java -@@ -0,0 +1,398 @@ +@@ -0,0 +1,406 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3672,8 +3677,10 @@ index 0000000..6279edb + private NativeUiScene.Node indexedRoot; + private List managers = List.of(); + private long layoutRevision; ++ private long opacityRevision; + + public long layoutRevision() { return this.layoutRevision; } ++ public long opacityRevision() { return this.opacityRevision; } + + void reconcile(final Map replacements, final long now) { + final Map retained = new IdentityHashMap<>(); @@ -3924,7 +3931,13 @@ index 0000000..6279edb + this.u = new TextureCoordinate(0, 1, definition.uvFrom.x + (definition.uvTo.x - definition.uvFrom.x) * factor); + this.v = new TextureCoordinate(0, 1, definition.uvFrom.y + (definition.uvTo.y - definition.uvFrom.y) * factor); + } -+ if (definition.alpha) this.current = (reset ? definition.track.from : definition.track.value(elapsed)) * definition.alphaScale; ++ if (definition.alpha) { ++ final float alpha = (reset ? definition.track.from : definition.track.value(elapsed)) * definition.alphaScale; ++ if (this.current != alpha) { ++ this.current = alpha; ++ NativeUiAnimations.this.opacityRevision++; ++ } ++ } + if (definition.colorFrom != null && this.node.type().equals("image")) + this.color = reset ? definition.colorFrom : definition.colorFrom.interpolate(definition.colorTo, definition.track.factor(elapsed)); + if (definition.from == null) return; diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index 2793bf98..8bf75df4 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -16,6 +16,10 @@ Prerequisites: ## Validation and remaining limits +Opacity revisions let container screens reuse their draw lists between visual changes. +Alpha samples and reset events invalidate retained opacity. +Layout changes retain a separate revision. + Regression coverage includes `BedrockBitmapFontResourcesTest`, `BedrockGuiActorAppearanceTest`, `BedrockGuiActorContextTest`, `BedrockResourceLibraryTest`. At 1280 × 720, all 16 current-font labels match native 26.60 at UI scales 1 and 2. All 16 legacy labels match at UI scale 2. Direct and ViaProxy controls pass Java GUI scale changes and resource reloads. The point-sampling GPU regression fails before the correction and passes on Intel and software rendering. From eb400a7ad0ce80acbbcd09b7334d2ca4be449339 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 03:11:47 +0100 Subject: [PATCH 22/33] feat(inventory): allow Java inventory screens Add a persistent presentation setting for clients affected by native menu rendering or mod compatibility problems. Keep Bedrock layouts enabled by default because server packs can depend on their controls. Gate storage, cooking and player admission before scene construction and recheck delayed presentation. Preserve core inventory authority, server forms and the existing ordinary input path. Document the setting and its custom-layout limitation. Validate the full add-on stack and dependencies, the disabled production presentation boundary, and a live BDS session covering all four screen families, pickup, return and reopen with accepted inventory requests. --- docs/native-player-inventory.md | 11 ++ ...9-present-authored-native-containers.patch | 101 +++++++++++++++--- ...9-present-authored-native-containers.pr.md | 10 ++ ...1-edit-native-command-block-settings.patch | 4 +- 4 files changed, 107 insertions(+), 19 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index fc9952e9..296ed767 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -3,6 +3,17 @@ Use this reference when changing the E-key inventory, creative catalogs, or authored player-screen integration. The [compatibility status](bedrock-coverage.md) lists unresolved reports. +## Use Java inventory screens + +In the ViaFabricPlus Bedrock settings, turn off **Bedrock inventory screens**. +Then reopen the inventory or container. +This setting covers player inventory, crafting, cooking, and storage screens on direct and ViaProxy connections. +It leaves server forms, HUD rendering, and inventory protocol handling unchanged. + +Bedrock inventory screens are enabled by default. +Server resource packs can depend on their layouts and controls. +Java screens do not reproduce those custom layouts. + ## Screen lifetime Open the Bedrock inventory only after Java installs the actual screen. 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 2fe803a0..8da25491 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 f559594942938ba211a4089ca61d4de13c1ae1e1 Mon Sep 17 00:00:00 2001 +From 004276ee1b70ed7773feab999fb4685e5768e1d7 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 @@ -609,6 +609,12 @@ branch. Alpha sampling supplies a separate revision for fades and reset events. Replaced layout roots invalidate geometry and presentation together. Targeted controls preserve fades and a same-pointer toggle transition. This does not establish full modpack frame-time or native GPU parity. + +Offer a persistent Bedrock inventory screens setting for clients that need +ordinary Java controls. Gate storage, cooking and player presentation before +scene construction and recheck delayed installation. Keep the default +enabled for server resource packs that depend on authored layouts. This +changes presentation only; core retains inventory and request authority. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -623,9 +629,9 @@ This does not establish full modpack frame-time or native GPU parity. .../render/MixinNativeGuiOversizedItem.java | 31 + .../container/BedrockFurnacePreferences.java | 60 + .../BedrockNativeContainerScreen.java | 1257 +++++++++++++++ - .../container/BedrockNativeContainers.java | 278 ++++ - .../container/BedrockNativeCooking.java | 145 ++ - .../BedrockNativePlayerInventory.java | 155 ++ + .../container/BedrockNativeContainers.java | 282 ++++ + .../container/BedrockNativeCooking.java | 147 ++ + .../BedrockNativePlayerInventory.java | 157 ++ .../container/BedrockPlayerInventory.java | 92 ++ .../NativeContainerActionPayload.java | 30 + .../NativeContainerCurrentPayload.java | 30 + @@ -666,6 +672,8 @@ This does not establish full modpack frame-time or native GPU parity. .../PlayerInventoryScreenClosePayload.java | 22 + .../PlayerInventoryScreenLifecycle.java | 74 + .../PlayerInventoryScreenRequestPayload.java | 22 + + .../settings/impl/BedrockSettings.java | 4 + + .../viafabricplus-bedrock/lang/en_us.json | 1 + .../viafabricplus-bedrock.mixins.json | 9 +- .../BedrockNativeContainerScreenTest.java | 605 +++++++ .../container/NativeContainerButtonTest.java | 280 ++++ @@ -678,7 +686,7 @@ This does not establish full modpack frame-time or native GPU parity. .../container/NativeCookingActionsTest.java | 346 ++++ ...tiveCookingPresentationControllerTest.java | 216 +++ .../NativeCookingPresentationTest.java | 144 ++ - .../container/NativeCookingScreenTest.java | 1394 +++++++++++++++++ + .../container/NativeCookingScreenTest.java | 1404 +++++++++++++++++ .../NativeFurnacePreferenceStateTest.java | 45 + .../container/NativeInventoryFlightsTest.java | 70 + .../NativeInventoryItemPresentationTest.java | 126 ++ @@ -688,7 +696,7 @@ This does not establish full modpack frame-time or native GPU parity. .../NativePlayerPresentationTest.java | 153 ++ .../container/NativePlayerScreenTest.java | 269 ++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 78 files changed, 10292 insertions(+), 62 deletions(-) + 80 files changed, 10315 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 @@ -2493,10 +2501,10 @@ index 0000000..0ffea5d +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainers.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainers.java new file mode 100644 -index 0000000..01c6d02 +index 0000000..2118e9f --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainers.java -@@ -0,0 +1,278 @@ +@@ -0,0 +1,282 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -2541,6 +2549,10 @@ index 0000000..01c6d02 + + private BedrockNativeContainers() {} + ++ static boolean enabled() { ++ return ViaFabricPlusBedrock.impl().settings().nativeInventoryScreens().isActive(); ++ } ++ + public static void register() { + BedrockNativeCooking.register(); + BedrockPlayerInventory.register(); @@ -2616,7 +2628,7 @@ index 0000000..01c6d02 + final Minecraft client = Minecraft.getInstance(); + client.execute( + () -> { -+ if (!connected(owner) || client.player == null) return; ++ if (!enabled() || !connected(owner) || client.player == null) return; + final var menu = client.player.containerMenu; + if (!(client.gui.screen() instanceof AbstractContainerScreen ordinary) + || ordinary.getMenu() != menu @@ -2686,7 +2698,7 @@ index 0000000..01c6d02 + + private static void present(final Presentation presentation) { + final Minecraft client = Minecraft.getInstance(); -+ if (!current(presentation.owner, presentation.menu) ++ if (!enabled() || !current(presentation.owner, presentation.menu) + || !(client.gui.screen() instanceof AbstractContainerScreen ordinary) + || ordinary.getMenu() != presentation.menu) return; + final var screen = @@ -2777,10 +2789,10 @@ index 0000000..01c6d02 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeCooking.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeCooking.java new file mode 100644 -index 0000000..70d29bd +index 0000000..3e78d4d --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeCooking.java -@@ -0,0 +1,145 @@ +@@ -0,0 +1,147 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -2879,6 +2891,7 @@ index 0000000..70d29bd + } + + private static void present(final Minecraft client) { ++ if (!BedrockNativeContainers.enabled()) return; + final var owner = client.getConnection(); + if (!connected(owner) || client.player == null || !ClientPlayNetworking.canSend(NativeCookingActionPayload.TYPE)) return; + final var menu = client.player.containerMenu; @@ -2900,6 +2913,7 @@ index 0000000..70d29bd + } + + private static void accept(final ClientPacketListener owner, final NativeCookingMessage message) { ++ if (!BedrockNativeContainers.enabled()) return; + final var client = Minecraft.getInstance(); + if (!connected(owner) || client.player == null) return; + final var menu = client.player.containerMenu; @@ -2928,10 +2942,10 @@ index 0000000..70d29bd +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativePlayerInventory.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativePlayerInventory.java new file mode 100644 -index 0000000..547d4d0 +index 0000000..251a3d1 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativePlayerInventory.java -@@ -0,0 +1,155 @@ +@@ -0,0 +1,157 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3006,6 +3020,7 @@ index 0000000..547d4d0 + } + + private static void accept(final ClientPacketListener listener, final NativePlayerInventoryMessage message) { ++ if (!BedrockNativeContainers.enabled()) return; + final var client = Minecraft.getInstance(); + if (owner != listener || client.getConnection() != listener || client.player == null + || !(client.gui.screen() instanceof AbstractContainerScreen displayed)) return; @@ -3033,6 +3048,7 @@ index 0000000..547d4d0 + } + + private static void tick(final Minecraft client) { ++ if (!BedrockNativeContainers.enabled()) return; + final var active = presentation; + if (active == null) return; + if (!current(owner, menu) || active.expired()) { @@ -6332,6 +6348,47 @@ index 0000000..a1d1f14 + @Override + public Type type() { return TYPE; } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/settings/impl/BedrockSettings.java b/src/main/java/com/viaversion/viafabricplus/bedrock/settings/impl/BedrockSettings.java +index de0e5b6..aaaaeb5 100644 +--- a/src/main/java/com/viaversion/viafabricplus/bedrock/settings/impl/BedrockSettings.java ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/settings/impl/BedrockSettings.java +@@ -50,6 +50,7 @@ public final class BedrockSettings { + private final BooleanSetting alwaysUseJavaHand; + private final BooleanSetting allowCameraShake; + private final EnumSetting nativeUiScale; ++ private final BooleanSetting nativeInventoryScreens; + private final ParticleViewDistanceSetting particleViewDistance; + private final BooleanSetting captions; + private final BooleanSetting hideOwnCaptions; +@@ -68,6 +69,7 @@ public final class BedrockSettings { + this.alwaysUseJavaHand = group.registerBoolean("always_use_java_hand", false); + this.allowCameraShake = group.registerBoolean("allow_camera_shake", true); + this.nativeUiScale = group.registerEnum("native_ui_scale", NativeUiScale.DEFAULT); ++ this.nativeInventoryScreens = group.registerBoolean("native_inventory_screens", true); + this.particleViewDistance = new ParticleViewDistanceSetting(); + group.register("particle_view_distance", this.particleViewDistance); + this.captions = group.registerBoolean("captions", false); +@@ -98,6 +100,8 @@ public final class BedrockSettings { + + public EnumSetting nativeUiScale() { return this.nativeUiScale; } + ++ public BooleanSetting nativeInventoryScreens() { return this.nativeInventoryScreens; } ++ + public ParticleViewDistanceSetting particleViewDistance() { return this.particleViewDistance; } + + public BooleanSetting captions() { return this.captions; } +diff --git a/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json b/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json +index d0fac3c..da0aa13 100644 +--- a/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json ++++ b/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json +@@ -389,6 +389,7 @@ + "bedrock_marketplace.viafabricplus.wallet_refresh": "Refresh balance", + "bedrock_marketplace.viafabricplus.wallet_sign_in_short": "Sign in", + "bedrock_settings.viafabricplus.native_ui_scale": "Bedrock UI scale offset", ++ "bedrock_settings.viafabricplus.native_inventory_screens": "Bedrock inventory screens", + "bedrock_settings.viafabricplus.native_ui_scale.default": "Default (0)", + "bedrock_settings.viafabricplus.allow_camera_shake": "Allow camera shake", + "bedrock_settings.viafabricplus.particle_view_distance": "Particle render distance", diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json index fef5b3f..241b180 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json @@ -9207,10 +9264,10 @@ index 0000000..0d86d83 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeCookingScreenTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeCookingScreenTest.java new file mode 100644 -index 0000000..52c9e95 +index 0000000..a0c53d7 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeCookingScreenTest.java -@@ -0,0 +1,1394 @@ +@@ -0,0 +1,1404 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -9226,6 +9283,7 @@ index 0000000..52c9e95 +import static org.mockito.Mockito.*; + +import com.viaversion.viafabricplus.bedrock.ViaFabricPlusBedrock; ++import com.viaversion.viafabricplus.api.settings.base.BooleanSetting; +import com.viaversion.viafabricplus.bedrock.screen.container.NativeCookingPreferenceControl.Kind; +import com.viaversion.viaversion.libs.gson.JsonObject; +import com.viaversion.viaversion.libs.gson.JsonParser; @@ -10033,7 +10091,7 @@ index 0000000..52c9e95 + } + + @Test -+ void productionPresentationInstallsOnlyTheOwnedScreenAndRestoresItWhenCapabilityDisappears() ++ void productionPresentationRespectsTheScreenSettingAndRestoresOrdinaryControlsWhenCapabilityDisappears() + throws Exception { + try (final var f = new Fixture(); + final var viewport = @@ -10054,6 +10112,12 @@ index 0000000..52c9e95 + final var present = + BedrockNativeCooking.class.getDeclaredMethod("present", Minecraft.class); + present.setAccessible(true); ++ when(f.nativeInventoryScreens.isActive()).thenReturn(false); ++ present.invoke(null, f.game); ++ assertSame(f.ordinary, f.displayed.get()); ++ assertTrue(f.sent.isEmpty()); ++ verify(f.player, never()).closeContainer(); ++ when(f.nativeInventoryScreens.isActive()).thenReturn(true); + present.invoke(null, f.game); + final var installed = + assertInstanceOf(BedrockNativeContainerScreen.class, f.displayed.get()); @@ -10094,6 +10158,7 @@ index 0000000..52c9e95 + final MockedStatic networking; + final MockedStatic entrypoint; + final Minecraft game = mock(Minecraft.class); ++ final BooleanSetting nativeInventoryScreens = mock(BooleanSetting.class); + final MouseHandler mouse = mock(MouseHandler.class); + final Window window = mock(Window.class); + final LocalPlayer player = player(); @@ -10181,6 +10246,8 @@ index 0000000..52c9e95 + final com.viaversion.viafabricplus.api.settings.base.EnumSetting uiScale = + mock(com.viaversion.viafabricplus.api.settings.base.EnumSetting.class); + when(implementation.settings()).thenReturn(settings); ++ when(settings.nativeInventoryScreens()).thenReturn(this.nativeInventoryScreens); ++ when(this.nativeInventoryScreens.isActive()).thenReturn(true); + when(settings.nativeUiScale()).thenReturn(uiScale); + when(uiScale.value()).thenReturn(com.viaversion.viafabricplus.bedrock.settings.base.NativeUiScale.DEFAULT); + when(implementation.logger()) 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 4b746206..9db1097f 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 @@ -8,6 +8,11 @@ Bind authored container geometry to authoritative Java menus, slots and cursor s Bind authored geometry and controls to the actual menu, cursor, slots, connection, and opening token. Keep request ownership in core and use ordinary screens when required components are unavailable. +The **Bedrock inventory screens** setting retains ordinary Java screens when disabled. +It gates storage, cooking, and player presentations before scene construction, including presentations that wait for container contents. +The default remains enabled for server packs that depend on authored layouts. +The setting does not disable forms or change inventory request ownership. + Prerequisites: - viafabricplus-bedrock/0029-share-native-ui-scene-rendering.patch: Land shared scene and GUI actor rendering before container presentation. @@ -16,6 +21,11 @@ Prerequisites: Regression coverage includes `BedrockNativeContainerScreenTest`, `NativeContainerButtonTest`, `NativeContainerGeometryTest`, `NativeContainerInputTest`, and `NativeContainerDrawsTest`. +An installed BDS control with Bedrock inventory screens disabled retains Java inventory, chest, furnace, and crafting screens. +Pickup, return, and reopening preserve all 64 logs. +BDS accepts both observed inventory requests without a synchronization timeout. +The production cooking test also retains ordinary controls when a ready presentation encounters the disabled setting. + Authored slot labels use [shared native count text](../../viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md). The consumer regression keeps raw binding quantities and genuine quickcraft preview text independent of display capping. diff --git a/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch b/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch index 6b8749fc..fb38767b 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch @@ -1,4 +1,4 @@ -From 56ae3be1fc46d4fb5cbdb8ae096637b9715f64f0 Mon Sep 17 00:00:00 2001 +From 738a9f38c7104c9632058f57352be49cb473a564 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(command): edit native command block settings @@ -330,7 +330,7 @@ index 0000000..98706a7 + @Override public Type type() { return TYPE; } +} diff --git a/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json b/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json -index d0fac3c..d106c6b 100644 +index da0aa13..f345d7c 100644 --- a/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json +++ b/src/main/resources/assets/viafabricplus-bedrock/lang/en_us.json @@ -1,4 +1,6 @@ From f4fa5080ae379e19c3a833f3d0ca26e49d402131 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 04:34:29 +0100 Subject: [PATCH 23/33] fix(inventory): finish authored screen exits Run authored pop exits before removing an inventory screen. Retain every participating completion track through binding refresh so a short size animation cannot truncate the longer fade. Show the gameplay HUD during the exit and consume input until the still-owned screen closes. Bedrock 1.26.60.5 Escape frames provide the slide and fade reference. Regression tests cover different durations, repeated close requests, refresh, successor chains, input retirement and replacement ownership. The full dependency/add-on build and installed inventory reopen checks pass without changing the item total. Generic storage still resolves content without its full host shell, and native closing-on-hurt policy remains unfinished. Keep the cooking guard and document those limits instead of claiming complete screen parity. --- docs/native-player-inventory.md | 10 + ...9-present-authored-native-containers.patch | 165 ++++++++++++-- ...9-present-authored-native-containers.pr.md | 9 +- ...0029-share-native-ui-scene-rendering.patch | 205 ++++++++++++++++-- ...0029-share-native-ui-scene-rendering.pr.md | 7 + 5 files changed, 363 insertions(+), 33 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 296ed767..4bc28797 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -28,6 +28,13 @@ Bind clicks, data, and closes to the connection, menu object, menu ID, and openi A stale reply must not operate on a replacement screen. Keep global player-inventory reconciliation independent of a retired temporary menu. +Escape starts the authored pop exit before the screen closes. +The gameplay HUD appears behind the outgoing scene. +The host consumes input and waits for all participating `screen.exit_end` tracks. +A repeated close request does not restart the exit. +Completion can close only the same screen and menu. +A scene without a completion path closes immediately. + The installed opening also owns a read-only recipe context while the native acknowledgment is pending. It reads the real cached crafting grid, hotbar, inventory, and cursor. The acknowledgment retains this model and its captured search policy. @@ -129,3 +136,6 @@ An unpositioned player inventory uses the fallback crafting context. Native model-position initialization and arbitrary custom component admission remain unverified. The original inventory-background, rollback, food-use, and locator-bar reports remain unresolved. Native registry probes and local model tests do not establish complete live inventory parity. +Generic storage scenes currently expand selected content without the complete host shell. +Their default host animations remain absent from the resolved scene. +The native cause filter and pending-state counter for closing on hurt still need complete runtime controls. 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 8da25491..53fe4851 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 004276ee1b70ed7773feab999fb4685e5768e1d7 Mon Sep 17 00:00:00 2001 +From d4b47610b67aefa54a002c02d4de4130fc9dcdcf 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 @@ -615,6 +615,25 @@ ordinary Java controls. Gate storage, cooking and player presentation before scene construction and recheck delayed installation. Keep the default enabled for server resource packs that depend on authored layouts. This changes presentation only; core retains inventory and request authority. + +Run authored screen.exit_pop tracks before removing a container, then +close only on screen.exit_end while the same screen and menu still own +the client. Repeated close requests do not restart the animation. Scenes +without an authored completion path keep immediate closing. + +Consume pointer, keyboard, drag and slot mutations during the outgoing +scene. Stop a held recipe request and deactivate native actions when the +exit starts, while retaining presentation resources until removal. Show +the gameplay HUD behind that scene, as in the target Bedrock 1.26.60.5 +Escape controls. The default authored container tracks provide the exit +duration; no host timeout substitutes for their completion event. + +Tests cover deferred closure, input retirement, one-time completion, +replacement screens and retired menu ownership. The separately observed +native hurt policy uses a cause filter and a queued 100-count gate. Keep +the cooking host-policy guard until that complete policy and required +bindings are implemented and compared; do not replace it with raw health +sampling or unconditional hurt closing. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -628,7 +647,7 @@ changes presentation only; core retains inventory and request authority. .../render/MixinNativeGuiItemState.java | 20 + .../render/MixinNativeGuiOversizedItem.java | 31 + .../container/BedrockFurnacePreferences.java | 60 + - .../BedrockNativeContainerScreen.java | 1257 +++++++++++++++ + .../BedrockNativeContainerScreen.java | 1302 +++++++++++++++ .../container/BedrockNativeContainers.java | 282 ++++ .../container/BedrockNativeCooking.java | 147 ++ .../BedrockNativePlayerInventory.java | 157 ++ @@ -675,7 +694,7 @@ changes presentation only; core retains inventory and request authority. .../settings/impl/BedrockSettings.java | 4 + .../viafabricplus-bedrock/lang/en_us.json | 1 + .../viafabricplus-bedrock.mixins.json | 9 +- - .../BedrockNativeContainerScreenTest.java | 605 +++++++ + .../BedrockNativeContainerScreenTest.java | 676 ++++++++ .../container/NativeContainerButtonTest.java | 280 ++++ .../container/NativeContainerDrawsTest.java | 70 + .../NativeContainerGeometryTest.java | 389 +++++ @@ -696,7 +715,7 @@ changes presentation only; core retains inventory and request authority. .../NativePlayerPresentationTest.java | 153 ++ .../container/NativePlayerScreenTest.java | 269 ++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 80 files changed, 10315 insertions(+), 62 deletions(-) + 80 files changed, 10431 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 @@ -971,7 +990,7 @@ index 1349cd6..0000000 -} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerHud.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerHud.java new file mode 100644 -index 0000000..1933bb3 +index 0000000..127d925 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerHud.java @@ -0,0 +1,28 @@ @@ -1000,7 +1019,7 @@ index 0000000..1933bb3 + @WrapWithCondition(method = "extractRenderState", at = @At(value = "INVOKE", target = + "Lnet/minecraft/client/gui/Hud;extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V")) + private boolean showGameplayHud(final Hud hud, final GuiGraphicsExtractor graphics, final DeltaTracker delta) { -+ return !(this.screen() instanceof BedrockNativeContainerScreen); ++ return !(this.screen() instanceof BedrockNativeContainerScreen container) || container.closing(); + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerPresentation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinNativeContainerPresentation.java @@ -1238,10 +1257,10 @@ index 0000000..1583ce0 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java new file mode 100644 -index 0000000..0ffea5d +index 0000000..30893c2 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java -@@ -0,0 +1,1257 @@ +@@ -0,0 +1,1302 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1337,6 +1356,7 @@ index 0000000..0ffea5d + private NativeUiScene.Node scene; + private NativeUiAnimationLifecycle lifecycle; + private long animationStarted; ++ private boolean exitFinished; + private long layoutRevision; + private long nativeInputRevision; + private NativeUiLayout.Box root; @@ -1601,12 +1621,34 @@ index 0000000..0ffea5d + return mapping == null ? null : mapping.getTranslatedKeyMessage().getString(); + } + ++ public boolean closing() { return this.lifecycle != null && this.lifecycle.exiting(); } ++ + @Override + public void onClose() { -+ if (this.current() && this.presentation instanceof NativePlayerPresentation player && player.closeHelp()) { ++ if (!this.current() || this.closing() || this.exitFinished) return; ++ if (this.presentation instanceof NativePlayerPresentation player && player.closeHelp()) { + this.refreshSafely(); + return; + } ++ try { ++ if (this.lifecycle != null && this.lifecycle.requestExit(this.animationTime())) { ++ this.setDragging(false); ++ this.scroll.release(); ++ if (this.search != null) this.search.setFocused(false); ++ final var press = this.recipePress; ++ this.recipePress = null; ++ if (press != null) BedrockNativeCooking.action(this.owner, this.menu, press.recipe, press.mode, 2); ++ this.capturedButtons.clear(); ++ this.nativeButtons.clear(); ++ this.keyboardPressed = false; ++ this.deactivate(); ++ return; ++ } ++ } catch (final RuntimeException error) { ++ this.fallback(error); ++ return; ++ } ++ this.exitFinished = true; + super.onClose(); + } + @@ -1662,6 +1704,7 @@ index 0000000..0ffea5d + + @Override + public boolean charTyped(final CharacterEvent event) { ++ if (this.closing()) return true; + if (this.current() && this.search != null && this.search.isFocused()) return this.search.charTyped(event); + return super.charTyped(event); + } @@ -1823,6 +1866,18 @@ index 0000000..0ffea5d + this.fallback(new IllegalStateException("Native cooking presentation retired")); + return; + } ++ if (this.closing()) { ++ try { ++ this.lifecycle.advance(this.animationTime()); ++ if (!this.exitFinished && this.lifecycle.exitComplete() && this.minecraft.gui.screen() == this) { ++ this.exitFinished = true; ++ super.onClose(); ++ } ++ } catch (final RuntimeException error) { ++ this.fallback(error); ++ } ++ return; ++ } + if (this.activationPending && ++this.activationTicks >= 100) { + this.fallback(new IllegalStateException("Native inventory activation timed out")); + return; @@ -1854,13 +1909,13 @@ index 0000000..0ffea5d + } + + public Slot nativeHoveredSlot(final double x, final double y) { -+ if (!this.current() || this.geometry == null) return null; ++ if (!this.current() || this.closing() || this.geometry == null) return null; + final int index = this.geometry.hovered(x, y); + return index >= 0 && this.menu.slots.get(index).isActive() ? this.menu.slots.get(index) : null; + } + + public boolean nativeIsHovering(final Slot slot, final double x, final double y) { -+ return this.current() && this.geometry != null && this.geometry.contains(slot.index, x, y); ++ return this.current() && !this.closing() && this.geometry != null && this.geometry.contains(slot.index, x, y); + } + + @Override @@ -1875,14 +1930,14 @@ index 0000000..0ffea5d + || this.nativeInput.visualRevision() != this.nativeInputRevision) { + this.layout(now, mouseX, mouseY); + } -+ if (this.updateWindows()) this.refresh(); ++ if (!this.closing() && this.updateWindows()) this.refresh(); + final var previousDraws = this.draws; + this.draws = this.presentationDraws.update(this.root, mouseX, mouseY, this.lifecycle.animations(), this.nativeInput, now); + if (this.draws != previousDraws) { + if (this.authoredController()) this.nativeInput.layout(this.root, this.draws, this.width, this.height); + this.updateGeometry(); + } -+ this.hoveredSlot = this.nativeHoveredSlot(mouseX, mouseY); ++ this.hoveredSlot = this.closing() ? null : this.nativeHoveredSlot(mouseX, mouseY); + if (this.hoveredSlot != null) NativeInventoryItemPresentation.requireTooltip(this.hoveredSlot.getItem()); + this.previews.clear(); + float layer = Float.NaN; @@ -2028,6 +2083,7 @@ index 0000000..0ffea5d + + @Override + protected void slotClicked(final Slot slot, final int index, final int button, final ContainerInput input) { ++ if (this.closing()) return; + if (!this.current()) return; + if (this.actionScope.subset()) return; + if (this.presentation instanceof NativePlayerPresentation && input == ContainerInput.QUICK_MOVE @@ -2056,6 +2112,7 @@ index 0000000..0ffea5d + + @Override + public boolean mouseScrolled(final double x, final double y, final double horizontal, final double vertical) { ++ if (this.closing()) return true; + if (!this.current()) return false; + if (this.waitingForPreference()) return true; + if (!this.scroll.wheel(this.root, x, y, horizontal, vertical)) return super.mouseScrolled(x, y, horizontal, vertical); @@ -2066,6 +2123,7 @@ index 0000000..0ffea5d + + @Override + public boolean mouseClicked(final MouseButtonEvent event, final boolean doubleClick) { ++ if (this.closing()) return true; + if (!this.current()) return false; + this.consumedReleases.remove(event.button()); + if (this.search != null && this.search.visible && this.searchBox != null @@ -2149,6 +2207,7 @@ index 0000000..0ffea5d + + @Override + public void mouseMoved(final double x, final double y) { ++ if (this.closing()) return; + this.pointerX = x; + this.pointerY = y; + if (this.current() && this.authoredController()) { @@ -2234,6 +2293,7 @@ index 0000000..0ffea5d + + @Override + public boolean mouseDragged(final MouseButtonEvent event, final double dx, final double dy) { ++ if (this.closing()) return true; + if (this.authoredController()) this.mouseMoved(event.x(), event.y()); + if (this.capturedButtons.contains(event.button()) || this.nativeButtons.contains(event.button())) return true; + if (!this.current()) return false; @@ -2247,6 +2307,7 @@ index 0000000..0ffea5d + + @Override + public boolean mouseReleased(final MouseButtonEvent event) { ++ if (this.closing()) return true; + if (this.current() && this.authoredController()) { + if (this.helpView) this.inventoryInput.button(source(event.button()), false, 1, this.clock.getAsLong(), false); + final var hover = this.nativeInput.pointer(event.x(), event.y(), 1, this.clock.getAsLong(), this.componentEnabled()); @@ -2277,6 +2338,7 @@ index 0000000..0ffea5d + + @Override + public boolean keyPressed(final KeyEvent event) { ++ if (this.closing()) return true; + if (!this.current()) return false; + if (this.presentation instanceof NativePlayerPresentation && !this.helpView && this.geometry != null + && this.geometry.recipe(this.pointerX, this.pointerY) != null && Minecraft.getInstance().options.keyDrop.matches(event)) { @@ -2427,6 +2489,7 @@ index 0000000..0ffea5d + + @Override + public boolean keyReleased(final KeyEvent event) { ++ if (this.closing()) return true; + final String source = keyboardSource(event.key()); + if (this.current() && this.authoredController() && source != null) { + if (this.helpView) this.inventoryInput.button(source, false, 1, this.clock.getAsLong(), false); @@ -2483,6 +2546,7 @@ index 0000000..0ffea5d + + @Override + public void removed() { ++ this.exitFinished = true; + try { + this.deactivate(); + this.retireCooking(); @@ -6424,10 +6488,10 @@ index fef5b3f..241b180 100644 "features.render.MixinNativeActorPickingEntity", diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreenTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreenTest.java new file mode 100644 -index 0000000..261962c +index 0000000..247ce99 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreenTest.java -@@ -0,0 +1,605 @@ +@@ -0,0 +1,676 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -6440,19 +6504,26 @@ index 0000000..261962c + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; ++import com.mojang.blaze3d.platform.InputConstants; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiLayout; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiPresentation; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiScene; +import java.util.List; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicLong; +import net.minecraft.SharedConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; ++import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.ContainerScreen; +import net.minecraft.client.gui.screens.inventory.DispenserScreen; +import net.minecraft.client.gui.screens.inventory.HopperScreen; +import net.minecraft.client.gui.screens.inventory.ShulkerBoxScreen; ++import net.minecraft.client.input.KeyEvent; ++import net.minecraft.client.input.MouseButtonEvent; ++import net.minecraft.client.input.MouseButtonInfo; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.client.player.LocalPlayer; @@ -6472,6 +6543,63 @@ index 0000000..261962c +import org.junit.jupiter.api.Test; + +final class BedrockNativeContainerScreenTest { ++ private enum ExitOwner { CURRENT, REPLACED_SCREEN, RETIRED_MENU } ++ ++ @Test ++ void authoredPopExitBlocksInputAndClosesOnlyTheStillOwnedScreenOnce() throws Exception { ++ SharedConstants.tryDetectVersion(); ++ Bootstrap.bootStrap(); ++ for (final var state : ExitOwner.values()) { ++ final var player = mock(LocalPlayer.class); ++ final var inventory = new Inventory(player, new EntityEquipment()); ++ final var menu = ChestMenu.threeRows(7, inventory); ++ player.containerMenu = menu; ++ final var game = mock(Minecraft.class); ++ game.player = player; ++ game.gameMode = mock(MultiPlayerGameMode.class); ++ final var gui = mock(Gui.class); ++ final var guiField = Minecraft.class.getField("gui"); ++ guiField.setAccessible(true); ++ guiField.set(game, gui); ++ final var clock = new AtomicLong(); ++ final var current = new AtomicBoolean(true); ++ try (final var singleton = mockStatic(Minecraft.class)) { ++ singleton.when(Minecraft::getInstance).thenReturn(game); ++ final var screen = new BedrockNativeContainerScreen(mock(ClientPacketListener.class), ++ new ContainerScreen(menu, inventory, Component.empty()), inventory, Component.empty(), ++ new NativeContainerPresentation(program(null, true), ContainerUi.Kind.CHEST_3_ROWS, List.of()), ++ false, 1, clock::get, current::get); ++ final var minecraft = Screen.class.getDeclaredField("minecraft"); ++ minecraft.setAccessible(true); ++ minecraft.set(screen, game); ++ when(gui.screen()).thenReturn(screen); ++ screen.prepare(mock(Font.class), 640, 320); ++ clock.set(2_000_000_000L); ++ screen.onClose(); ++ assertTrue(screen.closing()); ++ verify(player, never()).closeContainer(); ++ final var mouse = new MouseButtonEvent(110, 60, new MouseButtonInfo(0, 0)); ++ assertTrue(screen.mouseClicked(mouse, false)); ++ assertTrue(screen.mouseDragged(mouse, 5, 5)); ++ assertTrue(screen.mouseReleased(mouse)); ++ assertTrue(screen.keyPressed(new KeyEvent(InputConstants.KEY_ESCAPE, 0, 0))); ++ screen.slotClicked(menu.slots.getFirst(), 0, 0, ContainerInput.PICKUP); ++ verifyNoInteractions(game.gameMode); ++ clock.set(2_200_000_000L); ++ screen.onClose(); ++ screen.containerTick(); ++ verify(player, never()).closeContainer(); ++ if (state == ExitOwner.REPLACED_SCREEN) when(gui.screen()).thenReturn(mock(Screen.class)); ++ if (state == ExitOwner.RETIRED_MENU) current.set(false); ++ clock.set(2_401_000_000L); ++ screen.containerTick(); ++ screen.containerTick(); ++ verify(player, times(state == ExitOwner.CURRENT ? 1 : 0)).closeContainer(); ++ screen.discard(); ++ } ++ } ++ } ++ + @Test + void inventoryPointerEventsDoNotDispatchTheirCancelBinding() throws Exception { + SharedConstants.tryDetectVersion(); @@ -6955,6 +7083,13 @@ index 0000000..261962c + definitions.add("chest.move", animationDefinition); + final var animations = new com.viaversion.viaversion.libs.gson.JsonArray(); + animations.add("@chest.move"); ++ final var exitDefinition = animationDefinition.deepCopy(); ++ final var exit = exitDefinition.getAsJsonObject("value"); ++ exit.addProperty("duration", .4); ++ exit.addProperty("play_event", "screen.exit_pop"); ++ exit.addProperty("end_event", "screen.exit_end"); ++ definitions.add("chest.exit", exitDefinition); ++ animations.add("@chest.exit"); + root.add("anims", animations); + } + final var cell = 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 9db1097f..5adbf623 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 @@ -74,7 +74,14 @@ Live direct checks cover pickup, drag distribution, return to the hotbar, and re Bedrock accepts every observed stack request, and the client and server retain matching item totals. Authored containers suppress gameplay HUD extraction while they are the current screen. Official Bedrock 1.26.60.5 keeps the held log and shield visible behind the Survival inventory but hides the gameplay HUD. -An installed control checks open, close, and reopen. The HUD returns on close, the local visibility setting stays unchanged, and every slot remains intact. +Escape starts the authored pop exit and shows the gameplay HUD behind the outgoing scene. +The host consumes input until all participating completion tracks finish. +Repeated close requests retain the clock. Completion cannot close a replacement screen or retired menu. +Tests cover those ownership boundaries and input retirement. +Installed player-inventory controls show the slide and fade before removal and retain every slot after reopening. +Native Bedrock 1.26.60.5 Escape frames provide the visual reference; exact frame timing and complete pixels remain unverified. +Generic storage resolves selected content without its complete host shell, so its default exit animations remain absent. +Closing on hurt still needs complete native pending-state controls. The cooking host-policy guard remains. Broader modpack timing, the reported menu overlap, and shield presentation still need verification. The [owning patch](0029-present-authored-native-containers.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index 6ba39e01..16a8db91 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From 55346089df868c4570df108b271c0e477e671ca2 Mon Sep 17 00:00:00 2001 +From 4e6e998fbb1c31e39eac4bdc1df8bb67dca0ae45 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(ui): share native scene rendering and GUI actors @@ -721,6 +721,27 @@ Expose an opacity revision that advances when an alpha sample changes. Retained scene consumers can reuse painter traversal between visual changes while preserving fades and event-driven alpha resets. Layout changes and control replacement retain their separate invalidation. + +Expose completed authored animation events to the scene lifecycle. A pop +exit starts once and waits for its screen.exit_end tracks; a scene without a +reachable completion event retires immediately instead of hanging. Follow +unblocked next-track chains, retain monotonic clocks, and avoid invoking +screen mutations from an animation callback. + +Bedrock 1.26.60.5 authors container pop offsets and alpha tracks that emit +screen.exit_end. Native Escape captures show the scene slide and fade +before removal. The lifecycle tests cover elapsed play time, repeated +close requests, chained completion, and missing or blocked exit tracks. +This establishes the shared host mechanism, not complete native screen +policy or frame timing parity. + +Retain all participating completion tracks by control identity, including +through binding refresh. The target inventory has a shorter size track +alongside its longer alpha and offset tracks. Native Escape frames retain +the fading scene after that short track finishes. Removing the screen on +the first completion truncates that visible exit. Wait for the remaining +participating tracks, and release removed controls during reconciliation. +The regression combines different durations and a mid-exit root refresh. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../appearance/BedrockBoneAnimation.java | 3 + @@ -759,8 +780,8 @@ changes and control replacement retain their separate invalidation. .../bedrock/screen/ui/NativeHudResources.java | 21 + .../screen/ui/NativeUiActorCamera.java | 60 + .../screen/ui/NativeUiActorPortraits.java | 258 +++++ - .../screen/ui/NativeUiAnimationLifecycle.java | 131 +++ - .../bedrock/screen/ui/NativeUiAnimations.java | 406 +++++++ + .../screen/ui/NativeUiAnimationLifecycle.java | 165 +++ + .../bedrock/screen/ui/NativeUiAnimations.java | 455 ++++++++ .../bedrock/screen/ui/NativeUiAseprite.java | 54 + .../bedrock/screen/ui/NativeUiButton.java | 167 +++ .../screen/ui/NativeUiCollectionWindow.java | 92 ++ @@ -812,7 +833,7 @@ changes and control replacement retain their separate invalidation. .../screen/ui/NativeHudOverlayStateTest.java | 153 +++ .../screen/ui/NativeUiActorCameraTest.java | 102 ++ .../screen/ui/NativeUiActorPortraitsTest.java | 164 +++ - .../ui/NativeUiAnimationLifecycleTest.java | 155 +++ + .../ui/NativeUiAnimationLifecycleTest.java | 222 ++++ .../screen/ui/NativeUiAnimationsTest.java | 301 +++++ .../screen/ui/NativeUiAsepriteTest.java | 108 ++ .../bedrock/screen/ui/NativeUiButtonTest.java | 127 +++ @@ -838,7 +859,7 @@ changes and control replacement retain their separate invalidation. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ - 116 files changed, 13242 insertions(+), 36 deletions(-) + 116 files changed, 13392 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearance.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiActorRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinNativeUiInput.java @@ -3512,10 +3533,10 @@ index 0000000..286546d +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycle.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycle.java new file mode 100644 -index 0000000..687730c +index 0000000..2a1c763 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycle.java -@@ -0,0 +1,131 @@ +@@ -0,0 +1,165 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3536,6 +3557,9 @@ index 0000000..687730c +public final class NativeUiAnimationLifecycle { + private NativeUiScene.Node root; + private final NativeUiAnimations animations; ++ private boolean exiting; ++ private boolean exitComplete; ++ private Map> pendingExit = new IdentityHashMap<>(); + private final Map> paths = new IdentityHashMap<>(); + private final Set destroyed = Collections.newSetFromMap(new IdentityHashMap<>()); + @@ -3548,6 +3572,13 @@ index 0000000..687730c + control.properties().addProperty("ignored", true); + this.destroyed.add(control); + return true; ++ }, completion -> { ++ if (!this.exiting || !completion.event().equals("screen.exit_end")) return; ++ final var tracks = this.pendingExit.get(completion.node()); ++ if (tracks == null) return; ++ tracks.remove(completion.track()); ++ if (tracks.isEmpty()) this.pendingExit.remove(completion.node()); ++ this.exitComplete = this.pendingExit.isEmpty(); + }); + } + @@ -3557,6 +3588,20 @@ index 0000000..687730c + + public NativeUiAnimations animations() { return this.animations; } + ++ /** Returns whether an authored pop exit must finish before its host removes the scene. */ ++ public boolean requestExit(final long elapsedNanos) { ++ if (this.exiting) return !this.exitComplete; ++ this.advance(elapsedNanos); ++ this.exiting = true; ++ this.pendingExit = this.animations.completions(this.root, "screen.exit_pop", "screen.exit_end"); ++ this.exitComplete = this.pendingExit.isEmpty(); ++ this.animations.event(this.root, "screen.exit_pop", elapsedNanos); ++ return !this.exitComplete; ++ } ++ ++ public boolean exiting() { return this.exiting; } ++ public boolean exitComplete() { return this.exitComplete; } ++ + /** Binding refreshes retain control clocks; new controls start at the refresh time. */ + public void replaceRoot(final NativeUiScene.Node root, final long elapsedNanos) { + this.advance(elapsedNanos); @@ -3577,6 +3622,16 @@ index 0000000..687730c + replacements.put(old == null ? node : old, node); + } + this.animations.reconcile(replacements, elapsedNanos); ++ if (this.exiting) { ++ final Map> retainedExit = new IdentityHashMap<>(); ++ replacements.forEach((previousNode, currentNode) -> { ++ final var pending = this.pendingExit.get(previousNode); ++ if (pending != null && Objects.equals(previousNode.properties().get("animation_program"), currentNode.properties().get("animation_program"))) ++ retainedExit.put(currentNode, pending); ++ }); ++ this.pendingExit = retainedExit; ++ this.exitComplete = retainedExit.isEmpty(); ++ } + this.destroyed.clear(); + this.destroyed.addAll(retainedDestroyed); + this.root = root; @@ -3649,10 +3704,10 @@ index 0000000..687730c +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java new file mode 100644 -index 0000000..0c2c738 +index 0000000..b7aadf0 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimations.java -@@ -0,0 +1,406 @@ +@@ -0,0 +1,455 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3664,9 +3719,12 @@ index 0000000..0c2c738 +import com.google.gson.JsonObject; +import java.util.ArrayDeque; +import java.util.ArrayList; ++import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; ++import java.util.Set; ++import java.util.function.Consumer; +import java.util.function.BiPredicate; +import net.raphimc.viabedrock.api.model.CameraEase; + @@ -3674,6 +3732,7 @@ index 0000000..0c2c738 +public final class NativeUiAnimations { + private final Map entries = new IdentityHashMap<>(); + private final BiPredicate destroy; ++ private final Consumer completed; + private NativeUiScene.Node indexedRoot; + private List managers = List.of(); + private long layoutRevision; @@ -3735,7 +3794,31 @@ index 0000000..0c2c738 + + public NativeUiAnimations() { this((node, name) -> false); } + -+ NativeUiAnimations(final BiPredicate destroy) { this.destroy = destroy; } ++ NativeUiAnimations(final BiPredicate destroy) { this(destroy, event -> { }); } ++ ++ NativeUiAnimations(final BiPredicate destroy, ++ final Consumer completed) { ++ this.destroy = destroy; ++ this.completed = completed; ++ } ++ ++ record Completion(NativeUiScene.Node node, int track, String event) {} ++ ++ Map> completions(final NativeUiScene.Node node, final String play, final String completion) { ++ final Map> result = new IdentityHashMap<>(); ++ this.collectCompletions(node, play, completion, result); ++ return result; ++ } ++ ++ private void collectCompletions(final NativeUiScene.Node node, final String play, final String completion, ++ final Map> result) { ++ final var entry = this.entries.get(node); ++ if (entry != null) { ++ final var tracks = entry.completions(play, completion); ++ if (!tracks.isEmpty()) result.put(node, tracks); ++ } ++ for (final var child : node.children()) this.collectCompletions(child, play, completion, result); ++ } + + public NativeUiPresentation.Opacity opacity(final NativeUiScene.Node node, final long now) { + final Entry entry = this.entries.computeIfAbsent(node, value -> new Entry(value, now)); @@ -3853,6 +3936,24 @@ index 0000000..0c2c738 + } + } + ++ Set completions(final String play, final String completion) { ++ final Set result = new HashSet<>(); ++ for (final int root : this.active) { ++ if (!this.definitions.get(root).play.equals(play)) continue; ++ int index = root; ++ for (int visited = 0; visited < this.definitions.size(); visited++) { ++ final var definition = this.definitions.get(index); ++ if (index != root && !definition.play.isEmpty()) break; ++ if (definition.next < 0) { ++ if (definition.end.equals(completion)) result.add(index); ++ break; ++ } ++ index = definition.next; ++ } ++ } ++ return result; ++ } ++ + void advance(final long now) { + // Java partial ticks can retreat during a pause; native frame deltas never rewind. + if (now < this.last || this.advanced && now == this.last) return; @@ -3861,7 +3962,7 @@ index 0000000..0c2c738 + this.last = now; + if (this.active.isEmpty()) return; + final var next = new ArrayDeque(); -+ final var events = new ArrayList(); ++ final var events = new ArrayList(); + // Native managers update all current tracks before processing their queued successors. + for (final int index : List.copyOf(this.active)) update(index, delta, next, events); + int transitions = 0; @@ -3871,10 +3972,13 @@ index 0000000..0c2c738 + this.active.add(step.index); + update(step.index, step.delta, next, events); + } -+ for (final var event : events) event(event); ++ for (final var event : events) { ++ event(event.event()); ++ NativeUiAnimations.this.completed.accept(event); ++ } + } + -+ void update(final int index, final float delta, final ArrayDeque next, final List events) { ++ void update(final int index, final float delta, final ArrayDeque next, final List events) { + if (this.states[index] != State.PLAYING) return; + final var definition = this.definitions.get(index); + // Image tracks finish without consuming time when the control has no image component. @@ -3910,13 +4014,13 @@ index 0000000..0c2c738 + this.active.remove(Integer.valueOf(index)); + if (!definition.destroy.isEmpty() && NativeUiAnimations.this.destroy.test(this.node, definition.destroy)) { + NativeUiAnimations.this.layoutRevision++; -+ if (!definition.end.isEmpty()) events.add(definition.end); ++ if (!definition.end.isEmpty()) events.add(new Completion(this.node, index, definition.end)); + return; + } + if (definition.next >= 0) { + this.times[definition.next] = 0; + next.addLast(new Step(definition.next, Math.max(0, elapsed - definition.track.delay - definition.track.duration))); -+ } else if (!definition.end.isEmpty()) events.add(definition.end); ++ } else if (!definition.end.isEmpty()) events.add(new Completion(this.node, index, definition.end)); + } + + void sample(final int index, final Definition definition, final float elapsed, final boolean reset) { @@ -10510,10 +10614,10 @@ index 0000000..835f559 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycleTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycleTest.java new file mode 100644 -index 0000000..e407bbc +index 0000000..55565c7 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiAnimationLifecycleTest.java -@@ -0,0 +1,155 @@ +@@ -0,0 +1,222 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -10643,6 +10747,73 @@ index 0000000..e407bbc + assertEquals(.25F, lifecycle.animations().opacityAt(inserted, 500_000_000).current()); + } + ++ @Test ++ void popExitWaitsForAllAuthoredCompletionsAcrossRepeatedRequestsAndRefresh() { ++ final var root = node("container", List.of()); ++ final var track = new JsonObject(); ++ track.addProperty("anim_type", "alpha"); ++ track.addProperty("from", 1); ++ track.addProperty("to", 0); ++ track.addProperty("duration", .4); ++ track.addProperty("play_event", "screen.exit_pop"); ++ track.addProperty("end_event", "screen.exit_end"); ++ final var tracks = new JsonArray(); ++ tracks.add(track); ++ final var size = track.deepCopy(); ++ size.addProperty("anim_type", "wait"); ++ size.addProperty("duration", .25); ++ tracks.add(size); ++ AnimationPrograms.attach(root.properties(), tracks, false); ++ final var lifecycle = new NativeUiAnimationLifecycle(root); ++ lifecycle.advance(2_000_000_000L); ++ assertFalse(lifecycle.exitComplete()); ++ assertTrue(lifecycle.requestExit(2_000_000_000L)); ++ assertTrue(lifecycle.exiting()); ++ lifecycle.advance(2_200_000_000L); ++ assertEquals(.5F, lifecycle.animations().opacityAt(root, 2_200_000_000L).current(), .00001F); ++ assertTrue(lifecycle.requestExit(2_200_000_000L)); ++ lifecycle.advance(2_260_000_000L); ++ assertFalse(lifecycle.exitComplete()); ++ final var rebound = new NativeUiScene.Node(root.name(), root.type(), root.properties().deepCopy(), root.children(), root.action()); ++ lifecycle.replaceRoot(rebound, 2_260_000_000L); ++ lifecycle.advance(2_399_000_000L); ++ assertFalse(lifecycle.exitComplete()); ++ lifecycle.advance(2_401_000_000L); ++ assertTrue(lifecycle.exitComplete()); ++ assertFalse(lifecycle.requestExit(3_000_000_000L)); ++ } ++ ++ @Test ++ void popExitFollowsSuccessorsButDoesNotWaitForAnUnrelatedPlayEvent() { ++ final var root = node("container", List.of()); ++ final var first = new JsonObject(); ++ first.addProperty("anim_type", "wait"); ++ first.addProperty("duration", .1); ++ first.addProperty("play_event", "screen.exit_pop"); ++ final var last = new JsonObject(); ++ last.addProperty("anim_type", "wait"); ++ last.addProperty("duration", .2); ++ last.addProperty("end_event", "screen.exit_end"); ++ final var tracks = new JsonArray(); ++ tracks.add(first); ++ tracks.add(last); ++ AnimationPrograms.attach(root.properties(), tracks, true); ++ final var lifecycle = new NativeUiAnimationLifecycle(root); ++ assertTrue(lifecycle.requestExit(0)); ++ lifecycle.advance(299_000_000L); ++ assertFalse(lifecycle.exitComplete()); ++ lifecycle.advance(301_000_000L); ++ assertTrue(lifecycle.exitComplete()); ++ ++ last.addProperty("play_event", "other.event"); ++ final var blocked = new NativeUiAnimationLifecycle(root); ++ assertFalse(blocked.requestExit(0)); ++ assertTrue(blocked.exitComplete()); ++ final var absent = new NativeUiAnimationLifecycle(node("plain", List.of())); ++ assertFalse(absent.requestExit(0)); ++ assertTrue(absent.exitComplete()); ++ } ++ + private static NativeUiScene.Node animatedCollection(final int index) { + final var node = animatedSlot(index); + node.properties().remove("inventory_slot"); diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index 8bf75df4..e25b16a7 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -33,6 +33,13 @@ The existing independent actor material-coverage path remains separate. Fixed collection grids retain their full logical height while layout positions each instantiated cell by its original source index. Regressions compare complete and windowed geometry, distant scroll positions, resize, final rows, and offscreen keyboard neighbors. Overlapping windows retain animation clocks by source identity. + +The scene lifecycle tracks authored pop exits through their completion events. +It waits for all participating tracks, including a shorter size track alongside a longer fade. +Binding refresh retains pending completions by control identity. +Tests cover different durations, repeated requests, successor chains, missing completion paths, and refresh during an exit. +Native Bedrock 1.26.60.5 Escape frames retain the fade after the size track finishes. +This mechanism does not establish complete native host policy or exact frame timing. These controls do not establish identical native pooling or frame timing. Broader viewports, complete inventory pixels, entrance timing, and TrueType comparisons remain open. From 8dd7d2853b3f36b986583ab69fa6376723a840f6 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 05:58:24 +0100 Subject: [PATCH 24/33] fix(ui): retain native storage hosts and hurt policy Resolve storage programs from their complete authored screen so backdrops, close controls and exit tracks remain part of the scene. Forward the original local damage cause with its actor lifetime, then consume the player-owned pending check and counter across screen changes. Require a versioned core contract before admitting screens that depend on that policy, while retaining separate guards for unfinished cooking bindings. Determine paired-chest size before selecting its default caption and keep custom names intact. Fold these changes into their owning patches and update packet, lifecycle and transport fixtures for complete screen graphs. Native 1.26.60.5 runtime controls establish the server-event policy order. Full builds, fresh patch replays and installed direct/proxy controls cover storage families, accepted item requests and the shared hurt gate. Exact frame timing, client-predicted damage and the original modpack reports remain open. --- docs/bedrock-coverage.md | 1 + docs/native-player-inventory.md | 21 +- ...edrock-actor-events-to-java-statuses.patch | 20 +- ...erver-authoritative-bedrock-requests.patch | 90 ++-- ...edrock-recipes-and-place-ingredients.patch | 20 +- ...ain-bedrock-creative-catalog-entries.patch | 8 +- ...bedrock-merchant-inventories-in-core.patch | 6 +- ...ate-bedrock-loom-inventories-in-core.patch | 6 +- ...ranslate-bedrock-cartography-in-core.patch | 16 +- ...1-confirm-survival-offhand-transfers.patch | 20 +- ...rse-properties-and-composed-metadata.patch | 22 +- ...rate-authored-native-ui-presentation.patch | 29 +- ...are-native-ui-programs-and-resources.patch | 395 ++++++++++++++++-- ...are-native-ui-programs-and-resources.pr.md | 12 + ...removals-for-absent-unmapped-effects.patch | 6 +- ...9-present-authored-native-containers.patch | 114 +++-- ...9-present-authored-native-containers.pr.md | 13 +- ...ender-authored-forms-and-npc-dialogs.patch | 11 +- ...0029-share-native-ui-scene-rendering.patch | 224 +++++++++- ...0029-share-native-ui-scene-rendering.pr.md | 10 +- ...orted-native-world-actor-definitions.patch | 6 +- ...1-edit-native-command-block-settings.patch | 8 +- ...0038-predict-held-trapdoor-placement.patch | 6 +- ...ply-native-command-stacks-to-dry-air.patch | 6 +- 24 files changed, 857 insertions(+), 213 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 7af9f56b..e46f335c 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -39,6 +39,7 @@ No complete area has verified Bedrock parity. Retain these reports until a matching reproduction establishes the result: - WildFFA inventory opened with E shows items without the expected background. +- An update from 0.3.61 to 0.4.2 caused reported inventory lag spikes, overlapping menus, and an extreme shield pose. - An ender pearl followed by a wind charge can cause repeated rollbacks. - Food can repeat its use animation without feeding the player. - The locator bar can be absent. diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 4bc28797..4a1ee492 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -35,6 +35,17 @@ A repeated close request does not restart the exit. Completion can close only the same screen and menu. A scene without a completion path closes immediately. +Storage scenes retain the complete authored host, including the backdrop, close button, and exit tracks. +Server-delivered hurt events retain the original damage cause and local actor lifetime. +The shared player state applies the native cause filter and pending-event check before the screen policy. +At a zero counter, a pending event starts the 100-update gate even when the current screen stays open. +Events during that gate are consumed rather than deferred. +Health changes alone do not trigger this policy. + +Programs advertise this behavior through the versioned core hurt contract. +Without that contract, screens that require the hurt policy retain ordinary controls. +Separate admission checks still exclude unfinished cooking bindings. + The installed opening also owns a read-only recipe context while the native acknowledgment is pending. It reads the real cached crafting grid, hotbar, inventory, and cursor. The acknowledgment retains this model and its captured search policy. @@ -136,6 +147,10 @@ An unpositioned player inventory uses the fallback crafting context. Native model-position initialization and arbitrary custom component admission remain unverified. The original inventory-background, rollback, food-use, and locator-bar reports remain unresolved. Native registry probes and local model tests do not establish complete live inventory parity. -Generic storage scenes currently expand selected content without the complete host shell. -Their default host animations remain absent from the resolved scene. -The native cause filter and pending-state counter for closing on hurt still need complete runtime controls. +Installed direct and ViaProxy controls cover complete chest, hopper, brewing, and large-chest hosts, including exits and the close button. +Both routes retain 64 logs after server-accepted pickup and return requests. +Damage controls cover accepted and excluded causes, gated events, a false inventory policy, and remote actors. +Native Bedrock 1.26.60.5 runtime controls establish the pending-state and counter order for server-delivered hurt events. +Client-predicted damage, the live producer for actor event 81, and exact frame timing remain unverified. +The historic hurt-and-heal nonclosure with a 5.6-millisecond packet gap remains unexplained. +Matched chest geometry retains a one-byte label-shade difference. Complete screen pixels remain unverified. diff --git a/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch b/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch index 3d829a30..f0facc69 100644 --- a/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch +++ b/patches/viabedrock/deferred/0001-translate-bedrock-actor-events-to-java-statuses.patch @@ -1,4 +1,4 @@ -From a98a158c61f1561706f3ca962b5903f15c6681d4 Mon Sep 17 00:00:00 2001 +From 3efc13f7266e1826ee69977f548a28bd4711d68e 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(entity-events): translate Bedrock actor events to Java @@ -327,10 +327,10 @@ index b47f5471..88ce252d 100644 levelSoundEvent.send(BedrockProtocol.class, false); } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java -index 619ae050..53f67c81 100644 +index 4578a73f..7bb6b8f8 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java -@@ -39,6 +39,7 @@ import net.raphimc.viabedrock.api.model.NativeActorMessage; +@@ -40,6 +40,7 @@ import net.raphimc.viabedrock.api.model.NativeActorMessage; import net.raphimc.viabedrock.api.model.ActorMotionCommand; import net.raphimc.viabedrock.api.model.MovementCorrectionTiming; import net.raphimc.viabedrock.api.model.NativeAnimationMessage; @@ -338,7 +338,7 @@ index 619ae050..53f67c81 100644 import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity; import net.raphimc.viabedrock.api.model.entity.CustomEntity; import net.raphimc.viabedrock.api.model.entity.Entity; -@@ -50,10 +51,12 @@ import net.raphimc.viabedrock.api.util.RegistryUtil; +@@ -51,10 +52,12 @@ import net.raphimc.viabedrock.api.util.RegistryUtil; import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; @@ -351,7 +351,7 @@ index 619ae050..53f67c81 100644 import net.raphimc.viabedrock.protocol.data.enums.java.Relative; import net.raphimc.viabedrock.protocol.data.enums.java.generated.EquipmentSlot; import net.raphimc.viabedrock.protocol.data.generated.java.EntityDataFields; -@@ -62,6 +65,9 @@ import net.raphimc.viabedrock.protocol.model.*; +@@ -63,6 +66,9 @@ import net.raphimc.viabedrock.protocol.model.*; import net.raphimc.viabedrock.protocol.rewriter.EntityMetadataRewriter; import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; import net.raphimc.viabedrock.protocol.storage.EntityPropertyTracker; @@ -361,7 +361,7 @@ index 619ae050..53f67c81 100644 import net.raphimc.viabedrock.protocol.storage.ChannelStorage; import net.raphimc.viabedrock.protocol.storage.EntityTracker; import net.raphimc.viabedrock.protocol.storage.PlayerPredictionStorage; -@@ -72,6 +78,7 @@ import net.raphimc.viabedrock.protocol.types.BedrockTypes; +@@ -73,6 +79,7 @@ import net.raphimc.viabedrock.protocol.types.BedrockTypes; import java.util.ArrayList; import java.util.List; @@ -369,8 +369,8 @@ index 619ae050..53f67c81 100644 import java.util.logging.Level; public final class EntityPackets { -@@ -500,6 +507,43 @@ public final class EntityPackets { - return; +@@ -509,6 +516,43 @@ public final class EntityPackets { + hurt.send(BedrockProtocol.class); } switch (event) { + case FISHHOOK_FISHPOS, FISHHOOK_HOOKTIME, FISHHOOK_TEASE -> { @@ -413,7 +413,7 @@ index 619ae050..53f67c81 100644 case HURT -> { final CompoundTag damageTypeRegistry = gameSession.getJavaRegistries().getCompoundTag(RegistryKeys.DAMAGE_TYPE); final SharedTypes_Legacy_ActorDamageCause damageCause = SharedTypes_Legacy_ActorDamageCause.getByValue(data, SharedTypes_Legacy_ActorDamageCause.Override); -@@ -529,12 +573,17 @@ public final class EntityPackets { +@@ -538,12 +582,17 @@ public final class EntityPackets { } if (entity != entityTracker.getClientPlayer()) { entity.playSound("death"); @@ -434,7 +434,7 @@ index 619ae050..53f67c81 100644 } } }); -@@ -782,6 +831,34 @@ public final class EntityPackets { +@@ -791,6 +840,34 @@ public final class EntityPackets { return entity; } 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 dea1d861..182528fb 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 b7c8e6c022f5192e9f9eeddb569482433b6df174 Mon Sep 17 00:00:00 2001 +From 6b89b9b400b9e45e61e84b005b709ad3a250cc1f 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 @@ -742,6 +742,13 @@ lifecycles. Their matching schemas establish the unsigned container byte, signed recipe index, unique player ID and Start=0/Stop=1 values. Targeted wire regressions cover packet order, cancellation, server selection scope, deselection, malformed indexes and container IDs above 127. + +Select the default physical-container caption after determining its size. +Bedrock 1.26.60.5 shows Large Chest for the paired 54-slot chest and its +language resource names that caption container.chestDouble. Preserve the +original custom-name override and localization flag. The packet-to-menu +regression checks both the default and a named paired chest in the Java +open packet and exported native presentation. --- build.gradle | 3 +- .../api/model/PlayerInventoryScreenClose.java | 46 + @@ -856,7 +863,7 @@ deselection, malformed indexes and container IDs above 127. .../packet/HotbarSelectionPacketTest.java | 155 + .../protocol/packet/InventoryPacketsTest.java | 254 + .../packet/MountInventoryPacketTest.java | 163 + - .../NativeContainerOpeningPacketTest.java | 198 +- + .../NativeContainerOpeningPacketTest.java | 226 +- .../packet/Protocol2225InventoryTest.java | 225 + .../protocol/packet/SpearJabTest.java | 6 +- .../ItemRewriterBlockEntityItemTest.java | 85 + @@ -881,7 +888,7 @@ deselection, malformed indexes and container IDs above 127. .../item/NetworkItemConstructionTest.java | 171 + .../types/item/NetworkItemCountTest.java | 35 +- .../types/item/NetworkItemStackFixtures.java | 69 + - 138 files changed, 28623 insertions(+), 2046 deletions(-) + 138 files changed, 28650 insertions(+), 2047 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenClose.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenRequest.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/ContainerOpenContext.java @@ -9605,7 +9612,7 @@ index 01389150..081338cb 100644 } chunkTracker.trackBlockPrediction(sequence, position, placedPosition); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index d22d7616..20e14855 100644 +index d22d7616..375f5f0c 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -17,26 +17,28 @@ @@ -9808,7 +9815,7 @@ index d22d7616..20e14855 100644 wrapper.cancel(); } } -@@ -202,347 +287,864 @@ public final class InventoryPackets { +@@ -202,347 +287,863 @@ public final class InventoryPackets { } public static void register(final BedrockProtocol protocol) { @@ -10004,11 +10011,6 @@ index d22d7616..20e14855 100644 + ? null : chunkTracker.getBlockEntity(containerPosition); + final String blockTag = containerPosition == null ? null + : blockStateRewriter.tag(chunkTracker.getBlockState(containerPosition)); -+ final String defaultTitle = defaultContainerTitle(screenType, blockTag); -+ final ContainerTitle openingTitle = containerTitle(wrapper.user(), screenType, containerEntity, blockEntity, defaultTitle); -+ final TextComponent title = openingTitle.caption(); -+ -+ final Container container; + int size = 27; + if (blockEntity != null + && blockEntity.tag() != null @@ -10016,6 +10018,10 @@ index d22d7616..20e14855 100644 + && blockEntity.tag().contains("pairlead")) { + size = 54; + } ++ final String defaultTitle = defaultContainerTitle(screenType, blockTag, size); ++ final ContainerTitle openingTitle = containerTitle(wrapper.user(), screenType, containerEntity, blockEntity, defaultTitle); ++ final TextComponent title = openingTitle.caption(); ++ final Container container; + switch (screenType) { + case INVENTORY -> { + final InventoryContainer playerInventory = new InventoryContainer( @@ -10982,7 +10988,7 @@ index d22d7616..20e14855 100644 protocol.registerServerbound( ServerboundPackets26_3.EDIT_BOOK, null, -@@ -593,33 +1195,55 @@ public final class InventoryPackets { +@@ -593,33 +1194,55 @@ public final class InventoryPackets { updatedTag.put("pages", updatedPages); book.setTag(updatedTag); }); @@ -11060,7 +11066,7 @@ index d22d7616..20e14855 100644 if (type == ContainerType.CONTAINER && actor != null) { final String filtered = actorTitle(actor, ActorDataIds.FILTERED_NAME); final String name = filtered.isEmpty() ? actorTitle(actor, ActorDataIds.NAME) : filtered; -@@ -631,7 +1255,7 @@ public final class InventoryPackets { +@@ -631,7 +1254,7 @@ public final class InventoryPackets { final String raw = customName.getValue(); return new ContainerTitle(TextUtil.stringToTextComponent(user.get(ResourcePackStorage.class).getTexts().translate(raw)), raw, false); } @@ -11069,7 +11075,7 @@ index d22d7616..20e14855 100644 } private static String actorTitle(final Entity actor, final ActorDataIds field) { -@@ -657,21 +1281,23 @@ public final class InventoryPackets { +@@ -657,21 +1280,23 @@ public final class InventoryPackets { case HOPPER, MINECART_HOPPER -> size == 5 ? ContainerUi.Kind.HOPPER : null; case DISPENSER -> size == 9 ? ContainerUi.Kind.DISPENSER : null; case DROPPER -> size == 9 ? ContainerUi.Kind.DROPPER : null; @@ -11097,7 +11103,7 @@ index d22d7616..20e14855 100644 return; } final var resources = wrapper.user().get(ResourcePackStorage.class); -@@ -680,22 +1306,33 @@ public final class InventoryPackets { +@@ -680,22 +1305,33 @@ public final class InventoryPackets { } final var state = wrapper.user().get(NativeContainerUiState.class); final var ui = resources.getContainerUi(); @@ -11135,7 +11141,7 @@ index d22d7616..20e14855 100644 private static Dialog createFormDialog(final UserConnection user, final int id, final Form form, final ServerFormUi.Presentation presentation) { final ResourcePackStorage resourcePackStorage = user.get(ResourcePackStorage.class); -@@ -986,13 +1623,147 @@ public final class InventoryPackets { +@@ -986,13 +1622,148 @@ public final class InventoryPackets { } } @@ -11194,11 +11200,12 @@ index d22d7616..20e14855 100644 + }; + } + -+ private static String defaultContainerTitle(final ContainerType type, final String blockTag) { ++ private static String defaultContainerTitle(final ContainerType type, final String blockTag, final int size) { + final String name = switch (type) { + case CONTAINER -> CustomBlockTags.BARREL.equals(blockTag) ? "barrel" + : CustomBlockTags.SHULKER_BOX.equals(blockTag) ? "shulkerbox" -+ : CustomBlockTags.ENDER_CHEST.equals(blockTag) ? "enderchest" : "chest"; ++ : CustomBlockTags.ENDER_CHEST.equals(blockTag) ? "enderchest" ++ : size == 54 ? "chestDouble" : "chest"; + case CHEST_BOAT -> "chest"; + case MINECART_CHEST -> "minecart"; + case MINECART_HOPPER, HOPPER -> "hopper"; @@ -30518,7 +30525,7 @@ index 00000000..d1b158d3 + +} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/NativeContainerLegacyActionsTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeContainerLegacyActionsTest.java -index dc791319..c3e828a0 100644 +index 0f582f85..2fb78217 100644 --- a/src/test/java/net/raphimc/viabedrock/api/model/container/NativeContainerLegacyActionsTest.java +++ b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeContainerLegacyActionsTest.java @@ -30,7 +30,7 @@ import net.raphimc.viabedrock.protocol.storage.NativeContainerUiState; @@ -35050,7 +35057,7 @@ index 00000000..30faf553 + +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/NativeContainerOpeningPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/NativeContainerOpeningPacketTest.java -index 3472fd3b..81ef48e1 100644 +index 36598bdc..15279c82 100644 --- a/src/test/java/net/raphimc/viabedrock/protocol/packet/NativeContainerOpeningPacketTest.java +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/NativeContainerOpeningPacketTest.java @@ -18,6 +18,7 @@ import net.raphimc.viabedrock.platform.ViaBedrockPlatform; @@ -35308,7 +35315,7 @@ index 3472fd3b..81ef48e1 100644 assertNotNull(first); assertNotNull(second); assertEquals(1, first.sequence()); -@@ -327,7 +472,29 @@ class NativeContainerOpeningPacketTest { +@@ -328,7 +473,54 @@ class NativeContainerOpeningPacketTest { } fixture.open(123); assertEquals("container.chest", fixture.inventory.getCurrentContainer().title().asUnformattedString()); @@ -35336,10 +35343,35 @@ index 3472fd3b..81ef48e1 100644 + assertEquals(fixture.inventory.getCurrentContainer().title().asUnformattedString(), message.conditionTitle()); + assertEquals(entry.getValue(), message.conditionTitle()); + assertTrue(JsonParser.parseString(message.program()).getAsJsonObject() ++ .getAsJsonObject("globals").get("$localize_title").getAsBoolean()); ++ } ++ } ++ } ++ ++ @Test ++ void pairedChestUsesLargeDefaultTitleAndPreservesCustomNames() throws Exception { ++ for (final boolean named : List.of(false, true)) { ++ try (var fixture = new OpeningFixture()) { ++ final var tag = new CompoundTag(); ++ tag.putString("id", "Chest"); ++ tag.putByte("pairlead", (byte) 1); ++ if (named) { ++ tag.putString("CustomName", "actor.key"); ++ } ++ when(fixture.user.get(ChunkTracker.class).getBlockEntity(OpeningFixture.POSITION)) ++ .thenReturn(new BedrockBlockEntity(OpeningFixture.POSITION, tag)); ++ fixture.open(-1); ++ final var message = fixture.message(); ++ assertEquals(54, fixture.inventory.getCurrentContainer().size()); ++ assertEquals(ContainerUi.Kind.CHEST_6_ROWS, message.kind()); ++ assertEquals(named ? "Social Menu" : "container.chestDouble", ++ fixture.inventory.getCurrentContainer().title().asUnformattedString()); ++ assertEquals(named ? "actor.key" : "container.chestDouble", message.conditionTitle()); ++ assertEquals(!named, JsonParser.parseString(message.program()).getAsJsonObject() .getAsJsonObject("globals").get("$localize_title").getAsBoolean()); } } -@@ -380,7 +547,7 @@ class NativeContainerOpeningPacketTest { +@@ -381,7 +573,7 @@ class NativeContainerOpeningPacketTest { channels.addChannels(List.of(NativeContainerMessage.CHANNEL)); this.storage.put(ChannelStorage.class, channels); this.storage.put(NativeContainerUiState.class, new NativeContainerUiState()); @@ -35348,7 +35380,17 @@ index 3472fd3b..81ef48e1 100644 this.storage.put(InventoryTracker.class, this.inventory); this.storage.put(EntityTracker.class, this.entities); final var player = new ClientPlayerEntity(this.user, 1, new UUID(0, 1), new PlayerAbilities(1, (byte) 0, (byte) 0)); -@@ -453,8 +620,9 @@ class NativeContainerOpeningPacketTest { +@@ -439,7 +631,8 @@ class NativeContainerOpeningPacketTest { + try { + assertEquals(ClientboundPackets26_3.OPEN_SCREEN.getId(), Types.VAR_INT.readPrimitive(open)); + assertEquals(8, Types.VAR_INT.readPrimitive(open)); +- assertEquals(BedrockProtocol.MAPPINGS.getJavaMenuId("minecraft:generic_9x3"), Types.VAR_INT.readPrimitive(open)); ++ assertEquals(BedrockProtocol.MAPPINGS.getJavaMenuId(this.inventory.getCurrentContainer().size() == 54 ++ ? "minecraft:generic_9x6" : "minecraft:generic_9x3"), Types.VAR_INT.readPrimitive(open)); + assertEquals(TextUtil.textComponentToNbt(this.inventory.getCurrentContainer().title()), Types.TAG.read(open)); + assertFalse(open.isReadable()); + } finally { +@@ -454,8 +647,9 @@ class NativeContainerOpeningPacketTest { final var payload = Unpooled.wrappedBuffer(this.sent.get(1)); try { assertEquals(ClientboundPackets26_3.CUSTOM_PAYLOAD.getId(), Types.VAR_INT.readPrimitive(payload)); @@ -39435,7 +39477,7 @@ index 00000000..d43d9f79 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeContainerActionBackendTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeContainerActionBackendTest.java new file mode 100644 -index 00000000..e8458a8f +index 00000000..5a2ca105 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeContainerActionBackendTest.java @@ -0,0 +1,95 @@ @@ -39522,7 +39564,7 @@ index 00000000..e8458a8f + private static ContainerUi program() { + return ContainerUi.fromProgram(JsonParser.parseString(""" + {"definitions": { -+ "chest.small_chest_screen": {"parent":"", "value":{"$screen_content":"chest.small_chest_panel"}}, ++ "chest.small_chest_screen": {"parent":"", "value":{"type":"screen","controls":[{"content@$screen_content":{}}],"$screen_content":"chest.small_chest_panel"}}, + "chest.small_chest_panel": {"parent":"", "value":{"type":"grid", "collection_name":"container_items", + "grid_dimensions":[27,1], "controls":[{"action":{"type":"button", "grid_position":[9,0], + "bindings":[ 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 0f1e5b80..d7426e7a 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 7176feda38eaae0f109ccddeea64d2dd8952a285 Mon Sep 17 00:00:00 2001 +From f2ee5665d4139bc41970eba0d54db85f12cba349 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 @@ -3850,7 +3850,7 @@ index 11c4d002..9dc86539 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 20e14855..0672f7a7 100644 +index 375f5f0c..176bdfd5 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -93,6 +93,7 @@ import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.*; @@ -3939,7 +3939,7 @@ index 20e14855..0672f7a7 100644 } static void handleContainerClose(final PacketWrapper wrapper) { -@@ -527,7 +587,13 @@ public final class InventoryPackets { +@@ -526,7 +586,13 @@ public final class InventoryPackets { if (container instanceof LecternContainer lectern) { lectern.sendInitialState(); } @@ -3954,7 +3954,7 @@ index 20e14855..0672f7a7 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.CONTAINER_CLOSE, -@@ -564,6 +630,9 @@ public final class InventoryPackets { +@@ -563,6 +629,9 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeContent(container); @@ -3964,7 +3964,7 @@ index 20e14855..0672f7a7 100644 if (container instanceof StonecutterContainer stonecutter) { stonecutter.refreshInput(); wrapper.cancel(); -@@ -571,6 +640,7 @@ public final class InventoryPackets { +@@ -570,6 +639,7 @@ public final class InventoryPackets { } PacketFactory.writeJavaContainerSetContent(wrapper, container); PacketFactory.sendJavaCursorForHudUpdate(wrapper.user(), container); @@ -3972,7 +3972,7 @@ index 20e14855..0672f7a7 100644 } else { wrapper.cancel(); } -@@ -599,6 +669,12 @@ public final class InventoryPackets { +@@ -598,6 +668,12 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeSlot(container, slot); @@ -3985,7 +3985,7 @@ index 20e14855..0672f7a7 100644 if (container instanceof StonecutterContainer stonecutter && slot == 3) { stonecutter.refreshInput(); wrapper.cancel(); -@@ -622,6 +698,7 @@ public final class InventoryPackets { +@@ -621,6 +697,7 @@ public final class InventoryPackets { } wrapper.write( VersionedTypes.V26_3.item, container.getJavaItem(slot)); // item @@ -3993,7 +3993,7 @@ index 20e14855..0672f7a7 100644 } else { wrapper.cancel(); } -@@ -683,6 +760,14 @@ public final class InventoryPackets { +@@ -682,6 +759,14 @@ public final class InventoryPackets { inventoryTracker.removeDynamicContainer(containerName); } }); @@ -4008,7 +4008,7 @@ index 20e14855..0672f7a7 100644 protocol.registerClientbound( ClientboundBedrockPackets.CRAFTING_DATA, null, -@@ -692,7 +777,27 @@ public final class InventoryPackets { +@@ -691,7 +776,27 @@ public final class InventoryPackets { wrapper.user().get(CraftingDataTracker.class); final CraftingDataReader.Data data = CraftingDataReader.read(wrapper); tracker.updateCraftingDataList(data.recipes(), data.clearRecipes()); @@ -4036,7 +4036,7 @@ index 20e14855..0672f7a7 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.CONTAINER_SET_DATA, -@@ -905,6 +1010,46 @@ public final class InventoryPackets { +@@ -904,6 +1009,46 @@ public final class InventoryPackets { PacketFactory.sendJavaContainerSetContent(wrapper.user(), container); } }); 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 8beac5b1..d5f73875 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 2c5cb530c5425af3e9621ed465a41b701c3fcffc Mon Sep 17 00:00:00 2001 +From 8832b7dbbc095f61e79e22e231fde05ab780b2b1 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 @@ -1930,7 +1930,7 @@ index 00000000..5b3cdb3b + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 9d4ce882..8c64cdce 100644 +index c7461506..45e6cfc4 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -92,14 +92,20 @@ import net.raphimc.viabedrock.protocol.data.enums.bedrock.ComplexInventoryTransa @@ -2003,7 +2003,7 @@ index 9d4ce882..8c64cdce 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.CONTAINER_OPEN, -@@ -1149,41 +1167,7 @@ public final class InventoryPackets { +@@ -1148,41 +1166,7 @@ public final class InventoryPackets { protocol.registerServerbound( ServerboundPackets26_3.SET_CREATIVE_MODE_SLOT, null, @@ -2046,7 +2046,7 @@ index 9d4ce882..8c64cdce 100644 protocol.registerServerbound( ServerboundPackets26_3.CUSTOM_CLICK_ACTION, ServerboundBedrockPackets.MODAL_FORM_RESPONSE, -@@ -1295,7 +1279,7 @@ public final class InventoryPackets { +@@ -1294,7 +1278,7 @@ public final class InventoryPackets { if (container == null) { if (containerId == ContainerID.CONTAINER_ID_INVENTORY.getValue()) { wrapper.user().get(InventoryRequestTracker.class) diff --git a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch index af42d710..17b65a92 100644 --- a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch +++ b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch @@ -1,4 +1,4 @@ -From 98eee333b2a4b3d34cebd1224f3d9eccd3ad95a2 Mon Sep 17 00:00:00 2001 +From 4b81efed7c66e59415cb7609070408dff7502365 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(trading): translate Bedrock merchant inventories in core @@ -884,7 +884,7 @@ index 00000000..ea4d9561 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 8c64cdce..e0723f97 100644 +index 45e6cfc4..27fc99c9 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -79,6 +79,7 @@ import net.raphimc.viabedrock.api.model.container.NativeStorageContainers; @@ -907,7 +907,7 @@ index 8c64cdce..e0723f97 100644 if (inventoryTracker.isAnyScreenOpen()) { ViaBedrock.getPlatform() .getLogger() -@@ -1294,6 +1300,11 @@ public final class InventoryPackets { +@@ -1293,6 +1299,11 @@ public final class InventoryPackets { inventoryTracker.setCurrentContainerClosed(false); return; } diff --git a/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch b/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch index 12624951..13f5161f 100644 --- a/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch +++ b/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch @@ -1,4 +1,4 @@ -From 6fcfe3e86a415f991576b98f19d1646d5348e505 Mon Sep 17 00:00:00 2001 +From d877c061ad8a56d84800601e43ce2f0f845485e5 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 Bedrock loom inventories in core @@ -368,10 +368,10 @@ index a3b933c2..6ef8f93d 100644 + } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index e0723f97..eda923f3 100644 +index 27fc99c9..202937ad 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -@@ -535,6 +535,7 @@ public final class InventoryPackets { +@@ -534,6 +534,7 @@ public final class InventoryPackets { container = new SmithingContainer( wrapper.user(), containerId, title, containerPosition); diff --git a/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch b/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch index 53885827..e9506c28 100644 --- a/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch +++ b/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch @@ -1,4 +1,4 @@ -From 2975be474c327ac7e25d3667bb3f520eb73fb272 Mon Sep 17 00:00:00 2001 +From 20a8064a302c66ff605d5eb3b0a3337638647dc9 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 Bedrock cartography in core @@ -634,7 +634,7 @@ index 6ef8f93d..9d1c1819 100644 public static final String CREAKING_HEART = "creaking_heart"; public static final String DAYLIGHT_DETECTOR = "daylight_detector"; diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index eda923f3..e71eb224 100644 +index 202937ad..04b6f964 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -73,6 +73,7 @@ import net.lenni0451.mcstructs_bedrock.text.utils.BedrockTextUtils; @@ -655,7 +655,7 @@ index eda923f3..e71eb224 100644 inventoryTracker.setCurrentContainerClosed(serverInitiated); if (correlatedClose) { wrapper.cancel(); -@@ -535,6 +539,7 @@ public final class InventoryPackets { +@@ -534,6 +538,7 @@ public final class InventoryPackets { container = new SmithingContainer( wrapper.user(), containerId, title, containerPosition); @@ -663,7 +663,7 @@ index eda923f3..e71eb224 100644 case LOOM -> container = new LoomContainer(wrapper.user(), containerId, title, containerPosition); case STONECUTTER -> container = -@@ -599,6 +604,13 @@ public final class InventoryPackets { +@@ -598,6 +603,13 @@ public final class InventoryPackets { } container.resolveBlockBinding(); inventoryTracker.setCurrentContainer(container, openContext); @@ -677,7 +677,7 @@ index eda923f3..e71eb224 100644 wrapper.write(Types.VAR_INT, (int) containerId); // container id final int javaMenuId = screenType == ContainerType.CONTAINER && size == 54 -@@ -652,6 +664,13 @@ public final class InventoryPackets { +@@ -651,6 +663,13 @@ public final class InventoryPackets { (byte) containerId, containerName, storageItem); if (container != null && container.setItems(items)) { container.markServerUpdate(); @@ -691,7 +691,7 @@ index eda923f3..e71eb224 100644 wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeContent(container); wrapper.user().get(InventoryRequestTracker.class) -@@ -822,6 +841,9 @@ public final class InventoryPackets { +@@ -821,6 +840,9 @@ public final class InventoryPackets { .refreshCraftingOutput(); tracker.sendJavaUpdateRecipes(wrapper.user()); NativeCookingUiState.sendUpdate(wrapper.user()); @@ -701,7 +701,7 @@ index eda923f3..e71eb224 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.UNLOCKED_RECIPES, -@@ -1148,6 +1170,9 @@ public final class InventoryPackets { +@@ -1147,6 +1169,9 @@ public final class InventoryPackets { if (wrapper.user().get(InventoryTracker.class).getCurrentContainer() instanceof AnvilContainer anvil) { anvil.setRenameText(name); @@ -711,7 +711,7 @@ index eda923f3..e71eb224 100644 } }); protocol.registerServerbound( -@@ -1182,6 +1207,13 @@ public final class InventoryPackets { +@@ -1181,6 +1206,13 @@ public final class InventoryPackets { final String id = wrapper.read(Types.STRING); // id final CompoundTag payload = (CompoundTag) wrapper.read(Types.CUSTOM_CLICK_ACTION_TAG); // payload diff --git a/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch b/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch index ce2dfabb..5285e0f4 100644 --- a/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch +++ b/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch @@ -1,4 +1,4 @@ -From 8b7d8579b5d77b4a503c52755a5aa42e576415d2 Mon Sep 17 00:00:00 2001 +From 0fc8b84410513e50392eb681bf9fe975cbf5bae0 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(inventory): confirm survival offhand transfers from @@ -51,10 +51,10 @@ server acceptance and recovery remain separate controls. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/NativeContainerOffhandGuardTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 0672f7a7..9d4ce882 100644 +index 176bdfd5..c7461506 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -@@ -620,7 +620,6 @@ public final class InventoryPackets { +@@ -619,7 +619,6 @@ public final class InventoryPackets { wrapper.read(BedrockTypes.FULL_CONTAINER_NAME); // container name final BedrockItem storageItem = wrapper.read(itemRewriter.newItemType()); // storage item @@ -62,7 +62,7 @@ index 0672f7a7..9d4ce882 100644 final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); final Container container = -@@ -630,6 +629,10 @@ public final class InventoryPackets { +@@ -629,6 +628,10 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeContent(container); @@ -73,7 +73,7 @@ index 0672f7a7..9d4ce882 100644 if (container.type() == ContainerType.HUD) { inventoryTracker.getInventoryContainer().updateCraftingOutput(); } -@@ -659,7 +662,6 @@ public final class InventoryPackets { +@@ -658,7 +661,6 @@ public final class InventoryPackets { final BedrockItem storageItem = wrapper.read(itemRewriter.optionalNewItemType()); // storage item final BedrockItem item = wrapper.read(itemRewriter.newItemType()); // item @@ -81,7 +81,7 @@ index 0672f7a7..9d4ce882 100644 final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); final Container container = -@@ -669,6 +671,20 @@ public final class InventoryPackets { +@@ -668,6 +670,20 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeSlot(container, slot); @@ -102,7 +102,7 @@ index 0672f7a7..9d4ce882 100644 if (container.type() == ContainerType.HUD && ((slot >= 28 && slot <= 31) || slot == 50)) { inventoryTracker.getInventoryContainer().refreshCraftingOutput(); -@@ -962,6 +978,8 @@ public final class InventoryPackets { +@@ -961,6 +977,8 @@ public final class InventoryPackets { } final Container container = inventoryTracker.getContainerServerbound((byte) containerId); @@ -111,7 +111,7 @@ index 0672f7a7..9d4ce882 100644 final var nativeContainer = wrapper.user().get(NativeContainerUiState.class); if (nativeContainer != null && nativeContainer.blocksVanillaClicks()) { if (container != null) { -@@ -969,10 +987,11 @@ public final class InventoryPackets { +@@ -968,10 +986,11 @@ public final class InventoryPackets { } return; } @@ -125,7 +125,7 @@ index 0672f7a7..9d4ce882 100644 final boolean requestOpen = !requestTracker.isWaitingForInventoryOpen(); if (!requestTracker.queueInventoryOpenClick( revision, slot, button, action)) { -@@ -995,13 +1014,15 @@ public final class InventoryPackets { +@@ -994,13 +1013,15 @@ public final class InventoryPackets { wrapper.cancel(); return; } @@ -143,7 +143,7 @@ index 0672f7a7..9d4ce882 100644 if (!container.handleClick(revision, slot, button, action)) { if (container.type() != ContainerType.INVENTORY) { PacketFactory.sendJavaContainerSetContent( -@@ -1066,7 +1087,10 @@ public final class InventoryPackets { +@@ -1065,7 +1086,10 @@ public final class InventoryPackets { return; } final InventoryRequestTracker requestTracker = wrapper.user().get(InventoryRequestTracker.class); diff --git a/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch b/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch index daf2b71e..0ef5f619 100644 --- a/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch +++ b/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.patch @@ -1,4 +1,4 @@ -From d1cc1cf3f521b2459b17d6ed53fb3aed6f59b4ab Mon Sep 17 00:00:00 2001 +From b81cbf7868415686067e2c14657f8ef269de84b2 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(metadata): preserve sparse properties and composed actor @@ -538,7 +538,7 @@ index b98af526..3514995f 100644 CRAWLING(114), TIMER_FLAG_1(115), diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java -index b8ba20f5..fd0640c4 100644 +index 9c4b8cec..1c23b759 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java @@ -20,9 +20,6 @@ package net.raphimc.viabedrock.protocol.packet; @@ -560,7 +560,7 @@ index b8ba20f5..fd0640c4 100644 import com.viaversion.viaversion.api.type.Types; import com.viaversion.viaversion.api.type.types.version.VersionedTypes; import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets26_3; -@@ -44,8 +43,6 @@ import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity; +@@ -45,8 +44,6 @@ import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity; import net.raphimc.viabedrock.api.model.entity.CustomEntity; import net.raphimc.viabedrock.api.model.entity.Entity; import net.raphimc.viabedrock.api.model.entity.LivingEntity; @@ -569,7 +569,7 @@ index b8ba20f5..fd0640c4 100644 import net.raphimc.viabedrock.api.resourcepack.definition.EntityDefinitions; import net.raphimc.viabedrock.api.util.MathUtil; import net.raphimc.viabedrock.api.util.PacketFactory; -@@ -54,6 +51,7 @@ import net.raphimc.viabedrock.api.util.TextUtil; +@@ -55,6 +52,7 @@ import net.raphimc.viabedrock.api.util.TextUtil; import net.raphimc.viabedrock.protocol.BedrockProtocol; import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; import net.raphimc.viabedrock.protocol.data.enums.Direction; @@ -577,7 +577,7 @@ index b8ba20f5..fd0640c4 100644 import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.*; import net.raphimc.viabedrock.protocol.data.enums.java.AnimateAction; import net.raphimc.viabedrock.protocol.data.enums.java.Relative; -@@ -61,6 +59,7 @@ import net.raphimc.viabedrock.protocol.data.enums.java.generated.EquipmentSlot; +@@ -62,6 +60,7 @@ import net.raphimc.viabedrock.protocol.data.enums.java.generated.EquipmentSlot; import net.raphimc.viabedrock.protocol.data.generated.java.EntityDataFields; import net.raphimc.viabedrock.protocol.data.generated.java.RegistryKeys; import net.raphimc.viabedrock.protocol.model.*; @@ -585,7 +585,7 @@ index b8ba20f5..fd0640c4 100644 import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; import net.raphimc.viabedrock.protocol.storage.EntityPropertyTracker; import net.raphimc.viabedrock.protocol.storage.ChannelStorage; -@@ -80,6 +79,12 @@ public final class EntityPackets { +@@ -81,6 +80,12 @@ public final class EntityPackets { private static final int INTERPOLATION_STEP_TICKS = 3; private static final float PAINTING_POS_OFFSET = -0.46875F; private static final SwingAnimation DEFAULT_SWING_ANIMATION = new SwingAnimation(EnumTypes.SWING_ANIMATION1_21_11.idFromName("whack"), 6); @@ -598,7 +598,7 @@ index b8ba20f5..fd0640c4 100644 static Position3f mergePosition(final Position3f current, final boolean hasX, final float x, final boolean hasY, final float y, final boolean hasZ, final float z) { -@@ -143,7 +148,7 @@ public final class EntityPackets { +@@ -144,7 +149,7 @@ public final class EntityPackets { attributes[i] = new EntityAttribute(name, currentValue, minValue, maxValue); } final EntityData[] entityData = wrapper.read(BedrockTypes.ENTITY_DATA_ARRAY); // entity data @@ -607,7 +607,7 @@ index b8ba20f5..fd0640c4 100644 final EntityLink[] links = wrapper.read(BedrockTypes.ENTITY_LINK_ARRAY); // entity links wrapper.read(BedrockTypes.OPTIONAL_PASSENGER_OF_BLOCKS); // passenger of blocks -@@ -192,7 +197,7 @@ public final class EntityPackets { +@@ -193,7 +198,7 @@ public final class EntityPackets { if (entity instanceof LivingEntity livingEntity) { livingEntity.updateAttributes(attributes); } @@ -616,7 +616,7 @@ index b8ba20f5..fd0640c4 100644 for (EntityLink link : links) { entityTracker.applyEntityLink(link); } -@@ -587,53 +592,6 @@ public final class EntityPackets { +@@ -596,53 +601,6 @@ public final class EntityPackets { wrapper.write(Types.VAR_INT, entity.javaId()); // entity id wrapper.write(VersionedTypes.V26_3.entityDataList, javaEntityData); // entity data }); @@ -670,7 +670,7 @@ index b8ba20f5..fd0640c4 100644 protocol.registerClientbound(ClientboundBedrockPackets.MOB_EFFECT, ClientboundPackets26_3.UPDATE_MOB_EFFECT, wrapper -> { final long entityRuntimeId = wrapper.read(BedrockTypes.UNSIGNED_VAR_LONG); // entity runtime id final MobEffectPacketPayload_Event event = MobEffectPacketPayload_Event.getByValue(wrapper.read(Types.BYTE), MobEffectPacketPayload_Event.Invalid); // event id -@@ -738,8 +696,18 @@ public final class EntityPackets { +@@ -747,8 +705,18 @@ public final class EntityPackets { wrapper.write(Types.BYTE, (byte) EquipmentSlot.MAINHAND.ordinal()); // slot wrapper.write(VersionedTypes.V26_3.item, itemRewriter.javaItem(item)); // item } else if (containerId == ContainerID.CONTAINER_ID_OFFHAND.getValue()) { @@ -689,7 +689,7 @@ index b8ba20f5..fd0640c4 100644 } else { wrapper.cancel(); } -@@ -759,6 +727,10 @@ public final class EntityPackets { +@@ -768,6 +736,10 @@ public final class EntityPackets { wrapper.write(Types.VAR_INT, collectorEntity.javaId()); // collector entity id wrapper.write(Types.VAR_INT, 0); // amount }); 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 875c6f1e..cc3acc0f 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 f30328f70b25d66cd894c6d984636be3d68ba970 Mon Sep 17 00:00:00 2001 +From 901059506f1308a361a2ea84495598e90c4180ec 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 @@ -663,6 +663,12 @@ modifier 4 and limit 64 components. Keep these values in data rather than inferring storage from a Java carrier or a vanilla identifier. Component construction defaults, complete tooltip selection, action routing and installed pixels remain separate comparisons. + +Declare the screen content controls in protocol and legacy-action test +fixtures. Storage now resolves the complete authored host rather than +extracting its selected panel. Preserve the action and title assertions +through that actual screen/content boundary instead of depending on the +former content-only shortcut. --- Data Asset Sources.md | 33 + build.gradle | 1 + @@ -740,13 +746,13 @@ installed pixels remain separate comparisons. .../model/NativeItemDefinitionsTest.java | 207 + .../protocol/model/NativeItemNameTest.java | 61 + .../packet/ModalFormTransitionTest.java | 123 + - .../NativeContainerOpeningPacketTest.java | 485 + + .../NativeContainerOpeningPacketTest.java | 486 + .../protocol/packet/NativeHudPacketTest.java | 160 + .../packet/NpcDialoguePacketTest.java | 301 +- .../packet/WrittenBookUsePacketTest.java | 1 + .../storage/NativeContainerUiStateTest.java | 85 + .../inventory/native-item-storage.json | 568 + - 82 files changed, 36367 insertions(+), 362 deletions(-) + 82 files changed, 36368 insertions(+), 362 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/NativeItemStacking.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeItemStorageData.java @@ -35055,7 +35061,7 @@ index 00000000..3b25d258 +} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/NativeContainerLegacyActionsTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeContainerLegacyActionsTest.java new file mode 100644 -index 00000000..dc791319 +index 00000000..0f582f85 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/model/container/NativeContainerLegacyActionsTest.java @@ -0,0 +1,323 @@ @@ -35295,7 +35301,7 @@ index 00000000..dc791319 + final int columns = kind == ContainerUi.Kind.HOPPER ? 5 : 3; + return ContainerUi.fromProgram(JsonParser.parseString(""" + {"definitions": { -+ "redstone.%s": {"parent":"", "value":{"$screen_content":"redstone.panel"}}, ++ "redstone.%s": {"parent":"", "value":{"type":"screen","controls":[{"content@$screen_content":{}}],"$screen_content":"redstone.panel"}}, + "redstone.panel": {"parent":"", "value":{"type":"grid", "collection_name":"container_items", + "grid_dimensions":[%d,%d], "controls":[{"action":{"type":"button", "grid_position":[%d,%d], + "bindings":[ @@ -35325,7 +35331,7 @@ index 00000000..dc791319 + private static ContainerUi program() { + return ContainerUi.fromProgram(JsonParser.parseString(""" + {"definitions": { -+ "chest.small_chest_screen": {"parent":"", "value":{"$screen_content":"chest.small_chest_panel"}}, ++ "chest.small_chest_screen": {"parent":"", "value":{"type":"screen","controls":[{"content@$screen_content":{}}],"$screen_content":"chest.small_chest_panel"}}, + "chest.small_chest_panel": {"parent":"", "value":{"type":"grid", "collection_name":"container_items", + "grid_dimensions":[27,1], "controls":[{"action":{"type":"button", "grid_position":[9,0], + "bindings":[ @@ -37060,10 +37066,10 @@ index 00000000..51cdb2bf +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/NativeContainerOpeningPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/NativeContainerOpeningPacketTest.java new file mode 100644 -index 00000000..3472fd3b +index 00000000..36598bdc --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/NativeContainerOpeningPacketTest.java -@@ -0,0 +1,485 @@ +@@ -0,0 +1,486 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -37167,8 +37173,8 @@ index 00000000..3472fd3b + content.putString("manifest.json", "{\"format_version\":2,\"header\":{\"uuid\":\"" + UUID.randomUUID() + + "\",\"version\":[1,0,0],\"name\":\"test\"}}"); + content.putString("ui/chest_screen.json", """ -+ {"namespace":"chest","small_chest_screen":{"$screen_content":"chest.small_chest_panel"}, -+ "large_chest_screen":{"$screen_content":"chest.large_chest_panel"}, ++ {"namespace":"chest","small_chest_screen":{"type":"screen","controls":[{"content@$screen_content":{}}],"$screen_content":"chest.small_chest_panel"}, ++ "large_chest_screen":{"type":"screen","controls":[{"content@$screen_content":{}}],"$screen_content":"chest.large_chest_panel"}, + "small_chest_panel":{"type":"panel","controls":[{"caption":{"type":"label","text":"$container_title","localize":"$localize_title"}}]}, + "large_chest_panel@chest.small_chest_panel":{}} + """); @@ -37366,7 +37372,8 @@ index 00000000..3472fd3b + final var scene = ContainerUi.fromProgram(program).resolve(new ContainerUi.MenuView(8, + ContainerUi.Kind.CHEST_3_ROWS, "Java caption must not replace raw conditions", slots, Map.of(), List.of())); + assertNotNull(scene); -+ final var caption = scene.getAsJsonObject("root").getAsJsonArray("children").get(0).getAsJsonObject(); ++ final var content = scene.getAsJsonObject("root").getAsJsonArray("children").get(0).getAsJsonObject(); ++ final var caption = content.getAsJsonArray("children").get(0).getAsJsonObject(); + assertEquals(filtered.equals("actor.key") ? displayed : selected, caption.get("text").getAsString()); + if (!filtered.isEmpty()) { + actor.entityData().put(ActorDataIds.FILTERED_NAME, new EntityData(132, EntityDataTypesBedrock.STRING, "Later update")); diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch index e17962dd..f765e75d 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.patch @@ -1,4 +1,4 @@ -From b9653dadb51b0c19ac1ae17e12bc8e9d61294bf4 Mon Sep 17 00:00:00 2001 +From 4848b046be5a57d29dd4cdeace1a429f07317806 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): share bounded native UI programs and resource @@ -442,6 +442,45 @@ within-pack and cross-pack priority in both directions, and resolve the same image and metadata after UI program transport. Pinned native source assets establish JPEG use; actual rendering and full installed pack admission remain end-to-end checks. + +Preserve original local-player ActorEvent hurt causes in a negotiated, +versioned UI channel, bound to the native actor lifetime. Forward events +2 and 81 before Java damage mappings can merge their causes. Ordinary +clients and add-ons without the actor-state contract keep existing paths; +remote actor hurt never requests a local screen check. + +Target Bedrock 1.26.60.5 execution confirms the native player pending byte +and counter with hardware data watchpoints. Selected causes queue one +check. The next player update consumes it, evaluates the current scene +only at counter zero, sets 100 even for a false policy, clears pending, +and decrements the positive counter. Immediate healing does not cancel +the check. Fire and health-boost removal do not queue it in the controls. + +Publish that player-owned state separately from screen lifetime. Retire +pending state and its counter with the player, not with an inventory +opening. Tests cover original cause filtering, both event kinds, rapid +check consumption, the false-policy gate, retirement and strict wire +framing. The original no-damage event branch is statically established; +the owned vanilla BDS fixture emits event 2 for the live controls. + +Resolve storage through its complete authored host instead of selecting +only $screen_content. Keep the native backdrop, screen animation managers, +close policy and host controls around the actual item collections. Export +the host's real definition closure instead of requiring an unused fallback +panel. Share pointer close-button visibility with the base controller. + +Advertise the native hurt policy contract in exported programs and carry +its availability into resolved hurt-enabled roots. Imported legacy +programs cannot enable it through an authored property. This lets clients +retain ordinary controls when an older core cannot publish original hurt +causes, without inferring capability from a Java damage packet. + +Storage regressions retain all slot addresses across complete-host +transport and validate the native translucent backdrop, its animation +program and the host policy. Synthetic fixtures now declare real screen +content controls rather than relying on the old content-only resolver. +A contract boundary control rejects unknown policy versions and prevents +legacy programs from claiming the new host behavior. --- build.gradle | 11 + .../api/model/NativeContainerAction.java | 93 + @@ -459,9 +498,11 @@ admission remain end-to-end checks. .../api/model/NativeItemHoverText.java | 106 + .../viabedrock/api/model/NativeItemNames.java | 69 + .../api/model/NativeNpcDialogueMessage.java | 60 + + .../api/model/NativeUiHurtEvent.java | 55 + + .../api/model/NativeUiHurtState.java | 48 + .../http/ConvertedResourcePackCache.java | 6 +- .../api/resourcepack/ui/BrewingStandUi.java | 44 + - .../api/resourcepack/ui/ContainerUi.java | 511 +++++ + .../api/resourcepack/ui/ContainerUi.java | 508 +++++ .../resourcepack/ui/CookingNavigation.java | 83 + .../resourcepack/ui/CookingRecipeOrder.java | 62 + .../resourcepack/ui/CookingSearchText.java | 45 + @@ -473,12 +514,12 @@ admission remain end-to-end checks. .../api/resourcepack/ui/HudTitleUi.java | 28 + .../resourcepack/ui/InventoryItemFilter.java | 43 + .../resourcepack/ui/InventoryStackCount.java | 21 + - .../api/resourcepack/ui/JsonUiProgram.java | 1703 +++++++++++++++++ + .../api/resourcepack/ui/JsonUiProgram.java | 1720 +++++++++++++++++ .../resourcepack/ui/NativeRecipeSearch.java | 124 ++ .../ui/NativeSearchNormalization.java | 189 ++ .../api/resourcepack/ui/NpcDialogueUi.java | 316 +++ .../resourcepack/ui/NpcEditorBindings.java | 329 ++++ - .../api/resourcepack/ui/NpcEditorState.java | 322 ++++ + .../api/resourcepack/ui/NpcEditorState.java | 322 +++ .../api/resourcepack/ui/RecipeCollection.java | 266 +++ .../api/resourcepack/ui/ServerFormUi.java | 253 +++ .../resourcepack/ui/UiAsepriteMetadata.java | 95 + @@ -489,6 +530,7 @@ admission remain end-to-end checks. .../api/resourcepack/ui/UiTextureLimits.java | 24 + .../api/resourcepack/ui/UiTextures.java | 101 + .../viabedrock/api/util/NativeSort.java | 168 ++ + .../protocol/packet/EntityPackets.java | 9 + .../rewriter/ResourcePackRewriter.java | 2 + .../GlyphSheetResourceRewriter.java | 64 +- .../resourcepack/JsonUiResourceRewriter.java | 36 + @@ -508,8 +550,9 @@ admission remain end-to-end checks. .../api/model/NativeItemHoverTextTest.java | 56 + .../api/model/NativeItemNamesTest.java | 123 ++ .../model/NativeNpcDialogueMessageTest.java | 59 + + .../api/model/NativeUiHurtStateTest.java | 81 + .../resourcepack/ui/BrewingStandUiTest.java | 49 + - .../api/resourcepack/ui/ContainerUiTest.java | 403 ++++ + .../api/resourcepack/ui/ContainerUiTest.java | 429 ++++ .../ui/CookingNavigationTest.java | 87 + .../resourcepack/ui/CookingProgressTest.java | 47 + .../ui/CookingRecipeOrderTest.java | 82 + @@ -526,7 +569,7 @@ admission remain end-to-end checks. .../resourcepack/ui/RecipeCollectionTest.java | 227 +++ .../ui/ServerCustomFormUiTest.java | 146 ++ .../ui/ServerFormUiSliderStateTest.java | 126 ++ - .../api/resourcepack/ui/ServerFormUiTest.java | 427 +++++ + .../api/resourcepack/ui/ServerFormUiTest.java | 427 ++++ .../ui/UiAsepriteMetadataTest.java | 112 ++ .../resourcepack/ui/UiBitmapFontsTest.java | 285 +++ .../ui/UiCollectionWindowTest.java | 131 ++ @@ -535,7 +578,7 @@ admission remain end-to-end checks. .../api/resourcepack/ui/UiTexturesTest.java | 231 +++ .../ui/native-inventory-item-filter.json | 311 +++ .../generator/SearchUnicodeDataGenerator.java | 126 ++ - 92 files changed, 14514 insertions(+), 60 deletions(-) + 96 files changed, 14747 insertions(+), 60 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerData.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeContainerMessage.java @@ -551,6 +594,8 @@ admission remain end-to-end checks. create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeItemHoverText.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeItemNames.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeNpcDialogueMessage.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeUiHurtEvent.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativeUiHurtState.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/BrewingStandUi.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUi.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingNavigation.java @@ -596,6 +641,7 @@ admission remain end-to-end checks. create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/NativeItemHoverTextTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/NativeItemNamesTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/NativeNpcDialogueMessageTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/NativeUiHurtStateTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/BrewingStandUiTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUiTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/CookingNavigationTest.java @@ -2031,6 +2077,121 @@ index 00000000..0e2c78ba + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/NativeUiHurtEvent.java b/src/main/java/net/raphimc/viabedrock/api/model/NativeUiHurtEvent.java +new file mode 100644 +index 00000000..d8f446aa +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/NativeUiHurtEvent.java +@@ -0,0 +1,55 @@ ++/* ++ * 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.viaversion.api.type.Type; ++import com.viaversion.viaversion.api.type.Types; ++import io.netty.buffer.ByteBuf; ++import java.util.Objects; ++import java.util.UUID; ++import net.raphimc.viabedrock.protocol.data.ProtocolConstants; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ActorEvent; ++ ++/** Original local-player damage cause, before Java damage registry mappings coalesce causes. */ ++public record NativeUiHurtEvent(UUID lifetime, int event, int cause) { ++ ++ public static final String CHANNEL = "viabedrock:native_ui_hurt_v1"; ++ public static final Type TYPE = new Type<>(NativeUiHurtEvent.class) { ++ @Override ++ public NativeUiHurtEvent read(final ByteBuf buffer) { ++ if (buffer.readableBytes() > 32 || Types.VAR_INT.readPrimitive(buffer) != 1 ++ || Types.VAR_INT.readPrimitive(buffer) != ProtocolConstants.BEDROCK_PROTOCOL_VERSION) { ++ throw new IllegalArgumentException("Unsupported native UI hurt event"); ++ } ++ final var event = new NativeUiHurtEvent(Types.UUID.read(buffer), buffer.readUnsignedByte(), Types.VAR_INT.readPrimitive(buffer)); ++ if (buffer.isReadable()) { ++ throw new IllegalArgumentException("Trailing native UI hurt event data"); ++ } ++ return event; ++ } ++ ++ @Override ++ public void write(final ByteBuf buffer, final NativeUiHurtEvent event) { ++ Types.VAR_INT.writePrimitive(buffer, 1); ++ Types.VAR_INT.writePrimitive(buffer, ProtocolConstants.BEDROCK_PROTOCOL_VERSION); ++ Types.UUID.write(buffer, event.lifetime); ++ buffer.writeByte(event.event); ++ Types.VAR_INT.writePrimitive(buffer, event.cause); ++ } ++ }; ++ ++ public NativeUiHurtEvent { ++ Objects.requireNonNull(lifetime); ++ if (!isHurt(event)) { ++ throw new IllegalArgumentException("Not a native hurt actor event"); ++ } ++ } ++ ++ public static boolean isHurt(final int event) { ++ return event == ActorEvent.HURT.getValue() || event == ActorEvent.HURT_WITHOUT_RECEIVING_DAMAGE.getValue(); ++ } ++ ++} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/NativeUiHurtState.java b/src/main/java/net/raphimc/viabedrock/api/model/NativeUiHurtState.java +new file mode 100644 +index 00000000..f7b0cb2a +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/NativeUiHurtState.java +@@ -0,0 +1,48 @@ ++/* ++ * 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 net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.SharedTypes_Legacy_ActorDamageCause; ++ ++/** Player-owned pending check and update counter, independent of the current screen's lifetime. */ ++public final class NativeUiHurtState { ++ ++ private boolean pending; ++ private int counter; ++ ++ public void accept(final NativeUiHurtEvent event) { ++ final var cause = SharedTypes_Legacy_ActorDamageCause.getByValue(event.cause()); ++ final boolean triggersCheck = switch (cause) { ++ case EntityAttack, Projectile, EntityExplosion, Magic, SonicBoom, MaceSmash -> true; ++ case null, default -> false; ++ }; ++ if (triggersCheck) { ++ this.pending = true; ++ } ++ } ++ ++ /** Consume the pending check, then decrement a positive counter during this player update. */ ++ public boolean tick(final boolean closeOnPlayerHurt) { ++ boolean close = false; ++ if (this.pending) { ++ if (this.counter < 1) { ++ close = closeOnPlayerHurt; ++ this.counter = 100; ++ } ++ this.pending = false; ++ } ++ if (this.counter > 0) { ++ this.counter--; ++ } ++ return close; ++ } ++ ++ public void clear() { ++ this.pending = false; ++ this.counter = 0; ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/http/ConvertedResourcePackCache.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/http/ConvertedResourcePackCache.java index f7460be9..7c48ba83 100644 --- a/src/main/java/net/raphimc/viabedrock/api/resourcepack/http/ConvertedResourcePackCache.java @@ -2115,10 +2276,10 @@ index 00000000..a823211d +} 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 new file mode 100644 -index 00000000..cd1d083d +index 00000000..76e62199 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUi.java -@@ -0,0 +1,511 @@ +@@ -0,0 +1,508 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -2181,10 +2342,9 @@ index 00000000..cd1d083d + } + try { + final String host = kind.host; -+ final String fallback = kind.panel; -+ final JsonObject exported = this.program.export(Set.of(host, fallback), new JsonPrimitive(title.raw)); ++ final JsonObject exported = this.program.export(Set.of(host), new JsonPrimitive(title.raw)); + final JsonObject definitions = exported.getAsJsonObject("definitions"); -+ if (!definitions.has(host) || !definitions.has(fallback)) { ++ if (!definitions.has(host)) { + return null; + } + exported.addProperty("condition_title", title.raw); @@ -2210,8 +2370,7 @@ index 00000000..cd1d083d + JsonObject resolveScene(final MenuView menu) { + final Controller controller = new Controller(menu, this.title == null ? new Title(menu.title, false) : this.title, this.nativeActions); + final String host = menu.kind.host; -+ final String fallback = menu.kind.panel; -+ final var resolved = this.program.resolveScreen(host, fallback, controller); ++ final var resolved = this.program.resolve(host, controller); + if (resolved.root() == null) { + return null; + } @@ -2304,26 +2463,24 @@ index 00000000..cd1d083d + + public enum Kind { + -+ CHEST_3_ROWS(0, 27, "chest.small_chest_screen", "chest.small_chest_panel"), -+ CHEST_6_ROWS(1, 54, "chest.large_chest_screen", "chest.large_chest_panel"), -+ ENDER_CHEST(2, 27, "chest.ender_chest_screen", "chest.ender_chest_panel"), -+ SHULKER_BOX(3, 27, "chest.shulker_box_screen", "chest.shulker_box_panel"), -+ BARREL(4, 27, "chest.barrel_screen", "chest.barrel_panel"), -+ HOPPER(5, 5, "redstone.hopper_screen", "redstone.panel"), -+ DISPENSER(6, 9, "redstone.dispenser_screen", "redstone.panel"), -+ DROPPER(7, 9, "redstone.dropper_screen", "redstone.panel"), -+ BREWING_STAND(8, 5, "brewing_stand.brewing_stand_screen", "brewing_stand.brewing_stand_panel"); ++ CHEST_3_ROWS(0, 27, "chest.small_chest_screen"), ++ CHEST_6_ROWS(1, 54, "chest.large_chest_screen"), ++ ENDER_CHEST(2, 27, "chest.ender_chest_screen"), ++ SHULKER_BOX(3, 27, "chest.shulker_box_screen"), ++ BARREL(4, 27, "chest.barrel_screen"), ++ HOPPER(5, 5, "redstone.hopper_screen"), ++ DISPENSER(6, 9, "redstone.dispenser_screen"), ++ DROPPER(7, 9, "redstone.dropper_screen"), ++ BREWING_STAND(8, 5, "brewing_stand.brewing_stand_screen"); + + private final int wireId; + private final int containerSlots; + private final String host; -+ private final String panel; + -+ Kind(final int wireId, final int containerSlots, final String host, final String panel) { ++ Kind(final int wireId, final int containerSlots, final String host) { + this.wireId = wireId; + this.containerSlots = containerSlots; + this.host = host; -+ this.panel = panel; + } + + public int wireId() { @@ -2448,6 +2605,7 @@ index 00000000..cd1d083d + 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("#close_button_visible", new JsonPrimitive(true)); + this.globals.putAll(cursor); + final JsonObject cursorReference = new JsonObject(); + cursorReference.addProperty("cursor", true); @@ -3890,10 +4048,10 @@ index 00000000..5dfbb3f8 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java new file mode 100644 -index 00000000..5a5b7d16 +index 00000000..05f99df4 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/JsonUiProgram.java -@@ -0,0 +1,1703 @@ +@@ -0,0 +1,1720 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -3952,8 +4110,10 @@ index 00000000..5a5b7d16 + private final Set customizedFiles = new HashSet<>(); + private final UnaryOperator translator; + private final Map formattingColors; ++ private final boolean nativeHurtPolicy; + + public JsonUiProgram(final ResourcePackStorage storage) { ++ this.nativeHurtPolicy = true; + this.translator = storage.getTexts()::translate; + this.formattingColors = storage.getTextFormatting().colors(); + final Map builtin = Builtins.FILES; @@ -4015,6 +4175,12 @@ index 00000000..5a5b7d16 + + private JsonUiProgram(final List> layers, final UnaryOperator translator, + final Map formattingColors) { ++ this(layers, translator, formattingColors, true); ++ } ++ ++ private JsonUiProgram(final List> layers, final UnaryOperator translator, ++ final Map formattingColors, final boolean nativeHurtPolicy) { ++ this.nativeHurtPolicy = nativeHurtPolicy; + this.translator = translator; + this.formattingColors = formattingColors; + for (final Map files : layers) { @@ -4051,8 +4217,11 @@ index 00000000..5a5b7d16 + colors.put(entry.getKey().charAt(0), entry.getValue().getAsInt()); + }); + } ++ if (program.has("native_hurt_policy") && !new JsonPrimitive(1).equals(program.get("native_hurt_policy"))) { ++ throw new IllegalArgumentException("Unsupported native UI hurt policy"); ++ } + final JsonUiProgram ui = new JsonUiProgram(List.of(), -+ key -> translations.has(key) ? translations.get(key).getAsString() : key, Map.copyOf(colors)); ++ key -> translations.has(key) ? translations.get(key).getAsString() : key, Map.copyOf(colors), program.has("native_hurt_policy")); + final JsonObject definitions = program.getAsJsonObject("definitions"); + if (definitions.size() > MAX_NODES) { + throw new IllegalArgumentException("Too many UI definitions"); @@ -4114,6 +4283,9 @@ index 00000000..5a5b7d16 + public Resolution resolve(final String reference, final UiBindings bindings) { + final Session session = new Session(bindings); + final JsonObject root = session.expand(reference, new JsonObject(), session.initial, null, null, 0); ++ if (root != null && UiExpression.truth(root.get("close_on_player_hurt"))) { ++ root.addProperty("native_hurt_policy", this.nativeHurtPolicy ? 1 : 0); ++ } + return new Resolution(root, Set.copyOf(session.requiredDefinitions), Set.copyOf(session.missingTemplates)); + } + @@ -4320,6 +4492,9 @@ index 00000000..5a5b7d16 + } + } + final JsonObject result = new JsonObject(); ++ if (this.nativeHurtPolicy) { ++ result.addProperty("native_hurt_policy", 1); ++ } + final JsonObject definitions = new JsonObject(); + for (final var entry : this.definitions.entrySet()) { + if (closure.contains(entry.getKey())) { @@ -8827,6 +9002,33 @@ index 00000000..20278d84 + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +index b8ba20f5..9c4b8cec 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +@@ -36,6 +36,7 @@ import com.viaversion.viaversion.api.type.types.version.VersionedTypes; + import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets26_3; + import com.viaversion.viaversion.util.Key; + import net.raphimc.viabedrock.ViaBedrock; ++import net.raphimc.viabedrock.api.model.NativeUiHurtEvent; + import net.raphimc.viabedrock.api.model.NativeActorMessage; + import net.raphimc.viabedrock.api.model.ActorMotionCommand; + import net.raphimc.viabedrock.api.model.MovementCorrectionTiming; +@@ -494,6 +495,14 @@ public final class EntityPackets { + wrapper.cancel(); + return; + } ++ final ChannelStorage channels = wrapper.user().get(ChannelStorage.class); ++ if (entity == entityTracker.getClientPlayer() && NativeUiHurtEvent.isHurt(rawEvent) ++ && channels.hasChannel(NativeUiHurtEvent.CHANNEL) && NativeActorMessage.hasStateChannel(channels::hasChannel)) { ++ final PacketWrapper hurt = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, wrapper.user()); ++ hurt.write(Types.STRING, NativeUiHurtEvent.CHANNEL); ++ hurt.write(NativeUiHurtEvent.TYPE, new NativeUiHurtEvent(entity.nativeActorSnapshot().lifetime(), rawEvent, data)); ++ hurt.send(BedrockProtocol.class); ++ } + switch (event) { + case HURT -> { + final CompoundTag damageTypeRegistry = gameSession.getJavaRegistries().getCompoundTag(RegistryKeys.DAMAGE_TYPE); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ResourcePackRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ResourcePackRewriter.java index 245bb449..88eb0f0c 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ResourcePackRewriter.java @@ -11350,6 +11552,93 @@ index 00000000..15e84626 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/api/model/NativeUiHurtStateTest.java b/src/test/java/net/raphimc/viabedrock/api/model/NativeUiHurtStateTest.java +new file mode 100644 +index 00000000..55e246cf +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/model/NativeUiHurtStateTest.java +@@ -0,0 +1,81 @@ ++/* ++ * 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 io.netty.buffer.Unpooled; ++import java.util.UUID; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++final class NativeUiHurtStateTest { ++ ++ @Test ++ void consumesRapidChecksWithoutDeferringThemUntilTheSharedCounterExpires() { ++ final var state = new NativeUiHurtState(); ++ final var hurt = new NativeUiHurtEvent(UUID.randomUUID(), 2, 14); ++ state.accept(hurt); ++ assertTrue(state.tick(true)); ++ for (int tick = 0; tick < 98; tick++) { ++ assertFalse(state.tick(true)); ++ } ++ state.accept(hurt); ++ assertFalse(state.tick(true)); ++ assertFalse(state.tick(true)); ++ state.accept(hurt); ++ assertTrue(state.tick(true)); ++ } ++ ++ @Test ++ void falsePolicyStillStartsTheCounterAndPlayerRetirementClearsIt() { ++ final var state = new NativeUiHurtState(); ++ final var hurt = new NativeUiHurtEvent(UUID.randomUUID(), 2, 2); ++ state.accept(hurt); ++ assertFalse(state.tick(false)); ++ state.accept(hurt); ++ assertFalse(state.tick(true)); ++ state.clear(); ++ assertFalse(state.tick(true)); ++ state.accept(hurt); ++ assertTrue(state.tick(true)); ++ } ++ ++ @Test ++ void filtersOriginalCausesForBothNativeHurtEvents() { ++ final var state = new NativeUiHurtState(); ++ final UUID lifetime = UUID.randomUUID(); ++ for (final int event : new int[]{2, 81}) { ++ for (final int cause : new int[]{6, 10, 35, -1, Integer.MAX_VALUE}) { ++ state.accept(new NativeUiHurtEvent(lifetime, event, cause)); ++ assertFalse(state.tick(true)); ++ } ++ for (final int cause : new int[]{2, 3, 11, 14, 31, 34}) { ++ state.clear(); ++ state.accept(new NativeUiHurtEvent(lifetime, event, cause)); ++ assertTrue(state.tick(true)); ++ } ++ state.clear(); ++ } ++ } ++ ++ @Test ++ void wireRetainsActorLifetimeAndRawCauseAndRejectsTrailingOrNonHurtEvents() { ++ final var buffer = Unpooled.buffer(); ++ try { ++ final var event = new NativeUiHurtEvent(UUID.randomUUID(), 81, -1); ++ NativeUiHurtEvent.TYPE.write(buffer, event); ++ assertEquals(event, NativeUiHurtEvent.TYPE.read(buffer)); ++ buffer.clear(); ++ NativeUiHurtEvent.TYPE.write(buffer, event); ++ buffer.writeByte(0); ++ assertThrows(IllegalArgumentException.class, () -> NativeUiHurtEvent.TYPE.read(buffer)); ++ assertThrows(IllegalArgumentException.class, () -> new NativeUiHurtEvent(event.lifetime(), 3, 14)); ++ } finally { ++ buffer.release(); ++ } ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/BrewingStandUiTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/BrewingStandUiTest.java new file mode 100644 index 00000000..4b21c99a @@ -11407,10 +11696,10 @@ index 00000000..4b21c99a +} diff --git a/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUiTest.java b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUiTest.java new file mode 100644 -index 00000000..7e5618a1 +index 00000000..24333fa6 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/resourcepack/ui/ContainerUiTest.java -@@ -0,0 +1,403 @@ +@@ -0,0 +1,429 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -11420,6 +11709,7 @@ index 00000000..7e5618a1 + +import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; +import com.viaversion.viaversion.libs.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonParser; +import com.viaversion.viaversion.libs.gson.JsonPrimitive; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; @@ -11434,9 +11724,9 @@ index 00000000..7e5618a1 + + private static final String UI = """ + {"namespace":"chest", -+ "small_chest_screen":{"$screen_content":"chest.panel","variables":[ ++ "small_chest_screen":{"type":"screen","controls":[{"content@$screen_content":{}}],"$screen_content":"chest.panel","variables":[ + {"requires":"($container_title = 'Alternative')","$screen_content":"chest.alternative"}]}, -+ "large_chest_screen":{"$screen_content":"chest.panel"}, ++ "large_chest_screen":{"type":"screen","controls":[{"content@$screen_content":{}}],"$screen_content":"chest.panel"}, + "alternative@chest.panel":{"size":[220,180]}, + "small_chest_panel@chest.panel":{},"large_chest_panel@chest.panel":{}, + "panel":{"type":"panel","controls":[ @@ -11483,6 +11773,25 @@ index 00000000..7e5618a1 + return result; + } + ++ private static JsonObject content(final JsonObject scene) { ++ return scene.getAsJsonObject("root").getAsJsonArray("children").get(0).getAsJsonObject(); ++ } ++ ++ @Test ++ void importedLegacyProgramsCannotEnableHurtPolicyThroughAuthoredProperties() { ++ final var document = JsonParser.parseString(UI).getAsJsonObject(); ++ document.getAsJsonObject("small_chest_screen").addProperty("close_on_player_hurt", true); ++ document.getAsJsonObject("small_chest_screen").addProperty("native_hurt_policy", 1); ++ final var ui = new ContainerUi(new JsonUiProgram(List.of(Map.of("ui/chest_screen.json", document.toString().getBytes(StandardCharsets.UTF_8))))); ++ final var view = menu(ContainerUi.Kind.CHEST_3_ROWS, "Actual", 0); ++ final var program = ui.exportProgram(view.kind(), new ContainerUi.Title(view.title(), false)); ++ assertEquals(1, ContainerUi.fromProgram(program).resolve(view).getAsJsonObject("root").get("native_hurt_policy").getAsInt()); ++ program.remove("native_hurt_policy"); ++ assertEquals(0, ContainerUi.fromProgram(program).resolve(view).getAsJsonObject("root").get("native_hurt_policy").getAsInt()); ++ program.addProperty("native_hurt_policy", 2); ++ assertThrows(IllegalArgumentException.class, () -> ContainerUi.fromProgram(program)); ++ } ++ + @Test + void bundledStorageFamiliesRetainActualSlotBindingsAcrossTransport() { + org.junit.jupiter.api.Assumptions.assumeTrue( @@ -11494,6 +11803,12 @@ index 00000000..7e5618a1 + assertNotNull(exported, kind.name()); + final var scene = ui.resolveScene(view); + assertNotNull(scene, kind.name()); ++ assertTrue(scene.getAsJsonObject("root").get("close_on_player_hurt").getAsBoolean(), kind.name()); ++ assertEquals(1, scene.getAsJsonObject("root").get("native_hurt_policy").getAsInt(), kind.name()); ++ final var backdrop = nodes(scene.getAsJsonObject("root")).stream() ++ .filter(node -> "bg_no_safezone_screen_panel".equals(JsonUiProgram.string(node, "name"))).findFirst().orElseThrow(); ++ assertEquals(.4F, backdrop.get("alpha").getAsFloat(), kind.name()); ++ assertTrue(backdrop.has("animation_program"), kind.name()); + assertTrue(scene.getAsJsonArray("missing_templates").isEmpty(), kind.name()); + final Set slots = nodes(scene.getAsJsonObject("root")).stream() + .filter(node -> node.has("inventory_slot")) @@ -11612,7 +11927,7 @@ index 00000000..7e5618a1 + final var first = ui().resolve(menu(ContainerUi.Kind.CHEST_3_ROWS, "Initial", 16)); + final var second = ui().resolve(menu(ContainerUi.Kind.CHEST_3_ROWS, "Alternative", 8)); + assertNotNull(second); -+ assertEquals(220, second.getAsJsonObject("root").getAsJsonArray("size").get(0).getAsInt()); ++ assertEquals(220, content(second).getAsJsonArray("size").get(0).getAsInt()); + for (final var scene : List.of(first, second)) { + final var nodes = nodes(scene.getAsJsonObject("root")); + final var count = nodes.stream().filter(node -> node.has("inventory_slot") && node.get("inventory_slot").getAsInt() == 0 @@ -11652,7 +11967,7 @@ index 00000000..7e5618a1 + assertNotNull(exported); + final var scene = ContainerUi.fromProgram(exported).resolve(menu(ContainerUi.Kind.CHEST_3_ROWS, "Unrelated Java caption", 8)); + assertNotNull(scene); -+ assertEquals(220, scene.getAsJsonObject("root").getAsJsonArray("size").get(0).getAsInt()); ++ assertEquals(220, content(scene).getAsJsonArray("size").get(0).getAsInt()); + final var caption = nodes(scene.getAsJsonObject("root")).stream() + .filter(node -> "title".equals(JsonUiProgram.string(node, "name"))).findFirst().orElseThrow(); + assertEquals(localize ? "Translated caption" : "Alternative", caption.get("text").getAsString()); @@ -11690,8 +12005,8 @@ index 00000000..7e5618a1 + @Test + void partialPackLabelsRetainCountSourcesAndTitlesForBothChestFamilies() { + final String lower = """ -+ {"namespace":"chest","small_chest_screen":{"$screen_content":"chest.panel"}, -+ "large_chest_screen":{"$screen_content":"chest.panel"}, ++ {"namespace":"chest","small_chest_screen":{"type":"screen","controls":[{"content@$screen_content":{}}],"$screen_content":"chest.panel"}, ++ "large_chest_screen":{"type":"screen","controls":[{"content@$screen_content":{}}],"$screen_content":"chest.panel"}, + "small_chest_panel@chest.panel":{},"large_chest_panel@chest.panel":{}, + "panel":{"type":"panel","controls":[{"title@chest.title":{}}, + {"external":{"type":"grid","collection_name":"container_items","grid_item_template":"chest.cell"}}, @@ -11729,7 +12044,7 @@ index 00000000..7e5618a1 + + private static ContainerUi manualGrid(final String controls) { + final String definitions = """ -+ {"namespace":"chest","small_chest_screen":{"$screen_content":"chest.panel"}, ++ {"namespace":"chest","small_chest_screen":{"type":"screen","controls":[{"content@$screen_content":{}}],"$screen_content":"chest.panel"}, + "panel":{"type":"grid","collection_name":"container_items","grid_dimensions":[1000000,1], + "maximum_grid_items":1,"controls":%s},"small_chest_panel@chest.panel":{}, + "cell":{"type":"button","button_mappings":[{"to_button_id":"button.container_take_all_place_all"}], @@ -11750,7 +12065,7 @@ index 00000000..7e5618a1 + final var view = menu(ContainerUi.Kind.CHEST_3_ROWS, "Actual", 16); + final var scene = ui.resolve(view); + assertNotNull(scene); -+ final var cells = scene.getAsJsonObject("root").getAsJsonArray("children"); ++ final var cells = content(scene).getAsJsonArray("children"); + assertEquals(3, cells.size()); + for (int index = 0; index < 3; index++) { + final var cell = cells.get(index).getAsJsonObject(); @@ -11779,7 +12094,7 @@ index 00000000..7e5618a1 + """); + final var scene = ui.resolve(menu(ContainerUi.Kind.CHEST_3_ROWS, "Actual", 16)); + assertNotNull(scene); -+ final var nested = scene.getAsJsonObject("root").getAsJsonArray("children").get(0).getAsJsonObject(); ++ final var nested = content(scene).getAsJsonArray("children").get(0).getAsJsonObject(); + assertEquals(2, nested.get("inventory_slot").getAsInt()); + final var children = nested.getAsJsonArray("children"); + final var selected = children.get(0).getAsJsonObject(); diff --git a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md index 57a51c3a..dccaea77 100644 --- a/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md +++ b/patches/viabedrock/upstreamable/0095-share-native-ui-programs-and-resources.pr.md @@ -14,6 +14,18 @@ Prerequisites: ## Validation and remaining limits +Storage programs export and resolve the complete host graph, including backdrop, close controls, and animation tracks. +Synthetic screen fixtures declare their content controls explicitly. +The bundled-family regression retains authoritative slots and the host hurt policy across program transport. + +The versioned hurt payload retains the original cause and local actor lifetime before Java damage translation. +Native Bedrock 1.26.60.5 runtime controls establish the cause filter, pending-state consumption, and 100-update gate. +A false screen policy still starts the gate. Gated events do not become delayed closures. +Regressions cover that order, both hurt event IDs, strict decoding, and unknown causes. +Programs without the core contract cannot advertise supported hurt policy. +Installed direct and ViaProxy controls preserve item totals and the shared gate across screen changes. +Client-predicted damage and the live producer for event 81 remain unverified. + Grid factories populate each collection once and respect the bound maximum item count. Relative cell templates use the defining namespace. The captured Lifeboat Navigator now resolves eight main cells, matching native Bedrock 1.26.60.5, instead of 46 duplicated cells. Reduced regressions cover factory ownership, template lookup, and item limits. diff --git a/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.patch b/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.patch index 4608a2a4..2f81aaeb 100644 --- a/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.patch +++ b/patches/viabedrock/upstreamable/0105-ignore-removals-for-absent-unmapped-effects.patch @@ -1,4 +1,4 @@ -From 3da144d089da71bc3ac62d5aed7bf594e89dc4b6 Mon Sep 17 00:00:00 2001 +From 86b7b04308a33aade7cb9884538cea97e197c9b4 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:36 +0100 Subject: [PATCH] fix(effects): ignore removals for absent unmapped effects @@ -32,10 +32,10 @@ probes can crash and do not establish behavior for arbitrary integers. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/MobEffectPacketTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java -index fd0640c4..619ae050 100644 +index 1c23b759..4578a73f 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java -@@ -609,7 +609,12 @@ public final class EntityPackets { +@@ -618,7 +618,12 @@ public final class EntityPackets { } final String bedrockIdentifier = BedrockProtocol.MAPPINGS.getBedrockEffects().inverse().get(effectId); 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 53fe4851..ef02ad88 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 d4b47610b67aefa54a002c02d4de4130fc9dcdcf Mon Sep 17 00:00:00 2001 +From 28e0ae4fc297df0fc4c6aea86ba42e175a4eaa8a 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 @@ -281,12 +281,12 @@ Retain cooking screen hosts as real scene controls and admit supported host-owned navigation through the same retained input component state as buttons and input panels. Preserve the root across authoritative updates so a held activation does not retrigger. Use explicit authored screen graphs -in interaction fixtures. Reject the unimplemented player-hurt close policy -before enabling a screen instead of silently dropping that host behavior. +in interaction fixtures. Require the versioned core contract for an +active player-hurt close policy before enabling the screen. The host-navigation regression fails against the prior content-only core and exercises press, update-while-held, release and subsequent activation. -Full default host event routing, held-input/reset, player-hurt handling, -search/edit controls, devices and native pixels remain required work. +Full default host event routing, held-input/reset, search/edit controls, +devices and native pixels remain required work. Route authored VIEW arrow activations through retained input components instead of navigating directly on Java key callbacks. Repeat and release @@ -629,11 +629,24 @@ Escape controls. The default authored container tracks provide the exit duration; no host timeout substitutes for their completion event. Tests cover deferred closure, input retirement, one-time completion, -replacement screens and retired menu ownership. The separately observed -native hurt policy uses a cause filter and a queued 100-count gate. Keep -the cooking host-policy guard until that complete policy and required -bindings are implemented and compared; do not replace it with raw health -sampling or unconditional hurt closing. +replacement screens and retired menu ownership. + +Apply close_on_player_hurt through the shared player-owned pending state, +original server damage cause and 100-update gate. Native Bedrock 1.26.60.5 +runtime controls establish that a false scene policy still starts the gate +and that gated events are consumed rather than deferred. Use the existing +pop-exit lifecycle instead of sampling health or closing on every hurt. +Admit that policy only when the program advertises the core hurt contract. +Legacy cores retain ordinary controls instead of omitting the policy. +Keep the separate controller and visible-control gates for unfinished +cooking bindings. Client-predicted damage and exact frame timing remain +unverified. + +Storage presentation now receives the complete native host graph, +including background and animation controls. Test fixtures declare their +screen content controls explicitly. The cooking boundary test covers +both negotiated policy admission and ordinary fallback without the core +contract, before any inventory input is dispatched. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -647,7 +660,7 @@ sampling or unconditional hurt closing. .../render/MixinNativeGuiItemState.java | 20 + .../render/MixinNativeGuiOversizedItem.java | 31 + .../container/BedrockFurnacePreferences.java | 60 + - .../BedrockNativeContainerScreen.java | 1302 +++++++++++++++ + .../BedrockNativeContainerScreen.java | 1307 +++++++++++++++ .../container/BedrockNativeContainers.java | 282 ++++ .../container/BedrockNativeCooking.java | 147 ++ .../BedrockNativePlayerInventory.java | 157 ++ @@ -694,7 +707,7 @@ sampling or unconditional hurt closing. .../settings/impl/BedrockSettings.java | 4 + .../viafabricplus-bedrock/lang/en_us.json | 1 + .../viafabricplus-bedrock.mixins.json | 9 +- - .../BedrockNativeContainerScreenTest.java | 676 ++++++++ + .../BedrockNativeContainerScreenTest.java | 681 ++++++++ .../container/NativeContainerButtonTest.java | 280 ++++ .../container/NativeContainerDrawsTest.java | 70 + .../NativeContainerGeometryTest.java | 389 +++++ @@ -705,7 +718,7 @@ sampling or unconditional hurt closing. .../container/NativeCookingActionsTest.java | 346 ++++ ...tiveCookingPresentationControllerTest.java | 216 +++ .../NativeCookingPresentationTest.java | 144 ++ - .../container/NativeCookingScreenTest.java | 1404 +++++++++++++++++ + .../container/NativeCookingScreenTest.java | 1409 +++++++++++++++++ .../NativeFurnacePreferenceStateTest.java | 45 + .../container/NativeInventoryFlightsTest.java | 70 + .../NativeInventoryItemPresentationTest.java | 126 ++ @@ -715,7 +728,7 @@ sampling or unconditional hurt closing. .../NativePlayerPresentationTest.java | 153 ++ .../container/NativePlayerScreenTest.java | 269 ++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 80 files changed, 10431 insertions(+), 62 deletions(-) + 80 files changed, 10446 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 @@ -794,11 +807,11 @@ sampling or unconditional hurt closing. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenLifecycleTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 376b15f..7c832ac 100644 +index fe230de..6071d4a 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -@@ -103,6 +103,8 @@ import org.apache.logging.log4j.Logger; - import com.viaversion.viafabricplus.bedrock.screen.ui.BedrockHudOverlays; +@@ -104,6 +104,8 @@ import com.viaversion.viafabricplus.bedrock.screen.ui.BedrockHudOverlays; + import com.viaversion.viafabricplus.bedrock.screen.ui.BedrockNativeUiHurt; import com.viaversion.viafabricplus.bedrock.screen.form.BedrockNativeForms; import com.viaversion.viafabricplus.bedrock.screen.npc.BedrockNativeNpcs; +import com.viaversion.viafabricplus.bedrock.screen.container.BedrockNativeContainers; @@ -806,9 +819,9 @@ index 376b15f..7c832ac 100644 public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { -@@ -125,6 +127,8 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { - BedrockNativeForms.register(); +@@ -127,6 +129,8 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { BedrockHudOverlays.register(); + BedrockNativeUiHurt.register(); BedrockNativeNpcs.register(); + BedrockNativeContainers.register(); + BedrockFurnacePreferences.register(); @@ -1257,10 +1270,10 @@ index 0000000..1583ce0 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java new file mode 100644 -index 0000000..30893c2 +index 0000000..ccec3da --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java -@@ -0,0 +1,1302 @@ +@@ -0,0 +1,1307 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1563,8 +1576,8 @@ index 0000000..30893c2 + } + this.helpView = help; + } -+ if (this.presentation instanceof NativeCookingPresentation && this.scene.flag("close_on_player_hurt", false)) -+ throw new IllegalArgumentException("Unsupported native cooking player-hurt screen policy"); ++ if (this.scene.flag("close_on_player_hurt", false) && this.scene.number("native_hurt_policy", 0) != 1) ++ throw new IllegalArgumentException("Native player-hurt screen policy is unavailable"); + if (this.lifecycle == null) { + this.animationStarted = this.clock.getAsLong(); + this.lifecycle = new NativeUiAnimationLifecycle(this.scene); @@ -1621,6 +1634,11 @@ index 0000000..30893c2 + return mapping == null ? null : mapping.getTranslatedKeyMessage().getString(); + } + ++ @Override ++ public boolean closeOnPlayerHurt() { ++ return this.current() && this.scene != null && this.scene.flag("close_on_player_hurt", false); ++ } ++ + public boolean closing() { return this.lifecycle != null && this.lifecycle.exiting(); } + + @Override @@ -6488,10 +6506,10 @@ index fef5b3f..241b180 100644 "features.render.MixinNativeActorPickingEntity", diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreenTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreenTest.java new file mode 100644 -index 0000000..247ce99 +index 0000000..2d657a9 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreenTest.java -@@ -0,0 +1,676 @@ +@@ -0,0 +1,681 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -6504,6 +6522,7 @@ index 0000000..247ce99 + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; ++import com.viaversion.viaversion.libs.gson.JsonParser; +import com.mojang.blaze3d.platform.InputConstants; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiLayout; +import com.viaversion.viafabricplus.bedrock.screen.ui.NativeUiPresentation; @@ -7145,6 +7164,10 @@ index 0000000..247ce99 + host.addProperty("parent", ""); + final var hostValue = new com.viaversion.viaversion.libs.gson.JsonObject(); + hostValue.addProperty("$screen_content", "chest.panel"); ++ hostValue.addProperty("type", "screen"); ++ hostValue.add("controls", JsonParser.parseString(""" ++ [{"content@$screen_content":{}}] ++ """).getAsJsonArray()); + host.add("value", hostValue); + for (final String name : List.of("chest.small_chest_screen", "chest.large_chest_screen", + "chest.ender_chest_screen", "chest.shulker_box_screen", "chest.barrel_screen", @@ -9399,10 +9422,10 @@ index 0000000..0d86d83 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeCookingScreenTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeCookingScreenTest.java new file mode 100644 -index 0000000..a0c53d7 +index 0000000..a3823ec --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeCookingScreenTest.java -@@ -0,0 +1,1404 @@ +@@ -0,0 +1,1409 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -9509,22 +9532,27 @@ index 0000000..a0c53d7 + } + + @Test -+ void anUnsupportedPlayerHurtHostPolicyRetainsTheOrdinaryMenuBeforeInput() throws Exception { -+ try (final var f = new Fixture()) { -+ f.enablePreferences(); -+ f.navigation(2, 1, 4, 2, false, NavigationHost.SCREEN); -+ final var before = f.presentation.message(); -+ final var exported = JsonParser.parseString(before.program()).getAsJsonObject(); -+ exported.getAsJsonObject("definitions").getAsJsonObject("furnace.furnace_screen").getAsJsonObject("value") -+ .addProperty("close_on_player_hurt", true); -+ final var changed = NativeCookingMessage.create(CookingUi.fromProgram(exported), before.view(), before.sequence(), 3, -+ before.menuRevision(), before.inventoryRevision(), before.cursorRevision(), before.recipeItems()); -+ assertTrue(f.controller.accept(f.owner, f.menu, 5, changed, _ -> new ItemStack(Items.IRON_INGOT))); -+ f.screen.containerTick(); -+ assertSame(f.ordinary, f.displayed.get()); -+ assertTrue(f.preferences.isEmpty()); -+ assertTrue(f.sent.isEmpty()); -+ verifyNoInteractions(f.game.gameMode); ++ void playerHurtPolicyRequiresTheCoreContractBeforeInput() throws Exception { ++ for (final boolean available : List.of(false, true)) { ++ try (final var f = new Fixture()) { ++ f.enablePreferences(); ++ f.navigation(2, 1, 4, 2, false, NavigationHost.SCREEN); ++ final var before = f.presentation.message(); ++ final var exported = JsonParser.parseString(before.program()).getAsJsonObject(); ++ if (available) exported.addProperty("native_hurt_policy", 1); ++ else exported.remove("native_hurt_policy"); ++ exported.getAsJsonObject("definitions").getAsJsonObject("furnace.furnace_screen").getAsJsonObject("value") ++ .addProperty("close_on_player_hurt", true); ++ final var changed = NativeCookingMessage.create(CookingUi.fromProgram(exported), before.view(), before.sequence(), 3, ++ before.menuRevision(), before.inventoryRevision(), before.cursorRevision(), before.recipeItems()); ++ assertTrue(f.controller.accept(f.owner, f.menu, 5, changed, _ -> new ItemStack(Items.IRON_INGOT))); ++ f.screen.containerTick(); ++ assertSame(available ? f.screen : f.ordinary, f.displayed.get()); ++ if (available) assertTrue(f.screen.closeOnPlayerHurt()); ++ assertTrue(f.preferences.isEmpty()); ++ assertTrue(f.sent.isEmpty()); ++ verifyNoInteractions(f.game.gameMode); ++ } + } + } + 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 5adbf623..34d7e5c7 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 @@ -80,8 +80,17 @@ Repeated close requests retain the clock. Completion cannot close a replacement Tests cover those ownership boundaries and input retirement. Installed player-inventory controls show the slide and fade before removal and retain every slot after reopening. Native Bedrock 1.26.60.5 Escape frames provide the visual reference; exact frame timing and complete pixels remain unverified. -Generic storage resolves selected content without its complete host shell, so its default exit animations remain absent. -Closing on hurt still needs complete native pending-state controls. The cooking host-policy guard remains. +Storage resolves its complete host shell with the backdrop, close button, and default exit tracks. +Installed direct and ViaProxy controls cover chest, hopper, brewing, and large-chest screens, including exits and the close button. +Both routes retain 64 logs after accepted pickup and return requests. +The native hurt policy uses the original server cause, local actor lifetime, pending-event consumption, and a shared 100-update gate. +Controls cover excluded fire damage, accepted magic damage, gated events, a false inventory policy, and remote actors. +Native Bedrock 1.26.60.5 runtime controls establish that a false policy still starts the gate. +Programs without the versioned core contract retain ordinary controls when the scene requires this policy. +The separate cooking binding and visible-control checks remain. +Client-predicted damage, the live producer for actor event 81, and exact native frame timing remain unverified. +The historic hurt-and-heal nonclosure with a 5.6-millisecond packet gap remains unexplained. +Matched chest geometry retains a one-byte label-shade difference. Complete screen pixels remain unverified. Broader modpack timing, the reported menu overlap, and shield presentation still need verification. The [owning patch](0029-present-authored-native-containers.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.patch b/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.patch index d32029ff..c789c4b8 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-render-authored-forms-and-npc-dialogs.patch @@ -1,4 +1,4 @@ -From 80ecdfd0e8b0da5fc22ae03d3421d57f50f85347 Mon Sep 17 00:00:00 2001 +From 17c1979986c39d38d6c1ad977a4a7d982f94b11f 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(ui): render authored Bedrock forms and NPC dialogs @@ -195,24 +195,25 @@ listeners that share one still-connected network connection. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/npc/BedrockNpcConversationTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 1017163..376b15f 100644 +index b468961..fe230de 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -@@ -101,6 +101,8 @@ import com.viaversion.viafabricplus.bedrock.appearance.ViaFabricPlusEmoteProvide - import org.apache.logging.log4j.LogManager; +@@ -102,6 +102,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.viaversion.viafabricplus.bedrock.screen.ui.BedrockHudOverlays; + import com.viaversion.viafabricplus.bedrock.screen.ui.BedrockNativeUiHurt; +import com.viaversion.viafabricplus.bedrock.screen.form.BedrockNativeForms; +import com.viaversion.viafabricplus.bedrock.screen.npc.BedrockNativeNpcs; public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { -@@ -120,7 +122,9 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { +@@ -121,8 +123,10 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { public void onPreSettingsLoading() { com.viaversion.viafabricplus.bedrock.appearance.BedrockSkinChange.register(); com.viaversion.viafabricplus.bedrock.appearance.BedrockEmotePlayback.register(); + BedrockNativeForms.register(); BedrockHudOverlays.register(); + BedrockNativeUiHurt.register(); + BedrockNativeNpcs.register(); PayloadTypeRegistry.clientboundPlay().register(NativeSkinPayload.TYPE, NativeSkinPayload.CODEC); // Capture the listener identity at INIT. Fabric Context.player() returns the current client player. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index 16a8db91..4a6b145c 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From 4e6e998fbb1c31e39eac4bdc1df8bb67dca0ae45 Mon Sep 17 00:00:00 2001 +From 32187ea25a2a8b6dee0474803ff6b84c8ac1e7dd 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(ui): share native scene rendering and GUI actors @@ -742,8 +742,26 @@ the fading scene after that short track finishes. Removing the screen on the first completion truncates that visible exit. Wait for the remaining participating tracks, and release removed controls during reconciliation. The regression combines different durations and a mid-exit root refresh. + +Consume the negotiated native local-player hurt event through the shared +player-owned UI state. Evaluate the current scene's close policy at the +next client update and use its normal close lifecycle. Keep the pending +check and counter across screen changes, including false-policy scenes +and gameplay without a screen. + +Capture the connection at receiver registration and validate the current +native actor lifetime before queuing an event. Replacing or retiring that +player clears its pending check and counter; a late disconnect cannot +clear another connection's state. Tests reject stale owners and lifetimes, +evaluate the scene at consumption, and accept a fresh lifetime while a +previous player's counter was active. + +Target Bedrock 1.26.60.5 hardware watchpoints establish pending writes, +next-update consumption, selected causes and the 100-count gate. False +player-inventory policy still starts that gate. This bridge preserves the +original cause instead of deriving it from a merged Java damage type. --- - .../bedrock/ViaFabricPlusBedrock.java | 2 + + .../bedrock/ViaFabricPlusBedrock.java | 4 + .../appearance/BedrockBoneAnimation.java | 3 + .../appearance/BedrockEmotePlayback.java | 19 + .../appearance/BedrockGuiActorAppearance.java | 94 ++ @@ -775,6 +793,7 @@ The regression combines different durations and a mid-exit root refresh. .../render/font/BedrockBitmapGlyph.java | 105 ++ .../render/font/BedrockBitmapPipeline.java | 49 + .../bedrock/screen/ui/BedrockHudOverlays.java | 129 +++ + .../screen/ui/BedrockNativeUiHurt.java | 67 ++ .../screen/ui/NativeHudOverlayState.java | 66 ++ .../bedrock/screen/ui/NativeHudPayload.java | 36 + .../bedrock/screen/ui/NativeHudResources.java | 21 + @@ -792,6 +811,7 @@ The regression combines different durations and a mid-exit root refresh. .../bedrock/screen/ui/NativeUiFocus.java | 329 ++++++ .../bedrock/screen/ui/NativeUiFont.java | 58 + .../bedrock/screen/ui/NativeUiGraphics.java | 47 + + .../screen/ui/NativeUiHurtPayload.java | 22 + .../bedrock/screen/ui/NativeUiImageCache.java | 229 ++++ .../screen/ui/NativeUiImageRegions.java | 132 +++ .../bedrock/screen/ui/NativeUiImages.java | 192 ++++ @@ -805,7 +825,7 @@ The regression combines different durations and a mid-exit root refresh. .../bedrock/screen/ui/NativeUiRenderer.java | 153 +++ .../bedrock/screen/ui/NativeUiScene.java | 107 ++ .../screen/ui/NativeUiSceneScreen.java | 823 ++++++++++++++ - .../bedrock/screen/ui/NativeUiScreen.java | 11 + + .../bedrock/screen/ui/NativeUiScreen.java | 13 + .../bedrock/screen/ui/NativeUiScroll.java | 69 ++ .../bedrock/screen/ui/NativeUiSlider.java | 123 ++ .../bedrock/screen/ui/NativeUiText.java | 64 ++ @@ -830,6 +850,7 @@ The regression combines different durations and a mid-exit root refresh. .../render/font/BedrockBitmapGlyphTest.java | 71 ++ .../font/BedrockBitmapShaderGpuTest.java | 291 +++++ .../bedrock/screen/ui/AnimationPrograms.java | 23 + + .../screen/ui/BedrockNativeUiHurtTest.java | 71 ++ .../screen/ui/NativeHudOverlayStateTest.java | 153 +++ .../screen/ui/NativeUiActorCameraTest.java | 102 ++ .../screen/ui/NativeUiActorPortraitsTest.java | 164 +++ @@ -859,7 +880,7 @@ The regression combines different durations and a mid-exit root refresh. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ - 116 files changed, 13392 insertions(+), 36 deletions(-) + 119 files changed, 13556 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearance.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiActorRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinNativeUiInput.java @@ -878,6 +899,7 @@ The regression combines different durations and a mid-exit root refresh. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapGlyph.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapPipeline.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockHudOverlays.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockNativeUiHurt.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeHudOverlayState.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeHudPayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeHudResources.java @@ -895,6 +917,7 @@ The regression combines different durations and a mid-exit root refresh. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiFocus.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiFont.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiGraphics.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiHurtPayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImageCache.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImageRegions.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImages.java @@ -924,6 +947,7 @@ The regression combines different durations and a mid-exit root refresh. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapGlyphTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/font/BedrockBitmapShaderGpuTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/AnimationPrograms.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockNativeUiHurtTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeHudOverlayStateTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiActorCameraTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiActorPortraitsTest.java @@ -955,22 +979,24 @@ The regression combines different durations and a mid-exit root refresh. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiViewportTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 7fa3316..1017163 100644 +index 7fa3316..b468961 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -@@ -100,6 +100,7 @@ import net.raphimc.viabedrock.protocol.provider.EmoteProvider; +@@ -100,6 +100,8 @@ import net.raphimc.viabedrock.protocol.provider.EmoteProvider; import com.viaversion.viafabricplus.bedrock.appearance.ViaFabricPlusEmoteProvider; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import com.viaversion.viafabricplus.bedrock.screen.ui.BedrockHudOverlays; ++import com.viaversion.viafabricplus.bedrock.screen.ui.BedrockNativeUiHurt; public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { -@@ -119,6 +120,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { +@@ -119,6 +121,8 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { public void onPreSettingsLoading() { com.viaversion.viafabricplus.bedrock.appearance.BedrockSkinChange.register(); com.viaversion.viafabricplus.bedrock.appearance.BedrockEmotePlayback.register(); + BedrockHudOverlays.register(); ++ BedrockNativeUiHurt.register(); PayloadTypeRegistry.clientboundPlay().register(NativeSkinPayload.TYPE, NativeSkinPayload.CODEC); // Capture the listener identity at INIT. Fabric Context.player() returns the current client player. ClientPlayConnectionEvents.INIT.register((listener, client) -> @@ -3060,6 +3086,79 @@ index 0000000..04a8f13 + closeResources(); + } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockNativeUiHurt.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockNativeUiHurt.java +new file mode 100644 +index 0000000..dcc7c1e +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockNativeUiHurt.java +@@ -0,0 +1,67 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.screen.ui; ++ ++import com.viaversion.viafabricplus.bedrock.render.BedrockActorState; ++import java.util.Objects; ++import java.util.UUID; ++import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; ++import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; ++import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; ++import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; ++import net.minecraft.client.Minecraft; ++import net.minecraft.network.Connection; ++import net.raphimc.viabedrock.api.model.NativeUiHurtEvent; ++import net.raphimc.viabedrock.api.model.NativeUiHurtState; ++ ++/** The native pending check belongs to the current player, including while no screen can close. */ ++public final class BedrockNativeUiHurt { ++ private static final NativeUiHurtState STATE = new NativeUiHurtState(); ++ private static Connection owner; ++ private static UUID lifetime; ++ ++ private BedrockNativeUiHurt() {} ++ ++ public static void register() { ++ PayloadTypeRegistry.clientboundPlay().register(NativeUiHurtPayload.TYPE, NativeUiHurtPayload.CODEC); ++ ClientPlayConnectionEvents.INIT.register((listener, client) -> ++ ClientPlayNetworking.registerReceiver(NativeUiHurtPayload.TYPE, (payload, context) -> ++ client.execute(() -> apply(listener.getConnection(), payload.event(), client)))); ++ ClientPlayConnectionEvents.DISCONNECT.register((listener, client) -> { ++ if (listener.getConnection() == owner) { ++ STATE.clear(); ++ owner = null; ++ lifetime = null; ++ } ++ }); ++ ClientTickEvents.START_CLIENT_TICK.register(BedrockNativeUiHurt::tick); ++ } ++ ++ private static void synchronize(final Minecraft client) { ++ final var listener = client.getConnection(); ++ final Connection current = listener == null || client.level == null || client.player == null ++ || !listener.getConnection().isConnected() ? null : listener.getConnection(); ++ final UUID currentLifetime = current == null ? null : BedrockActorState.lifetime(client.player.getUUID()); ++ if (current != owner || !Objects.equals(currentLifetime, lifetime)) { ++ STATE.clear(); ++ owner = current; ++ lifetime = currentLifetime; ++ } ++ } ++ ++ static void apply(final Connection expected, final NativeUiHurtEvent event, final Minecraft client) { ++ synchronize(client); ++ if (expected == owner && event.lifetime().equals(lifetime)) STATE.accept(event); ++ } ++ ++ static void tick(final Minecraft client) { ++ synchronize(client); ++ if (owner == null || lifetime == null) return; ++ final var screen = client.gui.screen(); ++ final boolean policy = screen instanceof NativeUiScreen nativeScreen && nativeScreen.closeOnPlayerHurt(); ++ if (STATE.tick(policy) && client.gui.screen() == screen) screen.onClose(); ++ } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeHudOverlayState.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeHudOverlayState.java new file mode 100644 index 0000000..6db85a8 @@ -5202,6 +5301,34 @@ index 0000000..bc30c34 + this.viewport.pixel(left), this.viewport.pixel(top), this.viewport.pixel(right), this.viewport.pixel(bottom)); + } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiHurtPayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiHurtPayload.java +new file mode 100644 +index 0000000..5016ff0 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiHurtPayload.java +@@ -0,0 +1,22 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.screen.ui; ++ ++import net.minecraft.network.RegistryFriendlyByteBuf; ++import net.minecraft.network.codec.StreamCodec; ++import net.minecraft.network.protocol.common.custom.CustomPacketPayload; ++import net.minecraft.resources.Identifier; ++import net.raphimc.viabedrock.api.model.NativeUiHurtEvent; ++ ++public record NativeUiHurtPayload(NativeUiHurtEvent event) implements CustomPacketPayload { ++ public static final Type TYPE = new Type<>(Identifier.parse(NativeUiHurtEvent.CHANNEL)); ++ public static final StreamCodec CODEC = CustomPacketPayload.codec( ++ (payload, buffer) -> NativeUiHurtEvent.TYPE.write(buffer, payload.event), ++ buffer -> new NativeUiHurtPayload(NativeUiHurtEvent.TYPE.read(buffer))); ++ ++ @Override ++ public Type type() { return TYPE; } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImageCache.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiImageCache.java new file mode 100644 index 0000000..1ffe21b @@ -8385,10 +8512,10 @@ index 0000000..2a38ec5 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiScreen.java new file mode 100644 -index 0000000..9d7804c +index 0000000..bb9713e --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiScreen.java -@@ -0,0 +1,11 @@ +@@ -0,0 +1,13 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -8398,6 +8525,8 @@ index 0000000..9d7804c + +/** Screens whose layout and input use the target Bedrock GuiData viewport. */ +public interface NativeUiScreen { ++ default boolean closeOnPlayerHurt() { return false; } ++ + default NativeUiViewport nativeViewport() { return NativeUiViewport.current(); } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiScroll.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiScroll.java @@ -10175,6 +10304,83 @@ index 0000000..f0c01a7 + node.add("animation_program", program); + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockNativeUiHurtTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockNativeUiHurtTest.java +new file mode 100644 +index 0000000..3e7bcdc +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/BedrockNativeUiHurtTest.java +@@ -0,0 +1,71 @@ ++/* ++ * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.screen.ui; ++ ++import com.viaversion.viafabricplus.bedrock.render.BedrockActorState; ++import java.util.UUID; ++import net.minecraft.SharedConstants; ++import net.minecraft.server.Bootstrap; ++import net.minecraft.client.Minecraft; ++import net.minecraft.client.gui.Gui; ++import net.minecraft.client.gui.screens.Screen; ++import net.minecraft.client.multiplayer.ClientLevel; ++import net.minecraft.client.multiplayer.ClientPacketListener; ++import net.minecraft.client.player.LocalPlayer; ++import net.minecraft.network.Connection; ++import net.raphimc.viabedrock.api.model.NativeUiHurtEvent; ++import org.junit.jupiter.api.Test; ++ ++import static org.mockito.Mockito.*; ++ ++final class BedrockNativeUiHurtTest { ++ @Test ++ void checksTheSceneAtConsumptionAndRejectsRetiredConnectionsAndActorLifetimes() throws Exception { ++ SharedConstants.tryDetectVersion(); ++ Bootstrap.bootStrap(); ++ final var client = mock(Minecraft.class); ++ final var player = mock(LocalPlayer.class); ++ client.player = player; ++ client.level = mock(ClientLevel.class); ++ final var listener = mock(ClientPacketListener.class); ++ final var connection = mock(Connection.class); ++ when(listener.getConnection()).thenReturn(connection); ++ when(connection.isConnected()).thenReturn(true); ++ when(client.getConnection()).thenReturn(listener); ++ final var gui = mock(Gui.class); ++ final var guiField = Minecraft.class.getField("gui"); ++ guiField.setAccessible(true); ++ guiField.set(client, gui); ++ final UUID playerId = UUID.randomUUID(), lifetime = UUID.randomUUID(); ++ when(player.getUUID()).thenReturn(playerId); ++ final var screen = mock(Screen.class, withSettings().extraInterfaces(NativeUiScreen.class)); ++ when(((NativeUiScreen) screen).closeOnPlayerHurt()).thenReturn(true); ++ when(gui.screen()).thenReturn(screen); ++ try (final var actors = mockStatic(BedrockActorState.class)) { ++ actors.when(() -> BedrockActorState.lifetime(playerId)).thenReturn(lifetime); ++ BedrockNativeUiHurt.apply(mock(Connection.class), new NativeUiHurtEvent(lifetime, 2, 14), client); ++ BedrockNativeUiHurt.apply(connection, new NativeUiHurtEvent(UUID.randomUUID(), 2, 14), client); ++ BedrockNativeUiHurt.tick(client); ++ verify(screen, never()).onClose(); ++ ++ // The scene at receipt is not the scene the next player update necessarily evaluates. ++ when(gui.screen()).thenReturn(null); ++ BedrockNativeUiHurt.apply(connection, new NativeUiHurtEvent(lifetime, 2, 14), client); ++ when(gui.screen()).thenReturn(screen); ++ BedrockNativeUiHurt.tick(client); ++ verify(screen).onClose(); ++ ++ final UUID respawn = UUID.randomUUID(); ++ actors.when(() -> BedrockActorState.lifetime(playerId)).thenReturn(respawn); ++ BedrockNativeUiHurt.apply(connection, new NativeUiHurtEvent(lifetime, 2, 14), client); ++ BedrockNativeUiHurt.tick(client); ++ verify(screen).onClose(); ++ BedrockNativeUiHurt.apply(connection, new NativeUiHurtEvent(respawn, 81, 2), client); ++ BedrockNativeUiHurt.tick(client); ++ verify(screen, times(2)).onClose(); ++ } ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeHudOverlayStateTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeHudOverlayStateTest.java new file mode 100644 index 0000000..16d186d diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index e25b16a7..69cc8a07 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -39,10 +39,18 @@ It waits for all participating tracks, including a shorter size track alongside Binding refresh retains pending completions by control identity. Tests cover different durations, repeated requests, successor chains, missing completion paths, and refresh during an exit. Native Bedrock 1.26.60.5 Escape frames retain the fade after the size track finishes. -This mechanism does not establish complete native host policy or exact frame timing. +This mechanism does not establish exact native frame timing. These controls do not establish identical native pooling or frame timing. Broader viewports, complete inventory pixels, entrance timing, and TrueType comparisons remain open. +The shared hurt receiver binds queued events to the connection and local actor lifetime. +It consumes pending events against the current scene and retains the counter across ordinary screen changes. +Accepted events use the existing screen exit lifecycle. +Regressions reject stale connections and actor lifetimes and retain scene selection at consumption. +Installed direct and ViaProxy controls cover cause filtering, gated events, a false inventory policy, and remote actor damage. +Native Bedrock 1.26.60.5 runtime controls establish the server-event policy order. +Client-predicted damage and exact native frame timing remain unverified. + Alpha tracks honor `scale_from_starting_alpha` and retain their initial scale across scene refreshes. Native Bedrock 1.26.60.5 captures ImageComponent alpha when creating the track and multiplies the interpolated value by that scale. The default Survival backdrop uses this flag with opacity 0.4. Ignoring it makes the settled backdrop opaque. diff --git a/patches/viafabricplus-bedrock/upstreamable/0030-use-supported-native-world-actor-definitions.patch b/patches/viafabricplus-bedrock/upstreamable/0030-use-supported-native-world-actor-definitions.patch index 12755fbb..46ece2fc 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0030-use-supported-native-world-actor-definitions.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0030-use-supported-native-world-actor-definitions.patch @@ -1,4 +1,4 @@ -From f0cd8e09b56162090e8bacad3e6899ae8f70ae27 Mon Sep 17 00:00:00 2001 +From b312a8bb53be58a1c9e428cc41d1cdfd126e685f 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(render): use supported native world actor definitions @@ -239,10 +239,10 @@ index e1bd73b..ef45d56 100644 val stackAnvilMavenRepo = providers.gradleProperty("stackanvilMavenRepo") if (stackAnvilMavenRepo.isPresent) { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 7c832ac..50ed2a7 100644 +index 6071d4a..d908685 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -@@ -122,6 +122,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { +@@ -123,6 +123,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { @Override public void onPreSettingsLoading() { diff --git a/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch b/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch index fb38767b..7ff21b62 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0031-edit-native-command-block-settings.patch @@ -1,4 +1,4 @@ -From 738a9f38c7104c9632058f57352be49cb473a564 Mon Sep 17 00:00:00 2001 +From e7f07347b5a9ffe63223e669762996d1a37133a0 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(command): edit native command block settings @@ -23,7 +23,7 @@ Open the Java command editor for an authoritative native COMMAND_BLOCK window. R create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/CommandBlockOpenPayload.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 50ed2a7..4fbe931 100644 +index d908685..f7bc20a 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -100,6 +100,8 @@ import net.raphimc.viabedrock.protocol.provider.EmoteProvider; @@ -33,9 +33,9 @@ index 50ed2a7..4fbe931 100644 +import com.viaversion.viafabricplus.bedrock.screen.BedrockCommandBlockScreen; +import com.viaversion.viafabricplus.bedrock.screen.CommandBlockEditPayload; import com.viaversion.viafabricplus.bedrock.screen.ui.BedrockHudOverlays; + import com.viaversion.viafabricplus.bedrock.screen.ui.BedrockNativeUiHurt; import com.viaversion.viafabricplus.bedrock.screen.form.BedrockNativeForms; - import com.viaversion.viafabricplus.bedrock.screen.npc.BedrockNativeNpcs; -@@ -206,6 +208,8 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { +@@ -208,6 +210,8 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { ClientPlayNetworking.registerGlobalReceiver(NativeParticlePlaybackPayload.TYPE, (payload, context) -> BedrockServerParticles.accept(payload.message())); PayloadTypeRegistry.serverboundPlay().register(RawInputPayload.TYPE, RawInputPayload.CODEC); diff --git a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch index e6569202..dfbb9351 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch @@ -1,4 +1,4 @@ -From f2a72e8adc2fcc0d6286ac752b54e55acd6348fc Mon Sep 17 00:00:00 2001 +From 1eee620b0197015e5e1858fbabc8b9a323233994 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:36:18 +0100 Subject: [PATCH] feat(interaction): predict held trapdoor placement @@ -61,7 +61,7 @@ proxy hits retained Java's 0.8125 bottom instead of native 0.8175. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacementTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 4fbe931..45ddb93 100644 +index f7bc20a..ef06561 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -86,6 +86,7 @@ import com.viaversion.viafabricplus.bedrock.hud.BedrockHudVisibility; @@ -72,7 +72,7 @@ index 4fbe931..45ddb93 100644 import com.viaversion.viafabricplus.bedrock.input.PlayerPredictionPayload; import com.viaversion.viafabricplus.bedrock.input.BedrockPlayerCorrections; import com.viaversion.viafabricplus.bedrock.input.BedrockMovementEffects; -@@ -208,6 +209,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { +@@ -210,6 +211,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { ClientPlayNetworking.registerGlobalReceiver(NativeParticlePlaybackPayload.TYPE, (payload, context) -> BedrockServerParticles.accept(payload.message())); PayloadTypeRegistry.serverboundPlay().register(RawInputPayload.TYPE, RawInputPayload.CODEC); diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch b/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch index 75655bd2..47d3a8f9 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0039-apply-native-command-stacks-to-dry-air.patch @@ -1,4 +1,4 @@ -From 1d4599b435664116174a6a190c6b8509ae7bddae Mon Sep 17 00:00:00 2001 +From f8e383b01d46aab1afd45ce4075930d13ad387c1 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:28:02 +0100 Subject: [PATCH] feat(fog): apply native biome and command settings @@ -69,7 +69,7 @@ BREAKING CHANGE: The fog receiver requires the matching core payload version 2. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/fog/FogSessionTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 45ddb93..5440156 100644 +index ef06561..c6b6f3d 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -70,6 +70,7 @@ import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; @@ -80,7 +80,7 @@ index 45ddb93..5440156 100644 import com.viaversion.viafabricplus.bedrock.camera.BedrockCameraFade; import com.viaversion.viafabricplus.bedrock.camera.BedrockCameraFov; import com.viaversion.viafabricplus.bedrock.camera.CameraFovPayload; -@@ -229,6 +230,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { +@@ -231,6 +232,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { ClientPlayConnectionEvents.INIT.register((listener, client) -> ClientPlayNetworking.registerReceiver(LocatorBarPayload.TYPE, (payload, context) -> BedrockLocatorBar.accept(listener.getConnection(), payload.snapshot()))); From 6d4a6426cd28cbd4d1554ea81fb0bbb426b5d933 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 06:25:26 +0100 Subject: [PATCH 25/33] fix(ui): match native text color conversion Convert text channels with single-precision multiplication, truncation and byte clamping, including opacity. Rounding changes authored 0.3 to byte 77 instead of the native 76 and shifts settled container captions. Fold the correction into the shared renderer patch and keep image tint independent. Native 1.26.60.5 instructions, fractional-byte controls and live glyph writes establish the conversion. Both installed Java routes match the native title and inventory-label rectangles. Complete screen pixels and animation timing remain separate requirements. --- docs/native-player-inventory.md | 3 +- ...9-present-authored-native-containers.pr.md | 3 +- ...0029-share-native-ui-scene-rendering.patch | 55 +++++++++++++++---- ...0029-share-native-ui-scene-rendering.pr.md | 9 +++ 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 4a1ee492..c409f67f 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -153,4 +153,5 @@ Damage controls cover accepted and excluded causes, gated events, a false invent Native Bedrock 1.26.60.5 runtime controls establish the pending-state and counter order for server-delivered hurt events. Client-predicted damage, the live producer for actor event 81, and exact frame timing remain unverified. The historic hurt-and-heal nonclosure with a 5.6-millisecond packet gap remains unexplained. -Matched chest geometry retains a one-byte label-shade difference. Complete screen pixels remain unverified. +Text channels use native byte truncation and clamping, including opacity. +Complete screen pixels remain unverified. 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 34d7e5c7..384afbc2 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 @@ -90,7 +90,8 @@ Programs without the versioned core contract retain ordinary controls when the s The separate cooking binding and visible-control checks remain. Client-predicted damage, the live producer for actor event 81, and exact native frame timing remain unverified. The historic hurt-and-heal nonclosure with a 5.6-millisecond packet gap remains unexplained. -Matched chest geometry retains a one-byte label-shade difference. Complete screen pixels remain unverified. +Shared text conversion corrects the measured one-byte label-shade difference. +Complete screen pixels remain unverified. Broader modpack timing, the reported menu overlap, and shield presentation still need verification. The [owning patch](0029-present-authored-native-containers.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch index 4a6b145c..843b7a05 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.patch @@ -1,4 +1,4 @@ -From 32187ea25a2a8b6dee0474803ff6b84c8ac1e7dd Mon Sep 17 00:00:00 2001 +From a40914495edf5de66e2a919bb1ccf3dd4152e224 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(ui): share native scene rendering and GUI actors @@ -760,6 +760,19 @@ Target Bedrock 1.26.60.5 hardware watchpoints establish pending writes, next-update consumption, selected causes and the 100-count gate. False player-inventory policy still starts that gate. This bridge preserves the original cause instead of deriving it from a merged Java damage type. + +Submit native text color with per-channel single-precision multiplication +by 255, truncation and byte clamping, including opacity. Target Bedrock +1.26.60.5 cached-glyph instructions 140e6d97f..140e6da3c establish this +conversion independently of image tint. All 140 original instruction +controls cover fractional byte boundaries and independent RGBA channels. +Fresh native hopper glyph writes bind that path to visible UI labels and +convert authored 0.3 to byte 76. Rounding produces 77 and changes settled +container captions. Keep image component color conversion independent. + +The renderer regression fails with the previous rounding and checks mixed +channels, missing-channel defaults and opacity bounds. Full text material, +image tint and native frame timing parity remain separate requirements. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../appearance/BedrockBoneAnimation.java | 3 + @@ -822,7 +835,7 @@ original cause instead of deriving it from a merged Java damage type. .../screen/ui/NativeUiMultilineInput.java | 267 +++++ .../screen/ui/NativeUiPlayerCamera.java | 72 ++ .../screen/ui/NativeUiPresentation.java | 162 +++ - .../bedrock/screen/ui/NativeUiRenderer.java | 153 +++ + .../bedrock/screen/ui/NativeUiRenderer.java | 157 +++ .../bedrock/screen/ui/NativeUiScene.java | 107 ++ .../screen/ui/NativeUiSceneScreen.java | 823 ++++++++++++++ .../bedrock/screen/ui/NativeUiScreen.java | 13 + @@ -872,7 +885,7 @@ original cause instead of deriving it from a merged Java damage type. .../screen/ui/NativeUiMultilineInputTest.java | 210 ++++ .../screen/ui/NativeUiPlayerCameraTest.java | 132 +++ .../screen/ui/NativeUiPresentationTest.java | 258 +++++ - .../screen/ui/NativeUiRendererTest.java | 71 ++ + .../screen/ui/NativeUiRendererTest.java | 85 ++ .../NativeUiSceneScreenInteractionTest.java | 359 ++++++ .../bedrock/screen/ui/NativeUiSceneTest.java | 40 + .../bedrock/screen/ui/NativeUiSliderTest.java | 120 ++ @@ -880,7 +893,7 @@ original cause instead of deriving it from a merged Java damage type. .../ui/NativeUiTextureAnimationTest.java | 108 ++ .../bedrock/screen/ui/NativeUiToggleTest.java | 86 ++ .../screen/ui/NativeUiViewportTest.java | 95 ++ - 119 files changed, 13556 insertions(+), 36 deletions(-) + 119 files changed, 13574 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockGuiActorAppearance.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockGuiActorRenderer.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinNativeUiInput.java @@ -7411,10 +7424,10 @@ index 0000000..99d79fd +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRenderer.java new file mode 100644 -index 0000000..0d033a4 +index 0000000..0480b87 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRenderer.java -@@ -0,0 +1,153 @@ +@@ -0,0 +1,157 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -7561,10 +7574,14 @@ index 0000000..0d033a4 + } + + public static int color(final NativeUiScene.Node node, final float alpha) { -+ final int red = Math.round(Math.clamp(NativeUiLayout.vector(node, "color", 0, 1), 0, 1) * 255); -+ final int green = Math.round(Math.clamp(NativeUiLayout.vector(node, "color", 1, 1), 0, 1) * 255); -+ final int blue = Math.round(Math.clamp(NativeUiLayout.vector(node, "color", 2, 1), 0, 1) * 255); -+ return Math.round(alpha * 255) << 24 | red << 16 | green << 8 | blue; ++ final int red = textChannel(NativeUiLayout.vector(node, "color", 0, 1)); ++ final int green = textChannel(NativeUiLayout.vector(node, "color", 1, 1)); ++ final int blue = textChannel(NativeUiLayout.vector(node, "color", 2, 1)); ++ return textChannel(alpha) << 24 | red << 16 | green << 8 | blue; ++ } ++ ++ private static int textChannel(final float value) { ++ return Math.clamp((int) (value * 255), 0, 255); + } + +} @@ -14724,10 +14741,10 @@ index 0000000..d8bc9ec +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRendererTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRendererTest.java new file mode 100644 -index 0000000..a69a064 +index 0000000..f38690b --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/ui/NativeUiRendererTest.java -@@ -0,0 +1,71 @@ +@@ -0,0 +1,85 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -14736,6 +14753,7 @@ index 0000000..a69a064 +package com.viaversion.viafabricplus.bedrock.screen.ui; + +import com.google.gson.JsonObject; ++import com.google.gson.JsonParser; +import java.util.List; +import java.util.UUID; +import net.minecraft.SharedConstants; @@ -14793,6 +14811,19 @@ index 0000000..a69a064 + verify(font, times(2)).split(any(FormattedText.class), eq(50)); + } + ++ @Test ++ void truncatesNativeTextChannelsAndClampsOpacityAtSubmission() { ++ final var node = label("A"); ++ node.properties().add("color", JsonParser.parseString("[0.3,0.5,0.75]")); ++ // Bedrock 1.26.60.5 cached-glyph writes truncate each scaled RGBA channel. ++ assertEquals(0x7F4C7FBF, NativeUiRenderer.color(node, .5F)); ++ node.properties().add("color", JsonParser.parseString("[0.25]")); ++ assertEquals(0xFF3FFFFF, NativeUiRenderer.color(node, 1)); ++ node.properties().add("color", JsonParser.parseString("[-0.01,1.01,0.1]")); ++ assertEquals(0xFF00FF19, NativeUiRenderer.color(node, 1.2F)); ++ assertEquals(0x0000FF19, NativeUiRenderer.color(node, -.1F)); ++ } ++ + private static NativeUiScene.Node label(final String value) { + final var properties = new JsonObject(); + properties.addProperty("text", value); diff --git a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md index 69cc8a07..4141ffe9 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0029-share-native-ui-scene-rendering.pr.md @@ -22,6 +22,15 @@ Layout changes retain a separate revision. Regression coverage includes `BedrockBitmapFontResourcesTest`, `BedrockGuiActorAppearanceTest`, `BedrockGuiActorContextTest`, `BedrockResourceLibraryTest`. +Text channels follow the native cached-glyph conversion: multiply by 255, truncate, then clamp to a byte. +Bedrock 1.26.60.5 original instructions cover 140 cases, including fractional byte boundaries and independent RGBA channels. +Fresh native hopper glyph writes use that path and convert authored 0.3 to byte 76. +The renderer regression fails with the previous rounding and covers mixed channels, defaults, and opacity bounds. +Image component tint retains its separate conversion path. +At 1280 × 720 and native UI scale 2, both installed Java routes match all 672 glyph pixels in two chest labels. +The complete title and inventory-label rectangles also match, including their panel backgrounds. +Other controls, world pixels, items, and frame timing remain outside that comparison. + At 1280 × 720, all 16 current-font labels match native 26.60 at UI scales 1 and 2. All 16 legacy labels match at UI scale 2. Direct and ViaProxy controls pass Java GUI scale changes and resource reloads. The point-sampling GPU regression fails before the correction and passes on Intel and software rendering. Unanchored manual-grid cells use their grid origins. From dfdd93f939f53a0c174e11da0de210bb64d92d18 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:27:14 +0100 Subject: [PATCH 26/33] fix(inventory): restore authored tab and layout selection Supply the controller indices referenced by the Bedrock 1.26.60 inventory graph. Missing indices resolved to null and made physical category and layout selections send index zero. Keep the correction in the Creative catalog owner and exercise physical selection through exported programs in both layouts. Direct and ViaProxy controls retain the complete 1,980-item source, and BDS accepts 64-rail pickup and placement. Native profile initialization and the reported modpack lag remain open. --- docs/bedrock-coverage.md | 2 +- docs/native-player-inventory.md | 4 ++- ...ain-bedrock-creative-catalog-entries.patch | 22 ++++++++++--- ...ain-bedrock-creative-catalog-entries.pr.md | 10 +++++- ...9-present-authored-native-containers.patch | 31 +++++++++++++++---- ...9-present-authored-native-containers.pr.md | 6 +++- 6 files changed, 60 insertions(+), 15 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index e46f335c..3be721d9 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -26,7 +26,7 @@ No complete area has verified Bedrock parity. | World resources | World base-game version selects vanilla metadata, textures, actor graphs, and audio before server overrides. The context survives ViaProxy. Server legacy biome bindings retain unqualified names across versions. | Native current/older/current cat, sound, and remote biome-name controls cover selected paths. Broader pose and audio behavior need comparisons. | | Held trapdoors | Retained facing and half, interaction suppression, use timing, cancellation, rejection, and prediction reconciliation. | Native, direct, and proxy controls cover specific held-placement histories. Other placement and interaction families need separate comparisons. | | Fonts and UI | Bitmap metadata, extended mapping, same-pack ownership, legacy layouts, authored scenes, and native GUI sampling. | At 1280 × 720, current controls match all 16 labels at native UI scales 1 and 2; legacy scale-2 controls match 16/16. Both routes pass Java GUI scale changes and reloads. Broader viewports and TrueType comparisons remain open. | -| Player inventory and crafting | Opening ownership, recipe catalogs, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, full Creative catalogs, bundle actions/tooltips, bulk recipe actions, cursor replacement, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | +| Player inventory and crafting | Opening ownership, recipe catalogs, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, bulk recipe actions, cursor replacement, profile initialization, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | | Cooking | Snapshot-bound recipe actions, transfer batches, preferences, search, and progress. | Default cooking screens retain ordinary controls until authored bindings are complete. See the [cooking reference](native-cooking-actions.md). | | Skins and appearance | Complete skin transport, classic changes, persona assembly, animation graphs, costumes, and attachables. | Broader wardrobe masking, profile/LOD changes, shield rendering, remote appearances, and account/platform flows need further native comparisons. See [skin support](skin-flow.md). | | Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Native Fancy controls establish the tick-based expiry blend and distance curve at noon and midnight. Other fluid movement, vision combinations and media, mount variants, network conditions, and broader replay behavior remain open. | diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index c409f67f..f30a53be 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -140,7 +140,9 @@ Unknown storage snapshots restore ordinary controls. Hovering or carrying storage items also restores ordinary controls until native tooltips and selection are complete. Unsupported visible Help glyphs, recipe auxiliary actions, and held-selection commands also restore ordinary controls. Creative hotbar destruction restores ordinary controls before item changes or requests. -Large Creative catalogs exceed the current scene work budget. Viewport expansion remains unfinished. +Installed direct and ViaProxy controls retain the complete 1,980-item catalog from a local protocol-2225 server. +Tab and layout controls use the target inventory indices. +Both routes search for rails and place a server-accepted stack of 64 in the hotbar. Initial bundle insertion, bulk recipe actions, Creative cursor replacement, and complete installed native parity remain unfinished. Native constructor timing relative to the open acknowledgment and frame-level population timing remain unverified. An unpositioned player inventory uses the fallback crafting context. 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 d5f73875..99b1f1f4 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 8832b7dbbc095f61e79e22e231fde05ab780b2b1 Mon Sep 17 00:00:00 2001 +From 218b8c24c844f4fbb4689f0df1318b9616b7c773 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 @@ -250,12 +250,14 @@ full stack only for destination admission and cursor prediction. This preserves the official 1.26.60.5 captured unit-result/full-Take flow. A request-boundary regression checks stack sizes one and 64, the packet actions, cursor prediction and its pre-request restoration snapshot. + +Supply player-inventory tab and layout variables from the protocol-2225 enums. The 1.26.60 inventory graph references these controller-owned indices, just as cooking graphs reference their own indices. Without them, forced toggle indices resolve to null and mouse selection sends zero, leaving an unselected profile unable to choose Search or change layouts. Preserve absent profile preferences instead of inventing a selected tab. --- .../model/NativePlayerInventoryAction.java | 88 ++++ .../model/NativePlayerInventoryMessage.java | 271 ++++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ .../model/container/NativeRecipeCounter.java | 74 +++ - .../resourcepack/ui/PlayerInventoryUi.java | 387 ++++++++++++++ + .../resourcepack/ui/PlayerInventoryUi.java | 397 +++++++++++++++ .../viabedrock/protocol/BedrockProtocol.java | 1 + .../protocol/model/CreativeCatalog.java | 266 ++++++++++ .../model/CreativeInventoryModel.java | 198 ++++++++ @@ -281,7 +283,7 @@ actions, cursor prediction and its pre-request restoration snapshot. .../storage/CreativeInventoryActionTest.java | 214 ++++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ .../storage/NativePlayerCreativePickTest.java | 66 +++ - 30 files changed, 4209 insertions(+), 170 deletions(-) + 30 files changed, 4219 insertions(+), 170 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 @@ -817,10 +819,10 @@ index fe874172..3ccb1532 100644 /** A captured native collection. Slots in results are relative to this collection. */ 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..19993219 +index 00000000..6bad8d57 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUi.java -@@ -0,0 +1,387 @@ +@@ -0,0 +1,397 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -832,6 +834,8 @@ index 00000000..19993219 +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.protocol.data.enums.bedrock.generated.InventoryLeftTabIndex; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.InventoryLayout; + +import java.util.ArrayList; +import java.util.HashMap; @@ -1099,6 +1103,14 @@ index 00000000..19993219 + super(menu.state.workbench ? 10 : 9, menu.slots, menu.cursor, new ContainerUi.Title("", false), true); + this.state = menu.state; + this.windows = windows == null ? null : Map.copyOf(windows); ++ this.globals.put("$construction_index", new JsonPrimitive(InventoryLeftTabIndex.RecipeConstruction.getValue())); ++ this.globals.put("$equipment_index", new JsonPrimitive(InventoryLeftTabIndex.RecipeEquipment.getValue())); ++ this.globals.put("$items_index", new JsonPrimitive(InventoryLeftTabIndex.RecipeItems.getValue())); ++ this.globals.put("$nature_index", new JsonPrimitive(InventoryLeftTabIndex.RecipeNature.getValue())); ++ this.globals.put("$search_index", new JsonPrimitive(InventoryLeftTabIndex.RecipeSearch.getValue())); ++ this.globals.put("$survival_layout_index", new JsonPrimitive(InventoryLayout.InventoryOnly.getValue())); ++ this.globals.put("$recipe_book_layout_index", new JsonPrimitive(InventoryLayout.Default.getValue())); ++ this.globals.put("$creative_layout_index", new JsonPrimitive(InventoryLayout.RecipeBookOnly.getValue())); + final Map slots = new HashMap<>(); + menu.slots.forEach(slot -> slots.put(slot.javaSlot(), slot)); + this.collections.put("crafting_input_items", collection(slots, 1, this.state.workbench ? 9 : 4)); diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index 9e3b431d..495d28f5 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -113,4 +113,12 @@ A live add-on check removes the red disabled backgrounds. The picker validates the full cursor stack before transfer admission while preserving the unit-sized native craft result. Request regressions cover stack sizes one and 64, cursor prediction, and the restoration snapshot. An installed add-on test selects 64 rails and places them in the hotbar; BDS accepts both requests. -Proxy validation of this quantity fix remains open. Cursor replacement, bulk actions, and complete native screen parity remain unfinished. +Installed direct and ViaProxy controls select 64 rails and place them in separate hotbar slots; BDS accepts the requests. +The ViaProxy screen retains both stacks after reopening. +Cursor replacement, bulk actions, and complete native screen parity remain unfinished. + +Player and workbench graphs receive their tab and layout indices from the protocol-2225 enums. +The native inventory graph references these controller variables in forced toggle indices. +Missing variables resolve to null and make category clicks request index zero. +Screen regressions click category and layout controls through exported programs in both inventory layouts. +These bindings preserve absent profile preferences and do not select a default tab. 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 ef02ad88..c6daf31e 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 28e0ae4fc297df0fc4c6aea86ba42e175a4eaa8a Mon Sep 17 00:00:00 2001 +From a6ebd0524d0aced090eeb17b9dc2dd6380ebd80c 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 @@ -647,6 +647,8 @@ including background and animation controls. Test fixtures declare their screen content controls explicitly. The cooking boundary test covers both negotiated policy admission and ordinary fallback without the core contract, before any inventory input is dispatched. + +Exercise physical category and layout selection through exported player-inventory programs in both recipe-book and Creative layouts. The regression rejects index-zero preference commands when the authored controls require Construction or a different layout, retaining unrelated tabs and crafting preferences. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -726,9 +728,9 @@ contract, before any inventory input is dispatched. .../NativeInventoryProgressTest.java | 83 + .../container/NativePlayerHelpTextTest.java | 34 + .../NativePlayerPresentationTest.java | 153 ++ - .../container/NativePlayerScreenTest.java | 269 ++++ + .../container/NativePlayerScreenTest.java | 286 ++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 80 files changed, 10446 insertions(+), 62 deletions(-) + 80 files changed, 10463 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 @@ -11474,10 +11476,10 @@ index 0000000..d3902fa +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java new file mode 100644 -index 0000000..a248a1b +index 0000000..2f7e81d --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java -@@ -0,0 +1,269 @@ +@@ -0,0 +1,286 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -11530,6 +11532,7 @@ index 0000000..a248a1b +import net.raphimc.viabedrock.api.model.NativeItemStorageData; +import net.raphimc.viabedrock.api.resourcepack.ui.PlayerInventoryUi; +import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; ++import net.raphimc.viabedrock.protocol.model.InventoryOptions; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; @@ -11661,9 +11664,11 @@ index 0000000..a248a1b + set(Options.class, options, name, mock(KeyMapping.class)); + set(Minecraft.class, game, "options", options); + set(Minecraft.class, game, "gui", mock(Gui.class)); -+ try (final var singleton = mockStatic(Minecraft.class); final var entrypoint = mockStatic(ViaFabricPlusBedrock.class)) { ++ try (final var singleton = mockStatic(Minecraft.class); final var entrypoint = mockStatic(ViaFabricPlusBedrock.class); ++ final var network = mockStatic(BedrockNativePlayerInventory.class, CALLS_REAL_METHODS)) { + singleton.when(Minecraft::getInstance).thenReturn(game); + entrypoint.when(ViaFabricPlusBedrock::impl).thenReturn(impl); ++ network.when(() -> BedrockNativePlayerInventory.preferences(any(), any(), any())).thenReturn(true); + final var ui = new PlayerInventoryUi(new ResourcePackStorage(List.of()).getJsonUiProgram()); + final var cells = IntStream.range(0, 2000).mapToObj(id -> + new PlayerInventoryUi.Cell(id, id + 10, -1, 0, 0, 1, false, false, "", Map.of())).toList(); @@ -11710,6 +11715,20 @@ index 0000000..a248a1b + assertTrue(mockingDetails(logger).getInvocations().isEmpty(), () -> mockingDetails(logger).getInvocations().stream() + .map(invocation -> Arrays.toString(invocation.getArguments())).collect(Collectors.joining("\n"))); + verifyNoInteractions(game.gameMode); ++ final var category = pointer(screen, draw -> draw.box().node().string("focus_identifier", "").equals("construction_tab") ++ && draw.box().node().type().equals("toggle")); ++ assertTrue(screen.mouseClicked(category, false)); ++ assertTrue(screen.mouseReleased(category)); ++ network.verify(() -> BedrockNativePlayerInventory.preferences(owner, menu, ++ new InventoryOptions(1, 2, false, layout, 2))); ++ final int nextLayout = layout == 2 ? 3 : 2; ++ final String nextFocus = layout == 2 ? "creative_layout_toggle" : "recipe_book_layout_toggle"; ++ final var layoutButton = pointer(screen, draw -> draw.box().node().string("focus_identifier", "").equals(nextFocus) ++ && draw.box().node().type().equals("toggle")); ++ assertTrue(screen.mouseClicked(layoutButton, false)); ++ assertTrue(screen.mouseReleased(layoutButton)); ++ network.verify(() -> BedrockNativePlayerInventory.preferences(owner, menu, ++ new InventoryOptions(5, 2, false, nextLayout, 2))); + if (layout == 3) { + final var stack = new ItemStack(Items.IRON_INGOT, 3); + menu.getSlot(36).set(stack); 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 384afbc2..217db1bc 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 @@ -55,7 +55,11 @@ Screen regressions use 2,000 retained items in the recipe-book and Creative layo They cover distant scrolling, resize, original action IDs, and keyboard navigation. Creative hotbar destruction restores ordinary controls before item changes or requests. Passive hover notifications do not trigger the fallback for unsupported recipe actions. -Full Creative catalog expansion, initial bundle insertion, bulk recipe actions, and Creative cursor replacement remain unresolved. +Installed direct and ViaProxy controls retain all 1,980 catalog entries from a local protocol-2225 server. +Physical category and layout changes preserve their native indices through exported programs. +Both routes search for rails and place a server-accepted stack of 64 in the hotbar. +ViaProxy retains both stacks after reopening. +Initial bundle insertion, bulk recipe actions, and Creative cursor replacement remain unresolved. Complete cooking integration, live transfers, and filled-bundle pixels also remain unresolved. Stable frames reuse the draw list, parsed hit regions, and input ancestor geometry. From 43b28d358e8012e0f4f557e52fd4613d75068f1a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:56:49 +0100 Subject: [PATCH 27/33] fix(inventory): initialize native player profile defaults An absent inventory-options packet leaves the native Player profile at its constructor defaults. The previous implementation treated the profile as absent too, retaining the controller's transient filter flag and publishing an unselected, empty first inventory. Use version-matched native defaults while preserving later packet values, including explicit zero tabs. Keep filter-only Java edits from replacing the automatic layouts. Fold the correction into the owning options and retained-model patches, with behavioral model and packet regressions. Ghidra, original-instruction controls, complete fresh-world direct/proxy journals, and first/reopen screen observations validate the correction. Native frame timing and the original modpack regressions remain open. --- docs/bedrock-coverage.md | 2 +- docs/native-player-inventory.md | 8 ++- ...edrock-recipes-and-place-ingredients.patch | 58 +++++++++------ ...edrock-recipes-and-place-ingredients.pr.md | 7 ++ ...ain-bedrock-creative-catalog-entries.patch | 70 ++++++++++++++----- ...ain-bedrock-creative-catalog-entries.pr.md | 11 ++- 6 files changed, 108 insertions(+), 48 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 3be721d9..a5e08568 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -26,7 +26,7 @@ No complete area has verified Bedrock parity. | World resources | World base-game version selects vanilla metadata, textures, actor graphs, and audio before server overrides. The context survives ViaProxy. Server legacy biome bindings retain unqualified names across versions. | Native current/older/current cat, sound, and remote biome-name controls cover selected paths. Broader pose and audio behavior need comparisons. | | Held trapdoors | Retained facing and half, interaction suppression, use timing, cancellation, rejection, and prediction reconciliation. | Native, direct, and proxy controls cover specific held-placement histories. Other placement and interaction families need separate comparisons. | | Fonts and UI | Bitmap metadata, extended mapping, same-pack ownership, legacy layouts, authored scenes, and native GUI sampling. | At 1280 × 720, current controls match all 16 labels at native UI scales 1 and 2; legacy scale-2 controls match 16/16. Both routes pass Java GUI scale changes and reloads. Broader viewports and TrueType comparisons remain open. | -| Player inventory and crafting | Opening ownership, recipe catalogs, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, bulk recipe actions, cursor replacement, profile initialization, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | +| Player inventory and crafting | Opening ownership, recipe catalogs, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, bulk recipe actions, cursor replacement, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | | Cooking | Snapshot-bound recipe actions, transfer batches, preferences, search, and progress. | Default cooking screens retain ordinary controls until authored bindings are complete. See the [cooking reference](native-cooking-actions.md). | | Skins and appearance | Complete skin transport, classic changes, persona assembly, animation graphs, costumes, and attachables. | Broader wardrobe masking, profile/LOD changes, shield rendering, remote appearances, and account/platform flows need further native comparisons. See [skin support](skin-flow.md). | | Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Native Fancy controls establish the tick-based expiry blend and distance curve at noon and midnight. Other fluid movement, vision combinations and media, mount variants, network conditions, and broader replay behavior remain open. | diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index f30a53be..f9464746 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -54,9 +54,11 @@ A workbench model belongs to its exact container generation. `NativePlayerRecipeState` advances the retained collections and refreshes them when sources, recipes, unlocks, rules, or preferences change. Catalog replacement refreshes source collections without reconstructing the backend or replacing its query and selected output. -Missing profile preferences remain absent. -The backend starts with the observed native filtering value, then accepts actual profile settings. -No selected tab is inferred from Java recipe-book defaults. +The native player profile starts with Search, Crafting, filtering off, and automatic layouts. +The controller reads this profile before an inventory-options packet arrives. +Server packets replace all five fields, including explicit zero tabs. +Java filter changes retain the native tabs and automatic layouts. +Fresh direct and ViaProxy sessions retain this state across first opening and reopening without an inventory-options packet. The same negotiated messages serve direct and ViaProxy connections. Late capability announcements must not duplicate a legacy opening. 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 d7426e7a..83739ed2 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 f2ee5665d4139bc41970eba0d54db85f12cba349 Mon Sep 17 00:00:00 2001 +From 71ecc23b682645014a48aae03a2f3c8e913b6b1f 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 @@ -382,8 +382,8 @@ native fields. Player inventory and workbench share one Java crafting book but own independent native layouts. Reproject on actual menu ownership changes; changing filtering alone must preserve the original layout, both tab selections and the other screen's layout, including unknown int32s. -Keep the native None sentinel for unseen fields when Java supplies its -first open/filter choice. +Initialize the constructed player profile with its native tab/filter defaults +and automatic layouts. Java filter edits preserve those profile fields. Generalize FurnaceOptionsStorage to InventoryOptionsStorage so all four Java books retain one connection-local snapshot. Update every consumer @@ -833,6 +833,20 @@ The packet regression preserves potion metadata and definition identity, keeps an absent Block absent, and reads secondary search text. Existing constructor controls retain associated Blocks at runtime zero. The live protocol-2225 Creative search supplied the disconnect regression. + +Initialize the inventory profile from the original 1.26.60.5 Player +constructor: Search5, Crafting2, filteringfalse and automatic layouts0/0. +Original writes at1402b5b86..1402b5ba9, the named NBT loader1402c9fe0, +controller14501f680 and packet307 producer146f59400 establish ownership. +The packet producer's vtable identifies SetPlayerInventoryOptionsPacket. +Four original-instruction controls preserve surrounding bytes and reproduce +the initial fields. The native first-open recording is populated before any +options packet. Absence of that packet is not an absent player profile. + +Project the native automatic layout as an open Java book initially. A +filter-only Java edit must retain automatic layout0 and initial tabs rather +than invent None tabs or force layout2. Later packets replace all raw fields, +including explicit zeros and unknown saved values, through the existing path. --- .../api/model/NativeFurnaceOptions.java | 44 + .../container/CookingRecipeAvailability.java | 30 + @@ -5496,7 +5510,7 @@ index 73165b00..1b68e72e 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryOptionsStorage.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryOptionsStorage.java new file mode 100644 -index 00000000..44131a45 +index 00000000..f13adecf --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryOptionsStorage.java @@ -0,0 +1,186 @@ @@ -5517,6 +5531,8 @@ index 00000000..44131a45 +import net.raphimc.viabedrock.protocol.BedrockProtocol; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.FurnaceLayout; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.InventoryLayout; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.InventoryLeftTabIndex; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.InventoryRightTabIndex; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.FurnaceLeftTabIndex; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.SetPlayerFurnaceOptionsPacketPayload_FurnaceType; +import net.raphimc.viabedrock.protocol.model.FurnaceOptions; @@ -5533,9 +5549,11 @@ index 00000000..44131a45 +public final class InventoryOptionsStorage implements StorableObject { + + private final Map options = new HashMap<>(); -+ private InventoryOptions inventoryOptions; ++ private InventoryOptions inventoryOptions = new InventoryOptions( ++ InventoryLeftTabIndex.RecipeSearch.getValue(), InventoryRightTabIndex.Crafting.getValue(), false, ++ InventoryLayout.None.getValue(), InventoryLayout.None.getValue()); + private final BookSettings[] javaBooks = { -+ new BookSettings(false, false), new BookSettings(false, false), ++ new BookSettings(true, false), new BookSettings(false, false), + new BookSettings(false, false), new BookSettings(false, false) + }; + @@ -5550,9 +5568,6 @@ index 00000000..44131a45 + + /** Java uses one crafting book for two independent native screen layouts. */ + public boolean projectInventory(final boolean crafting) { -+ if (this.inventoryOptions == null) { -+ return false; -+ } + final int layout = crafting ? this.inventoryOptions.craftingLayout() : this.inventoryOptions.inventoryLayout(); + // Native construction maps zero and positive out-of-range layouts to its default. + // Both native defaults (recipe-book and creative) show the book. Negative layouts @@ -5566,10 +5581,9 @@ index 00000000..44131a45 + + public InventoryOptions changeJavaInventoryBook(final boolean crafting, final boolean open, final boolean filtering) { + final int requestedLayout = (open ? InventoryLayout.Default : InventoryLayout.InventoryOnly).getValue(); -+ final boolean unseen = this.inventoryOptions == null; -+ final var previous = unseen ? new InventoryOptions(0, 0, false, 0, 0) : this.inventoryOptions; ++ final var previous = this.inventoryOptions; + final int oldLayout = crafting ? previous.craftingLayout() : previous.inventoryLayout(); -+ final int layout = unseen || this.javaBooks[0].open() != open ? requestedLayout : oldLayout; ++ final int layout = this.javaBooks[0].open() != open ? requestedLayout : oldLayout; + this.inventoryOptions = new InventoryOptions(previous.leftTab(), previous.rightTab(), filtering, + crafting ? previous.inventoryLayout() : layout, crafting ? layout : previous.craftingLayout()); + this.javaBooks[0] = new BookSettings(open, filtering); @@ -9413,7 +9427,7 @@ index 00000000..d2126b57 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/FurnaceOptionsPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/FurnaceOptionsPacketTest.java new file mode 100644 -index 00000000..6e4f72ff +index 00000000..f148bb80 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/FurnaceOptionsPacketTest.java @@ -0,0 +1,162 @@ @@ -9517,7 +9531,7 @@ index 00000000..6e4f72ff + InventoryPackets.handleJavaRecipeBookSettings(crafting); + assertFalse(crafting.isCancelled()); + assertSame(ServerboundBedrockPackets.SET_PLAYER_INVENTORY_OPTIONS, crafting.getPacketType()); -+ assertEquals(new InventoryOptions(0, 0, true, 2, 0), ++ assertEquals(new InventoryOptions(5, 2, true, 0, 0), + crafting.get(BedrockTypes.INVENTORY_OPTIONS, 0)); + assertEquals(new FurnaceOptions(0, 0, true, 0), fixture.preferences.options(0)); + } @@ -9535,7 +9549,7 @@ index 00000000..6e4f72ff + } + final var reopened = fixture.nativePacket(new FurnaceOptions(2, 1, false, 2)); + InventoryPackets.handleFurnaceOptions(reopened); -+ assertArrayEquals(new boolean[]{false, false, false, false, true, false, false, false}, snapshot(reopened)); ++ assertArrayEquals(new boolean[]{true, false, false, false, true, false, false, false}, snapshot(reopened)); + assertNull(new InventoryOptionsStorage().options(2)); + } + @@ -9581,7 +9595,7 @@ index 00000000..6e4f72ff +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryOptionsPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryOptionsPacketTest.java new file mode 100644 -index 00000000..3560ff25 +index 00000000..590c47d4 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryOptionsPacketTest.java @@ -0,0 +1,211 @@ @@ -9743,17 +9757,17 @@ index 00000000..3560ff25 + } + + @Test -+ void unknownLayoutsSurviveFilterChangesAndUnseenPreferencesUseOnlyTheRequestedScreen() { ++ void filterEditsPreserveNativeInitialTabsAutomaticLayoutsAndUnknownSavedValues() { + final var preferences = new InventoryOptionsStorage(); + preferences.applyInventory(new InventoryOptions(-7, Integer.MAX_VALUE, false, -99, Integer.MIN_VALUE), false); + assertEquals(new InventoryOptions(-7, Integer.MAX_VALUE, true, -99, Integer.MIN_VALUE), -+ preferences.changeJavaInventoryBook(false, false, true)); ++ preferences.changeJavaInventoryBook(false, true, true)); + preferences.projectInventory(true); -+ assertEquals(new InventoryOptions(-7, Integer.MAX_VALUE, false, -99, 2), -+ preferences.changeJavaInventoryBook(true, true, false)); -+ assertEquals(new InventoryOptions(0, 0, true, 2, 0), ++ assertEquals(new InventoryOptions(-7, Integer.MAX_VALUE, false, -99, 1), ++ preferences.changeJavaInventoryBook(true, false, false)); ++ assertEquals(new InventoryOptions(5, 2, true, 0, 0), + new InventoryOptionsStorage().changeJavaInventoryBook(false, true, true)); -+ assertEquals(new InventoryOptions(0, 0, false, 0, 1), ++ assertEquals(new InventoryOptions(5, 2, false, 0, 1), + new InventoryOptionsStorage().changeJavaInventoryBook(true, false, false)); + } + diff --git a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md index e94c4f2a..31d19528 100644 --- a/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md +++ b/patches/viabedrock/deferred/0004-expose-bedrock-recipes-and-place-ingredients.pr.md @@ -20,3 +20,10 @@ Creative descriptor parsing uses the same connection-owned Item construction as The packet regression retains potion metadata and definition identity without inventing a Block association for wire runtime zero. It also reads secondary search text and preserves packet alignment after empty group icons. Associated Blocks at runtime zero retain their separate constructor regression. + +Initialize the player inventory profile from the original 1.26.60.5 constructor. +Its fields start with Search, Crafting, filtering off, and automatic layouts. +Ghidra connects these fields to their named NBT keys, controller reads, and packet-307 producer. +Four original-instruction controls reproduce the writes and retain surrounding bytes. +Java filter edits preserve the native tabs and automatic layouts. +Later packets retain their explicit raw values. 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 99b1f1f4..4abaf695 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 218b8c24c844f4fbb4689f0df1318b9616b7c773 Mon Sep 17 00:00:00 2001 +From 9a84a2a44225e2ddbdad771daad7e0f4387a0cb2 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 @@ -152,8 +152,8 @@ container generations. A late player close cannot retire a workbench model. Bind progressive collection ticks to the inventory lifecycle and invalidate collections when source contents, recipes, unlocks, rules, abilities or actual -profile preferences change. Missing preferences stay absent; do not derive a -selected native tab from Java defaults. Catalog replacement rebuilds collection +profile preferences change. Read the constructed native player-profile defaults before any server options +packet, independently of Java book defaults. Preserve explicit packet zeros. Catalog replacement rebuilds collection sources while retaining the backend's captured policies, query and selection. Current 1.26.60.5/protocol2225 instruction mapping identifies derived model @@ -251,7 +251,18 @@ preserves the official 1.26.60.5 captured unit-result/full-Take flow. A request-boundary regression checks stack sizes one and 64, the packet actions, cursor prediction and its pre-request restoration snapshot. -Supply player-inventory tab and layout variables from the protocol-2225 enums. The 1.26.60 inventory graph references these controller-owned indices, just as cooking graphs reference their own indices. Without them, forced toggle indices resolve to null and mouse selection sends zero, leaving an unselected profile unable to choose Search or change layouts. Preserve absent profile preferences instead of inventing a selected tab. +Supply player-inventory tab and layout variables from the protocol-2225 enums. The 1.26.60 inventory graph references these controller-owned indices, just as cooking graphs reference their own indices. Without them, forced toggle indices resolve to null and mouse selection sends zero, leaving an unselected profile unable to choose Search or change layouts. Keep these authored indices separate from the player profile values. + +The original 1.26.60.5 player constructor initializes inventory profile fields +to Search5/Crafting2/filteringfalse/layout0/layout0 at 1402b5b86..1402b5ba9. +The named NBT loader and SetPlayerInventoryOptionsPacket307 producer bind +those fields to the same controller reads. Model creation must use the +constructed profile rather than retain the controller's earlier transient +filter flag. Later packets, including explicit zero tabs, replace the profile while +retaining the model, query and selection. Filtering changes reinstall the +existing policy callbacks; tab-only updates preserve their captured state. +A regression opens without an options packet and observes the populated +Search source, then applies an explicit zero-tab/filter update. --- .../model/NativePlayerInventoryAction.java | 88 ++++ .../model/NativePlayerInventoryMessage.java | 271 ++++++++++ @@ -277,13 +288,13 @@ Supply player-inventory tab and layout variables from the protocol-2225 enums. T .../ui/PlayerInventoryUiTest.java | 126 +++++ .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++++++ .../protocol/packet/InventoryPacketsTest.java | 267 ++++++++-- - .../rewriter/PlayerRecipeModelTest.java | 450 +++++++++++++++++ + .../rewriter/PlayerRecipeModelTest.java | 471 +++++++++++++++++ ...egisteredPlayerRecipeAvailabilityTest.java | 5 +- .../RegisteredPlayerRecipeSearchTest.java | 3 +- .../storage/CreativeInventoryActionTest.java | 214 ++++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ .../storage/NativePlayerCreativePickTest.java | 66 +++ - 30 files changed, 4219 insertions(+), 170 deletions(-) + 30 files changed, 4240 insertions(+), 170 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 @@ -2697,7 +2708,7 @@ index 04ad1756..4a6f0c14 100644 private void updateInventoryOptions() { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java new file mode 100644 -index 00000000..6bdac2db +index 00000000..6c6230ee --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java @@ -0,0 +1,475 @@ @@ -2809,8 +2820,8 @@ index 00000000..6bdac2db + this.preferences = this.user().get(InventoryOptionsStorage.class).inventoryOptions(); + final var items = this.user().get(ItemRewriter.class); + this.catalog = items.creativeCatalog(); -+ // The 1.26.60 controller initializes filtering to true before profile callbacks arrive. -+ this.model = items.playerRecipeModel(context, this.preferences == null || this.preferences.filtering(), true, new byte[0]); ++ // The native player profile exists before this controller reads its filtering policy. ++ this.model = items.playerRecipeModel(context, this.preferences.filtering(), true, new byte[0]); + } + + public void closePlayer(final long opening) { @@ -2843,7 +2854,7 @@ index 00000000..6bdac2db + this.model.replaceCatalog(items); + this.inputs = null; + } -+ if (preferences != null && (this.preferences == null || preferences.filtering() != this.preferences.filtering())) { ++ if (preferences.filtering() != this.preferences.filtering()) { + this.model.reinstallPolicies(preferences.filtering()); + } + this.preferences = preferences; @@ -2862,8 +2873,8 @@ index 00000000..6bdac2db + this.presentationDirty = true; + this.publicationFailed = false; + } -+ // None advances population without inventing a native profile's selected tab. -+ final boolean changed = this.model.tick(preferences == null ? 0 : preferences.leftTab()); ++ // Population advances against the retained native player-profile tab. ++ final boolean changed = this.model.tick(preferences.leftTab()); + if (changed) { + this.presentationDirty = true; + } @@ -3075,7 +3086,7 @@ index 00000000..6bdac2db + this.model.refresh(view, false); + counts.add(this.model.collections().collection(view).snapshot().eligibleCount()); + } -+ final int tab = this.preferences == null ? 0 : this.preferences.leftTab(); ++ final int tab = this.preferences.leftTab(); + final var active = Arrays.stream(CreativeInventoryModel.View.values()).filter(view -> view.leftTab() == tab).findFirst().orElse(null); + final var cells = new ArrayList(); + final Map previews = new HashMap<>(); @@ -3127,7 +3138,7 @@ index 00000000..6bdac2db + "#container_item_background_texture", new JsonPrimitive(texture)))); + } + } -+ final boolean filtering = this.preferences == null || this.preferences.filtering(); ++ final boolean filtering = this.preferences.filtering(); + final String key = switch (tab) { + case 1 -> "craftingScreen.tab.construction"; + case 2 -> "craftingScreen.tab.equipment"; @@ -3150,8 +3161,8 @@ index 00000000..6bdac2db + } + } + final var state = new PlayerInventoryUi.State(this.context.workbench(), creative, -+ this.preferences == null ? 0 : this.preferences.inventoryLayout(), this.preferences == null ? 0 : this.preferences.craftingLayout(), -+ tab, this.preferences == null ? 0 : this.preferences.rightTab(), filtering, NbtUtf8.decode(this.model.query()), ++ this.preferences.inventoryLayout(), this.preferences.craftingLayout(), ++ tab, this.preferences.rightTab(), filtering, NbtUtf8.decode(this.model.query()), + key.isEmpty() ? "" : resources.getTexts().translate(key), resources.getTexts().translate(label), counts, cells, "", -1); + return new Presentation(state, previews); + } @@ -4416,10 +4427,10 @@ index 19fe63ab..8cc84c93 100644 } diff --git a/src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java new file mode 100644 -index 00000000..c5f9da3a +index 00000000..92b4e7cd --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java -@@ -0,0 +1,450 @@ +@@ -0,0 +1,471 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -4739,9 +4750,30 @@ index 00000000..c5f9da3a + } + + @Test ++ void openingWithoutAnOptionsPacketPopulatesTheNativeInitialSearchView() throws Exception { ++ final var f = installed(true, false); ++ final var options = new InventoryOptionsStorage(); ++ when(f.context.items.user().get(InventoryOptionsStorage.class)).thenReturn(options); ++ f.screens.request(new PlayerInventoryScreenRequest(1, true)); ++ final var model = f.state.model(); ++ for (int tick = 0; tick < 20; tick++) { ++ f.state.tick(); ++ } ++ assertEquals(4, model.collections().collection(CreativeInventoryModel.View.SEARCH).snapshot().eligibleCount()); ++ assertEquals(new InventoryOptions(5, 2, false, 0, 0), f.state.preferences()); ++ options.applyInventory(new InventoryOptions(0, 0, true, 0, 0), false); ++ f.state.tick(); ++ assertSame(model, f.state.model()); ++ assertEquals(new InventoryOptions(0, 0, true, 0, 0), f.state.preferences()); ++ model.refresh(CreativeInventoryModel.View.SEARCH, false); ++ assertEquals(1, model.collections().collection(CreativeInventoryModel.View.SEARCH).snapshot().eligibleCount()); ++ } ++ ++ @Test + void profileArrivalAndCatalogReplacementKeepTheBackendAndItsOwnedQuery() throws Exception { + final var f = installed(true, false); + final var options = new InventoryOptionsStorage(); ++ options.applyInventory(new InventoryOptions(5, 0, true, 0, 0), false); + when(f.context.items.user().get(InventoryOptionsStorage.class)).thenReturn(options); + f.screens.request(new PlayerInventoryScreenRequest(1, true)); + final var model = f.state.model(); @@ -4750,7 +4782,7 @@ index 00000000..c5f9da3a + } + model.updateQuery("on"); + model.selectOutput(new BedrockItem(IDENTIFIER)); -+ assertNull(f.state.preferences()); ++ assertTrue(f.state.preferences().filtering()); + assertEquals(1, model.collections().collection(CreativeInventoryModel.View.SEARCH).snapshot().eligibleCount()); + options.applyInventory(new InventoryOptions(5, 0, false, 2, 2), false); + f.state.tick(); diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index 495d28f5..61f7ebc2 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -61,8 +61,13 @@ Acknowledged container identity and generation still control item actions. A workbench model retains its exact container generation. Retired openings cannot read replacement sources or retire a later workbench model. -Real profile settings update filtering and the selected tab. -Missing profile preferences remain absent; the initial filtering value follows the observed current native constructor. +The constructed player profile starts with Search, Crafting, filtering off, and automatic layouts. +Later server packets replace all five fields, including explicit zero tabs. +The backend reads the current profile rather than retaining the controller's transient filter flag. +A regression opens without an options packet and observes the populated Search source. +An explicit zero-tab packet then replaces the profile without replacing the model. +Fresh-world direct and ViaProxy controls retain all 1,980 items across two openings, with no inventory-options packet in either journal. +Both installed add-on hashes match the current build. Source, recipe, unlock, ability, rule, and preference changes invalidate the retained collections. Catalog replacement refreshes source collections while preserving the backend's installed policies and owned query. The state publishes the retained rows and full item previews through a bounded, compressed protocol-2225 snapshot. @@ -121,4 +126,4 @@ Player and workbench graphs receive their tab and layout indices from the protoc The native inventory graph references these controller variables in forced toggle indices. Missing variables resolve to null and make category clicks request index zero. Screen regressions click category and layout controls through exported programs in both inventory layouts. -These bindings preserve absent profile preferences and do not select a default tab. +These authored control indices remain independent of the selected profile values. From be7c44bf85b8f5a96d81374e87e97b720c3ef28d Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 08:27:15 +0100 Subject: [PATCH 28/33] docs(inventory): record verified bundle transfer behavior Record server-accepted insertion, extraction, and reinsertion on direct and ViaProxy routes, including contents retained after reopening. Keep official Bedrock first-request behavior and the reported modpack rendering problems explicitly unresolved. --- docs/native-player-inventory.md | 7 +++++-- ...3-translate-server-authoritative-bedrock-requests.pr.md | 3 +++ .../0029-present-authored-native-containers.pr.md | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index f9464746..757dd994 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -136,7 +136,10 @@ Both routes return from Help with a carried stack and confirm its placement thro Inactive item icons use separate lighting and opacity. The default disabled light multiplier is 0.25. Typed overrides and RGB conversion match selected native instruction controls. Complete material and pixel parity remain unverified. Storage bars read a native weight snapshot independently of Java bundle contents. -Installed direct and ViaProxy controls retain a server-issued empty bundle in the Survival menu after reopening. +Installed direct and ViaProxy controls insert eight logs into a server-issued empty bundle, extract them, and insert them again. +Both routes retain the filled bundle after reopening, with server responses that accept each request. +These checks use ordinary Java 26.3 controls after the storage-tooltip fallback. Left-click inserts items; right-click extracts them. +The first insertion request from the official Bedrock client remains unverified. Filled-bundle bars still need installed native comparison. Unknown storage snapshots restore ordinary controls. Hovering or carrying storage items also restores ordinary controls until native tooltips and selection are complete. @@ -145,7 +148,7 @@ Creative hotbar destruction restores ordinary controls before item changes or re Installed direct and ViaProxy controls retain the complete 1,980-item catalog from a local protocol-2225 server. Tab and layout controls use the target inventory indices. Both routes search for rails and place a server-accepted stack of 64 in the hotbar. -Initial bundle insertion, bulk recipe actions, Creative cursor replacement, and complete installed native parity remain unfinished. +Bulk recipe actions, Creative cursor replacement, and complete installed native parity remain unfinished. Native constructor timing relative to the open acknowledgment and frame-level population timing remain unverified. An unpositioned player inventory uses the fallback crafting context. Native model-position initialization and arbitrary custom component admission remain unverified. diff --git a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.pr.md b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.pr.md index fe8ae02c..b05d54d0 100644 --- a/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.pr.md +++ b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.pr.md @@ -14,4 +14,7 @@ Regression coverage includes `ContainerTest`, `HorseContainerTest`, `NativeArmor Late replies and replacement menus have targeted regressions. Complete native response-manager behavior, live transactions, and every container remain unfinished. +Installed direct and ViaProxy checks accept bundle insertion, extraction, and reinsertion requests and retain the contents after reopening. +See the [inventory reference](../../../docs/native-player-inventory.md) for the control path and remaining native comparison limit. + The [owning patch](0003-translate-server-authoritative-bedrock-requests.patch) retains implementation rationale and versioned evidence. 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 217db1bc..7d02eb1d 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 @@ -59,7 +59,9 @@ Installed direct and ViaProxy controls retain all 1,980 catalog entries from a l Physical category and layout changes preserve their native indices through exported programs. Both routes search for rails and place a server-accepted stack of 64 in the hotbar. ViaProxy retains both stacks after reopening. -Initial bundle insertion, bulk recipe actions, and Creative cursor replacement remain unresolved. +Installed direct and ViaProxy controls cover initial bundle insertion, extraction, reinsertion, and retention after reopening through ordinary Java controls. +The [inventory reference](../../../docs/native-player-inventory.md) states the verified input behavior and remaining native comparison limit. +Bulk recipe actions and Creative cursor replacement remain unresolved. Complete cooking integration, live transfers, and filled-bundle pixels also remain unresolved. Stable frames reuse the draw list, parsed hit regions, and input ancestor geometry. From 0a6cd0143ab77b67dc530b043dfb7a4f165d2097 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 08:44:40 +0100 Subject: [PATCH 29/33] docs(rendering): record shield movement and projectile controls Record the installed movement and genuine skeleton-block checks without claiming a fix for the reported extreme shield pose or native hit-timing parity. --- .../0008-share-native-actor-and-resource-runtime.pr.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md index c9540b13..6f757570 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md @@ -42,8 +42,11 @@ Crouching raises and tilts the shield. Releasing crouch restores the resting pos The installed add-on retains these transitions on local BDS 1.26.60.6 through NetherNet HTTP, using crouch and Use inputs. Read-only controller snapshots show that blocking clears and the resting transform returns after either input. Opening and closing the Survival inventory leaves the resting shield transform unchanged in early and later samples. +Installed direct controls also cover forward movement, strafing, and skeleton projectile blocks. +The packet journal contains blocked-shield and damaged-shield flags, while the projectile samples retain full health. +Releasing the controls restores the resting shield transform. These controls do not reproduce the reported extreme shield pose. -Movement, hit reactions, patterned shields, and third-party mod combinations remain open. +Exact native movement and hit-animation timing, patterned shields, and third-party mod combinations remain open. Animation timelines retain the remaining animation when an individual Molang string has a syntax error. The failed script keeps a zero-valued entry at its authored time. Structural validation and resource limits still reject invalid input. From a7159376700e90cd9d26af54c810a522e519ccac Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:11:01 +0100 Subject: [PATCH 30/33] refactor(blocks): load conditional registrations from Cloudburst NBT Use the unmodified public data-driven block snapshot as the source of truth instead of maintaining a derived JSON list. All 98 registrations match the captured BDS and native reference, while palette ordering and current StartGame admission stay unchanged. Record the pinned source, checksum, update procedure, and bundled license. The full build, palette regressions, artifact checks, and fresh patch replay pass. --- ...e-block-state-geometry-and-materials.patch | 503 +++++++++++++----- ...e-block-state-geometry-and-materials.pr.md | 7 + ...rate-authored-native-ui-presentation.patch | 6 +- 3 files changed, 373 insertions(+), 143 deletions(-) diff --git a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch index d69d4a0b..e802d863 100644 --- a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch +++ b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.patch @@ -1,4 +1,4 @@ -From 48a516a162681ecaf30e90a7a478b95c31a27a17 Mon Sep 17 00:00:00 2001 +From 89e6d5255d1d47f9795ff066cab20861f7562650 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:35 +0200 Subject: [PATCH] feat(blocks): render Bedrock state geometry and materials @@ -387,14 +387,22 @@ client traces StartGame, LevelChunk and registry enumeration; the live registry comparison establishes the registration boundary. Preserve the canonical state objects, hashes and order for advertised vanilla types. Keep server-specific packets and process observations private. + +Read conditional registration names directly from the pinned, unmodified +CloudburstMC/Data data_driven_blocks.nbt snapshot. Its 98 names exactly +match the stock BDS and native registry control. Keep the compressed NBT +as the source asset instead of maintaining a derived JSON list and a +conversion tool. Bundle its Apache-2.0 license and document the source, +checksum, and update procedure in Data Asset Sources.md. Runtime palette +selection still depends on the current StartGame registrations. --- - Data Asset Sources.md | 27 + + Data Asset Sources.md | 49 + .../chunk/blockstate/BlockItemDescriptor.java | 29 + .../blockstate/BlockItemDescriptors.java | 141 +++ .../api/chunk/blockstate/BlockItemStates.java | 134 ++ .../chunk/blockstate/BlockRegistryNames.java | 145 +++ .../chunk/blockstate/RuntimeBlockPalette.java | 68 + - .../chunk/blockstate/VanillaBlockStates.java | 60 + + .../chunk/blockstate/VanillaBlockStates.java | 73 ++ .../api/model/CustomBlockCulling.java | 206 +++ .../api/model/CustomBlockFaceLighting.java | 98 ++ .../api/model/CustomBlockLighting.java | 59 + @@ -426,16 +434,17 @@ Keep server-specific packets and process observations private. .../protocol/storage/VoxelShapeRegistry.java | 125 ++ .../protocol/storage/WorldPacketBuffer.java | 75 ++ .../protocol/task/KeepAliveTask.java | 10 +- + .../META-INF/licenses/cloudburst-data.txt | 201 +++ .../data/bedrock/block_item_states.json | 1121 +++++++++++++++++ .../data/bedrock/block_property_values.json | 979 ++++++++++++++ .../data/bedrock/block_registry_aliases.json | 66 + .../bedrock/block_restriction_groups.json | 549 ++++++++ - .../data/bedrock/server_defined_blocks.json | 105 ++ + .../data/bedrock/data_driven_blocks.nbt | Bin 0 -> 3707 bytes .../blockstate/BlockItemDescriptorsTest.java | 119 ++ .../chunk/blockstate/BlockItemStatesTest.java | 135 ++ .../blockstate/BlockRegistryNamesTest.java | 107 ++ .../blockstate/RuntimeBlockPaletteTest.java | 61 + - .../blockstate/VanillaBlockStatesTest.java | 78 ++ + .../blockstate/VanillaBlockStatesTest.java | 79 ++ .../api/model/CustomBlockCullingTest.java | 54 + .../api/model/CustomBlockLightingTest.java | 171 +++ .../api/model/CustomBlockOffsetTest.java | 80 ++ @@ -456,7 +465,7 @@ Keep server-specific packets and process observations private. .../storage/VoxelShapeRegistryTest.java | 130 ++ .../storage/WorldPacketBufferTest.java | 230 ++++ .../protocol/task/KeepAliveTaskTest.java | 78 ++ - 68 files changed, 9856 insertions(+), 112 deletions(-) + 69 files changed, 9988 insertions(+), 112 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemDescriptor.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemDescriptors.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemStates.java @@ -481,11 +490,12 @@ Keep server-specific packets and process observations private. create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/storage/CustomBlockPackStorage.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/storage/VoxelShapeRegistry.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/storage/WorldPacketBuffer.java + create mode 100644 src/main/resources/META-INF/licenses/cloudburst-data.txt create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/block_item_states.json create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/block_property_values.json create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/block_registry_aliases.json create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/block_restriction_groups.json - create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/server_defined_blocks.json + create mode 100644 src/main/resources/assets/viabedrock/data/bedrock/data_driven_blocks.nbt create mode 100644 src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemDescriptorsTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockItemStatesTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/chunk/blockstate/BlockRegistryNamesTest.java @@ -511,13 +521,35 @@ Keep server-specific packets and process observations private. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/task/KeepAliveTaskTest.java diff --git a/Data Asset Sources.md b/Data Asset Sources.md -index 7912edc8..bd94d2dd 100644 +index 7912edc8..6dbf4ed5 100644 --- a/Data Asset Sources.md +++ b/Data Asset Sources.md -@@ -25,6 +25,26 @@ The Java data generator and its setup steps are in [generator/README.md](generat +@@ -25,6 +25,48 @@ The Java data generator and its setup steps are in [generator/README.md](generat [GeyserMC/Geyser](https://github.com/GeyserMC/Geyser/blob/fd35d5918ae8e8b6c05d91179315fa00413c1f65/core/src/main/resources/bedrock/block_palette.26_60.nbt) ++### data/bedrock/data_driven_blocks.nbt ++ ++Unmodified [CloudburstMC/Data snapshot](https://github.com/CloudburstMC/Data/blob/0021592b1a4253528bad1e1d8b537e306e097557/data_driven_blocks.nbt). ++Its SHA-256 is `6177d78243a36ec6e28d4b0d22df1fa3c191e0cd61082ab8c85df0b15a1e1d9d`. ++The bundled [Cloudburst data license](src/main/resources/META-INF/licenses/cloudburst-data.txt) covers this file. ++ ++The file contains a gzip-compressed, big-endian NBT compound keyed by block identifier. ++Its 98 keys match the server-defined vanilla registrations captured from BDS 1.26.60.6, protocol 2225. ++Official Bedrock 1.26.60.5 registries confirm that these types require definitions in `StartGame`. ++The loader uses their names to filter the canonical palette. It preserves canonical state objects, hashes, and ordering. ++ ++To restore this verified snapshot, run: ++ ++```bash ++curl --fail --location https://raw.githubusercontent.com/CloudburstMC/Data/0021592b1a4253528bad1e1d8b537e306e097557/data_driven_blocks.nbt --output src/main/resources/assets/viabedrock/data/bedrock/data_driven_blocks.nbt ++sha256sum src/main/resources/assets/viabedrock/data/bedrock/data_driven_blocks.nbt ++``` ++ ++For a protocol update, select a matching source revision and compare its registrations with stock BDS and the native client. ++Then update the source link, checksum, and palette regression expectations together. ++The complete block palette alone does not identify conditional registrations. ++ +### data/bedrock/block_item_states.json + +This file records defaults, auxiliary variants, and legacy serialization names for Bedrock 1.26.51.1, protocol 2193. @@ -541,7 +573,7 @@ index 7912edc8..bd94d2dd 100644 ### data/bedrock/block_sounds.json This file has been generated using `./gradlew generateBedrockSoundLists` from the official bedrock client's assets folder. -@@ -134,3 +154,10 @@ It adds `minecraft:ice_ball` with numeric ID 156 to the previous stable table. +@@ -134,3 +176,10 @@ It adds `minecraft:ice_ball` with numeric ID 156 to the previous stable table. The generated experimental sound table also requires `minecraft:frostbite`. Its identifier, spawn egg, and numeric ID 155 come from the matching client definition and `ActorType` metadata. The Frostbite entry is a curated experimental addition, not part of the default server registry capture. @@ -1101,10 +1133,10 @@ index 00000000..72d7bd19 +} diff --git a/src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStates.java b/src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStates.java new file mode 100644 -index 00000000..0b101cc2 +index 00000000..d5f4825d --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/chunk/blockstate/VanillaBlockStates.java -@@ -0,0 +1,60 @@ +@@ -0,0 +1,73 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -1112,19 +1144,21 @@ index 00000000..0b101cc2 + */ +package net.raphimc.viabedrock.api.chunk.blockstate; + -+import com.viaversion.viaversion.libs.gson.JsonParser; ++import com.viaversion.nbt.io.NBTIO; ++import com.viaversion.nbt.limiter.TagLimiter; ++import com.viaversion.nbt.tag.CompoundTag; +import net.raphimc.viabedrock.api.model.BedrockBlockState; +import net.raphimc.viabedrock.protocol.model.BlockProperties; + ++import java.io.DataInputStream; +import java.io.IOException; -+import java.io.InputStreamReader; -+import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; ++import java.util.zip.GZIPInputStream; + +public final class VanillaBlockStates { + @@ -1150,15 +1184,26 @@ index 00000000..0b101cc2 + } + + private static Set loadServerDefined() { -+ final String path = "/assets/viabedrock/data/bedrock/server_defined_blocks.json"; ++ final String path = "/assets/viabedrock/data/bedrock/data_driven_blocks.nbt"; + try (var input = VanillaBlockStates.class.getResourceAsStream(path)) { + if (input == null) { + throw new IllegalStateException("Missing server-defined vanilla block registry"); + } -+ final var json = JsonParser.parseReader(new InputStreamReader(input, StandardCharsets.UTF_8)).getAsJsonObject(); -+ final Set names = new HashSet<>(); -+ json.getAsJsonArray("blocks").forEach(name -> names.add(name.getAsString())); -+ return Set.copyOf(names); ++ try (var data = new DataInputStream(new GZIPInputStream(input))) { ++ final CompoundTag definitions = NBTIO.readTag(data, TagLimiter.create(8 << 20, 64), true, CompoundTag.class); ++ if (definitions.isEmpty() || data.read() != -1) { ++ throw new IOException("Invalid server-defined vanilla block registry"); ++ } ++ final Set names = new HashSet<>(); ++ for (final var entry : definitions) { ++ if (!(entry.getValue() instanceof CompoundTag properties) ++ || new BlockProperties(entry.getKey(), properties).legacyId() == null) { ++ throw new IOException("Missing vanilla registration for " + entry.getKey()); ++ } ++ names.add(entry.getKey()); ++ } ++ return Set.copyOf(names); ++ } + } catch (final IOException error) { + throw new IllegalStateException("Failed to load server-defined vanilla block registry", error); + } @@ -4943,6 +4988,213 @@ index 3f0dc0ae..c8b89e1d 100644 keepAlive.write(Types.LONG, ThreadLocalRandom.current().nextLong()); // id keepAlive.send(BedrockProtocol.class); } catch (final Throwable e) { +diff --git a/src/main/resources/META-INF/licenses/cloudburst-data.txt b/src/main/resources/META-INF/licenses/cloudburst-data.txt +new file mode 100644 +index 00000000..261eeb9e +--- /dev/null ++++ b/src/main/resources/META-INF/licenses/cloudburst-data.txt +@@ -0,0 +1,201 @@ ++ Apache License ++ Version 2.0, January 2004 ++ http://www.apache.org/licenses/ ++ ++ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION ++ ++ 1. Definitions. ++ ++ "License" shall mean the terms and conditions for use, reproduction, ++ and distribution as defined by Sections 1 through 9 of this document. ++ ++ "Licensor" shall mean the copyright owner or entity authorized by ++ the copyright owner that is granting the License. ++ ++ "Legal Entity" shall mean the union of the acting entity and all ++ other entities that control, are controlled by, or are under common ++ control with that entity. For the purposes of this definition, ++ "control" means (i) the power, direct or indirect, to cause the ++ direction or management of such entity, whether by contract or ++ otherwise, or (ii) ownership of fifty percent (50%) or more of the ++ outstanding shares, or (iii) beneficial ownership of such entity. ++ ++ "You" (or "Your") shall mean an individual or Legal Entity ++ exercising permissions granted by this License. ++ ++ "Source" form shall mean the preferred form for making modifications, ++ including but not limited to software source code, documentation ++ source, and configuration files. ++ ++ "Object" form shall mean any form resulting from mechanical ++ transformation or translation of a Source form, including but ++ not limited to compiled object code, generated documentation, ++ and conversions to other media types. ++ ++ "Work" shall mean the work of authorship, whether in Source or ++ Object form, made available under the License, as indicated by a ++ copyright notice that is included in or attached to the work ++ (an example is provided in the Appendix below). ++ ++ "Derivative Works" shall mean any work, whether in Source or Object ++ form, that is based on (or derived from) the Work and for which the ++ editorial revisions, annotations, elaborations, or other modifications ++ represent, as a whole, an original work of authorship. For the purposes ++ of this License, Derivative Works shall not include works that remain ++ separable from, or merely link (or bind by name) to the interfaces of, ++ the Work and Derivative Works thereof. ++ ++ "Contribution" shall mean any work of authorship, including ++ the original version of the Work and any modifications or additions ++ to that Work or Derivative Works thereof, that is intentionally ++ submitted to Licensor for inclusion in the Work by the copyright owner ++ or by an individual or Legal Entity authorized to submit on behalf of ++ the copyright owner. For the purposes of this definition, "submitted" ++ means any form of electronic, verbal, or written communication sent ++ to the Licensor or its representatives, including but not limited to ++ communication on electronic mailing lists, source code control systems, ++ and issue tracking systems that are managed by, or on behalf of, the ++ Licensor for the purpose of discussing and improving the Work, but ++ excluding communication that is conspicuously marked or otherwise ++ designated in writing by the copyright owner as "Not a Contribution." ++ ++ "Contributor" shall mean Licensor and any individual or Legal Entity ++ on behalf of whom a Contribution has been received by Licensor and ++ subsequently incorporated within the Work. ++ ++ 2. Grant of Copyright License. Subject to the terms and conditions of ++ this License, each Contributor hereby grants to You a perpetual, ++ worldwide, non-exclusive, no-charge, royalty-free, irrevocable ++ copyright license to reproduce, prepare Derivative Works of, ++ publicly display, publicly perform, sublicense, and distribute the ++ Work and such Derivative Works in Source or Object form. ++ ++ 3. Grant of Patent License. Subject to the terms and conditions of ++ this License, each Contributor hereby grants to You a perpetual, ++ worldwide, non-exclusive, no-charge, royalty-free, irrevocable ++ (except as stated in this section) patent license to make, have made, ++ use, offer to sell, sell, import, and otherwise transfer the Work, ++ where such license applies only to those patent claims licensable ++ by such Contributor that are necessarily infringed by their ++ Contribution(s) alone or by combination of their Contribution(s) ++ with the Work to which such Contribution(s) was submitted. If You ++ institute patent litigation against any entity (including a ++ cross-claim or counterclaim in a lawsuit) alleging that the Work ++ or a Contribution incorporated within the Work constitutes direct ++ or contributory patent infringement, then any patent licenses ++ granted to You under this License for that Work shall terminate ++ as of the date such litigation is filed. ++ ++ 4. Redistribution. You may reproduce and distribute copies of the ++ Work or Derivative Works thereof in any medium, with or without ++ modifications, and in Source or Object form, provided that You ++ meet the following conditions: ++ ++ (a) You must give any other recipients of the Work or ++ Derivative Works a copy of this License; and ++ ++ (b) You must cause any modified files to carry prominent notices ++ stating that You changed the files; and ++ ++ (c) You must retain, in the Source form of any Derivative Works ++ that You distribute, all copyright, patent, trademark, and ++ attribution notices from the Source form of the Work, ++ excluding those notices that do not pertain to any part of ++ the Derivative Works; and ++ ++ (d) If the Work includes a "NOTICE" text file as part of its ++ distribution, then any Derivative Works that You distribute must ++ include a readable copy of the attribution notices contained ++ within such NOTICE file, excluding those notices that do not ++ pertain to any part of the Derivative Works, in at least one ++ of the following places: within a NOTICE text file distributed ++ as part of the Derivative Works; within the Source form or ++ documentation, if provided along with the Derivative Works; or, ++ within a display generated by the Derivative Works, if and ++ wherever such third-party notices normally appear. The contents ++ of the NOTICE file are for informational purposes only and ++ do not modify the License. You may add Your own attribution ++ notices within Derivative Works that You distribute, alongside ++ or as an addendum to the NOTICE text from the Work, provided ++ that such additional attribution notices cannot be construed ++ as modifying the License. ++ ++ You may add Your own copyright statement to Your modifications and ++ may provide additional or different license terms and conditions ++ for use, reproduction, or distribution of Your modifications, or ++ for any such Derivative Works as a whole, provided Your use, ++ reproduction, and distribution of the Work otherwise complies with ++ the conditions stated in this License. ++ ++ 5. Submission of Contributions. Unless You explicitly state otherwise, ++ any Contribution intentionally submitted for inclusion in the Work ++ by You to the Licensor shall be under the terms and conditions of ++ this License, without any additional terms or conditions. ++ Notwithstanding the above, nothing herein shall supersede or modify ++ the terms of any separate license agreement you may have executed ++ with Licensor regarding such Contributions. ++ ++ 6. Trademarks. This License does not grant permission to use the trade ++ names, trademarks, service marks, or product names of the Licensor, ++ except as required for reasonable and customary use in describing the ++ origin of the Work and reproducing the content of the NOTICE file. ++ ++ 7. Disclaimer of Warranty. Unless required by applicable law or ++ agreed to in writing, Licensor provides the Work (and each ++ Contributor provides its Contributions) on an "AS IS" BASIS, ++ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or ++ implied, including, without limitation, any warranties or conditions ++ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A ++ PARTICULAR PURPOSE. You are solely responsible for determining the ++ appropriateness of using or redistributing the Work and assume any ++ risks associated with Your exercise of permissions under this License. ++ ++ 8. Limitation of Liability. In no event and under no legal theory, ++ whether in tort (including negligence), contract, or otherwise, ++ unless required by applicable law (such as deliberate and grossly ++ negligent acts) or agreed to in writing, shall any Contributor be ++ liable to You for damages, including any direct, indirect, special, ++ incidental, or consequential damages of any character arising as a ++ result of this License or out of the use or inability to use the ++ Work (including but not limited to damages for loss of goodwill, ++ work stoppage, computer failure or malfunction, or any and all ++ other commercial damages or losses), even if such Contributor ++ has been advised of the possibility of such damages. ++ ++ 9. Accepting Warranty or Additional Liability. While redistributing ++ the Work or Derivative Works thereof, You may choose to offer, ++ and charge a fee for, acceptance of support, warranty, indemnity, ++ or other liability obligations and/or rights consistent with this ++ License. However, in accepting such obligations, You may act only ++ on Your own behalf and on Your sole responsibility, not on behalf ++ of any other Contributor, and only if You agree to indemnify, ++ defend, and hold each Contributor harmless for any liability ++ incurred by, or claims asserted against, such Contributor by reason ++ of your accepting any such warranty or additional liability. ++ ++ END OF TERMS AND CONDITIONS ++ ++ APPENDIX: How to apply the Apache License to your work. ++ ++ To apply the Apache License to your work, attach the following ++ boilerplate notice, with the fields enclosed by brackets "[]" ++ replaced with your own identifying information. (Don't include ++ the brackets!) The text should be enclosed in the appropriate ++ comment syntax for the file format. We also recommend that a ++ file or class name and description of purpose be included on the ++ same "printed page" as the copyright notice for easier ++ identification within third-party archives. ++ ++ Copyright [yyyy] [name of copyright owner] ++ ++ Licensed under the Apache License, Version 2.0 (the "License"); ++ you may not use this file except in compliance with the License. ++ You may obtain a copy of the License at ++ ++ http://www.apache.org/licenses/LICENSE-2.0 ++ ++ Unless required by applicable law or agreed to in writing, software ++ distributed under the License is distributed on an "AS IS" BASIS, ++ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ See the License for the specific language governing permissions and ++ limitations under the License. diff --git a/src/main/resources/assets/viabedrock/data/bedrock/block_item_states.json b/src/main/resources/assets/viabedrock/data/bedrock/block_item_states.json new file mode 100644 index 00000000..450ca96e @@ -7682,117 +7934,87 @@ index 00000000..0343c445 + "minecraft:piglin_head" + ] +} -diff --git a/src/main/resources/assets/viabedrock/data/bedrock/server_defined_blocks.json b/src/main/resources/assets/viabedrock/data/bedrock/server_defined_blocks.json +diff --git a/src/main/resources/assets/viabedrock/data/bedrock/data_driven_blocks.nbt b/src/main/resources/assets/viabedrock/data/bedrock/data_driven_blocks.nbt new file mode 100644 -index 00000000..c5825696 ---- /dev/null -+++ b/src/main/resources/assets/viabedrock/data/bedrock/server_defined_blocks.json -@@ -0,0 +1,105 @@ -+{ -+ "format": 1, -+ "protocol": 2225, -+ "gameVersion": "1.26.60.5", -+ "blocks": [ -+ "minecraft:black_concrete_double_slab", -+ "minecraft:black_concrete_slab", -+ "minecraft:black_concrete_stairs", -+ "minecraft:black_wool_double_slab", -+ "minecraft:black_wool_slab", -+ "minecraft:black_wool_stairs", -+ "minecraft:blue_concrete_double_slab", -+ "minecraft:blue_concrete_slab", -+ "minecraft:blue_concrete_stairs", -+ "minecraft:blue_wool_double_slab", -+ "minecraft:blue_wool_slab", -+ "minecraft:blue_wool_stairs", -+ "minecraft:brown_concrete_double_slab", -+ "minecraft:brown_concrete_slab", -+ "minecraft:brown_concrete_stairs", -+ "minecraft:brown_wool_double_slab", -+ "minecraft:brown_wool_slab", -+ "minecraft:brown_wool_stairs", -+ "minecraft:cyan_concrete_double_slab", -+ "minecraft:cyan_concrete_slab", -+ "minecraft:cyan_concrete_stairs", -+ "minecraft:cyan_wool_double_slab", -+ "minecraft:cyan_wool_slab", -+ "minecraft:cyan_wool_stairs", -+ "minecraft:gray_concrete_double_slab", -+ "minecraft:gray_concrete_slab", -+ "minecraft:gray_concrete_stairs", -+ "minecraft:gray_wool_double_slab", -+ "minecraft:gray_wool_slab", -+ "minecraft:gray_wool_stairs", -+ "minecraft:green_concrete_double_slab", -+ "minecraft:green_concrete_slab", -+ "minecraft:green_concrete_stairs", -+ "minecraft:green_wool_double_slab", -+ "minecraft:green_wool_slab", -+ "minecraft:green_wool_stairs", -+ "minecraft:light_blue_concrete_double_slab", -+ "minecraft:light_blue_concrete_slab", -+ "minecraft:light_blue_concrete_stairs", -+ "minecraft:light_blue_wool_double_slab", -+ "minecraft:light_blue_wool_slab", -+ "minecraft:light_blue_wool_stairs", -+ "minecraft:light_gray_concrete_double_slab", -+ "minecraft:light_gray_concrete_slab", -+ "minecraft:light_gray_concrete_stairs", -+ "minecraft:light_gray_wool_double_slab", -+ "minecraft:light_gray_wool_slab", -+ "minecraft:light_gray_wool_stairs", -+ "minecraft:lime_concrete_double_slab", -+ "minecraft:lime_concrete_slab", -+ "minecraft:lime_concrete_stairs", -+ "minecraft:lime_wool_double_slab", -+ "minecraft:lime_wool_slab", -+ "minecraft:lime_wool_stairs", -+ "minecraft:magenta_concrete_double_slab", -+ "minecraft:magenta_concrete_slab", -+ "minecraft:magenta_concrete_stairs", -+ "minecraft:magenta_wool_double_slab", -+ "minecraft:magenta_wool_slab", -+ "minecraft:magenta_wool_stairs", -+ "minecraft:orange_concrete_double_slab", -+ "minecraft:orange_concrete_slab", -+ "minecraft:orange_concrete_stairs", -+ "minecraft:orange_wool_double_slab", -+ "minecraft:orange_wool_slab", -+ "minecraft:orange_wool_stairs", -+ "minecraft:pink_concrete_double_slab", -+ "minecraft:pink_concrete_slab", -+ "minecraft:pink_concrete_stairs", -+ "minecraft:pink_wool_double_slab", -+ "minecraft:pink_wool_slab", -+ "minecraft:pink_wool_stairs", -+ "minecraft:purple_concrete_double_slab", -+ "minecraft:purple_concrete_slab", -+ "minecraft:purple_concrete_stairs", -+ "minecraft:purple_wool_double_slab", -+ "minecraft:purple_wool_slab", -+ "minecraft:purple_wool_stairs", -+ "minecraft:red_concrete_double_slab", -+ "minecraft:red_concrete_slab", -+ "minecraft:red_concrete_stairs", -+ "minecraft:red_shrub", -+ "minecraft:red_wool_double_slab", -+ "minecraft:red_wool_slab", -+ "minecraft:red_wool_stairs", -+ "minecraft:shelf_mushroom", -+ "minecraft:white_concrete_double_slab", -+ "minecraft:white_concrete_slab", -+ "minecraft:white_concrete_stairs", -+ "minecraft:white_wool_double_slab", -+ "minecraft:white_wool_slab", -+ "minecraft:white_wool_stairs", -+ "minecraft:yellow_concrete_double_slab", -+ "minecraft:yellow_concrete_slab", -+ "minecraft:yellow_concrete_stairs", -+ "minecraft:yellow_wool_double_slab", -+ "minecraft:yellow_wool_slab", -+ "minecraft:yellow_wool_stairs" -+ ] -+} +index 0000000000000000000000000000000000000000..e0565ec39bb3bddb719644d68208700a8830ea5c +GIT binary patch +literal 3707 +zcmV->4utU^iwFP!00000|LtAZbK6E9URt6p(Qy)2Np@r_al71u$9cV(jIAW&%-#6G +zV|#LvUyvoZA~6O51OO%LYOeR*d#=~H-fMEbB-eYHxu^R(?zdnuNMcD~4S?kG=b1P# +z00N8M@B4n=_7Wi_a-wcnOn2#q{~Xh()_kq%(rrz*E!|~4(>$LVt~XES%ciYgze=m# +z93iAYPTl;O;c1ScU#B;iX85eGRhZ|y4c#}&CM%JWZr2^#VwUff$jZ%Mh40eJ+ZtRk +ztZI=IH>ufR=l-{`0f{>naldV84t0G)H<>p>Zf_q$k{gx}X*)YdcKPgv-*6crb7Z$~ +zSiV+ge$B3sSqoAE&pjOe=*JpH1rqJE;NkP+_RjI;2E3!hzqdf{)T#BLZ9#Te7lWS^ +z=18geMVYjYdwHtbkd1NlgIrz2$S@e9{@tT(}xOV*^!)?9@GIW2# +zV_GXQx@kBL{DP3~JaAbBo?!8BwJJO>HVuEfKn_sTw70zH!_%(XSA6C?WN%rJdb0rI($;qcDLuqT>FsR2Jh~a1pKn< +z+6^b1NREMr`N=aDt+O*t02RnVfJx1$RG6h17JNXxPOXYZ$PB#8HmOy8gt;Ei3PSdj +z$i7W#8Kz0KAoH{e^=XOR(fkK>AzyvSb0~l^g|?LNureSz(({#^jjGph#Ti!INBGTAFX$rnX_bTE&IJEb!b4y}Il|-iPR8 +z(lU}q74k?BV_8fLfJR2N*juq192xu-EvTOmLNs)5fy_g4nkPt`F{RK`x(V}RX~Nuf +zGc{0D*O4#CmoekZaS=l0>GF&;T|x+Q8m$}Z(!?m^xi;umaPHyafezS2*wBDu|XspXjsomu*J +zfZXobv(0$K)()V +zhzJyg)Rp+2A&ogbQd}4=#lMp_El2yJ0=j%9rFMdy%Skat<=x9~q$zWEv^5zho_YHd +zWwrCLz!xUh%#WuMXn8ip&~wyL)X|aaXngmVP)mQ5Mw>}5okuUC$e3wo%R8K^&v;%z~SChu81p$O(Q0p7gv1lne7GbxC +zI*gfMGc?jtmuiRM5;6RBQzk>gNYj)6ZDo90M6u788R5SVCOkZLG%`Ygrt5b +zHAxM@5lYwG(CQ7Z=Gu0>r%Xdll)7!22G0|%Y~PqCMfkfVKSeTIr#IGCFF_K6!}Zm( +z?Za!Uk2enuYwi5`_Tl>Z&e?0LcLtE##ViWB0LboYqXa-$O%>8y(7QRZNOhe#K8Jn# +zil=RWTr~@Mp6qKqNUOB`6)gLU*or +zekh0t`$939*>KU_<^bf3*@QAF8z%4t!pSbZ0fhw8914J~Q)hbK=+%K-b7W8JvS!k4 +z=7xtIcU?Hsuqtc=u2n)mY05IZwqyX2d}G6aC!Fq2YUHaS-WXLV3ifd8t_!yNT<=N% +z&;r?Gc#qt70p8Q30l?d0!1;@;O7-olwsYC`X)81xfzb3oAYVY9i=1SuWzTk<*5zG| +zO)YpvN`aNc46HjH+i^IYrNU}x5_+4xAXE?{ES)C}C>`AE!L9S{8{m8~$l4;=OY3C= +ziliwBEg^&V-R4mJI#8nw0^}8;C|w5S>bBvwKuxDcH6IYIdGL6yWl*%05O5Df!zco^ +zJ=Hk}zS?k%X0gGyK$>Zrork(FmRXH%8sG=o@DHhw$IO--fc}J3W1j4^U88DPMR@Vd +zlTQ$`3J2#vX6DJPXS{#~=T}z<;VN|gzhp=o&Q6Q+?;Rd9qCLVqWHoc^jpi2xVA|Cq +ztsw~x8kZq**Mqxf$d1cvJbgtnb9wC&KjNp>;r~1MsrAmOYwf$PU1}cfxUzOGP}Z3% +zYn@Z;?NjTWQ`g$3IzPO+);?HoA6#o<=4g$2&+}}n^>q(0NGN-R0#BO^S^!v|J?k`N;g(-JeecTs_ph8?Idy6!;+~N3P`6@O0Qio8H-C9L(C^bE +zg?ucGe?Ds2Q>ok{6X;Q +zQTPMds2e0Wof96{7g9xfpqa#V3(b%o1E;XDteDF`+3J9nOc +z(xDA%n!G!PRvDDO=^Ks-Bsv#-<{gHoaf;U&HF>EPW=M$_U8h4}N4v*o-xhz&25^Tz +zC#vV?3*l~@-9a~w=9rW}rWBqf=mLxZgN<@R*(0P&vw8-KE7}Po>VHqH)+7TRUX0LT +z-L*XrJ5Dm_I4L|k%kVJYp=DM*CA&S#-ZJfKmFGh#yauexKqqv@*(eiwo+8aZPxXc# +zko)$3nf9c_?tO1{o|hp8M@r}myL!Sq;s}B!l$#Cbjr01XGbU+Zq=>F9EwJ8jJnJh(^!FJRO_4LxZc*^{?8a09J|qOz&3)i^9h +zaSXfh)ih?b_wE6X+(GV41a}ri7>>c5@29d+VE1O=NE{^2L=a~|0O1(C`KB~)LPH_C +zb_K`IZ6wVkkmg_{eq&JPwYjZ3+`pm&@=6h2O>o0eyj5QkwjI4GuP)B+$`Lj&<=(~F +zQ#(!G#PmmL%W_;~^$`ZSz~C-X{!5_XKWVH9+P5=2R{4~Qvg*3mB_W7oIN0x|(MTVz +z^x*)JoVSSYs&OY5&(EedIOL>QOfjhDJ#45ioFzxbztRY+p^auy8FF4~JG{wn^5TCY +zi&b@R?u?0O$a#t7(59rx!SMOiz#zuw=wX0Xd)gbchDNT0ZCP*4vMozUZe$WNY1B*_ +z7W5Mm=>s`!;%>v`D%M;^Saazu0SUMA=QMhU8k0FJ?{$FtEZrAu$>XjxQL +zR&_1Q{s{EOFe|UfX_`ZbQRr0E;nM^=m19xhl(#AG%&9dhGOeTMld|?is9_0x$tO}9 +zHF#G~if{}EO6zz@>v0i6<>mF~X{;9|Ow1UtMp*$JC|3zZ^slKbt&LkEjVglP(q>4X +z$zt4n-};!RpjI7Y4i)j5ssir5No3_>k(YuXKJtnD%rI@G7qn935h +zXCMY6B+RG@6K`M9gp7GlPRm7yQjDXx6y=t>MsIl_kYkv=ucfi0UnGan&MCy8364(p +z4#uf`g!+{b&!3>c(r(^CuqOV|kYk2J=`Mh|}EN0Glgj3|rv>VbBLWs(H(O*atCRS?3fH%t8kHLzS(1QFe +zjRDTs;i;%-s%tj(N1!)`&v;c%1DtV-PbK*@!6xKb6gcJm$D4E7F*isnR)!Vjle+O~ +zK>*>cRRp +z#T6Y@R7`b!#{LNO#_$-g$*CS3VieYc)#1|wJCI{h;FNbC@6KtWZ)93W&nIQQiBQ85 +z`jStjHbmfPPl~WnL{V{7*Q_iEARNQ6d?$@YksjWRNA(U_Gwqh-xCo*0w&a&-G>Qll +zGX|_t)_4q*tAys`?`brO;DW7+DyF(VV}AsCV|a|0b5e;#8SW>c*!90fb}3s4u5c55|d`>au2nqtI~?LggdTuhXap<7Caz)`M?(n&v;# +z*zd8qGz?aR)sswhjmCi}_{MM { -+ final CompoundTag properties = new CompoundTag(); -+ final CompoundTag vanilla = new CompoundTag(); -+ vanilla.putInt("block_id", 1); -+ properties.put("vanilla_block_data", vanilla); -+ return new BlockProperties(name, properties); -+ }).toArray(BlockProperties[]::new); ++ final List registrations = new ArrayList<>(); ++ try (var input = VanillaBlockStatesTest.class.getResourceAsStream("/assets/viabedrock/data/bedrock/data_driven_blocks.nbt"); ++ var data = new DataInputStream(new GZIPInputStream(input))) { ++ final CompoundTag definitions = NBTIO.readTag(data, TagLimiter.create(8 << 20, 64), true, CompoundTag.class); ++ for (final var entry : definitions) { ++ registrations.add(new BlockProperties(entry.getKey(), (CompoundTag) entry.getValue())); ++ } ++ } ++ final BlockProperties[] definitions = registrations.toArray(BlockProperties[]::new); + final var selected = VanillaBlockStates.forServer(canonical, definitions); + final var sequential = new RuntimeBlockPalette(selected, false); + assertEquals(canonical, selected); diff --git a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md index b717268c..149faa3b 100644 --- a/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md +++ b/patches/viabedrock/upstreamable/0054-render-native-block-state-geometry-and-materials.pr.md @@ -21,6 +21,13 @@ BDS advertises these types; Lifeboat omits them. All 22,543 shared native state The palette now follows that registration boundary. Installed add-on and ViaProxy tests spawn in air and walk through the rendered lobby. The authored Navigator opens on both routes. Full native appearance remains unverified. +The registration names now come directly from the unmodified [CloudburstMC/Data NBT snapshot](https://github.com/CloudburstMC/Data/blob/0021592b1a4253528bad1e1d8b537e306e097557/data_driven_blocks.nbt). +Its 98 root keys exactly match the captured list. The JSON copy and conversion step are unnecessary. +`Data Asset Sources.md` records the source revision, checksum, license, and update procedure. +Palette regressions read the bundled definitions and retain the native BDS and Lifeboat state IDs. +A fresh full-stack replay and the four-project build pass. +Core, add-on, and proxy packages retain the original NBT bytes and its license. + Shape, state, culling, and conversion tests cover supported definitions. Carrier capacity, unsupported materials, and complete native appearance remain limits. The [owning patch](0054-render-native-block-state-geometry-and-materials.patch) retains implementation rationale and versioned evidence. 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 cc3acc0f..df775605 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 901059506f1308a361a2ea84495598e90c4180ec Mon Sep 17 00:00:00 2001 +From 95fd05989b7878cf13dd2963683637e20c05365e 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 @@ -812,10 +812,10 @@ former content-only shortcut. create mode 100644 src/test/resources/inventory/native-item-storage.json diff --git a/Data Asset Sources.md b/Data Asset Sources.md -index bd94d2dd..700343cc 100644 +index 6dbf4ed5..a40ed7ab 100644 --- a/Data Asset Sources.md +++ b/Data Asset Sources.md -@@ -161,3 +161,36 @@ Curated native block aliases for Bedrock 1.26.51.1, build 51061372, protocol 219 +@@ -183,3 +183,36 @@ Curated native block aliases for Bedrock 1.26.51.1, build 51061372, protocol 219 The matching executable and private registry lookup probes supply these identities. Keep primary registrations ahead of alias lookup. Namespace fallback and complete runtime registration still need native comparison. From acfa5136089cdd072e2d592e5aacf8486d42da94 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:38:49 +0100 Subject: [PATCH 31/33] fix(inventory): clear occupied Creative catalog cursors Match official Bedrock 1.26.60.5 primary catalog clicks: discard the held stack first, then pick the selected item on the following click. Fold the action into the Creative catalog patch with confirmed cursor identity, discard admission, and a pre-request snapshot for rejection rollback. Validate same-item and different-item clears in the native client and both installed Java routes. Each route completes four server-accepted requests with cursor counts 64, zero, 64, zero. Full builds and fresh core patch replay pass; secondary and bulk actions remain open. --- docs/bedrock-coverage.md | 2 +- docs/native-player-inventory.md | 5 +- ...ain-bedrock-creative-catalog-entries.patch | 158 ++++++++++++++---- ...ain-bedrock-creative-catalog-entries.pr.md | 13 +- 4 files changed, 143 insertions(+), 35 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index a5e08568..b8025f33 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -26,7 +26,7 @@ No complete area has verified Bedrock parity. | World resources | World base-game version selects vanilla metadata, textures, actor graphs, and audio before server overrides. The context survives ViaProxy. Server legacy biome bindings retain unqualified names across versions. | Native current/older/current cat, sound, and remote biome-name controls cover selected paths. Broader pose and audio behavior need comparisons. | | Held trapdoors | Retained facing and half, interaction suppression, use timing, cancellation, rejection, and prediction reconciliation. | Native, direct, and proxy controls cover specific held-placement histories. Other placement and interaction families need separate comparisons. | | Fonts and UI | Bitmap metadata, extended mapping, same-pack ownership, legacy layouts, authored scenes, and native GUI sampling. | At 1280 × 720, current controls match all 16 labels at native UI scales 1 and 2; legacy scale-2 controls match 16/16. Both routes pass Java GUI scale changes and reloads. Broader viewports and TrueType comparisons remain open. | -| Player inventory and crafting | Opening ownership, recipe catalogs, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, bulk recipe actions, cursor replacement, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | +| Player inventory and crafting | Opening ownership, recipe catalogs, primary Creative cursor actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, bulk recipe actions, Creative secondary actions, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | | Cooking | Snapshot-bound recipe actions, transfer batches, preferences, search, and progress. | Default cooking screens retain ordinary controls until authored bindings are complete. See the [cooking reference](native-cooking-actions.md). | | Skins and appearance | Complete skin transport, classic changes, persona assembly, animation graphs, costumes, and attachables. | Broader wardrobe masking, profile/LOD changes, shield rendering, remote appearances, and account/platform flows need further native comparisons. See [skin support](skin-flow.md). | | Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Native Fancy controls establish the tick-based expiry blend and distance curve at noon and midnight. Other fluid movement, vision combinations and media, mount variants, network conditions, and broader replay behavior remain open. | diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 757dd994..4d32325a 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -148,7 +148,10 @@ Creative hotbar destruction restores ordinary controls before item changes or re Installed direct and ViaProxy controls retain the complete 1,980-item catalog from a local protocol-2225 server. Tab and layout controls use the target inventory indices. Both routes search for rails and place a server-accepted stack of 64 in the hotbar. -Bulk recipe actions, Creative cursor replacement, and complete installed native parity remain unfinished. +A primary Creative catalog click clears an occupied cursor. The next click picks the requested stack. +Official Bedrock 1.26.60.5 controls cover both same-item and different-item clicks. +Installed direct and ViaProxy controls reproduce both cases with successful server responses and an empty inventory after reopening. +Bulk recipe actions, Creative secondary actions, and complete installed native parity remain unfinished. Native constructor timing relative to the open acknowledgment and frame-level population timing remain unverified. An unpositioned player inventory uses the fallback crafting context. Native model-position initialization and arbitrary custom component admission remain unverified. 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 4abaf695..f2d51778 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 9a84a2a44225e2ddbdad771daad7e0f4387a0cb2 Mon Sep 17 00:00:00 2001 +From 88096082e58da0146e473ea426bc3994bb3bc799 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 @@ -195,8 +195,9 @@ Use retained collection identities for header expansion and recipe selection; never authorize from the Java preview item. Native Creative left-click produces the catalog's unit result before taking a full stack to the empty cursor. Ordinary Survival selection uses the existing grid -placement and reconciliation path. Bulk selection, cursor replacement, -full native cache timing and exact background selection remain unfinished. +placement and reconciliation path. Bulk selection, secondary Creative +actions, full native cache timing and exact background selection remain +unfinished. Export the versioned How to Play graph with the player program. Resolve its real topic toggles, named view aliases, section titles, translated @@ -263,15 +264,23 @@ retaining the model, query and selection. Filtering changes reinstall the existing policy callbacks; tab-only updates preserve their captured state. A regression opens without an options packet and observes the populated Search source, then applies an explicit zero-tab/filter update. + +Match official Bedrock 1.26.60.5 primary catalog clicks with an occupied +cursor. Native local-world controls clear a full held stack for both the +same and a different catalog item, leave inventory empty, and pick the new +stack only on the next click. Send one Destroy for the actual cursor +network ID, retain native discard admission, and snapshot the cursor before +prediction so rejection restores its identity and count. This observation +establishes visible behavior, not original native request-byte parity. --- .../model/NativePlayerInventoryAction.java | 88 ++++ .../model/NativePlayerInventoryMessage.java | 271 ++++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ .../model/container/NativeRecipeCounter.java | 74 +++ - .../resourcepack/ui/PlayerInventoryUi.java | 397 +++++++++++++++ + .../resourcepack/ui/PlayerInventoryUi.java | 397 ++++++++++++++ .../viabedrock/protocol/BedrockProtocol.java | 1 + .../protocol/model/CreativeCatalog.java | 266 ++++++++++ - .../model/CreativeInventoryModel.java | 198 ++++++++ + .../model/CreativeInventoryModel.java | 198 +++++++ .../protocol/model/PlayerRecipeContext.java | 96 ++++ .../protocol/model/PlayerRecipeModel.java | 125 +++++ .../protocol/packet/InventoryPackets.java | 209 +++++--- @@ -280,7 +289,7 @@ Search source, then applies an explicit zero-tab/filter update. .../protocol/storage/CraftingDataTracker.java | 129 ++++- .../storage/InventoryRequestTracker.java | 24 + .../protocol/storage/InventoryTracker.java | 21 + - .../storage/NativePlayerRecipeState.java | 475 ++++++++++++++++++ + .../storage/NativePlayerRecipeState.java | 490 ++++++++++++++++++ .../storage/PlayerInventoryScreenTracker.java | 40 +- .../NativePlayerInventoryMessageTest.java | 160 ++++++ .../container/NativeRecipeCounterTest.java | 27 + @@ -293,8 +302,8 @@ Search source, then applies an explicit zero-tab/filter update. .../RegisteredPlayerRecipeSearchTest.java | 3 +- .../storage/CreativeInventoryActionTest.java | 214 ++++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ - .../storage/NativePlayerCreativePickTest.java | 66 +++ - 30 files changed, 4240 insertions(+), 170 deletions(-) + .../storage/NativePlayerCreativePickTest.java | 138 +++++ + 30 files changed, 4327 insertions(+), 170 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 @@ -2708,10 +2717,10 @@ index 04ad1756..4a6f0c14 100644 private void updateInventoryOptions() { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java new file mode 100644 -index 00000000..6c6230ee +index 00000000..ab42d899 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java -@@ -0,0 +1,475 @@ +@@ -0,0 +1,490 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -3037,31 +3046,46 @@ index 00000000..6c6230ee + return false; + } + -+ /** Native Creative left-click requests a unit result then takes the stack to the empty cursor. */ ++ /** Native Creative left-click clears an occupied cursor, or picks a stack into an empty one. */ + private boolean pickCreative(final CreativeCatalog.CollectionEntry entry) { + final var inventory = this.user().get(InventoryTracker.class); + final var hud = inventory.getHudContainer(); -+ if (entry.catalogEntry() == null || !hud.getItem(0).isEmpty()) { -+ return false; -+ } -+ final var result = entry.catalogEntry().bedrockItem().copy(); -+ final int count = Math.min(255, this.user().get(ItemRewriter.class).maxStackSize(result)); -+ if (count <= 0) { -+ return false; -+ } -+ final var cursor = result.copy(); -+ cursor.setAmount(count); -+ if (!hud.canPlaceItem(0, cursor, count)) { ++ if (entry.catalogEntry() == null) { + return false; + } ++ final var previous = hud.getItem(0); + final var requests = this.user().get(InventoryRequestTracker.class); -+ final int id = requests.nextRequestId(); -+ final var request = new ItemStackRequestInfo(id, List.of( -+ new ItemStackRequestAction.CraftCreativeAction(entry.catalogEntry().netId(), 1), -+ new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(result.copy()), 1), -+ new ItemStackRequestAction.TakeAction(count, -+ new ItemStackRequestSlotInfo(new FullContainerName(ContainerEnumName.CreatedOutputContainer, null), (byte) 50, id), -+ hud.requestSlotInfo(0, 0))), List.of(), TextProcessingEventOrigin.unknown); ++ final int id; ++ final BedrockItem cursor; ++ final List actions; ++ if (!previous.isEmpty()) { ++ if (previous.netId() == null || !hud.canDiscardItem(0, previous.amount())) { ++ return false; ++ } ++ id = requests.nextRequestId(); ++ cursor = BedrockItem.empty(); ++ actions = List.of(new ItemStackRequestAction.DestroyAction(previous.amount(), ++ hud.requestSlotInfo(0, previous.netId()))); ++ } else { ++ final var result = entry.catalogEntry().bedrockItem().copy(); ++ final int count = Math.min(255, this.user().get(ItemRewriter.class).maxStackSize(result)); ++ if (count <= 0) { ++ return false; ++ } ++ cursor = result.copy(); ++ cursor.setAmount(count); ++ if (!hud.canPlaceItem(0, cursor, count)) { ++ return false; ++ } ++ id = requests.nextRequestId(); ++ actions = List.of( ++ new ItemStackRequestAction.CraftCreativeAction(entry.catalogEntry().netId(), 1), ++ new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(result.copy()), 1), ++ new ItemStackRequestAction.TakeAction(count, ++ new ItemStackRequestSlotInfo(new FullContainerName(ContainerEnumName.CreatedOutputContainer, null), (byte) 50, id), ++ hud.requestSlotInfo(0, 0))); ++ } ++ final var request = new ItemStackRequestInfo(id, actions, List.of(), TextProcessingEventOrigin.unknown); + requests.addRequest(new InventoryRequestStorage(request, 0, hud.copy(), List.of())); + requests.setPredictingClick(true); + try { @@ -5269,10 +5293,10 @@ index b69f1868..037328f2 100644 final BedrockItem current = new BedrockItem(276); diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java new file mode 100644 -index 00000000..746f0b54 +index 00000000..92759c0c --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java -@@ -0,0 +1,66 @@ +@@ -0,0 +1,138 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -5280,6 +5304,7 @@ index 00000000..746f0b54 + */ +package net.raphimc.viabedrock.protocol.storage; + ++import com.viaversion.nbt.tag.CompoundTag; +import com.viaversion.viaversion.api.connection.UserConnection; +import net.raphimc.viabedrock.api.model.container.player.HudContainer; +import net.raphimc.viabedrock.api.util.PacketFactory; @@ -5289,6 +5314,7 @@ index 00000000..746f0b54 +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; +import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; @@ -5299,6 +5325,76 @@ index 00000000..746f0b54 +class NativePlayerCreativePickTest { + + @ParameterizedTest ++ @ValueSource(ints = {66, 67}) ++ void clearsTheOccupiedCursorForEitherCatalogItemAndRestoresItOnRejection(final int heldId) throws Exception { ++ final var user = mock(UserConnection.class); ++ final var inventory = mock(InventoryTracker.class); ++ final var requests = new InventoryRequestTracker(user); ++ when(user.get(InventoryTracker.class)).thenReturn(inventory); ++ when(user.get(InventoryRequestTracker.class)).thenReturn(requests); ++ final var hud = new HudContainer(user); ++ when(inventory.getHudContainer()).thenReturn(hud); ++ final var held = new BedrockItem(heldId, (short) 0, (byte) 64); ++ held.setNetId(23); ++ hud.setItem(0, held); ++ final var sample = new BedrockItem(66, (short) 0, (byte) 1); ++ final var catalog = new CreativeCatalog.Entry(1808, 0, sample, null, false, true); ++ final var state = new NativePlayerRecipeState(user); ++ final var pick = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class); ++ pick.setAccessible(true); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ assertEquals(true, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog))); ++ final var sent = ArgumentCaptor.forClass(ItemStackRequestInfo[].class); ++ packets.verify(() -> PacketFactory.sendBedrockInventoryRequest(eq(user), sent.capture())); ++ final var request = sent.getValue()[0]; ++ assertEquals(1, request.actions().size()); ++ final var destroy = assertInstanceOf(ItemStackRequestAction.DestroyAction.class, request.actions().getFirst()); ++ assertEquals(64, destroy.count()); ++ assertEquals(ContainerEnumName.CursorContainer, destroy.item().container().name()); ++ assertEquals(0, destroy.item().slot()); ++ assertEquals(23, destroy.item().stackNetworkId()); ++ assertTrue(hud.getItem(0).isEmpty()); ++ assertFalse(requests.isPredictingClick()); ++ final var saved = requests.getRequest(request.requestId()); ++ saved.capturePrediction(inventory); ++ requests.restoreRejectedRequest(saved); ++ assertEquals(held, hud.getItem(0)); ++ assertEquals(64, held.amount()); ++ assertEquals(1, sample.amount()); ++ } ++ } ++ ++ @Test ++ void preservesAnUnconfirmedOrLockedCursorWithoutDestroyingIt() throws Exception { ++ final var user = mock(UserConnection.class); ++ final var inventory = mock(InventoryTracker.class); ++ final var requests = new InventoryRequestTracker(user); ++ when(user.get(InventoryTracker.class)).thenReturn(inventory); ++ when(user.get(InventoryRequestTracker.class)).thenReturn(requests); ++ final var hud = new HudContainer(user); ++ when(inventory.getHudContainer()).thenReturn(hud); ++ final var held = new BedrockItem(67, (short) 0, (byte) 64); ++ hud.setItem(0, held); ++ final var sample = new BedrockItem(66, (short) 0, (byte) 1); ++ final var catalog = new CreativeCatalog.Entry(1808, 0, sample, null, false, true); ++ final var state = new NativePlayerRecipeState(user); ++ final var pick = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class); ++ pick.setAccessible(true); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ assertEquals(false, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog))); ++ held.setNetId(23); ++ final var tag = new CompoundTag(); ++ tag.putByte("minecraft:item_lock", (byte) 2); ++ held.setTag(tag); ++ assertEquals(false, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog))); ++ assertSame(held, hud.getItem(0)); ++ assertEquals(64, held.amount()); ++ assertFalse(requests.hasPending()); ++ packets.verify(() -> PacketFactory.sendBedrockInventoryRequest(eq(user), any()), never()); ++ } ++ } ++ ++ @ParameterizedTest + @ValueSource(ints = {1, 64}) + void takesTheFullCreativeStackWhileKeepingTheCraftResultAtOne(final int maximum) throws Exception { + final var user = mock(UserConnection.class); diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index 61f7ebc2..1c37c6f3 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -95,7 +95,7 @@ The current native callback selects How to Play topic 5 for Survival and Encyclo A physical 1.26.60.5 capture confirms the Survival Your First Craft topic. Translated Help paragraphs replace both plain text and rendered components, preserving the native color palette. Program round trips retain these components and navigation labels from authored topic and category variables. Full installed Help lifetimes and glyph appearance remain unverified. -Bulk recipe selection, Creative cursor replacement, and complete installed action parity remain unfinished. +Bulk recipe selection, secondary Creative actions, and complete installed action parity remain unfinished. The retained model exposes output quantity separately from availability and action admission. Candidate recipes share source consumption in priority order. Count reads retain the native crafting-input, cursor, and player-inventory order. @@ -117,10 +117,19 @@ A snapshot regression checks active Creative cells without ingredients and resto A live add-on check removes the red disabled backgrounds. The picker validates the full cursor stack before transfer admission while preserving the unit-sized native craft result. Request regressions cover stack sizes one and 64, cursor prediction, and the restoration snapshot. +Official Bedrock 1.26.60.5 local-world controls show that a primary catalog click clears an occupied cursor. +Both the same-item and different-item controls clear 64 items and leave inventory empty. +A second click picks the selected stack. The implementation sends one cursor Destroy with the current stack network ID. +Request regressions cover both identities, rejection rollback, and locked or unconfirmed cursor admission. +The native control establishes visible behavior, not original native request bytes. +Installed direct and ViaProxy controls reproduce both cases with rails and powered rails. +All four requests on each route succeed. Cursor responses contain 64, zero, 64, then zero items. +Each clear sends only Destroy with the preceding confirmed cursor network ID. +Both routes retain an empty cursor and inventory after reopening. An installed add-on test selects 64 rails and places them in the hotbar; BDS accepts both requests. Installed direct and ViaProxy controls select 64 rails and place them in separate hotbar slots; BDS accepts the requests. The ViaProxy screen retains both stacks after reopening. -Cursor replacement, bulk actions, and complete native screen parity remain unfinished. +Secondary Creative actions, bulk actions, and complete native screen parity remain unfinished. Player and workbench graphs receive their tab and layout indices from the protocol-2225 enums. The native inventory graph references these controller variables in forced toggle indices. From 3ab33bb0d76e4dec98b169c35b11a67aa764bae5 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:56:06 +0100 Subject: [PATCH 32/33] feat(inventory): support native Creative catalog inputs Match official Bedrock 1.26.60.5 secondary take/discard and Shift-click placement, including cursor preservation, merge priority, partial capacity, and a full-inventory no-op. Retain the pressed input through modifier release and snapshot all affected slots for rejection rollback. Suppress unchanged Java Creative slot echoes during reopening so existing stacks retain their network identity. Real count and native metadata changes still replace items. Fold the changes into their owning core and add-on patches; later inventory patches only refresh context offsets. Full builds, targeted regressions, plain patch replay, offline validation, and installed direct and ViaProxy request controls pass. Bulk Survival recipes and complete native screen parity remain open. --- docs/bedrock-coverage.md | 2 +- docs/native-player-inventory.md | 16 +- ...ain-bedrock-creative-catalog-entries.patch | 483 +++++++++++++++--- ...ain-bedrock-creative-catalog-entries.pr.md | 30 +- ...bedrock-merchant-inventories-in-core.patch | 10 +- ...ate-bedrock-loom-inventories-in-core.patch | 6 +- ...ranslate-bedrock-cartography-in-core.patch | 20 +- ...9-present-authored-native-containers.patch | 106 +++- ...9-present-authored-native-containers.pr.md | 10 +- 9 files changed, 570 insertions(+), 113 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index b8025f33..49423645 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -26,7 +26,7 @@ No complete area has verified Bedrock parity. | World resources | World base-game version selects vanilla metadata, textures, actor graphs, and audio before server overrides. The context survives ViaProxy. Server legacy biome bindings retain unqualified names across versions. | Native current/older/current cat, sound, and remote biome-name controls cover selected paths. Broader pose and audio behavior need comparisons. | | Held trapdoors | Retained facing and half, interaction suppression, use timing, cancellation, rejection, and prediction reconciliation. | Native, direct, and proxy controls cover specific held-placement histories. Other placement and interaction families need separate comparisons. | | Fonts and UI | Bitmap metadata, extended mapping, same-pack ownership, legacy layouts, authored scenes, and native GUI sampling. | At 1280 × 720, current controls match all 16 labels at native UI scales 1 and 2; legacy scale-2 controls match 16/16. Both routes pass Java GUI scale changes and reloads. Broader viewports and TrueType comparisons remain open. | -| Player inventory and crafting | Opening ownership, recipe catalogs, primary Creative cursor actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, bulk recipe actions, Creative secondary actions, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | +| Player inventory and crafting | Opening ownership, recipe catalogs, Creative catalog actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, bulk Survival recipe actions, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | | Cooking | Snapshot-bound recipe actions, transfer batches, preferences, search, and progress. | Default cooking screens retain ordinary controls until authored bindings are complete. See the [cooking reference](native-cooking-actions.md). | | Skins and appearance | Complete skin transport, classic changes, persona assembly, animation graphs, costumes, and attachables. | Broader wardrobe masking, profile/LOD changes, shield rendering, remote appearances, and account/platform flows need further native comparisons. See [skin support](skin-flow.md). | | Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Native Fancy controls establish the tick-based expiry blend and distance curve at noon and midnight. Other fluid movement, vision combinations and media, mount variants, network conditions, and broader replay behavior remain open. | diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 4d32325a..8d6102fd 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -148,10 +148,18 @@ Creative hotbar destruction restores ordinary controls before item changes or re Installed direct and ViaProxy controls retain the complete 1,980-item catalog from a local protocol-2225 server. Tab and layout controls use the target inventory indices. Both routes search for rails and place a server-accepted stack of 64 in the hotbar. -A primary Creative catalog click clears an occupied cursor. The next click picks the requested stack. -Official Bedrock 1.26.60.5 controls cover both same-item and different-item clicks. -Installed direct and ViaProxy controls reproduce both cases with successful server responses and an empty inventory after reopening. -Bulk recipe actions, Creative secondary actions, and complete installed native parity remain unfinished. + +Official Bedrock 1.26.60.5 controls establish the Creative catalog pointer actions. +Primary click takes a full stack into an empty cursor or discards the entire held stack. +Secondary click takes one item into an empty cursor or discards one held item. +Shift-left-click and Shift-right-click place a full stack in inventory without changing the cursor. +Bulk placement merges existing stacks before filling empty hotbar and main-inventory slots, then stops at the available capacity. +A full inventory accepts the input without an inventory request. + +Unchanged Java Creative slot echoes preserve the existing native stack and network ID. +Installed direct and ViaProxy controls confirm these pointer, capacity, and reopening cases through successful server responses. + +Bulk Survival recipe actions and complete installed native parity remain unfinished. Native constructor timing relative to the open acknowledgment and frame-level population timing remain unverified. An unpositioned player inventory uses the fallback crafting context. Native model-position initialization and arbitrary custom component admission remain unverified. 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 f2d51778..223da472 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 88096082e58da0146e473ea426bc3994bb3bc799 Mon Sep 17 00:00:00 2001 +From 210863f8610c43bb08177ec80b97f85f616bc9cf 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 @@ -195,9 +195,8 @@ Use retained collection identities for header expansion and recipe selection; never authorize from the Java preview item. Native Creative left-click produces the catalog's unit result before taking a full stack to the empty cursor. Ordinary Survival selection uses the existing grid -placement and reconciliation path. Bulk selection, secondary Creative -actions, full native cache timing and exact background selection remain -unfinished. +placement and reconciliation path. Bulk Survival selection, full native +cache timing and exact background selection remain unfinished. Export the versioned How to Play graph with the player program. Resolve its real topic toggles, named view aliases, section titles, translated @@ -272,38 +271,58 @@ stack only on the next click. Send one Destroy for the actual cursor network ID, retain native discard admission, and snapshot the cursor before prediction so rejection restores its identity and count. This observation establishes visible behavior, not original native request-byte parity. + +Bind Creative secondary clicks and auto-place to official 1.26.60.5 +local-world controls. Secondary takes one item into an empty cursor or +discards one held item regardless of the selected catalog identity. +Auto-place preserves the cursor, merges across the combined inventory +before filling empty hotbar then main-inventory slots, and stops at the +available capacity. A full inventory accepts the input without a request. +Snapshot all affected slots before prediction; refusal restores counts +and confirmed identities. Native visible controls establish behavior, +not original native request-byte equality. + +Ignore unchanged Java Creative slot echoes before opening or writing the +native inventory. Installed Java 26.3 reopening otherwise issues two +CraftCreative/Destroy/Place replacements for unchanged 64-rail stacks and +replaces their confirmed network IDs. Compare counts and complete native +wire snapshots, including NBT, block state, restrictions and blocking +ticks, while excluding transient network identity. Genuine count or native +user-data changes retain replacement requests. Preserve drop permission, +foreign-container rejection and pending-close ownership checks. --- - .../model/NativePlayerInventoryAction.java | 88 ++++ - .../model/NativePlayerInventoryMessage.java | 271 ++++++++++ + .../model/NativePlayerInventoryAction.java | 88 +++ + .../model/NativePlayerInventoryMessage.java | 271 +++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ .../model/container/NativeRecipeCounter.java | 74 +++ - .../resourcepack/ui/PlayerInventoryUi.java | 397 ++++++++++++++ + .../resourcepack/ui/PlayerInventoryUi.java | 397 +++++++++++++ .../viabedrock/protocol/BedrockProtocol.java | 1 + - .../protocol/model/CreativeCatalog.java | 266 ++++++++++ + .../protocol/model/CreativeCatalog.java | 266 +++++++++ .../model/CreativeInventoryModel.java | 198 +++++++ - .../protocol/model/PlayerRecipeContext.java | 96 ++++ - .../protocol/model/PlayerRecipeModel.java | 125 +++++ - .../protocol/packet/InventoryPackets.java | 209 +++++--- + .../protocol/model/PlayerRecipeContext.java | 96 +++ + .../protocol/model/PlayerRecipeModel.java | 125 ++++ + .../protocol/packet/InventoryPackets.java | 213 +++++-- .../protocol/packet/MultiStatePackets.java | 22 + - .../protocol/rewriter/ItemRewriter.java | 62 ++- - .../protocol/storage/CraftingDataTracker.java | 129 ++++- + .../protocol/rewriter/ItemRewriter.java | 62 +- + .../protocol/storage/CraftingDataTracker.java | 129 +++- .../storage/InventoryRequestTracker.java | 24 + .../protocol/storage/InventoryTracker.java | 21 + - .../storage/NativePlayerRecipeState.java | 490 ++++++++++++++++++ + .../storage/NativePlayerRecipeState.java | 559 ++++++++++++++++++ .../storage/PlayerInventoryScreenTracker.java | 40 +- - .../NativePlayerInventoryMessageTest.java | 160 ++++++ + .../NativePlayerInventoryMessageTest.java | 160 +++++ .../container/NativeRecipeCounterTest.java | 27 + - .../container/NativeRecipeQuantityTest.java | 61 +++ - .../ui/PlayerInventoryUiTest.java | 126 +++++ - .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++++++ - .../protocol/packet/InventoryPacketsTest.java | 267 ++++++++-- - .../rewriter/PlayerRecipeModelTest.java | 471 +++++++++++++++++ + .../container/NativeRecipeQuantityTest.java | 61 ++ + .../ui/PlayerInventoryUiTest.java | 126 ++++ + .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++++ + .../protocol/packet/InventoryPacketsTest.java | 267 +++++++-- + .../rewriter/PlayerRecipeModelTest.java | 471 +++++++++++++++ ...egisteredPlayerRecipeAvailabilityTest.java | 5 +- .../RegisteredPlayerRecipeSearchTest.java | 3 +- - .../storage/CreativeInventoryActionTest.java | 214 ++++++++ + .../storage/CreativeInventoryActionTest.java | 262 ++++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ + .../NativePlayerCreativeAuxiliaryTest.java | 211 +++++++ .../storage/NativePlayerCreativePickTest.java | 138 +++++ - 30 files changed, 4327 insertions(+), 170 deletions(-) + 31 files changed, 4659 insertions(+), 170 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 @@ -319,6 +338,7 @@ establishes visible behavior, not original native request-byte parity. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/CreativeCatalogTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/CreativeInventoryActionTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java diff --git a/src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java b/src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java @@ -1962,10 +1982,18 @@ index 00000000..5b3cdb3b + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index c7461506..45e6cfc4 100644 +index c7461506..64053175 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -@@ -92,14 +92,20 @@ import net.raphimc.viabedrock.protocol.data.enums.bedrock.ComplexInventoryTransa +@@ -22,6 +22,7 @@ import net.raphimc.viabedrock.api.resourcepack.ui.ServerFormUi; + import net.raphimc.viabedrock.api.model.NativeFormMessage; + import net.raphimc.viabedrock.api.model.NativeContainerMessage; + import net.raphimc.viabedrock.api.model.NativeContainerData; ++import net.raphimc.viabedrock.api.model.NativeItemStackData; + import net.raphimc.viabedrock.protocol.data.generated.bedrock.CustomBlockTags; + import net.raphimc.viabedrock.api.resourcepack.ui.ContainerUi; + +@@ -92,14 +93,20 @@ import net.raphimc.viabedrock.protocol.data.enums.bedrock.ComplexInventoryTransa import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.*; import net.raphimc.viabedrock.protocol.data.enums.java.generated.ContainerInput; import net.raphimc.viabedrock.protocol.data.enums.java.generated.EquipmentSlot; @@ -1986,7 +2014,7 @@ index c7461506..45e6cfc4 100644 import net.raphimc.viabedrock.protocol.model.recipe.EnchantData; import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; -@@ -115,6 +121,7 @@ import net.raphimc.viabedrock.protocol.types.recipe.CraftingDataReader; +@@ -115,6 +122,7 @@ import net.raphimc.viabedrock.protocol.types.recipe.CraftingDataReader; import java.util.ArrayList; import java.util.List; @@ -1994,7 +2022,7 @@ index c7461506..45e6cfc4 100644 import java.util.logging.Level; public final class InventoryPackets { -@@ -358,22 +365,33 @@ public final class InventoryPackets { +@@ -358,22 +366,33 @@ public final class InventoryPackets { wrapper.cancel(); final ItemRewriter itemRewriter = wrapper.user().get(ItemRewriter.class); final int groups = wrapper.read(BedrockTypes.UNSIGNED_VAR_INT); @@ -2035,7 +2063,7 @@ index c7461506..45e6cfc4 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.CONTAINER_OPEN, -@@ -1148,41 +1166,7 @@ public final class InventoryPackets { +@@ -1148,41 +1167,7 @@ public final class InventoryPackets { protocol.registerServerbound( ServerboundPackets26_3.SET_CREATIVE_MODE_SLOT, null, @@ -2078,7 +2106,7 @@ index c7461506..45e6cfc4 100644 protocol.registerServerbound( ServerboundPackets26_3.CUSTOM_CLICK_ACTION, ServerboundBedrockPackets.MODAL_FORM_RESPONSE, -@@ -1294,7 +1278,7 @@ public final class InventoryPackets { +@@ -1294,7 +1279,7 @@ public final class InventoryPackets { if (container == null) { if (containerId == ContainerID.CONTAINER_ID_INVENTORY.getValue()) { wrapper.user().get(InventoryRequestTracker.class) @@ -2087,7 +2115,7 @@ index c7461506..45e6cfc4 100644 } wrapper.cancel(); return; -@@ -1867,31 +1851,126 @@ public final class InventoryPackets { +@@ -1867,31 +1852,129 @@ public final class InventoryPackets { return "container." + name; } @@ -2165,7 +2193,17 @@ index c7461506..45e6cfc4 100644 + : !target.canPlaceItem(slot, creativeItem, creativeItem.amount())) { + return false; + } -+ if (tracker.getCurrentContainer() == null) { ++ final Container current = tracker.getCurrentContainer(); ++ if (current != null && !(current instanceof InventoryContainer)) { ++ return false; ++ } ++ final BedrockItem previous = dropping ? BedrockItem.empty() : target.getItem(slot); ++ if (!dropping && (previous.isEmpty() && creativeItem.isEmpty() ++ || previous.amount() == creativeItem.amount() ++ && NativeItemStackData.fromItem(previous).equals(NativeItemStackData.fromItem(creativeItem)))) { ++ return true; ++ } ++ if (current == null) { + final InventoryRequestTracker requests = user.get(InventoryRequestTracker.class); + if (!requests.queueCreativeSlot(inventory, javaSlot, javaItem)) { + return false; @@ -2179,13 +2217,6 @@ index c7461506..45e6cfc4 100644 + } + return true; + } -+ if (!(tracker.getCurrentContainer() instanceof InventoryContainer)) { -+ return false; -+ } -+ final BedrockItem previous = dropping ? BedrockItem.empty() : target.getItem(slot); -+ if (creativeItem.isEmpty() && previous.isEmpty()) { -+ return true; -+ } + if (!previous.isEmpty() && (previous.netId() == null || !target.canDiscardItem(slot, previous.amount()))) { + return false; + } @@ -2717,10 +2748,10 @@ index 04ad1756..4a6f0c14 100644 private void updateInventoryOptions() { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java new file mode 100644 -index 00000000..ab42d899 +index 00000000..64c72344 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java -@@ -0,0 +1,490 @@ +@@ -0,0 +1,559 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -3014,20 +3045,21 @@ index 00000000..ab42d899 + if (!this.context.workbench() && this.user().get(PlayerInventoryScreenTracker.class).acknowledgedContainer(this.opening) != owner) { + return false; + } -+ if (action.kind() == NativePlayerInventoryAction.Kind.BULK) { ++ if (action.kind() == NativePlayerInventoryAction.Kind.BULK && !state.creative()) { + return false; + } + this.model.selectOutput(entry.item()); + this.presentationDirty = true; ++ if (state.creative()) { ++ return action.kind() == NativePlayerInventoryAction.Kind.BULK ? this.placeCreative(entry) ++ : this.pickCreative(entry, action.kind() == NativePlayerInventoryAction.Kind.SECONDARY); ++ } + if (action.kind() == NativePlayerInventoryAction.Kind.SECONDARY && !state.creative()) { + return true; + } + if (action.kind() != NativePlayerInventoryAction.Kind.PRIMARY) { + return false; + } -+ if (state.creative()) { -+ return this.pickCreative(entry); -+ } + final var recipes = this.user().get(CraftingDataTracker.class); + for (final var candidate : NativeSort.sorted(recipes.outputCandidates(entry.item(), this.context.recipeTags(recipes)), + Comparator.comparingInt(data -> data.recipe().getPriority()))) { @@ -3046,8 +3078,8 @@ index 00000000..ab42d899 + return false; + } + -+ /** Native Creative left-click clears an occupied cursor, or picks a stack into an empty one. */ -+ private boolean pickCreative(final CreativeCatalog.CollectionEntry entry) { ++ /** Primary takes/discards a stack; secondary takes/discards one item. */ ++ private boolean pickCreative(final CreativeCatalog.CollectionEntry entry, final boolean secondary) { + final var inventory = this.user().get(InventoryTracker.class); + final var hud = inventory.getHudContainer(); + if (entry.catalogEntry() == null) { @@ -3059,16 +3091,20 @@ index 00000000..ab42d899 + final BedrockItem cursor; + final List actions; + if (!previous.isEmpty()) { -+ if (previous.netId() == null || !hud.canDiscardItem(0, previous.amount())) { ++ final int count = secondary ? 1 : previous.amount(); ++ if (previous.netId() == null || !hud.canDiscardItem(0, count)) { + return false; + } + id = requests.nextRequestId(); -+ cursor = BedrockItem.empty(); -+ actions = List.of(new ItemStackRequestAction.DestroyAction(previous.amount(), ++ cursor = previous.amount() == count ? BedrockItem.empty() : previous.copy(); ++ if (!cursor.isEmpty()) { ++ cursor.setAmount(previous.amount() - count); ++ } ++ actions = List.of(new ItemStackRequestAction.DestroyAction(count, + hud.requestSlotInfo(0, previous.netId()))); + } else { + final var result = entry.catalogEntry().bedrockItem().copy(); -+ final int count = Math.min(255, this.user().get(ItemRewriter.class).maxStackSize(result)); ++ final int count = Math.min(secondary ? 1 : 255, this.user().get(ItemRewriter.class).maxStackSize(result)); + if (count <= 0) { + return false; + } @@ -3098,6 +3134,68 @@ index 00000000..ab42d899 + return true; + } + ++ /** Native Creative auto-place preserves the cursor and fills existing stacks before empty slots. */ ++ private boolean placeCreative(final CreativeCatalog.CollectionEntry entry) { ++ if (entry.catalogEntry() == null) { ++ return false; ++ } ++ final var tracker = this.user().get(InventoryTracker.class); ++ final var inventory = tracker.getInventoryContainer(); ++ final var items = this.user().get(ItemRewriter.class); ++ final var result = entry.catalogEntry().bedrockItem().copy(); ++ final int maximum = Math.min(255, items.maxStackSize(result)); ++ if (maximum <= 0) { ++ return false; ++ } ++ final var stack = result.copy(); ++ stack.setAmount(maximum); ++ final var placements = new ArrayList(); ++ int remaining = maximum; ++ for (final boolean merge : new boolean[] {true, false}) { ++ for (int slot = 0; slot < inventory.size() && remaining > 0; slot++) { ++ final var previous = inventory.getItem(slot); ++ if (merge ? previous.isEmpty() || previous.netId() == null || !items.canMerge(stack, previous) ++ : !previous.isEmpty()) { ++ continue; ++ } ++ final int count = Math.min(remaining, maximum - previous.amount()); ++ if (count <= 0 || !inventory.canPlaceItem(slot, stack, count)) { ++ continue; ++ } ++ final var next = previous.isEmpty() ? result.copy() : previous.copy(); ++ next.setAmount(previous.amount() + count); ++ placements.add(new CreativePlacement(slot, count, previous.isEmpty() ? 0 : previous.netId(), next)); ++ remaining -= count; ++ } ++ } ++ if (placements.isEmpty()) { ++ return true; ++ } ++ final var requests = this.user().get(InventoryRequestTracker.class); ++ final int id = requests.nextRequestId(); ++ final var output = new ItemStackRequestSlotInfo(new FullContainerName(ContainerEnumName.CreatedOutputContainer, null), (byte) 50, id); ++ final var actions = new ArrayList(); ++ actions.add(new ItemStackRequestAction.CraftCreativeAction(entry.catalogEntry().netId(), 1)); ++ actions.add(new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(result.copy()), 1)); ++ for (final var placement : placements) { ++ actions.add(new ItemStackRequestAction.PlaceAction(placement.count(), output, ++ inventory.requestSlotInfo(placement.slot(), placement.networkId()))); ++ } ++ final var request = new ItemStackRequestInfo(id, actions, List.of(), TextProcessingEventOrigin.unknown); ++ requests.addRequest(new InventoryRequestStorage(request, 0, tracker.getHudContainer().copy(), List.of(inventory.copy()))); ++ requests.setPredictingClick(true); ++ try { ++ for (final var placement : placements) { ++ inventory.setItem(placement.slot(), placement.next()); ++ } ++ } finally { ++ requests.setPredictingClick(false); ++ } ++ PacketFactory.sendBedrockInventoryRequest(this.user(), new ItemStackRequestInfo[] {request}); ++ PacketFactory.sendJavaContainerSetContent(this.user(), inventory); ++ return true; ++ } ++ + private Presentation snapshot() { + final var items = this.user().get(ItemRewriter.class); + final var recipes = this.user().get(CraftingDataTracker.class); @@ -3210,6 +3308,8 @@ index 00000000..ab42d899 + private record Inputs(List sources, List tags, long recipes, + boolean instabuild, boolean unlocks, boolean limitedCrafting, InventoryOptions preferences, GameType gameType, GameType levelGameType) { } + ++ private record CreativePlacement(int slot, int count, int networkId, BedrockItem next) { } ++ +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java index e7a9d17d..d3356aa9 100644 @@ -4979,10 +5079,10 @@ index 5f911215..ed7bae20 100644 diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/CreativeInventoryActionTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/CreativeInventoryActionTest.java new file mode 100644 -index 00000000..e2153b2f +index 00000000..084d6dc7 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/CreativeInventoryActionTest.java -@@ -0,0 +1,214 @@ +@@ -0,0 +1,262 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -5031,6 +5131,54 @@ index 00000000..e2153b2f +final class CreativeInventoryActionTest { + + @Test ++ void unchangedCreativeEchoPreservesTheStackWithoutOpeningOrWritingInventory() throws Exception { ++ for (final boolean open : new boolean[] {true, false}) { ++ final var fixture = new Fixture(); ++ final Item selected = fixture.palette("minecraft:apple", 3); ++ final var previous = fixture.selected.copy(); ++ previous.setAmount(3); ++ previous.setNetId(17); ++ final var tag = new CompoundTag(); ++ tag.putInt("custom", 5); ++ previous.setTag(tag); ++ fixture.selected.setTag(tag.copy()); ++ fixture.inventory.getInventoryContainer().setItem(0, previous); ++ if (!open) { ++ fixture.inventory.setCurrentContainerClosed(false); ++ } ++ assertTrue(fixture.apply(36, selected)); ++ assertTrue(fixture.sent.isEmpty()); ++ assertFalse(fixture.requests.hasPending()); ++ assertFalse(fixture.requests.isWaitingForInventoryOpen()); ++ assertSame(previous, fixture.inventory.getInventoryContainer().getItem(0)); ++ assertEquals(17, previous.netId()); ++ } ++ } ++ ++ @Test ++ void changedCountOrNativeUserDataStillReplacesTheCreativeSlot() throws Exception { ++ for (final boolean metadata : new boolean[] {true, false}) { ++ final var fixture = new Fixture(); ++ final Item selected = fixture.palette("minecraft:apple", metadata ? 3 : 4); ++ fixture.inventory.getInventoryContainer().setItem(0, fixture.item("minecraft:apple", 3, 17)); ++ if (metadata) { ++ final var tag = new CompoundTag(); ++ tag.putInt("custom", 5); ++ fixture.selected.setTag(tag); ++ } ++ assertTrue(fixture.apply(36, selected)); ++ assertEquals(1, fixture.sent.size()); ++ final var actions = fixture.sent.getFirst().actions(); ++ final var destroy = assertInstanceOf(ItemStackRequestAction.DestroyAction.class, actions.get(2)); ++ final var place = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.get(3)); ++ assertEquals(3, destroy.count()); ++ assertEquals(17, destroy.item().stackNetworkId()); ++ assertEquals(metadata ? 3 : 4, place.count()); ++ assertEquals(fixture.selected.tag(), fixture.inventory.getInventoryContainer().getItem(0).tag()); ++ } ++ } ++ ++ @Test + void creativePermissionUsesRawModesInsteadOfTheJavaDisplayMode() throws Exception { + for (final GameType mode : new GameType[]{GameType.Survival, GameType.Adventure, GameType.Spectator, GameType.Undefined}) { + final var fixture = new Fixture(); @@ -5291,9 +5439,226 @@ index b69f1868..037328f2 100644 @Test void successfulResponseUpdatesNameAndCountWithoutTreatingDurabilityCorrectionAsDamage() { final BedrockItem current = new BedrockItem(276); +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java +new file mode 100644 +index 00000000..c9f73ab3 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java +@@ -0,0 +1,211 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.storage; ++ ++import com.viaversion.viaversion.api.connection.UserConnection; ++import java.util.ArrayList; ++import java.util.List; ++import net.raphimc.viabedrock.api.util.PacketFactory; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.CreativeCatalog; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; ++import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.CsvSource; ++import org.junit.jupiter.params.provider.ValueSource; ++import org.mockito.MockedStatic; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class NativePlayerCreativeAuxiliaryTest { ++ ++ @ParameterizedTest ++ @ValueSource(ints = {1, 64}) ++ void secondaryPicksOneItemRegardlessOfStackMaximum(final int maximum) throws Exception { ++ try (var f = new Fixture()) { ++ when(f.items.maxStackSize(any())).thenReturn(maximum); ++ assertTrue(f.pick()); ++ assertEquals(1, f.inventory.getHudContainer().getItem(0).amount()); ++ final var result = assertInstanceOf(ItemStackRequestAction.CraftResultsDeprecatedAction.class, f.sent.getFirst().actions().get(1)); ++ assertEquals(1, result.resultItems().getFirst().amount()); ++ assertEquals(1, assertInstanceOf(ItemStackRequestAction.TakeAction.class, f.sent.getFirst().actions().get(2)).count()); ++ } ++ } ++ ++ @ParameterizedTest ++ @CsvSource({"66,1", "66,64", "67,1", "67,64"}) ++ void secondaryDiscardsOnlyOneHeldItemAndRejectionRestoresIt(final int heldId, final int count) throws Exception { ++ try (var f = new Fixture()) { ++ final var held = item(heldId, count, 23); ++ f.inventory.getHudContainer().setItem(0, held); ++ assertTrue(f.pick()); ++ assertEquals(1, f.sent.getFirst().actions().size()); ++ final var destroy = assertInstanceOf(ItemStackRequestAction.DestroyAction.class, f.sent.getFirst().actions().getFirst()); ++ assertEquals(1, destroy.count()); ++ assertEquals(23, destroy.item().stackNetworkId()); ++ assertEquals(count - 1, f.inventory.getHudContainer().getItem(0).amount()); ++ f.reject(); ++ assertEquals(held, f.inventory.getHudContainer().getItem(0)); ++ assertEquals(count, held.amount()); ++ } ++ } ++ ++ @Test ++ void bulkMergesMainInventoryBeforeEmptyHotbarAndPreservesCursorAcrossRollback() throws Exception { ++ try (var f = new Fixture()) { ++ final var held = item(67, 7, 24); ++ f.inventory.getHudContainer().setItem(0, held); ++ final var player = f.inventory.getInventoryContainer(); ++ f.set(14, item(66, 32, 23)); ++ assertTrue(f.place()); ++ final var actions = f.sent.getFirst().actions(); ++ assertEquals(4, actions.size()); ++ final var merge = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.get(2)); ++ final var empty = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.get(3)); ++ assertEquals(32, merge.count()); ++ assertEquals(14, merge.destination().slot()); ++ assertEquals(23, merge.destination().stackNetworkId()); ++ assertEquals(32, empty.count()); ++ assertEquals(0, empty.destination().slot()); ++ assertEquals(0, empty.destination().stackNetworkId()); ++ assertEquals(merge.source(), empty.source()); ++ assertEquals(f.sent.getFirst().requestId(), merge.source().stackNetworkId()); ++ assertEquals(64, player.getItem(14).amount()); ++ assertEquals(32, player.getItem(0).amount()); ++ assertSame(held, f.inventory.getHudContainer().getItem(0)); ++ f.reject(); ++ assertEquals(32, player.getItem(14).amount()); ++ assertEquals(23, player.getItem(14).netId()); ++ assertTrue(player.getItem(0).isEmpty()); ++ assertEquals(held, f.inventory.getHudContainer().getItem(0)); ++ } ++ } ++ ++ @ParameterizedTest ++ @ValueSource(ints = {0, 1}) ++ void bulkFillsOnlyAvailableSpaceAndAcceptsFullInventoryWithoutRequests(final int space) throws Exception { ++ try (var f = new Fixture()) { ++ final var player = f.inventory.getInventoryContainer(); ++ for (int slot = 0; slot < player.size(); slot++) { ++ f.set(slot, item(99, 64, slot + 100)); ++ } ++ f.set(0, item(66, 64 - space, 23)); ++ assertTrue(f.place()); ++ assertEquals(space, f.sent.size()); ++ assertEquals(64, player.getItem(0).amount()); ++ if (space != 0) { ++ final var place = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, f.sent.getFirst().actions().getLast()); ++ assertEquals(1, place.count()); ++ assertEquals(23, place.destination().stackNetworkId()); ++ } ++ assertTrue(f.inventory.getHudContainer().getItem(0).isEmpty()); ++ } ++ } ++ ++ @Test ++ void bulkDoesNotMergeAnUnconfirmedStack() throws Exception { ++ try (var f = new Fixture()) { ++ final var player = f.inventory.getInventoryContainer(); ++ for (int slot = 0; slot < player.size(); slot++) { ++ f.set(slot, item(99, 64, slot + 100)); ++ } ++ f.set(0, item(66, 63, null)); ++ assertTrue(f.place()); ++ assertTrue(f.sent.isEmpty()); ++ assertEquals(63, player.getItem(0).amount()); ++ } ++ } ++ ++ @Test ++ void bulkUsesMainInventoryAfterTheHotbarAndRespectsNonstackableItems() throws Exception { ++ try (var f = new Fixture()) { ++ when(f.items.maxStackSize(any())).thenReturn(1); ++ final var player = f.inventory.getInventoryContainer(); ++ for (int slot = 0; slot < 9; slot++) { ++ f.set(slot, item(99, 1, slot + 100)); ++ } ++ assertTrue(f.place()); ++ final var place = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, f.sent.getFirst().actions().getLast()); ++ assertEquals(9, place.destination().slot()); ++ assertEquals(1, place.count()); ++ assertEquals(1, player.getItem(9).amount()); ++ } ++ } ++ ++ private static BedrockItem item(final int id, final int count, final Integer networkId) { ++ final var item = new BedrockItem(id); ++ item.setAmount(count); ++ item.setNetId(networkId); ++ return item; ++ } ++ ++ private static final class Fixture implements AutoCloseable { ++ private final UserConnection user = mock(UserConnection.class); ++ private final InventoryTracker inventory = new InventoryTracker(this.user); ++ private final InventoryRequestTracker requests = new InventoryRequestTracker(this.user); ++ private final ItemRewriter items = mock(ItemRewriter.class); ++ private final NativePlayerRecipeState state = new NativePlayerRecipeState(this.user); ++ private final MockedStatic packets = mockStatic(PacketFactory.class); ++ private final List sent = new ArrayList<>(); ++ private final BedrockItem sample = item(66, 1, null); ++ private final CreativeCatalog.CollectionEntry entry = new CreativeCatalog.CollectionEntry(this.sample, ++ new CreativeCatalog.Entry(1808, 0, this.sample, null, false, true)); ++ ++ private Fixture() { ++ when(this.user.get(InventoryTracker.class)).thenReturn(this.inventory); ++ when(this.user.get(InventoryRequestTracker.class)).thenReturn(this.requests); ++ when(this.user.get(ItemRewriter.class)).thenReturn(this.items); ++ when(this.items.maxStackSize(any())).thenReturn(64); ++ when(this.items.canMerge(any(), any())).thenAnswer(call -> ++ ((BedrockItem) call.getArgument(0)).identifier() == ((BedrockItem) call.getArgument(1)).identifier()); ++ this.packets.when(() -> PacketFactory.sendBedrockInventoryRequest(eq(this.user), any())).thenAnswer(call -> { ++ final var request = ((ItemStackRequestInfo[]) call.getArgument(1))[0]; ++ this.sent.add(request); ++ this.requests.getRequest(request.requestId()).capturePrediction(this.inventory); ++ return null; ++ }); ++ } ++ ++ private boolean pick() throws Exception { ++ final var method = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class, boolean.class); ++ method.setAccessible(true); ++ return (boolean) method.invoke(this.state, this.entry, true); ++ } ++ ++ private boolean place() throws Exception { ++ final var method = NativePlayerRecipeState.class.getDeclaredMethod("placeCreative", CreativeCatalog.CollectionEntry.class); ++ method.setAccessible(true); ++ return (boolean) method.invoke(this.state, this.entry); ++ } ++ ++ private void set(final int slot, final BedrockItem item) { ++ this.requests.setPredictingClick(true); ++ try { ++ this.inventory.getInventoryContainer().setItem(slot, item); ++ } finally { ++ this.requests.setPredictingClick(false); ++ } ++ } ++ ++ private void reject() { ++ this.requests.setPredictingClick(true); ++ try { ++ this.requests.restoreRejectedRequest(this.requests.getRequest(this.sent.getLast().requestId())); ++ } finally { ++ this.requests.setPredictingClick(false); ++ } ++ } ++ ++ @Override ++ public void close() { ++ this.packets.close(); ++ } ++ } ++ ++} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java new file mode 100644 -index 00000000..92759c0c +index 00000000..247e1993 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativePickTest.java @@ -0,0 +1,138 @@ @@ -5340,10 +5705,10 @@ index 00000000..92759c0c + final var sample = new BedrockItem(66, (short) 0, (byte) 1); + final var catalog = new CreativeCatalog.Entry(1808, 0, sample, null, false, true); + final var state = new NativePlayerRecipeState(user); -+ final var pick = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class); ++ final var pick = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class, boolean.class); + pick.setAccessible(true); + try (var packets = mockStatic(PacketFactory.class)) { -+ assertEquals(true, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog))); ++ assertEquals(true, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog), false)); + final var sent = ArgumentCaptor.forClass(ItemStackRequestInfo[].class); + packets.verify(() -> PacketFactory.sendBedrockInventoryRequest(eq(user), sent.capture())); + final var request = sent.getValue()[0]; @@ -5378,15 +5743,15 @@ index 00000000..92759c0c + final var sample = new BedrockItem(66, (short) 0, (byte) 1); + final var catalog = new CreativeCatalog.Entry(1808, 0, sample, null, false, true); + final var state = new NativePlayerRecipeState(user); -+ final var pick = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class); ++ final var pick = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class, boolean.class); + pick.setAccessible(true); + try (var packets = mockStatic(PacketFactory.class)) { -+ assertEquals(false, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog))); ++ assertEquals(false, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog), false)); + held.setNetId(23); + final var tag = new CompoundTag(); + tag.putByte("minecraft:item_lock", (byte) 2); + held.setTag(tag); -+ assertEquals(false, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog))); ++ assertEquals(false, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog), false)); + assertSame(held, hud.getItem(0)); + assertEquals(64, held.amount()); + assertFalse(requests.hasPending()); @@ -5411,10 +5776,10 @@ index 00000000..92759c0c + final var sample = new BedrockItem(66, (short) 0, (byte) 1); + final var catalog = new CreativeCatalog.Entry(1808, 0, sample, null, false, true); + final var state = new NativePlayerRecipeState(user); -+ final var pick = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class); ++ final var pick = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class, boolean.class); + pick.setAccessible(true); + try (var packets = mockStatic(PacketFactory.class)) { -+ assertEquals(true, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog))); ++ assertEquals(true, pick.invoke(state, new CreativeCatalog.CollectionEntry(sample.copy(), catalog), false)); + final var saved = ArgumentCaptor.forClass(InventoryRequestStorage.class); + verify(requests).addRequest(saved.capture()); + final ItemStackRequestInfo request = saved.getValue().requestInfo(); diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index 1c37c6f3..ac0e9708 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -95,7 +95,7 @@ The current native callback selects How to Play topic 5 for Survival and Encyclo A physical 1.26.60.5 capture confirms the Survival Your First Craft topic. Translated Help paragraphs replace both plain text and rendered components, preserving the native color palette. Program round trips retain these components and navigation labels from authored topic and category variables. Full installed Help lifetimes and glyph appearance remain unverified. -Bulk recipe selection, secondary Creative actions, and complete installed action parity remain unfinished. +Bulk Survival recipe selection and complete installed action parity remain unfinished. The retained model exposes output quantity separately from availability and action admission. Candidate recipes share source consumption in priority order. Count reads retain the native crafting-input, cursor, and player-inventory order. @@ -117,19 +117,21 @@ A snapshot regression checks active Creative cells without ingredients and resto A live add-on check removes the red disabled backgrounds. The picker validates the full cursor stack before transfer admission while preserving the unit-sized native craft result. Request regressions cover stack sizes one and 64, cursor prediction, and the restoration snapshot. -Official Bedrock 1.26.60.5 local-world controls show that a primary catalog click clears an occupied cursor. -Both the same-item and different-item controls clear 64 items and leave inventory empty. -A second click picks the selected stack. The implementation sends one cursor Destroy with the current stack network ID. -Request regressions cover both identities, rejection rollback, and locked or unconfirmed cursor admission. -The native control establishes visible behavior, not original native request bytes. -Installed direct and ViaProxy controls reproduce both cases with rails and powered rails. -All four requests on each route succeed. Cursor responses contain 64, zero, 64, then zero items. -Each clear sends only Destroy with the preceding confirmed cursor network ID. -Both routes retain an empty cursor and inventory after reopening. -An installed add-on test selects 64 rails and places them in the hotbar; BDS accepts both requests. -Installed direct and ViaProxy controls select 64 rails and place them in separate hotbar slots; BDS accepts the requests. -The ViaProxy screen retains both stacks after reopening. -Secondary Creative actions, bulk actions, and complete native screen parity remain unfinished. +Official Bedrock 1.26.60.5 local-world controls establish primary stack take/discard and secondary one-item take/discard. +Both held-item cases cover same and different catalog identities. +Shift-left-click and Shift-right-click preserve the cursor and add a full stack through inventory destinations. +Native controls establish global merge priority, ascending hotbar then main-inventory order, partial capacity, and a full-inventory no-op. +These controls establish visible behavior, not original native request bytes. +Regressions cover quantities, split destinations, cursor preservation, unconfirmed stacks, discard admission, capacity, and rejection rollback. +Unchanged Java Creative slot echoes retain the original stack and network ID instead of creating a replacement. +The comparison includes count and the complete native wire snapshot. +Regressions cover open and closed inventory contexts, and retain real replacements for changed counts or user data. + +Installed direct and ViaProxy controls each complete 14 server-accepted requests for these catalog actions. +The controls cover one-item quantities, cursor preservation, split bulk placement, partial capacity, and full-inventory no-op. +Reopening preserves both stacks without Creative replacement requests. +Both installed add-ons and the running proxy match the rebuilt artifact hashes. +Bulk Survival actions and complete native screen parity remain unfinished. Player and workbench graphs receive their tab and layout indices from the protocol-2225 enums. The native inventory graph references these controller variables in forced toggle indices. diff --git a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch index 17b65a92..add84cae 100644 --- a/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch +++ b/patches/viabedrock/deferred/0014-translate-bedrock-merchant-inventories-in-core.patch @@ -1,4 +1,4 @@ -From 4b81efed7c66e59415cb7609070408dff7502365 Mon Sep 17 00:00:00 2001 +From af9037997d896db085045ce995bb7d5a10d02a69 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] feat(trading): translate Bedrock merchant inventories in core @@ -884,10 +884,10 @@ index 00000000..ea4d9561 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 45e6cfc4..27fc99c9 100644 +index 64053175..f8124d38 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -@@ -79,6 +79,7 @@ import net.raphimc.viabedrock.api.model.container.NativeStorageContainers; +@@ -80,6 +80,7 @@ import net.raphimc.viabedrock.api.model.container.NativeStorageContainers; import net.raphimc.viabedrock.api.model.container.Container; import net.raphimc.viabedrock.api.model.container.ContainerOpenContext; import net.raphimc.viabedrock.api.model.container.dynamic.BundleContainer; @@ -895,7 +895,7 @@ index 45e6cfc4..27fc99c9 100644 import net.raphimc.viabedrock.api.model.container.HorseContainer; import net.raphimc.viabedrock.api.model.container.block.*; import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; -@@ -431,6 +432,11 @@ public final class InventoryPackets { +@@ -432,6 +433,11 @@ public final class InventoryPackets { return; } @@ -907,7 +907,7 @@ index 45e6cfc4..27fc99c9 100644 if (inventoryTracker.isAnyScreenOpen()) { ViaBedrock.getPlatform() .getLogger() -@@ -1293,6 +1299,11 @@ public final class InventoryPackets { +@@ -1294,6 +1300,11 @@ public final class InventoryPackets { inventoryTracker.setCurrentContainerClosed(false); return; } diff --git a/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch b/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch index 13f5161f..82cb3605 100644 --- a/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch +++ b/patches/viabedrock/deferred/0015-translate-bedrock-loom-inventories-in-core.patch @@ -1,4 +1,4 @@ -From d877c061ad8a56d84800601e43ce2f0f845485e5 Mon Sep 17 00:00:00 2001 +From 85cb93fb6ba0f4f13ae04f535edd169ee8b0c1d8 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 Bedrock loom inventories in core @@ -368,10 +368,10 @@ index a3b933c2..6ef8f93d 100644 + } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 27fc99c9..202937ad 100644 +index f8124d38..c43f45dd 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -@@ -534,6 +534,7 @@ public final class InventoryPackets { +@@ -535,6 +535,7 @@ public final class InventoryPackets { container = new SmithingContainer( wrapper.user(), containerId, title, containerPosition); diff --git a/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch b/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch index e9506c28..7f92d44a 100644 --- a/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch +++ b/patches/viabedrock/deferred/0016-translate-bedrock-cartography-in-core.patch @@ -1,4 +1,4 @@ -From 20a8064a302c66ff605d5eb3b0a3337638647dc9 Mon Sep 17 00:00:00 2001 +From b7cea07eef2932a131d522d54a551b79ea2bbcc1 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 Bedrock cartography in core @@ -634,10 +634,10 @@ index 6ef8f93d..9d1c1819 100644 public static final String CREAKING_HEART = "creaking_heart"; public static final String DAYLIGHT_DETECTOR = "daylight_detector"; diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -index 202937ad..04b6f964 100644 +index c43f45dd..091ae27a 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java -@@ -73,6 +73,7 @@ import net.lenni0451.mcstructs_bedrock.text.utils.BedrockTextUtils; +@@ -74,6 +74,7 @@ import net.lenni0451.mcstructs_bedrock.text.utils.BedrockTextUtils; import net.raphimc.viabedrock.ViaBedrock; import net.raphimc.viabedrock.protocol.storage.CommandBlockSession; import net.raphimc.viabedrock.api.model.CommandBlockOpen; @@ -645,7 +645,7 @@ index 202937ad..04b6f964 100644 import net.raphimc.viabedrock.api.chunk.BedrockBlockEntity; import net.raphimc.viabedrock.api.model.container.ChestContainer; import net.raphimc.viabedrock.api.model.container.NativeStorageContainers; -@@ -278,6 +279,9 @@ public final class InventoryPackets { +@@ -279,6 +280,9 @@ public final class InventoryPackets { } final var screens = wrapper.user().get(PlayerInventoryScreenTracker.class); final boolean correlatedClose = container instanceof InventoryContainer && screens != null && screens.ownsClose(); @@ -655,7 +655,7 @@ index 202937ad..04b6f964 100644 inventoryTracker.setCurrentContainerClosed(serverInitiated); if (correlatedClose) { wrapper.cancel(); -@@ -534,6 +538,7 @@ public final class InventoryPackets { +@@ -535,6 +539,7 @@ public final class InventoryPackets { container = new SmithingContainer( wrapper.user(), containerId, title, containerPosition); @@ -663,7 +663,7 @@ index 202937ad..04b6f964 100644 case LOOM -> container = new LoomContainer(wrapper.user(), containerId, title, containerPosition); case STONECUTTER -> container = -@@ -598,6 +603,13 @@ public final class InventoryPackets { +@@ -599,6 +604,13 @@ public final class InventoryPackets { } container.resolveBlockBinding(); inventoryTracker.setCurrentContainer(container, openContext); @@ -677,7 +677,7 @@ index 202937ad..04b6f964 100644 wrapper.write(Types.VAR_INT, (int) containerId); // container id final int javaMenuId = screenType == ContainerType.CONTAINER && size == 54 -@@ -651,6 +663,13 @@ public final class InventoryPackets { +@@ -652,6 +664,13 @@ public final class InventoryPackets { (byte) containerId, containerName, storageItem); if (container != null && container.setItems(items)) { container.markServerUpdate(); @@ -691,7 +691,7 @@ index 202937ad..04b6f964 100644 wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeContent(container); wrapper.user().get(InventoryRequestTracker.class) -@@ -821,6 +840,9 @@ public final class InventoryPackets { +@@ -822,6 +841,9 @@ public final class InventoryPackets { .refreshCraftingOutput(); tracker.sendJavaUpdateRecipes(wrapper.user()); NativeCookingUiState.sendUpdate(wrapper.user()); @@ -701,7 +701,7 @@ index 202937ad..04b6f964 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.UNLOCKED_RECIPES, -@@ -1147,6 +1169,9 @@ public final class InventoryPackets { +@@ -1148,6 +1170,9 @@ public final class InventoryPackets { if (wrapper.user().get(InventoryTracker.class).getCurrentContainer() instanceof AnvilContainer anvil) { anvil.setRenameText(name); @@ -711,7 +711,7 @@ index 202937ad..04b6f964 100644 } }); protocol.registerServerbound( -@@ -1181,6 +1206,13 @@ public final class InventoryPackets { +@@ -1182,6 +1207,13 @@ public final class InventoryPackets { final String id = wrapper.read(Types.STRING); // id final CompoundTag payload = (CompoundTag) wrapper.read(Types.CUSTOM_CLICK_ACTION_TAG); // payload 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 c6daf31e..9da9eefa 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 a6ebd0524d0aced090eeb17b9dc2dd6380ebd80c Mon Sep 17 00:00:00 2001 +From ec51af5aa8ca0b7815564b9ca471b5c26672f2f4 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 @@ -649,6 +649,14 @@ both negotiated policy admission and ordinary fallback without the core contract, before any inventory input is dispatched. Exercise physical category and layout selection through exported player-inventory programs in both recipe-book and Creative layouts. The regression rejects index-zero preference commands when the authored controls require Construction or a different layout, retaining unrelated tabs and crafting preferences. + +Map Shift-left and Shift-right catalog clicks to menu_auto_place for the +retained player inventory. The versioned 1.26.60 graph maps that source to +recipe_tertiary; official local-world controls confirm full-stack auto-place +with an unchanged cursor. Retain the pressed source through mouse release +so releasing Shift first cannot leave the authored input pressed. Physical +regressions cover both buttons, repeated clicks, modifier release and the +following ordinary click through the exported player program. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -662,7 +670,7 @@ Exercise physical category and layout selection through exported player-inventor .../render/MixinNativeGuiItemState.java | 20 + .../render/MixinNativeGuiOversizedItem.java | 31 + .../container/BedrockFurnacePreferences.java | 60 + - .../BedrockNativeContainerScreen.java | 1307 +++++++++++++++ + .../BedrockNativeContainerScreen.java | 1312 +++++++++++++++ .../container/BedrockNativeContainers.java | 282 ++++ .../container/BedrockNativeCooking.java | 147 ++ .../BedrockNativePlayerInventory.java | 157 ++ @@ -728,9 +736,9 @@ Exercise physical category and layout selection through exported player-inventor .../NativeInventoryProgressTest.java | 83 + .../container/NativePlayerHelpTextTest.java | 34 + .../NativePlayerPresentationTest.java | 153 ++ - .../container/NativePlayerScreenTest.java | 286 ++++ + .../container/NativePlayerScreenTest.java | 347 ++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 80 files changed, 10463 insertions(+), 62 deletions(-) + 80 files changed, 10529 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 @@ -1272,10 +1280,10 @@ index 0000000..1583ce0 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java new file mode 100644 -index 0000000..ccec3da +index 0000000..b54fa19 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java -@@ -0,0 +1,1307 @@ +@@ -0,0 +1,1312 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1383,6 +1391,7 @@ index 0000000..ccec3da + private long previousPresentationRevision = -1; + private Map recipePreviews = Map.of(); + private Map> recipeActions = Map.of(); ++ private final Map pointerSources = new HashMap<>(); + private long cookingUpdate = -1; + private double pointerX = -1, pointerY = -1; + private final Set capturedButtons = new HashSet<>(); @@ -1660,6 +1669,7 @@ index 0000000..ccec3da + if (press != null) BedrockNativeCooking.action(this.owner, this.menu, press.recipe, press.mode, 2); + this.capturedButtons.clear(); + this.nativeButtons.clear(); ++ this.pointerSources.clear(); + this.keyboardPressed = false; + this.deactivate(); + return; @@ -2152,7 +2162,11 @@ index 0000000..ccec3da + return true; + } + if (this.search != null) this.search.setFocused(false); -+ final String componentSource = source(event.button()); ++ final String componentSource = this.presentation instanceof NativePlayerPresentation && event.hasShiftDown() ++ && this.geometry.recipe(event.x(), event.y()) != null ++ && (event.button() == InputConstants.MOUSE_BUTTON_LEFT || event.button() == InputConstants.MOUSE_BUTTON_RIGHT) ++ ? "button.menu_auto_place" : source(event.button()); ++ this.pointerSources.put(event.button(), componentSource); + NativeContainerInput.Result componentEvent = null; + if (this.authoredController()) { + final var hover = this.nativeInput.pointer(event.x(), event.y(), 1, this.clock.getAsLong(), this.componentEnabled()); @@ -2172,11 +2186,7 @@ index 0000000..ccec3da + this.refreshSafely(); + return true; + } -+ final String input = switch (event.button()) { -+ case InputConstants.MOUSE_BUTTON_LEFT -> "button.menu_select"; -+ case InputConstants.MOUSE_BUTTON_RIGHT -> "button.menu_secondary_select"; -+ default -> ""; -+ }; ++ final String input = componentSource; + if (componentEvent != null && this.componentEvents(componentEvent)) { + this.nativeButtons.add(event.button()); + return true; @@ -2328,10 +2338,12 @@ index 0000000..ccec3da + @Override + public boolean mouseReleased(final MouseButtonEvent event) { + if (this.closing()) return true; ++ final String pressedSource = this.pointerSources.remove(event.button()); ++ final String componentSource = pressedSource == null ? source(event.button()) : pressedSource; + if (this.current() && this.authoredController()) { -+ if (this.helpView) this.inventoryInput.button(source(event.button()), false, 1, this.clock.getAsLong(), false); ++ if (this.helpView) this.inventoryInput.button(componentSource, false, 1, this.clock.getAsLong(), false); + final var hover = this.nativeInput.pointer(event.x(), event.y(), 1, this.clock.getAsLong(), this.componentEnabled()); -+ final var released = this.nativeInput.button(source(event.button()), false, 1, this.clock.getAsLong(), this.componentEnabled()); ++ final var released = this.nativeInput.button(componentSource, false, 1, this.clock.getAsLong(), this.componentEnabled()); + if (!this.waitingForPreference()) { this.componentEvents(hover); this.componentEvents(released); } + } + if (this.nativeButtons.remove(event.button())) { @@ -2553,6 +2565,7 @@ index 0000000..ccec3da + private void retireCooking() { + this.capturedButtons.clear(); + this.nativeButtons.clear(); ++ this.pointerSources.clear(); + this.keyboardPressed = false; + this.consumedReleases.clear(); + this.recipePress = null; @@ -11476,10 +11489,10 @@ index 0000000..d3902fa +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java new file mode 100644 -index 0000000..2f7e81d +index 0000000..7804780 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java -@@ -0,0 +1,286 @@ +@@ -0,0 +1,347 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -11529,6 +11542,7 @@ index 0000000..2f7e81d +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.CustomData; +import net.raphimc.viabedrock.api.model.NativePlayerInventoryMessage; ++import net.raphimc.viabedrock.api.model.NativePlayerInventoryAction; +import net.raphimc.viabedrock.api.model.NativeItemStorageData; +import net.raphimc.viabedrock.api.resourcepack.ui.PlayerInventoryUi; +import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; @@ -11538,6 +11552,7 @@ index 0000000..2f7e81d +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; ++import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; @@ -11548,6 +11563,65 @@ index 0000000..2f7e81d + NativePlayerPresentationTest.bootstrap(); + } + ++ private static IntStream catalogButtons() { ++ return IntStream.of(InputConstants.MOUSE_BUTTON_LEFT, InputConstants.MOUSE_BUTTON_RIGHT); ++ } ++ ++ @ParameterizedTest ++ @MethodSource("catalogButtons") ++ void shiftCatalogClicksUseBulkAndReleaseTheirOriginalInputAfterShiftChanges(final int button) throws Exception { ++ final var game = mock(Minecraft.class); ++ final var player = mock(LocalPlayer.class); ++ final var inventory = new Inventory(player, new EntityEquipment()); ++ final var menu = new InventoryMenu(inventory, true, player); ++ final var owner = mock(ClientPacketListener.class); ++ final var ordinary = mock(AbstractContainerScreen.class); ++ final var options = mock(Options.class); ++ final var font = mock(Font.class); ++ final var clock = new AtomicLong(); ++ set(Minecraft.class, game, "gameMode", mock(MultiPlayerGameMode.class)); ++ when(ordinary.getMenu()).thenReturn(menu); ++ when(owner.getConnection()).thenReturn(mock(Connection.class)); ++ for (final String name : List.of("keyPickItem", "keyDrop", "keyInventory")) ++ set(Options.class, options, name, mock(KeyMapping.class)); ++ set(Minecraft.class, game, "options", options); ++ set(Minecraft.class, game, "gui", mock(Gui.class)); ++ try (final var singleton = mockStatic(Minecraft.class); ++ final var network = mockStatic(BedrockNativePlayerInventory.class)) { ++ singleton.when(Minecraft::getInstance).thenReturn(game); ++ network.when(() -> BedrockNativePlayerInventory.action(any(), any(), anyInt(), any())).thenReturn(true); ++ final var ui = new PlayerInventoryUi(new ResourcePackStorage(List.of()).getJsonUiProgram()); ++ final var cell = new PlayerInventoryUi.Cell(0, 10, -1, 0, 0, 1, false, false, "", Map.of()); ++ final var state = new PlayerInventoryUi.State(false, true, 2, 2, 5, 2, false, "", "All", "Crafting", ++ List.of(1, 0, 0, 0), List.of(cell), "", -1); ++ final var source = new NativePlayerInventoryMessage(0, 1, 8, 1, ui.exportProgram(false).toString(), ++ GsonUtil.getGson().toJson(state), Map.of(0, new StructuredItem(320, 1, new StructuredDataContainer()))); ++ final var presentation = new NativePlayerPresentation(source, Map.of(0, new ItemStack(Items.IRON_INGOT)), clock::get); ++ final var screen = new BedrockNativeContainerScreen(owner, ordinary, inventory, Component.empty(), presentation, ++ false, 1, clock::get, () -> true); ++ set(Screen.class, screen, "minecraft", game); ++ set(Screen.class, screen, "font", font); ++ try { ++ screen.prepare(font, 640, 360); ++ clock.set(1_000_000_000); ++ screen.prepare(font, 640, 360); ++ final var target = pointer(screen, draw -> draw.box().node().properties().has("recipe_actions")); ++ final var pressed = new MouseButtonEvent(target.x(), target.y(), new MouseButtonInfo(button, InputConstants.MOD_SHIFT)); ++ final var released = new MouseButtonEvent(target.x(), target.y(), new MouseButtonInfo(button, 0)); ++ for (int click = 0; click < 2; click++) { ++ assertTrue(screen.mouseClicked(pressed, false)); ++ assertTrue(screen.mouseReleased(released)); ++ } ++ network.verify(() -> BedrockNativePlayerInventory.action(owner, menu, 0, NativePlayerInventoryAction.Kind.BULK), times(2)); ++ assertTrue(screen.mouseClicked(released, false)); ++ assertTrue(screen.mouseReleased(released)); ++ final var plain = button == InputConstants.MOUSE_BUTTON_LEFT ? NativePlayerInventoryAction.Kind.PRIMARY : NativePlayerInventoryAction.Kind.SECONDARY; ++ network.verify(() -> BedrockNativePlayerInventory.action(owner, menu, 0, plain)); ++ verifyNoInteractions(game.gameMode); ++ } finally { screen.discard(); } ++ } ++ } ++ + @Test + void defaultHelpReturnsToTheSameInventoryAndCursorWithoutDispatchingContainerInput() throws Exception { + final var game = mock(Minecraft.class); 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 7d02eb1d..6d403fc1 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 @@ -57,11 +57,19 @@ Creative hotbar destruction restores ordinary controls before item changes or re Passive hover notifications do not trigger the fallback for unsupported recipe actions. Installed direct and ViaProxy controls retain all 1,980 catalog entries from a local protocol-2225 server. Physical category and layout changes preserve their native indices through exported programs. +Physical Shift-left and Shift-right catalog controls bind the authored menu_auto_place source to the bulk action. +Mouse release retains the pressed source even after Shift is released. +Screen regressions repeat both chords and then perform an ordinary click through the exported player program. Both routes search for rails and place a server-accepted stack of 64 in the hotbar. ViaProxy retains both stacks after reopening. Installed direct and ViaProxy controls cover initial bundle insertion, extraction, reinsertion, and retention after reopening through ordinary Java controls. The [inventory reference](../../../docs/native-player-inventory.md) states the verified input behavior and remaining native comparison limit. -Bulk recipe actions and Creative cursor replacement remain unresolved. + +Installed direct and ViaProxy controls each complete 14 server-accepted requests for these catalog actions. +The controls cover one-item quantities, cursor preservation, split bulk placement, partial capacity, and full-inventory no-op. +Reopening preserves both stacks without Creative replacement requests. +Both installed add-ons and the running proxy match the rebuilt artifact hashes. +Bulk Survival recipe actions remain unresolved. Complete cooking integration, live transfers, and filled-bundle pixels also remain unresolved. Stable frames reuse the draw list, parsed hit regions, and input ancestor geometry. From 308d97ec479fd43cdd4596414236952c43fd4dbf Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:52:20 +0100 Subject: [PATCH 33/33] fix(inventory): match native Creative hotbar actions Bind the catalog-only toolbar's full-stack destruction to a separately advertised backend capability. Validate the opening, generation, layout and confirmed discard permission before predicting changes, preserving the cursor and rollback snapshot. Dispatch authored quick-move commands directly for player slots. Java's pointer gesture swaps stacks when the cursor is occupied, while the target Bedrock client preserves the cursor and moves the selected stack. Fold both changes into their owning patches and retain version-matched native evidence and boundary regressions. --- docs/native-player-inventory.md | 6 +- ...ain-bedrock-creative-catalog-entries.patch | 284 +++++++++++++++--- ...ain-bedrock-creative-catalog-entries.pr.md | 11 + ...9-present-authored-native-containers.patch | 259 ++++++++++++++-- ...9-present-authored-native-containers.pr.md | 11 +- 5 files changed, 499 insertions(+), 72 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 8d6102fd..9acba120 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -144,7 +144,11 @@ Filled-bundle bars still need installed native comparison. Unknown storage snapshots restore ordinary controls. Hovering or carrying storage items also restores ordinary controls until native tooltips and selection are complete. Unsupported visible Help glyphs, recipe auxiliary actions, and held-selection commands also restore ordinary controls. -Creative hotbar destruction restores ordinary controls before item changes or requests. +The Creative catalog-only toolbar binds Shift-left-click and Shift-right-click to full-stack destruction. +The action preserves the cursor, requires a current acknowledged opening, and leaves empty slots unchanged. +Inventory-only and recipe-book layouts use the authored quick-move command to preserve an occupied cursor during stack transfer. +Backends without the separate destruction capability restore ordinary controls before item changes or requests. +Installed direct and ViaProxy controls confirm destruction and occupied-cursor transfer through successful server responses and retained inventory after reopening. Installed direct and ViaProxy controls retain the complete 1,980-item catalog from a local protocol-2225 server. Tab and layout controls use the target inventory indices. Both routes search for rails and place a server-accepted stack of 64 in the hotbar. 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 223da472..df67eb2a 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 210863f8610c43bb08177ec80b97f85f616bc9cf Mon Sep 17 00:00:00 2001 +From 06e3909613c2025b9a5c5881942c649a2e04508b 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 @@ -290,15 +290,30 @@ wire snapshots, including NBT, block state, restrictions and blocking ticks, while excluding transient network identity. Genuine count or native user-data changes retain replacement requests. Preserve drop permission, foreign-container rejection and pending-close ownership checks. + +Support the Creative catalog-only toolbar's container_auto_destroy action. +Official Bedrock 1.26.60.5/protocol 2225 local-world controls discard the +entire selected hotbar stack on Shift-left and Shift-right, preserve the +held cursor, and leave empty slots unchanged. The same Shift input moves +the stack to main inventory in the inventory-only layout. The matching +inventory_screen.json creative_hotbar_container_slot_button overrides +button_auto_place with button.container_auto_destroy. + +Advertise this destructive command on a separate optional channel. Bind it +to the current opening, generation and revision, require Creative toolbar +layout and confirmed discard permission, and predict only the selected +permanent hotbar slot. Preserve cursor and inventory snapshots for rejected +requests. These native controls establish visible behavior, not equality +with original-client request bytes. --- - .../model/NativePlayerInventoryAction.java | 88 +++ - .../model/NativePlayerInventoryMessage.java | 271 +++++++++ + .../model/NativePlayerInventoryAction.java | 94 +++ + .../model/NativePlayerInventoryMessage.java | 271 ++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ .../model/container/NativeRecipeCounter.java | 74 +++ - .../resourcepack/ui/PlayerInventoryUi.java | 397 +++++++++++++ + .../resourcepack/ui/PlayerInventoryUi.java | 397 ++++++++++++ .../viabedrock/protocol/BedrockProtocol.java | 1 + - .../protocol/model/CreativeCatalog.java | 266 +++++++++ - .../model/CreativeInventoryModel.java | 198 +++++++ + .../protocol/model/CreativeCatalog.java | 266 ++++++++ + .../model/CreativeInventoryModel.java | 198 ++++++ .../protocol/model/PlayerRecipeContext.java | 96 +++ .../protocol/model/PlayerRecipeModel.java | 125 ++++ .../protocol/packet/InventoryPackets.java | 213 +++++-- @@ -307,22 +322,22 @@ foreign-container rejection and pending-close ownership checks. .../protocol/storage/CraftingDataTracker.java | 129 +++- .../storage/InventoryRequestTracker.java | 24 + .../protocol/storage/InventoryTracker.java | 21 + - .../storage/NativePlayerRecipeState.java | 559 ++++++++++++++++++ + .../storage/NativePlayerRecipeState.java | 593 ++++++++++++++++++ .../storage/PlayerInventoryScreenTracker.java | 40 +- - .../NativePlayerInventoryMessageTest.java | 160 +++++ + .../NativePlayerInventoryMessageTest.java | 179 ++++++ .../container/NativeRecipeCounterTest.java | 27 + .../container/NativeRecipeQuantityTest.java | 61 ++ .../ui/PlayerInventoryUiTest.java | 126 ++++ - .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++++ - .../protocol/packet/InventoryPacketsTest.java | 267 +++++++-- - .../rewriter/PlayerRecipeModelTest.java | 471 +++++++++++++++ + .../protocol/model/CreativeCatalogTest.java | 382 +++++++++++ + .../protocol/packet/InventoryPacketsTest.java | 267 ++++++-- + .../rewriter/PlayerRecipeModelTest.java | 471 ++++++++++++++ ...egisteredPlayerRecipeAvailabilityTest.java | 5 +- .../RegisteredPlayerRecipeSearchTest.java | 3 +- .../storage/CreativeInventoryActionTest.java | 262 ++++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ - .../NativePlayerCreativeAuxiliaryTest.java | 211 +++++++ - .../storage/NativePlayerCreativePickTest.java | 138 +++++ - 31 files changed, 4659 insertions(+), 170 deletions(-) + .../NativePlayerCreativeAuxiliaryTest.java | 333 ++++++++++ + .../storage/NativePlayerCreativePickTest.java | 138 ++++ + 31 files changed, 4840 insertions(+), 170 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 @@ -343,10 +358,10 @@ foreign-container rejection and pending-close ownership checks. diff --git a/src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java b/src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java new file mode 100644 -index 00000000..62fb4706 +index 00000000..b3a09f75 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java -@@ -0,0 +1,88 @@ +@@ -0,0 +1,94 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -365,9 +380,10 @@ index 00000000..62fb4706 + +/** Commands bind to one displayed snapshot, independently of Java menu-zero reuse. */ +public record NativePlayerInventoryAction(int menuId, long opening, long generation, long revision, long request, -+ Kind kind, int cell, String query, InventoryOptions options) { ++ Kind kind, int target, String query, InventoryOptions options) { + + public static final String CHANNEL = "viabedrock:native_player_input"; ++ public static final String AUTO_DESTROY_CHANNEL = "viabedrock:native_player_auto_destroy"; + public static final Type TYPE = new Type<>(NativePlayerInventoryAction.class) { + @Override + public NativePlayerInventoryAction read(final ByteBuf buffer) { @@ -384,13 +400,13 @@ index 00000000..62fb4706 + if (kind >= Kind.values().length) { + throw new IllegalArgumentException("Unknown player inventory command"); + } -+ final int cell = Types.VAR_INT.readPrimitive(buffer); ++ final int target = Types.VAR_INT.readPrimitive(buffer); + final String query = Types.STRING.read(buffer); + final InventoryOptions options = kind == Kind.PREFERENCES.ordinal() ? BedrockTypes.INVENTORY_OPTIONS.read(buffer) : null; + if (buffer.isReadable()) { + throw new IllegalArgumentException("Trailing player inventory input"); + } -+ return new NativePlayerInventoryAction(menu, opening, generation, revision, request, Kind.values()[kind], cell, query, options); ++ return new NativePlayerInventoryAction(menu, opening, generation, revision, request, Kind.values()[kind], target, query, options); + } + + @Override @@ -403,7 +419,7 @@ index 00000000..62fb4706 + Types.VAR_LONG.writePrimitive(buffer, value.revision); + Types.VAR_LONG.writePrimitive(buffer, value.request); + buffer.writeByte(value.kind.ordinal()); -+ Types.VAR_INT.writePrimitive(buffer, value.cell); ++ Types.VAR_INT.writePrimitive(buffer, value.target); + Types.STRING.write(buffer, value.query); + if (value.kind == Kind.PREFERENCES) { + BedrockTypes.INVENTORY_OPTIONS.write(buffer, value.options); @@ -413,9 +429,10 @@ index 00000000..62fb4706 + + public NativePlayerInventoryAction { + if (menuId < 0 || menuId > 255 || opening < 0 || generation < 0 || revision <= 0 || request <= 0 -+ || kind == null || query == null || query.length() > 8192 || !StandardCharsets.UTF_8.newEncoder().canEncode(query) || cell < -1 ++ || kind == null || query == null || query.length() > 8192 || !StandardCharsets.UTF_8.newEncoder().canEncode(query) || target < -1 + || (kind == Kind.PREFERENCES) != (options != null) -+ || (kind == Kind.PRIMARY || kind == Kind.SECONDARY || kind == Kind.BULK) != (cell >= 0) ++ || (kind == Kind.PRIMARY || kind == Kind.SECONDARY || kind == Kind.BULK || kind == Kind.AUTO_DESTROY) != (target >= 0) ++ || kind == Kind.AUTO_DESTROY && (target < 36 || target > 44) + || kind != Kind.SEARCH && !query.isEmpty()) { + throw new IllegalArgumentException("Invalid player inventory input"); + } @@ -425,13 +442,17 @@ index 00000000..62fb4706 + } + } + ++ public String channel() { ++ return this.kind == Kind.AUTO_DESTROY ? AUTO_DESTROY_CHANNEL : CHANNEL; ++ } ++ + public boolean belongsTo(final NativePlayerInventoryMessage message, final long lastRequest) { + return message != null && this.menuId == message.menuId() && this.opening == message.opening() + && this.generation == message.generation() && this.revision == message.revision() && this.request > lastRequest; + } + + public enum Kind { -+ PREFERENCES, SEARCH, PRIMARY, SECONDARY, BULK ++ PREFERENCES, SEARCH, PRIMARY, SECONDARY, BULK, AUTO_DESTROY + } + +} @@ -2268,7 +2289,7 @@ index c7461506..64053175 100644 public static boolean handleLegacyOffhandClick(final UserConnection user, final Container container, diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java -index f5cbbaf0..243134d5 100644 +index f5cbbaf0..a3e297da 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java @@ -17,6 +17,9 @@ @@ -2285,12 +2306,12 @@ index f5cbbaf0..243134d5 100644 } return; } -+ if (channel.equals(NativePlayerInventoryAction.CHANNEL)) { ++ if (channel.equals(NativePlayerInventoryAction.CHANNEL) || channel.equals(NativePlayerInventoryAction.AUTO_DESTROY_CHANNEL)) { + final var state = wrapper.user().get(NativePlayerRecipeState.class); + if (state != null && state.actionsEnabled()) { + try { + final var action = wrapper.read(NativePlayerInventoryAction.TYPE); -+ final boolean accepted = state.handleAction(action); ++ final boolean accepted = action.channel().equals(channel) && state.handleAction(action); + state.tick(); + final var message = state.publication(); + final var result = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, wrapper.user()); @@ -2748,10 +2769,10 @@ index 04ad1756..4a6f0c14 100644 private void updateInventoryOptions() { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java new file mode 100644 -index 00000000..64c72344 +index 00000000..99f28e02 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java -@@ -0,0 +1,559 @@ +@@ -0,0 +1,593 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -2983,7 +3004,7 @@ index 00000000..64c72344 + } + final var packet = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, this.user()); + packet.write(Types.STRING, available ? "minecraft:register" : "minecraft:unregister"); -+ packet.write(Types.REMAINING_BYTES, NativePlayerInventoryAction.CHANNEL.getBytes(StandardCharsets.UTF_8)); ++ packet.write(Types.REMAINING_BYTES, (NativePlayerInventoryAction.CHANNEL + "\0" + NativePlayerInventoryAction.AUTO_DESTROY_CHANNEL).getBytes(StandardCharsets.UTF_8)); + packet.send(BedrockProtocol.class); + this.actionsAdvertised = available; + } @@ -3012,8 +3033,12 @@ index 00000000..64c72344 + return true; + } + final var state = message.state(); -+ if (action.cell() >= state.cells().size() || state.cells().get(action.cell()).id() != action.cell() -+ || state.cells().get(action.cell()).disabled()) { ++ if (action.kind() == NativePlayerInventoryAction.Kind.AUTO_DESTROY) { ++ return state.creative() && !state.workbench() && state.effectiveLayout() == 3 && this.canRequest(message) ++ && this.destroyCreativeHotbar(action.target() - 36); ++ } ++ if (action.target() >= state.cells().size() || state.cells().get(action.target()).id() != action.target() ++ || state.cells().get(action.target()).disabled()) { + return false; + } + final var view = Arrays.stream(CreativeInventoryModel.View.values()).filter(value -> value.leftTab() == state.leftTab()).findFirst().orElse(null); @@ -3021,10 +3046,10 @@ index 00000000..64c72344 + return false; + } + final var collection = this.model.collections().collection(view); -+ if (action.cell() >= collection.snapshot().rows().size()) { ++ if (action.target() >= collection.snapshot().rows().size()) { + return false; + } -+ final var row = collection.snapshot().rows().get(action.cell()); ++ final var row = collection.snapshot().rows().get(action.target()); + if (row instanceof RecipeCollection.Header header) { + if (action.kind() != NativePlayerInventoryAction.Kind.PRIMARY) { + return false; @@ -3036,15 +3061,10 @@ index 00000000..64c72344 + final var entry = row instanceof RecipeCollection.Item item + ? item.entry() : ((RecipeCollection.Member) row).entry(); + final var inventory = this.user().get(InventoryTracker.class); -+ final var requests = this.user().get(InventoryRequestTracker.class); -+ if (message.generation() == 0 || inventory.currentContainerGeneration() != message.generation() -+ || !requests.canStartRecipePlacement() || requests.isPredictingClick()) { ++ if (!this.canRequest(message)) { + return false; + } + final var owner = inventory.getCurrentContainer(); -+ if (!this.context.workbench() && this.user().get(PlayerInventoryScreenTracker.class).acknowledgedContainer(this.opening) != owner) { -+ return false; -+ } + if (action.kind() == NativePlayerInventoryAction.Kind.BULK && !state.creative()) { + return false; + } @@ -3078,6 +3098,41 @@ index 00000000..64c72344 + return false; + } + ++ private boolean canRequest(final NativePlayerInventoryMessage message) { ++ final var inventory = this.user().get(InventoryTracker.class); ++ final var requests = this.user().get(InventoryRequestTracker.class); ++ return message.generation() != 0 && inventory.currentContainerGeneration() == message.generation() ++ && requests.canStartRecipePlacement() && !requests.isPredictingClick() ++ && (this.context.workbench() || this.user().get(PlayerInventoryScreenTracker.class).acknowledgedContainer(this.opening) == inventory.getCurrentContainer()); ++ } ++ ++ /** The catalog-only toolbar discards its selected stack without touching the held item. */ ++ private boolean destroyCreativeHotbar(final int slot) { ++ final var tracker = this.user().get(InventoryTracker.class); ++ final var inventory = tracker.getInventoryContainer(); ++ final var previous = inventory.getItem(slot); ++ if (previous.isEmpty()) { ++ return true; ++ } ++ if (previous.netId() == null || !inventory.canDiscardItem(slot, previous.amount())) { ++ return false; ++ } ++ final var requests = this.user().get(InventoryRequestTracker.class); ++ final int id = requests.nextRequestId(); ++ final var action = new ItemStackRequestAction.DestroyAction(previous.amount(), inventory.requestSlotInfo(slot, previous.netId())); ++ final var request = new ItemStackRequestInfo(id, List.of(action), List.of(), TextProcessingEventOrigin.unknown); ++ requests.addRequest(new InventoryRequestStorage(request, 0, tracker.getHudContainer().copy(), List.of(inventory.copy()))); ++ requests.setPredictingClick(true); ++ try { ++ inventory.setItem(slot, BedrockItem.empty()); ++ } finally { ++ requests.setPredictingClick(false); ++ } ++ PacketFactory.sendBedrockInventoryRequest(this.user(), new ItemStackRequestInfo[] {request}); ++ PacketFactory.sendJavaContainerSetContent(this.user(), inventory); ++ return true; ++ } ++ + /** Primary takes/discards a stack; secondary takes/discards one item. */ + private boolean pickCreative(final CreativeCatalog.CollectionEntry entry, final boolean secondary) { + final var inventory = this.user().get(InventoryTracker.class); @@ -3425,10 +3480,10 @@ index e7a9d17d..d3356aa9 100644 } diff --git a/src/test/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryMessageTest.java b/src/test/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryMessageTest.java new file mode 100644 -index 00000000..4cdb4cc6 +index 00000000..ab5e2214 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryMessageTest.java -@@ -0,0 +1,160 @@ +@@ -0,0 +1,179 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -3571,6 +3626,25 @@ index 00000000..4cdb4cc6 + } + + @Test ++ void hotbarDestroyRoundTripsOnlyPermanentJavaHotbarSlots() { ++ for (final int target : List.of(36, 44)) { ++ final var action = new NativePlayerInventoryAction(0, 1, 2, 3, 4, NativePlayerInventoryAction.Kind.AUTO_DESTROY, target, "", null); ++ final var wire = Unpooled.buffer(); ++ try { ++ NativePlayerInventoryAction.TYPE.write(wire, action); ++ assertEquals(action, NativePlayerInventoryAction.TYPE.read(wire)); ++ assertFalse(wire.isReadable()); ++ } finally { ++ wire.release(); ++ } ++ } ++ for (final int target : List.of(-1, 0, 35, 45)) { ++ assertThrows(IllegalArgumentException.class, () -> new NativePlayerInventoryAction(0, 1, 2, 3, 4, ++ NativePlayerInventoryAction.Kind.AUTO_DESTROY, target, "", null)); ++ } ++ } ++ ++ @Test + void coercedStateAndCrossFamilyOpeningsCannotBecomePresentations() { + final var value = message(); + for (final String name : List.of("creative", "leftTab", "query")) { @@ -5441,10 +5515,10 @@ index b69f1868..037328f2 100644 final BedrockItem current = new BedrockItem(276); diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java new file mode 100644 -index 00000000..c9f73ab3 +index 00000000..1d7a8938 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativePlayerCreativeAuxiliaryTest.java -@@ -0,0 +1,211 @@ +@@ -0,0 +1,333 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -5452,9 +5526,16 @@ index 00000000..c9f73ab3 + */ +package net.raphimc.viabedrock.protocol.storage; + ++import com.viaversion.nbt.tag.CompoundTag; +import com.viaversion.viaversion.api.connection.UserConnection; ++import com.viaversion.viaversion.util.GsonUtil; ++import net.raphimc.viabedrock.api.model.NativePlayerInventoryAction; ++import net.raphimc.viabedrock.api.model.NativePlayerInventoryMessage; ++import net.raphimc.viabedrock.api.resourcepack.ui.PlayerInventoryUi; ++import net.raphimc.viabedrock.protocol.model.PlayerRecipeContext; +import java.util.ArrayList; +import java.util.List; ++import java.util.Map; +import net.raphimc.viabedrock.api.util.PacketFactory; +import net.raphimc.viabedrock.protocol.model.BedrockItem; +import net.raphimc.viabedrock.protocol.model.CreativeCatalog; @@ -5585,6 +5666,94 @@ index 00000000..c9f73ab3 + } + } + ++ @ParameterizedTest ++ @CsvSource({"0,1", "0,64", "8,1", "8,64"}) ++ void hotbarDestroyPreservesOtherSlotsAndCursorAndRestoresRejectedStack(final int slot, final int count) throws Exception { ++ try (var f = new Fixture()) { ++ final var held = item(67, 7, 24); ++ final var previous = item(66, count, 23); ++ f.inventory.getHudContainer().setItem(0, held); ++ f.set(slot, previous); ++ f.set(4, item(68, 32, 25)); ++ f.presentation(true, false, 3); ++ final var action = f.destroyAction(slot, 1); ++ assertTrue(f.state.handleAction(action)); ++ assertFalse(f.state.handleAction(action)); ++ assertFalse(f.state.handleAction(f.destroyAction(4, 2))); ++ assertEquals(1, f.sent.size()); ++ final var destroy = assertInstanceOf(ItemStackRequestAction.DestroyAction.class, f.sent.getFirst().actions().getFirst()); ++ assertEquals(1, f.sent.getFirst().actions().size()); ++ assertEquals(count, destroy.count()); ++ assertEquals(f.inventory.getInventoryContainer().requestSlotInfo(slot, 23), destroy.item()); ++ assertTrue(f.inventory.getInventoryContainer().getItem(slot).isEmpty()); ++ assertEquals(32, f.inventory.getInventoryContainer().getItem(4).amount()); ++ assertSame(held, f.inventory.getHudContainer().getItem(0)); ++ f.reject(); ++ assertEquals(previous, f.inventory.getInventoryContainer().getItem(slot)); ++ assertEquals(held, f.inventory.getHudContainer().getItem(0)); ++ } ++ } ++ ++ @Test ++ void emptyHotbarDestroyDoesNotCreateRequestsOrChangeTheCursor() throws Exception { ++ try (var f = new Fixture()) { ++ final var held = item(67, 7, 24); ++ f.inventory.getHudContainer().setItem(0, held); ++ f.presentation(true, false, 0); ++ assertTrue(f.state.handleAction(f.destroyAction(1, 1))); ++ assertTrue(f.sent.isEmpty()); ++ assertSame(held, f.inventory.getHudContainer().getItem(0)); ++ } ++ } ++ ++ @ParameterizedTest ++ @CsvSource({"false,false,3", "true,false,1", "true,false,2", "true,true,3"}) ++ void hotbarDestroyCannotDiscardOutsideTheCreativeToolbar(final boolean creative, final boolean workbench, final int layout) throws Exception { ++ try (var f = new Fixture()) { ++ final var previous = item(66, 64, 23); ++ f.set(0, previous); ++ f.presentation(creative, workbench, layout); ++ assertFalse(f.state.handleAction(f.destroyAction(0, 1))); ++ assertTrue(f.sent.isEmpty()); ++ assertSame(previous, f.inventory.getInventoryContainer().getItem(0)); ++ } ++ } ++ ++ @ParameterizedTest ++ @ValueSource(ints = {-1, 1, 2}) ++ void hotbarDestroyRejectsUnconfirmedOrLockedStacks(final int lock) throws Exception { ++ try (var f = new Fixture()) { ++ final var previous = item(66, 64, lock == -1 ? null : 23); ++ if (lock >= 0) { ++ final var tag = new CompoundTag(); ++ tag.putByte("minecraft:item_lock", (byte) lock); ++ previous.setTag(tag); ++ } ++ f.set(0, previous); ++ f.presentation(true, false, 3); ++ assertFalse(f.state.handleAction(f.destroyAction(0, 1))); ++ assertTrue(f.sent.isEmpty()); ++ assertSame(previous, f.inventory.getInventoryContainer().getItem(0)); ++ } ++ } ++ ++ @Test ++ void hotbarDestroyRequiresTheAcknowledgedOpeningAndCurrentGeneration() throws Exception { ++ try (var f = new Fixture()) { ++ final var previous = item(66, 64, 23); ++ f.set(0, previous); ++ f.presentation(true, false, 3); ++ when(f.screens.acknowledgedContainer(1)).thenReturn(null); ++ assertFalse(f.state.handleAction(f.destroyAction(0, 1))); ++ when(f.screens.acknowledgedContainer(1)).thenReturn(f.inventory.getInventoryContainer()); ++ f.inventory.setCurrentContainerClosed(false); ++ f.inventory.setCurrentContainer(f.inventory.getInventoryContainer()); ++ assertFalse(f.state.handleAction(f.destroyAction(0, 2))); ++ assertTrue(f.sent.isEmpty()); ++ assertSame(previous, f.inventory.getInventoryContainer().getItem(0)); ++ } ++ } ++ + private static BedrockItem item(final int id, final int count, final Integer networkId) { + final var item = new BedrockItem(id); + item.setAmount(count); @@ -5597,7 +5766,9 @@ index 00000000..c9f73ab3 + private final InventoryTracker inventory = new InventoryTracker(this.user); + private final InventoryRequestTracker requests = new InventoryRequestTracker(this.user); + private final ItemRewriter items = mock(ItemRewriter.class); -+ private final NativePlayerRecipeState state = new NativePlayerRecipeState(this.user); ++ private final NativePlayerRecipeState state = spy(new NativePlayerRecipeState(this.user)); ++ private final PlayerInventoryScreenTracker screens = mock(PlayerInventoryScreenTracker.class); ++ private NativePlayerInventoryMessage message; + private final MockedStatic packets = mockStatic(PacketFactory.class); + private final List sent = new ArrayList<>(); + private final BedrockItem sample = item(66, 1, null); @@ -5619,6 +5790,31 @@ index 00000000..c9f73ab3 + }); + } + ++ private void presentation(final boolean creative, final boolean workbench, final int layout) throws Exception { ++ this.inventory.setCurrentContainer(this.inventory.getInventoryContainer()); ++ when(this.user.get(PlayerInventoryScreenTracker.class)).thenReturn(this.screens); ++ when(this.screens.acknowledgedContainer(1)).thenReturn(this.inventory.getInventoryContainer()); ++ final var context = NativePlayerRecipeState.class.getDeclaredField("context"); ++ context.setAccessible(true); ++ context.set(this.state, PlayerRecipeContext.forContainer(this.user, this.inventory.getInventoryContainer())); ++ final var opening = NativePlayerRecipeState.class.getDeclaredField("opening"); ++ opening.setAccessible(true); ++ opening.setLong(this.state, 1); ++ final var snapshot = new PlayerInventoryUi.State(workbench, creative, layout, 2, 5, 2, false, "", "All", "Crafting", ++ List.of(0, 0, 0, 0), List.of(), "", -1); ++ final var ui = new PlayerInventoryUi(new ResourcePackStorage(List.of()).getJsonUiProgram()); ++ this.message = new NativePlayerInventoryMessage(workbench ? 1 : 0, workbench ? 0 : 1, this.inventory.currentContainerGeneration(), 1, ++ ui.exportProgram(workbench).toString(), GsonUtil.getGson().toJson(snapshot), Map.of()); ++ doNothing().when(this.state).tick(); ++ doReturn(true).when(this.state).actionsEnabled(); ++ doReturn(this.message).when(this.state).publication(); ++ } ++ ++ private NativePlayerInventoryAction destroyAction(final int slot, final long request) { ++ return new NativePlayerInventoryAction(this.message.menuId(), this.message.opening(), this.message.generation(), this.message.revision(), ++ request, NativePlayerInventoryAction.Kind.AUTO_DESTROY, slot + 36, "", null); ++ } ++ + private boolean pick() throws Exception { + final var method = NativePlayerRecipeState.class.getDeclaredMethod("pickCreative", CreativeCatalog.CollectionEntry.class, boolean.class); + method.setAccessible(true); diff --git a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md index ac0e9708..345cbd24 100644 --- a/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md +++ b/patches/viabedrock/deferred/0012-retain-bedrock-creative-catalog-entries.pr.md @@ -131,6 +131,17 @@ Installed direct and ViaProxy controls each complete 14 server-accepted requests The controls cover one-item quantities, cursor preservation, split bulk placement, partial capacity, and full-inventory no-op. Reopening preserves both stacks without Creative replacement requests. Both installed add-ons and the running proxy match the rebuilt artifact hashes. +The catalog-only toolbar also accepts full-stack destruction through a separately advertised optional channel. +Official Bedrock 1.26.60.5 local-world controls cover Shift-left, Shift-right, an occupied cursor, and an empty hotbar slot. +The same input moves the stack into main inventory in the inventory-only layout. +The matching `inventory_screen.json` maps the Creative toolbar auto-place input to `button.container_auto_destroy`. +The backend requires the current acknowledged opening, generation, revision, Creative toolbar layout, network ID, and discard permission. +Regressions cover rollback, cursor preservation, pending requests, layout restrictions, typed locks, and stale generation rejection. +These native controls establish visible behavior; original native request bytes remain unverified. +Installed direct and ViaProxy controls each complete seven server-accepted requests for hotbar destruction and inventory-only transfer. +Both routes preserve the occupied cursor, leave empty slots unchanged, and retain the resulting inventory after reopening without Creative replacement requests. +The installed add-ons and proxy match the rebuilt artifact hashes. + Bulk Survival actions and complete native screen parity remain unfinished. Player and workbench graphs receive their tab and layout indices from the protocol-2225 enums. 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 9da9eefa..c568d549 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 ec51af5aa8ca0b7815564b9ca471b5c26672f2f4 Mon Sep 17 00:00:00 2001 +From a83ca770d78b82f6acc0214688e5838e2b6e0493 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 @@ -657,6 +657,27 @@ with an unchanged cursor. Retain the pressed source through mouse release so releasing Shift first cannot leave the authored input pressed. Physical regressions cover both buttons, repeated clicks, modifier release and the following ordinary click through the exported player program. + +Bind the Creative catalog-only toolbar's authored container_auto_destroy +command to an optional backend channel before queuing a command. Preserve +ordinary transfers in other layouts and restore the ordinary screen when +the backend lacks this capability. Shift-left and Shift-right use the +native menu_auto_place mapping and retain the original source on release. + +Official Bedrock 1.26.60.5/protocol 2225 local-world controls discard a full +hotbar stack with either chord, preserve an occupied cursor, and leave an +empty slot unchanged. The inventory-only layout instead moves the stack +into main inventory. Matching inventory_screen.json declarations provide +the toolbar override. These controls establish visible behavior, not +original-client request-byte equality. + +Dispatch authored quick-move commands directly for player slot chords. +Java's pointer gesture falls back to pickup when the cursor is occupied, +which swaps the held stack with the hotbar stack in the installed native +inventory layout. Bedrock 1.26.60.5 preserves the cursor and moves the +selected stack into main inventory. Retain the real menu's quick-move +transport while bypassing only Java's incompatible pointer classification. +Occupied-cursor regressions cover inventory-only and recipe-book layouts. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -670,17 +691,17 @@ following ordinary click through the exported player program. .../render/MixinNativeGuiItemState.java | 20 + .../render/MixinNativeGuiOversizedItem.java | 31 + .../container/BedrockFurnacePreferences.java | 60 + - .../BedrockNativeContainerScreen.java | 1312 +++++++++++++++ + .../BedrockNativeContainerScreen.java | 1330 ++++++++++++++++ .../container/BedrockNativeContainers.java | 282 ++++ .../container/BedrockNativeCooking.java | 147 ++ - .../BedrockNativePlayerInventory.java | 157 ++ + .../BedrockNativePlayerInventory.java | 163 ++ .../container/BedrockPlayerInventory.java | 92 ++ .../NativeContainerActionPayload.java | 30 + .../NativeContainerCurrentPayload.java | 30 + .../container/NativeContainerDataPayload.java | 30 + .../container/NativeContainerDraws.java | 36 + .../NativeContainerFamiliesPayload.java | 30 + - .../container/NativeContainerGeometry.java | 272 ++++ + .../container/NativeContainerGeometry.java | 273 ++++ .../container/NativeContainerInput.java | 509 ++++++ .../container/NativeContainerPayload.java | 22 + .../container/NativeContainerPending.java | 65 + @@ -707,7 +728,7 @@ following ordinary click through the exported player program. .../NativeInventoryPresentation.java | 36 + .../container/NativeInventoryProgress.java | 70 + .../container/NativePlayerHelpText.java | 64 + - .../NativePlayerInventoryActionPayload.java | 22 + + .../NativePlayerInventoryActionPayload.java | 23 + .../NativePlayerInventoryPayload.java | 22 + .../NativePlayerInventoryResultPayload.java | 22 + .../container/NativePlayerPresentation.java | 195 +++ @@ -736,9 +757,9 @@ following ordinary click through the exported player program. .../NativeInventoryProgressTest.java | 83 + .../container/NativePlayerHelpTextTest.java | 34 + .../NativePlayerPresentationTest.java | 153 ++ - .../container/NativePlayerScreenTest.java | 347 ++++ + .../container/NativePlayerScreenTest.java | 507 ++++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 80 files changed, 10529 insertions(+), 62 deletions(-) + 80 files changed, 10715 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 @@ -1280,10 +1301,10 @@ index 0000000..1583ce0 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java new file mode 100644 -index 0000000..b54fa19 +index 0000000..5deb3b1 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java -@@ -0,0 +1,1312 @@ +@@ -0,0 +1,1330 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -1356,6 +1377,7 @@ index 0000000..b54fa19 + "button.container_take_all_place_all", new SlotInput(ContainerInput.PICKUP, 0), + "button.container_take_half_place_one", new SlotInput(ContainerInput.PICKUP, 1), + "button.container_auto_place", new SlotInput(ContainerInput.QUICK_MOVE, 0), ++ "button.container_auto_destroy", new SlotInput(ContainerInput.QUICK_MOVE, 0), + "button.crafting_output_primary", new SlotInput(ContainerInput.PICKUP, 0), + "button.crafting_output_secondary", new SlotInput(ContainerInput.PICKUP, 1), + "button.crafting_output_tertiary", new SlotInput(ContainerInput.QUICK_MOVE, 0), @@ -2121,7 +2143,12 @@ index 0000000..b54fa19 + && node.properties().get("inventory_slot").getAsInt() == index + && NativeUiInput.parse(node.properties()).mappings().stream() + .anyMatch(mapping -> mapping.target() == NativeUiInput.symbol("button.container_auto_destroy")))) { -+ this.fallback(new IllegalArgumentException("Native Creative hotbar destruction is not supported")); ++ if (!BedrockNativePlayerInventory.autoDestroyAvailable(this.owner, this.menu)) ++ this.fallback(new IllegalArgumentException("Native Creative hotbar destruction is unavailable")); ++ else { ++ BedrockNativePlayerInventory.action(this.owner, this.menu, index, NativePlayerInventoryAction.Kind.AUTO_DESTROY); ++ this.refreshSafely(); ++ } + return; + } + super.slotClicked(slot, index, button, input); @@ -2163,7 +2190,7 @@ index 0000000..b54fa19 + } + if (this.search != null) this.search.setFocused(false); + final String componentSource = this.presentation instanceof NativePlayerPresentation && event.hasShiftDown() -+ && this.geometry.recipe(event.x(), event.y()) != null ++ && (this.geometry.recipe(event.x(), event.y()) != null || this.geometry.hovered(event.x(), event.y()) >= 0) + && (event.button() == InputConstants.MOUSE_BUTTON_LEFT || event.button() == InputConstants.MOUSE_BUTTON_RIGHT) + ? "button.menu_auto_place" : source(event.button()); + this.pointerSources.put(event.button(), componentSource); @@ -2191,6 +2218,18 @@ index 0000000..b54fa19 + this.nativeButtons.add(event.button()); + return true; + } ++ if (componentEvent != null && this.presentation instanceof NativePlayerPresentation) { ++ for (final var control : componentEvent.events()) { ++ if (control.button() == null || !control.button().transition()) continue; ++ final var slotInput = playerSlotInput(control.button().target()); ++ if (slotInput != null && slotInput.input() == ContainerInput.QUICK_MOVE ++ && control.node().properties().has("inventory_slot") ++ && this.playerSlotAction(control.node().properties().get("inventory_slot").getAsInt(), control.button().target())) { ++ this.nativeButtons.add(event.button()); ++ return true; ++ } ++ } ++ } + if (this.presentation instanceof NativePlayerPresentation player && this.geometry.help(event.x(), event.y(), input)) { + player.openHelp(); + this.nativeButtons.add(event.button()); @@ -3039,10 +3078,10 @@ index 0000000..3e78d4d +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativePlayerInventory.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativePlayerInventory.java new file mode 100644 -index 0000000..251a3d1 +index 0000000..96d2187 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativePlayerInventory.java -@@ -0,0 +1,157 @@ +@@ -0,0 +1,163 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3085,6 +3124,7 @@ index 0000000..251a3d1 + PayloadTypeRegistry.clientboundPlay().register(NativePlayerInventoryPayload.TYPE, NativePlayerInventoryPayload.CODEC); + PayloadTypeRegistry.clientboundPlay().register(NativePlayerInventoryResultPayload.TYPE, NativePlayerInventoryResultPayload.CODEC); + PayloadTypeRegistry.serverboundPlay().register(NativePlayerInventoryActionPayload.TYPE, NativePlayerInventoryActionPayload.CODEC); ++ PayloadTypeRegistry.serverboundPlay().register(NativePlayerInventoryActionPayload.AUTO_DESTROY_TYPE, NativePlayerInventoryActionPayload.CODEC); + ClientPlayConnectionEvents.INIT.register((listener, client) -> { + clear(); + owner = listener; @@ -3174,9 +3214,14 @@ index 0000000..251a3d1 + } + } + -+ static boolean action(final ClientPacketListener listener, final AbstractContainerMenu expected, final int cell, final NativePlayerInventoryAction.Kind kind) { ++ static boolean autoDestroyAvailable(final ClientPacketListener listener, final AbstractContainerMenu expected) { ++ return current(listener, expected) && presentation != null && ClientPlayNetworking.canSend(NativePlayerInventoryActionPayload.AUTO_DESTROY_TYPE); ++ } ++ ++ static boolean action(final ClientPacketListener listener, final AbstractContainerMenu expected, final int target, final NativePlayerInventoryAction.Kind kind) { + return current(listener, expected) && presentation != null -+ && presentation.send(kind, cell, "", null, BedrockNativePlayerInventory::send); ++ && (kind != NativePlayerInventoryAction.Kind.AUTO_DESTROY || ClientPlayNetworking.canSend(NativePlayerInventoryActionPayload.AUTO_DESTROY_TYPE)) ++ && presentation.send(kind, target, "", null, BedrockNativePlayerInventory::send); + } + + static boolean preferences(final ClientPacketListener listener, final AbstractContainerMenu expected, final InventoryOptions options) { @@ -3486,10 +3531,10 @@ index 0000000..3387649 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometry.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometry.java new file mode 100644 -index 0000000..3c762e5 +index 0000000..d88f727 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeContainerGeometry.java -@@ -0,0 +1,272 @@ +@@ -0,0 +1,273 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3513,7 +3558,7 @@ index 0000000..3c762e5 + +/** Authored, clipped controls share one front-to-back policy for slot and command input. */ +final class NativeContainerGeometry { -+ static final Set PLAYER_FALLBACK_ACTIONS = Set.of("button.container_reset_held", "button.destroy_selection", "button.container_auto_destroy"); ++ static final Set PLAYER_FALLBACK_ACTIONS = Set.of("button.container_reset_held", "button.destroy_selection"); + static final Set RECIPE_AUXILIARY_ACTIONS = Set.of("button.drop_one", "button.drop_all", "button.shape_drawing"); + private static final Set RECIPE_MAPPING_FIELDS = Set.of("from_button_id", "to_button_id", "mapping_type", "ignored", "consume_event"); + private final List controls; @@ -3664,6 +3709,7 @@ index 0000000..3c762e5 + if (recipe != null) supported.add("button.container_slot_hovered"); + if (recipe != null && player) supported.addAll(RECIPE_AUXILIARY_ACTIONS); + if (player) supported.addAll(PLAYER_FALLBACK_ACTIONS); ++ if (player && node.properties().has("inventory_slot")) supported.add("button.container_auto_destroy"); + if (player) supported.addAll(Set.of("button.help", "button.search_bar_clear", "button.left_panel_tab_increment", "button.left_panel_tab_decrement")); + if (node.properties().has("inventory_actions")) { + for (final var action : node.properties().getAsJsonArray("inventory_actions")) @@ -6026,10 +6072,10 @@ index 0000000..d1209c8 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerInventoryActionPayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerInventoryActionPayload.java new file mode 100644 -index 0000000..a383413 +index 0000000..b1eb6f2 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerInventoryActionPayload.java -@@ -0,0 +1,22 @@ +@@ -0,0 +1,23 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -6045,12 +6091,13 @@ index 0000000..a383413 + +public record NativePlayerInventoryActionPayload(NativePlayerInventoryAction action) implements CustomPacketPayload { + public static final Type TYPE = new Type<>(Identifier.parse(NativePlayerInventoryAction.CHANNEL)); ++ public static final Type AUTO_DESTROY_TYPE = new Type<>(Identifier.parse(NativePlayerInventoryAction.AUTO_DESTROY_CHANNEL)); + public static final StreamCodec CODEC = CustomPacketPayload.codec( + (payload, buffer) -> NativePlayerInventoryAction.TYPE.write(buffer, payload.action), + buffer -> new NativePlayerInventoryActionPayload(NativePlayerInventoryAction.TYPE.read(buffer))); + + @Override -+ public Type type() { return TYPE; } ++ public Type type() { return this.action.kind() == NativePlayerInventoryAction.Kind.AUTO_DESTROY ? AUTO_DESTROY_TYPE : TYPE; } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerInventoryPayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerInventoryPayload.java new file mode 100644 @@ -6110,7 +6157,7 @@ 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..f83d636 +index 0000000..c64ae5c --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerPresentation.java @@ -0,0 +1,195 @@ @@ -6249,12 +6296,12 @@ index 0000000..f83d636 + if (!query.equals(this.state.query())) this.send(NativePlayerInventoryAction.Kind.SEARCH, -1, query, null, sender); + } + -+ boolean send(final NativePlayerInventoryAction.Kind kind, final int cell, final String query, ++ boolean send(final NativePlayerInventoryAction.Kind kind, final int target, final String query, + final InventoryOptions options, final Consumer sender) { + if (!this.idle()) return false; + final var message = this.message; + final var action = new NativePlayerInventoryAction(message.menuId(), message.opening(), message.generation(), message.revision(), -+ this.sequence = Math.incrementExact(this.sequence), kind, cell, query, options); ++ this.sequence = Math.incrementExact(this.sequence), kind, target, query, options); + this.pending = action; + this.sentAt = this.clock.getAsLong(); + sender.accept(action); @@ -11489,10 +11536,10 @@ index 0000000..d3902fa +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java new file mode 100644 -index 0000000..7804780 +index 0000000..400b180 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java -@@ -0,0 +1,347 @@ +@@ -0,0 +1,507 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -11517,6 +11564,7 @@ index 0000000..7804780 +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Predicate; ++import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Options; @@ -11536,6 +11584,7 @@ index 0000000..7804780 +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.EntityEquipment; +import net.minecraft.world.entity.player.Inventory; ++import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.inventory.ContainerInput; +import net.minecraft.world.item.ItemStack; @@ -11622,6 +11671,164 @@ index 0000000..7804780 + } + } + ++ @ParameterizedTest ++ @MethodSource("catalogButtons") ++ void shiftHotbarClicksDestroyWithoutDispatchingJavaClicksAndReleaseTheirOriginalInput(final int button) throws Exception { ++ final var game = mock(Minecraft.class); ++ final var player = mock(LocalPlayer.class); ++ final var inventory = new Inventory(player, new EntityEquipment()); ++ final var menu = new InventoryMenu(inventory, true, player); ++ final var owner = mock(ClientPacketListener.class); ++ final var ordinary = mock(AbstractContainerScreen.class); ++ final var options = mock(Options.class); ++ final var font = mock(Font.class); ++ final var clock = new AtomicLong(); ++ set(Minecraft.class, game, "gameMode", mock(MultiPlayerGameMode.class)); ++ when(ordinary.getMenu()).thenReturn(menu); ++ when(owner.getConnection()).thenReturn(mock(Connection.class)); ++ for (final String name : List.of("keyPickItem", "keyDrop", "keyInventory")) ++ set(Options.class, options, name, mock(KeyMapping.class)); ++ set(Minecraft.class, game, "options", options); ++ set(Minecraft.class, game, "gui", mock(Gui.class)); ++ try (final var singleton = mockStatic(Minecraft.class); ++ final var network = mockStatic(BedrockNativePlayerInventory.class)) { ++ singleton.when(Minecraft::getInstance).thenReturn(game); ++ network.when(() -> BedrockNativePlayerInventory.autoDestroyAvailable(owner, menu)).thenReturn(true); ++ network.when(() -> BedrockNativePlayerInventory.action(any(), any(), anyInt(), any())).thenReturn(true, false); ++ final var ui = new PlayerInventoryUi(new ResourcePackStorage(List.of()).getJsonUiProgram()); ++ final var cell = new PlayerInventoryUi.Cell(0, 10, -1, 0, 0, 1, false, false, "", Map.of()); ++ final var state = new PlayerInventoryUi.State(false, true, 3, 2, 5, 2, false, "", "All", "Crafting", ++ List.of(1, 0, 0, 0), List.of(cell), "", -1); ++ final var source = new NativePlayerInventoryMessage(0, 1, 8, 1, ui.exportProgram(false).toString(), ++ GsonUtil.getGson().toJson(state), Map.of(0, new StructuredItem(320, 1, new StructuredDataContainer()))); ++ final var presentation = new NativePlayerPresentation(source, Map.of(0, new ItemStack(Items.IRON_INGOT)), clock::get); ++ final var screen = new BedrockNativeContainerScreen(owner, ordinary, inventory, Component.empty(), presentation, ++ false, 1, clock::get, () -> true); ++ set(Screen.class, screen, "minecraft", game); ++ set(Screen.class, screen, "font", font); ++ try { ++ screen.prepare(font, 640, 360); ++ clock.set(1_000_000_000); ++ screen.prepare(font, 640, 360); ++ menu.getSlot(36).set(new ItemStack(Items.IRON_INGOT, 64)); ++ final var held = new ItemStack(Items.IRON_INGOT, 7); ++ menu.setCarried(held); ++ final var target = pointer(screen, draw -> draw.box().node().properties().has("inventory_slot") ++ && draw.box().node().properties().get("inventory_slot").getAsInt() == 36); ++ final var pressed = new MouseButtonEvent(target.x(), target.y(), new MouseButtonInfo(button, InputConstants.MOD_SHIFT)); ++ final var released = new MouseButtonEvent(target.x(), target.y(), new MouseButtonInfo(button, 0)); ++ for (int click = 0; click < 2; click++) { ++ assertTrue(screen.mouseClicked(pressed, false)); ++ assertTrue(screen.mouseReleased(released)); ++ } ++ network.verify(() -> BedrockNativePlayerInventory.action(owner, menu, 36, NativePlayerInventoryAction.Kind.AUTO_DESTROY), times(2)); ++ assertEquals(64, menu.getSlot(36).getItem().getCount()); ++ assertSame(held, menu.getCarried()); ++ verifyNoInteractions(game.gameMode); ++ } finally { screen.discard(); } ++ } ++ } ++ ++ @ParameterizedTest ++ @ValueSource(ints = {1, 2}) ++ void inventoryLayoutsQuickMoveWithAnOccupiedCursorInsteadOfSwappingStacks(final int layout) throws Exception { ++ final var game = mock(Minecraft.class); ++ final var player = mock(LocalPlayer.class); ++ final var inventory = new Inventory(player, new EntityEquipment()); ++ final var menu = new InventoryMenu(inventory, true, player); ++ final var owner = mock(ClientPacketListener.class); ++ final var ordinary = mock(AbstractContainerScreen.class); ++ final var options = mock(Options.class); ++ final var font = mock(Font.class); ++ final var clock = new AtomicLong(); ++ set(Minecraft.class, game, "gameMode", mock(MultiPlayerGameMode.class)); ++ when(ordinary.getMenu()).thenReturn(menu); ++ when(owner.getConnection()).thenReturn(mock(Connection.class)); ++ for (final String name : List.of("keyPickItem", "keyDrop", "keyInventory")) ++ set(Options.class, options, name, mock(KeyMapping.class)); ++ set(Minecraft.class, game, "options", options); ++ set(Minecraft.class, game, "gui", mock(Gui.class)); ++ try (final var singleton = mockStatic(Minecraft.class); ++ final var network = mockStatic(BedrockNativePlayerInventory.class)) { ++ singleton.when(Minecraft::getInstance).thenReturn(game); ++ final var ui = new PlayerInventoryUi(new ResourcePackStorage(List.of()).getJsonUiProgram()); ++ final var cell = new PlayerInventoryUi.Cell(0, 10, -1, 0, 0, 1, false, false, "", Map.of()); ++ final var state = new PlayerInventoryUi.State(false, true, layout, 2, 5, 2, false, "", "All", "Crafting", ++ List.of(1, 0, 0, 0), List.of(cell), "", -1); ++ final var source = new NativePlayerInventoryMessage(0, 1, 8, 1, ui.exportProgram(false).toString(), ++ GsonUtil.getGson().toJson(state), Map.of(0, new StructuredItem(320, 1, new StructuredDataContainer()))); ++ final var presentation = new NativePlayerPresentation(source, Map.of(0, new ItemStack(Items.IRON_INGOT)), clock::get); ++ final var screen = new BedrockNativeContainerScreen(owner, ordinary, inventory, Component.empty(), presentation, ++ false, 1, clock::get, () -> true); ++ set(Screen.class, screen, "minecraft", game); ++ set(Screen.class, screen, "font", font); ++ try { ++ screen.prepare(font, 640, 360); ++ clock.set(1_000_000_000); ++ screen.prepare(font, 640, 360); ++ menu.getSlot(36).set(new ItemStack(Items.IRON_INGOT, 64)); ++ final var held = new ItemStack(Items.IRON_INGOT, 7); ++ menu.setCarried(held); ++ final var target = pointer(screen, draw -> draw.box().node().properties().has("inventory_slot") ++ && draw.box().node().properties().get("inventory_slot").getAsInt() == 36); ++ final int button = InputConstants.MOUSE_BUTTON_LEFT; ++ final var pressed = new MouseButtonEvent(target.x(), target.y(), new MouseButtonInfo(button, InputConstants.MOD_SHIFT)); ++ final var released = new MouseButtonEvent(target.x(), target.y(), new MouseButtonInfo(button, 0)); ++ for (int click = 0; click < 2; click++) { ++ assertTrue(screen.mouseClicked(pressed, false)); ++ assertTrue(screen.mouseReleased(released)); ++ } ++ network.verifyNoInteractions(); ++ assertEquals(64, menu.getSlot(36).getItem().getCount()); ++ assertSame(held, menu.getCarried()); ++ verify(game.gameMode, times(2)).handleContainerInput(menu.containerId, 36, 0, ContainerInput.QUICK_MOVE, null); ++ verifyNoMoreInteractions(game.gameMode); ++ } finally { screen.discard(); } ++ } ++ } ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void hotbarDestroyRequiresItsBackendCapabilityBeforeBecomingPending(final boolean available) throws Exception { ++ final var game = mock(Minecraft.class); ++ final var player = mock(LocalPlayer.class); ++ final var inventory = new Inventory(player, new EntityEquipment()); ++ final var menu = new InventoryMenu(inventory, true, player); ++ final var owner = mock(ClientPacketListener.class); ++ final var connection = mock(Connection.class); ++ when(owner.getConnection()).thenReturn(connection); ++ when(connection.isConnected()).thenReturn(true); ++ when(game.getConnection()).thenReturn(owner); ++ game.player = player; ++ set(Player.class, player, "inventoryMenu", menu); ++ final var ui = new PlayerInventoryUi(new ResourcePackStorage(List.of()).getJsonUiProgram()); ++ final var state = new PlayerInventoryUi.State(false, true, 3, 2, 5, 2, false, "", "All", "Crafting", ++ List.of(0, 0, 0, 0), List.of(), "", -1); ++ final var message = new NativePlayerInventoryMessage(0, 1, 8, 1, ui.exportProgram(false).toString(), ++ GsonUtil.getGson().toJson(state), Map.of()); ++ final var presentation = new NativePlayerPresentation(message, Map.of(), () -> 0L); ++ set(BedrockNativePlayerInventory.class, null, "owner", owner); ++ set(BedrockNativePlayerInventory.class, null, "menu", menu); ++ set(BedrockNativePlayerInventory.class, null, "presentation", presentation); ++ try (final var singleton = mockStatic(Minecraft.class); ++ final var screens = mockStatic(BedrockPlayerInventory.class); ++ final var network = mockStatic(ClientPlayNetworking.class)) { ++ singleton.when(Minecraft::getInstance).thenReturn(game); ++ screens.when(() -> BedrockPlayerInventory.opening(owner, menu)).thenReturn(1L); ++ network.when(() -> ClientPlayNetworking.canSend(NativePlayerInventoryActionPayload.TYPE)).thenReturn(true); ++ network.when(() -> ClientPlayNetworking.canSend(NativePlayerInventoryActionPayload.AUTO_DESTROY_TYPE)).thenReturn(available); ++ assertEquals(available, BedrockNativePlayerInventory.autoDestroyAvailable(owner, menu)); ++ assertEquals(available, BedrockNativePlayerInventory.action(owner, menu, 36, NativePlayerInventoryAction.Kind.AUTO_DESTROY)); ++ assertEquals(!available, presentation.idle()); ++ network.verify(() -> ClientPlayNetworking.send(argThat(payload -> payload instanceof NativePlayerInventoryActionPayload input ++ && input.type() == NativePlayerInventoryActionPayload.AUTO_DESTROY_TYPE && input.action().target() == 36)), times(available ? 1 : 0)); ++ } finally { ++ set(BedrockNativePlayerInventory.class, null, "presentation", null); ++ set(BedrockNativePlayerInventory.class, null, "menu", null); ++ set(BedrockNativePlayerInventory.class, null, "owner", null); ++ } ++ } ++ + @Test + void defaultHelpReturnsToTheSameInventoryAndCursorWithoutDispatchingContainerInput() throws Exception { + final var game = mock(Minecraft.class); 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 6d403fc1..0d4ee519 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 @@ -53,7 +53,16 @@ Fixed recipe grids expand visible cells and retained keyboard neighbors over the Local window changes preserve the menu opening and authoritative revision. Screen regressions use 2,000 retained items in the recipe-book and Creative layouts. They cover distant scrolling, resize, original action IDs, and keyboard navigation. -Creative hotbar destruction restores ordinary controls before item changes or requests. +The Creative catalog-only toolbar binds the authored `container_auto_destroy` command to a separate optional backend capability. +Shift-left and Shift-right preserve the cursor and discard the selected stack. +Inventory-only and recipe-book layouts dispatch the authored quick-move command directly, including with an occupied cursor. +This avoids Java pointer handling that swaps the held and selected stacks. +Screen regressions cover both layouts and preserve the cursor. +Backends without that capability restore ordinary controls before item changes or requests. +The reference is official Bedrock 1.26.60.5, protocol 2225, and its matching inventory UI declarations. +Installed direct and ViaProxy controls each complete seven server-accepted requests for hotbar destruction and inventory-only transfer. +Both routes preserve the occupied cursor, leave empty slots unchanged, and retain the resulting inventory after reopening without Creative replacement requests. +The installed add-ons and proxy match the rebuilt artifact hashes. Passive hover notifications do not trigger the fallback for unsupported recipe actions. Installed direct and ViaProxy controls retain all 1,980 catalog entries from a local protocol-2225 server. Physical category and layout changes preserve their native indices through exported programs.