From 8e1ef8cf8403298b95eec2cc2a47c5b5c2cfe840 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:54:49 +0100 Subject: [PATCH 01/47] docs(inventory): record native bulk crafting constraints --- docs/native-player-inventory.md | 7 +++++++ ...-render-native-block-state-geometry-and-materials.pr.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 9acba120..8f06eab3 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -109,6 +109,13 @@ A normal recipe click fills the grid. Taking its output sends a separate craft r A workbench recipe Shift-click uses `CraftRecipeAuto` with explicit ingredient descriptors, followed by results, consumption, and placement actions. The request codec supports this format. Installed authored recipe actions remain unfinished. +Native 1.26.60.5 plank controls convert 16 of 32 logs into 64 planks per Shift-click. +Output merges existing stacks and can use a slot freed by consumption. +An unrelated held apple stays unchanged while output fits inventory. +When inventory is full, an empty cursor receives the output. +Held logs increase displayed availability, but bulk crafting leaves them unchanged. +Full occupied cursors and nonstackable outputs still need native packet controls. + Admit an authored player screen only when its required collections and controls are available. The shared UI renderer supports the local player portrait and separate body/head rotations. Portrait rendering alone does not establish a complete native inventory. 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 7a0cea7f..bf7a1794 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,7 +21,7 @@ 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. -Upstream ViaBedrock #472 now supplies the NBT asset and loads its registry once. +Upstream [ViaBedrock #472](https://github.com/ViaVersionAddons/ViaBedrock/pull/472) now supplies the NBT asset and loads its registry once. The palette filter reads that mapping registry and retains the native registration checks. The registration names now come directly from the unmodified [CloudburstMC/Data NBT snapshot](https://github.com/CloudburstMC/Data/blob/0021592b1a4253528bad1e1d8b537e306e097557/data_driven_blocks.nbt). From e70d34243b581a37f961d15f807d61bfda2c7a83 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:40:12 +0100 Subject: [PATCH 02/47] test(inventory): preserve native Survival bulk request fixtures Keep complete protocol-2225 request fixtures from accepted official-client controls for cursor overflow, nonstackable output, and split placement. These protect wildcard descriptors, chained network IDs, and action order before the Survival bulk planner is implemented. Update the inventory reference with the verified native constraints while keeping installed Survival bulk support explicitly unresolved. --- docs/native-player-inventory.md | 7 +- ...ne-bedrock-request-and-recipe-codecs.patch | 117 +++++++++++++++++- 2 files changed, 118 insertions(+), 6 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 8f06eab3..27efd4e6 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -114,7 +114,12 @@ Output merges existing stacks and can use a slot freed by consumption. An unrelated held apple stays unchanged while output fits inventory. When inventory is full, an empty cursor receives the output. Held logs increase displayed availability, but bulk crafting leaves them unchanged. -Full occupied cursors and nonstackable outputs still need native packet controls. +With full inventory, a full compatible cursor or an incompatible held stack prevents the craft request. +A partially filled compatible cursor instead drops its held stack and receives the full crafted stack. +Captured requests cover cursors with 60 and 63 planks; both produce 64 new planks. +Native wooden-sword bulk crafting produces one sword despite ingredients for 32. +Complete request fixtures preserve split placement, repeated source references, and cursor replacement. +The matching server accepts these requests. These fixtures do not implement the Survival bulk planner. Admit an authored player screen only when its required collections and controls are available. The shared UI renderer supports the local player portrait and separate body/head rotations. diff --git a/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.patch b/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.patch index 688919f7..7e0ab1ad 100644 --- a/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.patch +++ b/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.patch @@ -1,4 +1,4 @@ -From 9e9493dc95f6cf49535576b905d4f8294cee2dff Mon Sep 17 00:00:00 2001 +From 3d993f6d45704cffd586c22db30319b8abb70b7c Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:08:16 +0100 Subject: [PATCH] feat(inventory): define Bedrock request and recipe codecs @@ -60,6 +60,16 @@ A byte-for-byte native action fixture protects the ingredient boundary. Schema vectors cover the remaining descriptor variants and width limits. The codec enables native bulk requests; it does not publish a player screen or establish complete recipe-action behavior. + +Preserve complete accepted Survival bulk request fixtures from official +Bedrock 1.26.60.5 against BDS 1.26.60.6, protocol 2225. A full inventory +with 60 held planks consumes 16 logs, drops the held stack, then takes +64 created planks onto the cursor. Split output merges one plank before +placing seven into the consumed log slot. Wooden-sword bulk crafts once +and references the request ID after its first consumption from a shared +source slot. Keep wildcard ingredient metadata and created-output IDs +exactly as captured. These codec fixtures do not implement bulk planning +or establish complete installed recipe-action parity. --- .../inventory/ItemStackRequestAction.java | 149 +++++++++++ .../model/inventory/ItemStackRequestInfo.java | 28 +++ @@ -89,13 +99,13 @@ or establish complete recipe-action behavior. .../item/NetworkItemStackDescriptorType.java | 2 +- .../types/recipe/CraftingDataReader.java | 175 +++++++++++++ .../recipe/NetworkItemDescriptorType.java | 101 ++++++++ - .../inventory/ItemStackAutoCraftTypeTest.java | 91 +++++++ + .../inventory/ItemStackAutoCraftTypeTest.java | 188 ++++++++++++++ .../types/inventory/ItemStackCodecTest.java | 232 ++++++++++++++++++ .../ItemStackRequestResultTypeTest.java | 55 +++++ .../types/item/NetworkItemCountTest.java | 87 +++++++ .../types/recipe/CraftingDataReaderTest.java | 85 +++++++ .../recipe/NetworkItemDescriptorTypeTest.java | 144 +++++++++++ - 34 files changed, 2439 insertions(+), 5 deletions(-) + 34 files changed, 2536 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestInfo.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestSlotInfo.java @@ -2072,10 +2082,10 @@ index 00000000..a485cb82 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackAutoCraftTypeTest.java b/src/test/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackAutoCraftTypeTest.java new file mode 100644 -index 00000000..75faa496 +index 00000000..a7f7ad20 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackAutoCraftTypeTest.java -@@ -0,0 +1,91 @@ +@@ -0,0 +1,188 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -2089,8 +2099,15 @@ index 00000000..75faa496 + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.TextProcessingEventOrigin; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.FullContainerName; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; +import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptor; ++import net.raphimc.viabedrock.protocol.types.BedrockTypes; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; @@ -2104,6 +2121,96 @@ index 00000000..75faa496 +class ItemStackAutoCraftTypeTest { + + @Test ++ void bulkOverflowPreservesNativeDropBeforeCursorReplacement() { ++ // Official 1.26.60.5 -> BDS 1.26.60.6, protocol 2225, accepted request -7. ++ // Full inventory, 32 logs, and 60 held planks: drop the held stack, then take 64. ++ final var result = new BedrockItem(42, (short) 0, (byte) 4); ++ result.setBlockRuntimeId(1921718966); ++ final var request = new ItemStackRequestInfo(-7, List.of( ++ new ItemStackRequestAction.CraftRecipeAutoAction(3587, 16, ++ List.of(new ItemDescriptor.ItemNameDescriptor("minecraft:oak_log", 32767))), ++ new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(result), 16), ++ new ItemStackRequestAction.ConsumeAction(16, ++ slot(ContainerEnumName.CombinedHotbarAndInventoryContainer, 0, 9)), ++ new ItemStackRequestAction.DropAction(60, slot(ContainerEnumName.CursorContainer, 0, 7), false), ++ new ItemStackRequestAction.TakeAction(64, ++ slot(ContainerEnumName.CreatedOutputContainer, 50, -7), ++ slot(ContainerEnumName.CursorContainer, 0, -7))), List.of(), TextProcessingEventOrigin.unknown); ++ assertNativePacket(request, "minecraft:oak_planks", """ ++ 010d050b0d831c10010101116d696e6563726166743a6f616b5f6c6f67feff0301001214010101146d696e6563726166743a6f ++ 616b5f706c616e6b73000400b6b5ac94070a00000000000000000000100505100c00000900000003033c3b0000070000000000 ++ 00403c0032f9ffffff3b0000f9ffffff00ffffffff ++ """); ++ } ++ ++ @Test ++ void nonstackableBulkOutputPreservesRepeatedIngredientConsumption() { ++ // Accepted native request -15 crafts one sword despite ingredients for 32. ++ final var oak = new ItemDescriptor.ItemNameDescriptor("minecraft:oak_planks", 0); ++ final var request = new ItemStackRequestInfo(-15, List.of( ++ new ItemStackRequestAction.CraftRecipeAutoAction(4603, 1, ++ List.of(oak, oak, new ItemDescriptor.ItemNameDescriptor("minecraft:stick", 32767))), ++ new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(new BedrockItem(42)), 1), ++ new ItemStackRequestAction.ConsumeAction(1, ++ slot(ContainerEnumName.CombinedHotbarAndInventoryContainer, 0, 157)), ++ new ItemStackRequestAction.ConsumeAction(1, ++ slot(ContainerEnumName.CombinedHotbarAndInventoryContainer, 0, -15)), ++ new ItemStackRequestAction.ConsumeAction(1, ++ slot(ContainerEnumName.CombinedHotbarAndInventoryContainer, 1, 159)), ++ new ItemStackRequestAction.PlaceAction(1, ++ slot(ContainerEnumName.CreatedOutputContainer, 50, -15), ++ slot(ContainerEnumName.CombinedHotbarAndInventoryContainer, 2, 0))), ++ List.of(), TextProcessingEventOrigin.unknown); ++ assertNativePacket(request, "minecraft:wooden_sword", """ ++ 011d060b0dfb2301030101146d696e6563726166743a6f616b5f706c616e6b730001000101146d696e6563726166743a6f616b ++ 5f706c616e6b7300010001010f6d696e6563726166743a737469636bfeff0301001214010101166d696e6563726166743a776f6f ++ 64656e5f73776f7264000100000a00000000000000000000010505010c00009d0000000505010c0000f1ffffff0505010c0001 ++ 9f0000000101013c0032f1ffffff0c00020000000000ffffffff ++ """); ++ } ++ ++ @Test ++ void splitBulkPlacementPreservesMergeBeforeReusingConsumedSlot() { ++ // Accepted native request -23 turns two logs into eight planks beside an existing 63. ++ final var result = new BedrockItem(42, (short) 0, (byte) 4); ++ result.setBlockRuntimeId(1921718966); ++ final var created = slot(ContainerEnumName.CreatedOutputContainer, 50, -23); ++ final var request = new ItemStackRequestInfo(-23, List.of( ++ new ItemStackRequestAction.CraftRecipeAutoAction(3587, 2, ++ List.of(new ItemDescriptor.ItemNameDescriptor("minecraft:oak_log", 32767))), ++ new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(result), 2), ++ new ItemStackRequestAction.ConsumeAction(2, ++ slot(ContainerEnumName.CombinedHotbarAndInventoryContainer, 0, 381)), ++ new ItemStackRequestAction.PlaceAction(1, created, ++ slot(ContainerEnumName.CombinedHotbarAndInventoryContainer, 1, 383)), ++ new ItemStackRequestAction.PlaceAction(7, created, ++ slot(ContainerEnumName.CombinedHotbarAndInventoryContainer, 0, -23))), ++ List.of(), TextProcessingEventOrigin.unknown); ++ assertNativePacket(request, "minecraft:oak_planks", """ ++ 012d050b0d831c02010101116d696e6563726166743a6f616b5f6c6f67feff0301001214010101146d696e6563726166743a6f ++ 616b5f706c616e6b73000400b6b5ac94070a00000000000000000000020505020c00007d0100000101013c0032e9ffffff0c00 ++ 017f0100000101073c0032e9ffffff0c0000e9ffffff00ffffffff ++ """); ++ } ++ ++ private static ItemStackRequestSlotInfo slot(final ContainerEnumName container, final int slot, final int networkId) { ++ return new ItemStackRequestSlotInfo(new FullContainerName(container, null), (byte) slot, networkId); ++ } ++ ++ private static void assertNativePacket(final ItemStackRequestInfo request, final String resultName, final String nativePayload) { ++ final ByteBuf buffer = Unpooled.buffer(); ++ try { ++ BedrockTypes.UNSIGNED_VAR_INT.write(buffer, 1); ++ new ItemStackRequestType(new ItemStackRequestResultType(id -> id == 42 ? resultName : null, 0)).write(buffer, request); ++ final byte[] actual = new byte[buffer.readableBytes()]; ++ buffer.readBytes(actual); ++ assertArrayEquals(HexFormat.of().parseHex(nativePayload.replaceAll("\\s", "")), actual); ++ } finally { ++ buffer.release(); ++ } ++ } ++ ++ @Test + void workbenchBulkCraftMatchesNative2225IngredientDescriptors() { + // Official 1.26.60.5: Shift-click Chest with 30 oak and two birch planks. + // The selected candidate crafts three chests with eight named oak ingredients. From a91983c88eec8abbb97615e94363a5ad338d82ba Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:27:31 +0100 Subject: [PATCH 03/47] feat(connection): add a deferred server blacklist bypass Keep the ban-risk warning and require an explicit Join anyway action for each blocked server connection. Carry consent only into that connection; leave the global config and unapproved connections unchanged. Place this StackAnvil-only policy override in the deferred add-on stack as requested. Validate cancellation and retry in the installed client, and scope checks against the transformed core gate. The connection-button control uses loopback DNS and does not join The Hive. --- ...w-confirmed-blacklisted-server-joins.patch | 324 ++++++++++++++++++ ...w-confirmed-blacklisted-server-joins.pr.md | 15 + patches/viafabricplus-bedrock/series.json | 7 +- 3 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch create mode 100644 patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.pr.md diff --git a/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch b/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch new file mode 100644 index 00000000..e4a36155 --- /dev/null +++ b/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch @@ -0,0 +1,324 @@ +From c7a3824fe290f9f382329811a744c1d42bdc7cde Mon Sep 17 00:00:00 2001 +From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> +Date: Sun, 1 Nov 2026 14:06:07 +0100 +Subject: [PATCH] feat(connection): allow confirmed blacklisted server joins + +Keep ViaBedrock's server-ban warning visible and add an explicit Join +anyway action for StackAnvil users. Show the warning before connection +setup, including HTTP NetherNet targets, and retain Cancel as a normal +return to the previous screen. + +Scope confirmation to the selected ServerAddress during synchronous +ConnectScreen setup, then carry it on that screen's own worker and +UserConnection. Never change disable-server-blacklist globally or retain +approval for a reconnect, another address, or another thread. Keep the +core blacklist check active for all connections without explicit consent. + +This is a StackAnvil-only opt-in, not a claim that listed servers now +support translated clients. The upstream blacklist and its ban-risk +warning remain authoritative. Unit tests cover attempt identity, thread +isolation, failure cleanup, and nested setup. +--- + .../injection/access/IConnectScreen.java | 9 +++ + .../core/connection/MixinConnectScreen.java | 67 +++++++++++++++++++ + .../core/connection/MixinConnectScreen_1.java | 10 +++ + .../core/connection/MixinLoginPackets.java | 24 +++++++ + .../network/ServerBlacklistBypass.java | 29 ++++++++ + .../viafabricplus-bedrock/lang/en_us.json | 3 + + .../viafabricplus-bedrock.mixins.json | 2 + + .../network/ServerBlacklistBypassTest.java | 55 +++++++++++++++ + 8 files changed, 199 insertions(+) + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IConnectScreen.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinLoginPackets.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/ServerBlacklistBypass.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/ServerBlacklistBypassTest.java + +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IConnectScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IConnectScreen.java +new file mode 100644 +index 0000000..8c0d8bd +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IConnectScreen.java +@@ -0,0 +1,9 @@ ++/* ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.injection.access; ++ ++public interface IConnectScreen { ++ boolean viaFabricPlusBedrock$hasBlacklistConsent(); ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen.java +new file mode 100644 +index 0000000..f7578dc +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen.java +@@ -0,0 +1,67 @@ ++/* ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.injection.mixin.core.connection; ++ ++import com.viaversion.viafabricplus.ViaFabricPlus; ++import com.viaversion.viafabricplus.bedrock.injection.access.IConnectScreen; ++import com.viaversion.viafabricplus.bedrock.injection.access.IServerAddress; ++import com.viaversion.viafabricplus.bedrock.protocoltranslator.network.NetherNetHttpAddress; ++import com.viaversion.viafabricplus.bedrock.protocoltranslator.network.ServerBlacklistBypass; ++import com.viaversion.viafabricplus.injection.access.core.IServerData; ++import net.minecraft.client.Minecraft; ++import net.minecraft.client.gui.screens.ConfirmScreen; ++import net.minecraft.client.gui.screens.ConnectScreen; ++import net.minecraft.client.gui.screens.Screen; ++import net.minecraft.client.multiplayer.ServerData; ++import net.minecraft.client.multiplayer.TransferState; ++import net.minecraft.client.multiplayer.resolver.ServerAddress; ++import net.minecraft.network.chat.CommonComponents; ++import net.minecraft.network.chat.Component; ++import net.raphimc.viabedrock.api.BedrockProtocolVersion; ++import net.raphimc.viabedrock.api.util.ServerBlacklist; ++import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.Unique; ++import org.spongepowered.asm.mixin.injection.At; ++import org.spongepowered.asm.mixin.injection.Inject; ++import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; ++ ++@Mixin(ConnectScreen.class) ++public abstract class MixinConnectScreen implements IConnectScreen { ++ @Unique ++ private boolean viaFabricPlusBedrock$blacklistConsent; ++ ++ @Inject(method = "startConnecting", at = @At("HEAD"), cancellable = true) ++ private static void warnBeforeBlacklistedConnection(final Screen parent, final Minecraft minecraft, ++ final ServerAddress address, final ServerData server, final boolean quickPlay, ++ final TransferState transfer, final CallbackInfo ci) { ++ final var forced = server == null ? null : ((IServerData) server).viaFabricPlus$forcedVersion(); ++ final var version = forced != null ? forced : ViaFabricPlus.api().targetVersion(); ++ if (!BedrockProtocolVersion.BEDROCK_LATEST.equals(version) || ServerBlacklistBypass.isConfirmed(address)) return; ++ ++ final var transport = ((IServerAddress) (Object) address).viaFabricPlusBedrock$getNetherNetAddress(); ++ final String hostname = transport instanceof NetherNetHttpAddress http ? http.host() : address.getHost(); ++ if (!ServerBlacklist.isBlacklisted(hostname)) return; ++ ++ minecraft.gui.setScreen(new ConfirmScreen(confirmed -> { ++ minecraft.gui.setScreen(parent); ++ if (confirmed) ServerBlacklistBypass.connectOnce(address, ++ () -> ConnectScreen.startConnecting(parent, minecraft, address, server, quickPlay, transfer)); ++ }, Component.translatable("bedrock_server_warning.viafabricplus.title"), ++ Component.translatable("bedrock_server_warning.viafabricplus.message", hostname), ++ Component.translatable("bedrock_server_warning.viafabricplus.join_anyway"), CommonComponents.GUI_CANCEL)); ++ ci.cancel(); ++ } ++ ++ @Inject(method = "connect", at = @At("HEAD")) ++ private void retainWarningConsent(final Minecraft minecraft, final ServerAddress address, final ServerData server, ++ final TransferState transfer, final CallbackInfo ci) { ++ this.viaFabricPlusBedrock$blacklistConsent = ServerBlacklistBypass.isConfirmed(address); ++ } ++ ++ @Override ++ public boolean viaFabricPlusBedrock$hasBlacklistConsent() { ++ return this.viaFabricPlusBedrock$blacklistConsent; ++ } ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java +index 6561291..d6967f2 100644 +--- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java +@@ -27,18 +27,23 @@ import com.llamalad7.mixinextras.sugar.Local; + import com.viaversion.viafabricplus.ViaFabricPlus; + import com.viaversion.viafabricplus.bedrock.ViaFabricPlusBedrock; + import com.viaversion.viafabricplus.bedrock.injection.access.IEventLoopGroupHolder; ++import com.viaversion.viafabricplus.bedrock.injection.access.IConnectScreen; ++import com.viaversion.viafabricplus.bedrock.protocoltranslator.network.ServerBlacklistBypass; + import com.viaversion.viafabricplus.injection.access.core.IConnection; + import com.viaversion.viaversion.api.connection.UserConnection; + import java.io.IOException; + import java.security.KeyPair; + import java.util.UUID; + import net.minecraft.network.Connection; ++import net.minecraft.client.gui.screens.ConnectScreen; + import net.minecraft.server.network.EventLoopGroupHolder; + import net.raphimc.minecraftauth.bedrock.BedrockAuthManager; + import net.raphimc.minecraftauth.bedrock.model.MinecraftMultiplayerToken; + import net.raphimc.viabedrock.api.BedrockProtocolVersion; + import net.raphimc.viabedrock.protocol.storage.AuthData; + import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.Final; ++import org.spongepowered.asm.mixin.Shadow; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + import org.spongepowered.asm.mixin.injection.Redirect; +@@ -46,6 +51,8 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + + @Mixin(targets = "net.minecraft.client.gui.screens.ConnectScreen$1") + public abstract class MixinConnectScreen_1 { ++ @Shadow @Final ++ private ConnectScreen this$0; + + @WrapOperation(method = "run", at = @At(value = "INVOKE", target = "Ljava/lang/Exception;getMessage()Ljava/lang/String;", remap = false)) + private String handleNullExceptionMessage(final Exception instance, final Operation original) { +@@ -67,6 +74,9 @@ public abstract class MixinConnectScreen_1 { + } + + final UserConnection connection = ((IConnection) clientConnection).viaFabricPlus$getUserConnection(); ++ if (((IConnectScreen) this.this$0).viaFabricPlusBedrock$hasBlacklistConsent()) { ++ connection.put(new ServerBlacklistBypass()); ++ } + if (connection.has(AuthData.class)) { + return; // The identity used by NetherNet signaling is already stored for ViaBedrock login. + } +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinLoginPackets.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinLoginPackets.java +new file mode 100644 +index 0000000..9e543f2 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinLoginPackets.java +@@ -0,0 +1,24 @@ ++/* ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.injection.mixin.core.connection; ++ ++import com.llamalad7.mixinextras.injector.wrapoperation.Operation; ++import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; ++import com.llamalad7.mixinextras.sugar.Local; ++import com.viaversion.viafabricplus.bedrock.protocoltranslator.network.ServerBlacklistBypass; ++import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; ++import net.raphimc.viabedrock.protocol.packet.LoginPackets; ++import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.injection.At; ++ ++@Mixin(value = LoginPackets.class, remap = false) ++public abstract class MixinLoginPackets { ++ @WrapOperation(method = "*", at = @At(value = "INVOKE", ++ target = "Lnet/raphimc/viabedrock/api/util/ServerBlacklist;isBlacklisted(Ljava/lang/String;)Z")) ++ private static boolean requireConsentForBlacklistedServer(final String hostname, final Operation original, ++ @Local(argsOnly = true) final PacketWrapper wrapper) { ++ return original.call(hostname) && !wrapper.user().has(ServerBlacklistBypass.class); ++ } ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/ServerBlacklistBypass.java b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/ServerBlacklistBypass.java +new file mode 100644 +index 0000000..3d3ff32 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/ServerBlacklistBypass.java +@@ -0,0 +1,29 @@ ++/* ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.protocoltranslator.network; ++ ++import com.viaversion.viaversion.api.connection.StorableObject; ++import net.minecraft.client.multiplayer.resolver.ServerAddress; ++ ++/** Carries explicit warning consent into one newly created connection. */ ++public final class ServerBlacklistBypass implements StorableObject { ++ private static final ThreadLocal CONFIRMED_ADDRESS = new ThreadLocal<>(); ++ ++ public static boolean isConfirmed(final ServerAddress address) { ++ return address != null && CONFIRMED_ADDRESS.get() == address; ++ } ++ ++ /** ConnectScreen creates its worker synchronously; the worker retains its own consent flag. */ ++ public static void connectOnce(final ServerAddress address, final Runnable connect) { ++ final ServerAddress previous = CONFIRMED_ADDRESS.get(); ++ CONFIRMED_ADDRESS.set(address); ++ try { ++ connect.run(); ++ } finally { ++ if (previous == null) CONFIRMED_ADDRESS.remove(); ++ else CONFIRMED_ADDRESS.set(previous); ++ } ++ } ++} +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 f345d7c..40e7ae9 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,7 @@ + { ++ "bedrock_server_warning.viafabricplus.title": "Server compatibility warning", ++ "bedrock_server_warning.viafabricplus.message": "%s is blocked by ViaBedrock because its anti-cheat is known to ban translated clients. StackAnvil improvements do not guarantee that this server accepts your client. Join only if you accept the risk of an account ban.", ++ "bedrock_server_warning.viafabricplus.join_anyway": "Join anyway", + "command_block.viafabricplus.tick_delay": "Delay in ticks", + "command_block.viafabricplus.first_tick": "Execute on first tick", + "base.viafabricplus.accept": "Accept", +diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json +index ff5a82c..86671a8 100644 +--- a/src/main/resources/viafabricplus-bedrock.mixins.json ++++ b/src/main/resources/viafabricplus-bedrock.mixins.json +@@ -48,6 +48,8 @@ + "core.access.MixinEventLoopGroupHolder", + "core.access.MixinServerAddress", + "core.connection.MixinConnectScreen_1", ++ "core.connection.MixinConnectScreen", ++ "core.connection.MixinLoginPackets", + "core.connection.MixinConnection", + "core.connection.MixinConnection_1", + "core.connection.MixinNetherNetDiscovery", +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/ServerBlacklistBypassTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/ServerBlacklistBypassTest.java +new file mode 100644 +index 0000000..c2c91d2 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/ServerBlacklistBypassTest.java +@@ -0,0 +1,55 @@ ++/* ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package com.viaversion.viafabricplus.bedrock.protocoltranslator.network; ++ ++import net.minecraft.client.multiplayer.resolver.ServerAddress; ++import org.junit.jupiter.api.Test; ++ ++import java.util.concurrent.CompletableFuture; ++ ++import static org.junit.jupiter.api.Assertions.assertFalse; ++import static org.junit.jupiter.api.Assertions.assertThrows; ++import static org.junit.jupiter.api.Assertions.assertTrue; ++ ++class ServerBlacklistBypassTest { ++ @Test ++ void approvalAppliesOnlyToTheConfirmedAttemptDuringSynchronousConnectionSetup() { ++ final var selected = new ServerAddress("geo.hivebedrock.network", 19132); ++ final var reconnect = new ServerAddress("geo.hivebedrock.network", 19132); ++ assertFalse(ServerBlacklistBypass.isConfirmed(selected)); ++ ServerBlacklistBypass.connectOnce(selected, () -> { ++ assertTrue(ServerBlacklistBypass.isConfirmed(selected)); ++ assertFalse(ServerBlacklistBypass.isConfirmed(reconnect)); ++ assertFalse(CompletableFuture.supplyAsync(() -> ServerBlacklistBypass.isConfirmed(selected)).join()); ++ }); ++ assertFalse(ServerBlacklistBypass.isConfirmed(selected)); ++ assertFalse(ServerBlacklistBypass.isConfirmed(reconnect)); ++ } ++ ++ @Test ++ void failedConnectionSetupCannotLeaveApprovalForALaterAttempt() { ++ final var selected = new ServerAddress("geo.hivebedrock.network", 19132); ++ assertThrows(IllegalStateException.class, () -> ServerBlacklistBypass.connectOnce(selected, () -> { ++ throw new IllegalStateException("Connection setup failed"); ++ })); ++ assertFalse(ServerBlacklistBypass.isConfirmed(selected)); ++ } ++ ++ @Test ++ void nestedConnectionSetupRestoresTheOuterAttemptWithoutApprovingAnotherAddress() { ++ final var first = new ServerAddress("geo.hivebedrock.network", 19132); ++ final var second = new ServerAddress("donutsmp.net", 19132); ++ ServerBlacklistBypass.connectOnce(first, () -> { ++ ServerBlacklistBypass.connectOnce(second, () -> { ++ assertTrue(ServerBlacklistBypass.isConfirmed(second)); ++ assertFalse(ServerBlacklistBypass.isConfirmed(first)); ++ }); ++ assertTrue(ServerBlacklistBypass.isConfirmed(first)); ++ assertFalse(ServerBlacklistBypass.isConfirmed(second)); ++ }); ++ assertFalse(ServerBlacklistBypass.isConfirmed(first)); ++ assertFalse(ServerBlacklistBypass.isConfirmed(second)); ++ } ++} +-- +2.56.0 diff --git a/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.pr.md b/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.pr.md new file mode 100644 index 00000000..c439f362 --- /dev/null +++ b/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.pr.md @@ -0,0 +1,15 @@ +# Confirm a blocked server connection + +Keep the server warning and offer **Join anyway** beside **Cancel**. +Confirmation applies to one connection attempt. It does not change the global ViaBedrock blacklist config. +Reconnecting shows the warning again. + +This is a StackAnvil-only override, placed in the deferred stack at the user's request. +It does not establish that listed servers accept translated clients. +The warning retains the account-ban risk. + +Three tests cover attempt identity, thread isolation, failed setup, and nested setup. +The full dependency build passes 3,242 tests, with 325 skipped. +Installed checks cover warning display, cancellation, reopening, and proceeding to a connection attempt. +An isolated JVM resolves the test hostname to loopback. No live Hive join occurs. +Runtime checks verify that the core mixin admits an approved connection and keeps an unrelated connection blocked. diff --git a/patches/viafabricplus-bedrock/series.json b/patches/viafabricplus-bedrock/series.json index 399257ed..93feb9e8 100644 --- a/patches/viafabricplus-bedrock/series.json +++ b/patches/viafabricplus-bedrock/series.json @@ -341,5 +341,10 @@ ] } ], - "deferred": [] + "deferred": [ + { + "file": "0001-allow-confirmed-blacklisted-server-joins.patch", + "reason": "StackAnvil-only connection override explicitly requested by the user. Keep the upstream blacklist and ban-risk warning; offer per-attempt consent without changing the global config. This local policy change is not intended for the upstream PR stack." + } + ] } From fe62b8acd2da30cedbe450bd5bf77099a36d14fc Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:39:22 +0100 Subject: [PATCH 04/47] feat(inventory): plan native Survival bulk crafts Build catalog auto-craft requests from the first eligible ingredient allocation, capped at one output stack. Preserve concrete native descriptors, changed-slot references, split placement, cursor replacement, and prediction rollback across player and workbench grids. Official Bedrock 1.26.60.5 and protocol-2225 BDS controls establish capacity admission before consumption. Even one free inventory item-space admits a full batch and can replace an incompatible held stack with overflow. A full inventory and incompatible cursor refuse crafting before a source slot becomes free. Keep multiple outputs, ingredient remainders, and installed direct/proxy parity explicitly unfinished. Fold the implementation into its owning recipe patch. Add 13 boundary regressions and update the inventory reference. The full four-project build passes 3,259 tests with 325 skipped and no failures. --- docs/bedrock-coverage.md | 2 +- docs/native-player-inventory.md | 16 +- ...ain-bedrock-creative-catalog-entries.patch | 694 +++++++++++++++++- ...ain-bedrock-creative-catalog-entries.pr.md | 7 +- 4 files changed, 693 insertions(+), 26 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 49423645..93d3a48c 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, 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). | +| Player inventory and crafting | Opening ownership, recipe catalogs, Creative catalog actions, Survival bulk request planning, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, installed Survival bulk parity, recipe remainders, 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 27efd4e6..534a4059 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -107,7 +107,10 @@ A fallback acknowledgment must not consume a newer menu's request. Native 1.26.60.5 classic-screen controls distinguish recipe selection from crafting. A normal recipe click fills the grid. Taking its output sends a separate craft request. A workbench recipe Shift-click uses `CraftRecipeAuto` with explicit ingredient descriptors, followed by results, consumption, and placement actions. -The request codec supports this format. Installed authored recipe actions remain unfinished. +Core plans this request for single-output shaped and shapeless recipes. +The planner uses the first eligible ingredient allocation. +Requests retain concrete descriptors and use the request ID for later references to a changed slot. +Rejected requests restore the crafting grid, inventory, and cursor. Native 1.26.60.5 plank controls convert 16 of 32 logs into 64 planks per Shift-click. Output merges existing stacks and can use a slot freed by consumption. @@ -117,9 +120,16 @@ Held logs increase displayed availability, but bulk crafting leaves them unchang With full inventory, a full compatible cursor or an incompatible held stack prevents the craft request. A partially filled compatible cursor instead drops its held stack and receives the full crafted stack. Captured requests cover cursors with 60 and 63 planks; both produce 64 new planks. +With one free inventory item-space, native crafting places one plank, drops any held stack, and takes the remaining 63. +This behavior also applies to an incompatible cursor or a full compatible cursor. +The initial capacity check precedes consumption. +A full inventory and incompatible cursor prevent crafting even when consumption can free a source slot. +An empty cursor admits that same craft. Output can then use the freed source slot. Native wooden-sword bulk crafting produces one sword despite ingredients for 32. Complete request fixtures preserve split placement, repeated source references, and cursor replacement. -The matching server accepts these requests. These fixtures do not implement the Survival bulk planner. +The matching server accepts these native requests. +Planner tests cover these request and prediction boundaries, including rejection rollback. +Installed direct and ViaProxy comparisons remain unverified. Admit an authored player screen only when its required collections and controls are available. The shared UI renderer supports the local player portrait and separate body/head rotations. @@ -175,7 +185,7 @@ 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. +Multiple recipe outputs, ingredient remainders, 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 df67eb2a..ba281120 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 06e3909613c2025b9a5c5881942c649a2e04508b Mon Sep 17 00:00:00 2001 +From 6a315ebc19541295b23c8bee187f8f8b32eef044 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,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 Survival selection, full native -cache timing and exact background selection remain unfinished. +placement and reconciliation path. 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 @@ -305,42 +305,67 @@ 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. + +Plan Survival catalog bulk crafting from the first eligible native ingredient +allocation round, capped at one output stack. Exclude the carried stack from +consumption. Emit concrete CraftRecipeAuto descriptors, CraftResults, +ordered Consume and Place actions, and cursor Drop/Take overflow. Resolve +subsequent references to any mutated slot through the current request ID. +Snapshot the grid, player inventory and cursor before prediction so rejected +requests restore every affected collection. + +Official Bedrock 1.26.60.5 and BDS 1.26.60.6 protocol2225 captures establish +16logs-to64planks, one nonstackable sword, repeated source references, +split placement and cursor replacement. With one free inventory item-space, +native places one plank, drops an incompatible/full held stack and takes63. +Initial capacity is checked before consumption: a full inventory with an +incompatible cursor refuses even when consumption would free a source slot. +An empty cursor admits that same craft and output uses the freed source slot. +Targeted request/state tests preserve these boundaries and rejection rollback. + +This planner handles single-output shaped/shapeless candidates. Multiple +outputs, native ingredient remainder production and installed direct/proxy +parity remain separate validation work; do not claim complete inventory parity. --- .../model/NativePlayerInventoryAction.java | 94 +++ .../model/NativePlayerInventoryMessage.java | 271 ++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ - .../model/container/NativeRecipeCounter.java | 74 +++ - .../resourcepack/ui/PlayerInventoryUi.java | 397 ++++++++++++ + .../model/container/NativeRecipeCounter.java | 74 ++ + .../model/container/NativeRecipeCrafting.java | 238 +++++++ + .../block/CraftingTableContainer.java | 2 +- + .../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 | 213 +++++-- + .../protocol/packet/InventoryPackets.java | 213 ++++-- .../protocol/packet/MultiStatePackets.java | 22 + .../protocol/rewriter/ItemRewriter.java | 62 +- .../protocol/storage/CraftingDataTracker.java | 129 +++- .../storage/InventoryRequestTracker.java | 24 + .../protocol/storage/InventoryTracker.java | 21 + - .../storage/NativePlayerRecipeState.java | 593 ++++++++++++++++++ + .../storage/NativePlayerRecipeState.java | 649 ++++++++++++++++++ .../storage/PlayerInventoryScreenTracker.java | 40 +- - .../NativePlayerInventoryMessageTest.java | 179 ++++++ + .../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/packet/InventoryPacketsTest.java | 267 +++++-- + .../rewriter/PlayerRecipeModelTest.java | 471 +++++++++++++ ...egisteredPlayerRecipeAvailabilityTest.java | 5 +- .../RegisteredPlayerRecipeSearchTest.java | 3 +- - .../storage/CreativeInventoryActionTest.java | 262 ++++++++ + .../storage/CreativeInventoryActionTest.java | 262 +++++++ .../storage/InventoryRequestTrackerTest.java | 83 +++ - .../NativePlayerCreativeAuxiliaryTest.java | 333 ++++++++++ + .../NativePlayerCreativeAuxiliaryTest.java | 333 +++++++++ .../storage/NativePlayerCreativePickTest.java | 138 ++++ - 31 files changed, 4840 insertions(+), 170 deletions(-) + .../storage/NativeRecipeCraftingTest.java | 307 +++++++++ + 34 files changed, 5442 insertions(+), 171 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryMessage.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/NativePlayerInventoryResult.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCrafting.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUi.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeCatalog.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/CreativeInventoryModel.java @@ -355,6 +380,7 @@ with original-client request bytes. 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 + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.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 @@ -878,6 +904,263 @@ 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/model/container/NativeRecipeCrafting.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCrafting.java +new file mode 100644 +index 00000000..40fdd804 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCrafting.java +@@ -0,0 +1,238 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model.container; ++ ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.LinkedHashMap; ++import java.util.List; ++import java.util.Map; ++import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.FullContainerName; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; ++import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptor; ++import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import net.raphimc.viabedrock.protocol.storage.InventoryTracker; ++ ++/** Plans one native catalog auto-craft without mutating its authoritative sources. */ ++public final class NativeRecipeCrafting { ++ ++ private NativeRecipeCrafting() { } ++ ++ public static Plan plan(final int recipeId, final NativeRecipeCounter.Recipe recipe, ++ final List sources, final Container owner, ++ final InventoryTracker inventory, final BedrockItem result, final ItemRewriter items) { ++ final int maximum = Math.min(255, items.maxStackSize(result)); ++ if (recipeId == 0 || recipe.ingredientCount() == 0 || result.isEmpty() || result.amount() > maximum) { ++ return null; ++ } ++ final var player = inventory.getInventoryContainer(); ++ final var cursorSlot = new Slot(inventory.getHudContainer(), 0); ++ final var cursor = cursorSlot.item(); ++ final var stack = result.copy(); ++ stack.setAmount(maximum); ++ // Native 1.26.60 checks existing capacity before ingredient consumption. ++ boolean room = false; ++ for (int slot = 0; slot < player.size(); slot++) { ++ final var previous = player.getItem(slot); ++ if ((previous.isEmpty() || previous.netId() != null && items.canMerge(stack, previous) ++ && previous.amount() < maximum) && player.canPlaceItem(slot, stack, 1)) { ++ room = true; ++ break; ++ } ++ } ++ if (!room && !cursor.isEmpty() && (!items.canMerge(stack, cursor) || cursor.amount() >= maximum)) { ++ return null; ++ } ++ ++ final var counted = NativeRecipeCounter.count(recipe, sources, item -> { ++ final var key = items.recipeKey(item); ++ return key == null ? 0 : key.descriptorHash(); ++ }); ++ final var round = counted.rounds().getFirst(); ++ int crafts = maximum / result.amount(); ++ for (int index = 0; index < round.size(); index++) { ++ if (!recipe.emptyIngredient(index)) { ++ final int amount = recipe.ingredientAmount(index); ++ if (amount <= 0) { ++ return null; ++ } ++ crafts = Math.min(crafts, round.get(index).amount() / amount); ++ } ++ } ++ if (crafts <= 0) { ++ return null; ++ } ++ ++ final var changes = new LinkedHashMap(); ++ final var consumption = new ArrayList(); ++ final var descriptors = new ArrayList(); ++ for (int index = 0; index < round.size(); index++) { ++ if (recipe.emptyIngredient(index)) { ++ continue; ++ } ++ final var ingredient = round.get(index); ++ int remaining = crafts * recipe.ingredientAmount(index); ++ if (ingredient.sources().isEmpty()) { ++ return null; ++ } ++ final var first = source(ingredient.sources().getFirst(), owner, inventory).item(); ++ final var descriptor = items.recipeDescriptor(first); ++ if (descriptor == null) { ++ return null; ++ } ++ descriptors.add(new ItemDescriptor.ItemNameDescriptor(descriptor.definition().identifier(), ++ descriptor.metadata(), recipe.ingredientAmount(index))); ++ for (final var location : ingredient.sources()) { ++ if (remaining == 0) { ++ break; ++ } ++ final var slot = source(location, owner, inventory); ++ final var previous = changes.getOrDefault(slot, slot.item()); ++ final int count = Math.min(remaining, previous.amount()); ++ if (count == 0) { ++ continue; ++ } ++ if (slot.item().netId() == null || !slot.container.canDiscardItem(slot.index, count)) { ++ return null; ++ } ++ consumption.add(new Transfer(slot, count)); ++ final var next = previous.copy(); ++ next.setAmount(previous.amount() - count); ++ changes.put(slot, next.isEmpty() ? BedrockItem.empty() : next); ++ remaining -= count; ++ } ++ if (remaining != 0) { ++ return null; ++ } ++ } ++ ++ final var placements = new ArrayList(); ++ int remaining = crafts * result.amount(); ++ for (final boolean merge : new boolean[] {true, false}) { ++ for (int index = 0; index < player.size() && remaining > 0; index++) { ++ final var slot = new Slot(player, index); ++ final var previous = changes.getOrDefault(slot, slot.item()); ++ 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 || !player.canPlaceItem(index, stack, count)) { ++ continue; ++ } ++ final var next = previous.isEmpty() ? result.copy() : previous.copy(); ++ next.setAmount(previous.amount() + count); ++ changes.put(slot, next); ++ placements.add(new Transfer(slot, count)); ++ remaining -= count; ++ } ++ } ++ if (remaining > 0) { ++ if (!cursor.isEmpty() && (cursor.netId() == null ++ || !cursorSlot.container.canDiscardItem(0, cursor.amount()))) { ++ return null; ++ } ++ if (!cursorSlot.container.canPlaceItem(0, stack, remaining)) { ++ return null; ++ } ++ final var next = result.copy(); ++ next.setAmount(remaining); ++ changes.put(cursorSlot, next); ++ } ++ return new Plan(recipeId, crafts, result.copy(), List.copyOf(descriptors), List.copyOf(consumption), ++ List.copyOf(placements), cursorSlot, remaining, new LinkedHashMap<>(changes)); ++ } ++ ++ private static Slot source(final NativeRecipeCounter.Location location, final Container owner, ++ final InventoryTracker inventory) { ++ return switch (location.collection()) { ++ case CraftingInputContainer -> owner instanceof CraftingTableContainer ++ ? new Slot(owner, 32 + location.slot()) : new Slot(inventory.getHudContainer(), 28 + location.slot()); ++ case HotbarContainer -> new Slot(inventory.getInventoryContainer(), location.slot()); ++ case InventoryContainer -> new Slot(inventory.getInventoryContainer(), 9 + location.slot()); ++ default -> throw new IllegalArgumentException("Unsupported automatic craft source"); ++ }; ++ } ++ ++ private record Slot(Container container, int index) { ++ BedrockItem item() { ++ return this.container.getItem(this.index); ++ } ++ ++ ItemStackRequestSlotInfo reference(final int id) { ++ return this.container.requestSlotInfo(this.index, id); ++ } ++ } ++ ++ private record Transfer(Slot slot, int count) { } ++ ++ public static final class Plan { ++ ++ private final int recipeId; ++ private final int crafts; ++ private final BedrockItem result; ++ private final List ingredients; ++ private final List consumption; ++ private final List placements; ++ private final Slot cursor; ++ private final int cursorOutput; ++ private final Map changes; ++ ++ private Plan(final int recipeId, final int crafts, final BedrockItem result, ++ final List ingredients, final List consumption, ++ final List placements, final Slot cursor, final int cursorOutput, ++ final Map changes) { ++ this.recipeId = recipeId; ++ this.crafts = crafts; ++ this.result = result; ++ this.ingredients = ingredients; ++ this.consumption = consumption; ++ this.placements = placements; ++ this.cursor = cursor; ++ this.cursorOutput = cursorOutput; ++ this.changes = changes; ++ } ++ ++ public List actions(final int requestId) { ++ final var actions = new ArrayList(); ++ final var mutated = new HashSet(); ++ actions.add(new ItemStackRequestAction.CraftRecipeAutoAction(this.recipeId, this.crafts, this.ingredients)); ++ actions.add(new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(this.result.copy()), this.crafts)); ++ for (final var transfer : this.consumption) { ++ final var slot = transfer.slot; ++ actions.add(new ItemStackRequestAction.ConsumeAction(transfer.count, ++ slot.reference(mutated.contains(slot) ? requestId : slot.item().netId()))); ++ mutated.add(slot); ++ } ++ final var output = new ItemStackRequestSlotInfo(new FullContainerName(ContainerEnumName.CreatedOutputContainer, null), ++ (byte) 50, requestId); ++ for (final var transfer : this.placements) { ++ final var slot = transfer.slot; ++ actions.add(new ItemStackRequestAction.PlaceAction(transfer.count, output, ++ slot.reference(mutated.contains(slot) ? requestId : slot.item().isEmpty() ? 0 : slot.item().netId()))); ++ mutated.add(slot); ++ } ++ if (this.cursorOutput != 0) { ++ final var previous = this.cursor.item(); ++ if (!previous.isEmpty()) { ++ actions.add(new ItemStackRequestAction.DropAction(previous.amount(), this.cursor.reference(previous.netId()), false)); ++ } ++ actions.add(new ItemStackRequestAction.TakeAction(this.cursorOutput, output, ++ this.cursor.reference(previous.isEmpty() ? 0 : requestId))); ++ } ++ return List.copyOf(actions); ++ } ++ ++ public void apply() { ++ this.changes.forEach((slot, item) -> slot.container.setItem(slot.index, item)); ++ } ++ } ++ ++} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java b/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java +index 1b9d7c05..eac4c053 100644 +--- a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java +@@ -223,7 +223,7 @@ public final class CraftingTableContainer extends Container { + return true; + } + +- private void refreshOutput() { ++ public void refreshOutput() { + final CraftingDataStorage recipe = + this.user.get(CraftingDataTracker.class).getRecipeData(this, "crafting_table"); + this.setItem(50, recipe == null ? BedrockItem.empty() : result(recipe).copy()); diff --git a/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUi.java b/src/main/java/net/raphimc/viabedrock/api/resourcepack/ui/PlayerInventoryUi.java new file mode 100644 index 00000000..6bad8d57 @@ -2453,7 +2736,7 @@ index f0ff52af..2a48c2ad 100644 final ItemRecipeKey key = this.recipeKey(item); return key != null ? key.descriptorHash() : 0; diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java -index 1b68e72e..7db7c259 100644 +index 1b68e72e..ed519b19 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java @@ -27,14 +27,15 @@ import net.raphimc.viabedrock.api.util.Fnv1; @@ -2581,7 +2864,7 @@ index 1b68e72e..7db7c259 100644 + return this.outputCandidates(output, owner.recipeTags(this)).stream().map(this::playerRecipeCandidate).toList(); + } + -+ private NativeRecipeCounter.Candidate playerRecipeCandidate(final CraftingDataStorage data) { ++ public NativeRecipeCounter.Candidate playerRecipeCandidate(final CraftingDataStorage data) { + final List positions = this.recipeIngredients(data); + final List ingredients = switch (data.recipe()) { + case ShapedRecipe shaped -> shaped.getIngredients(); @@ -2769,10 +3052,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..99f28e02 +index 00000000..df8363e1 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java -@@ -0,0 +1,593 @@ +@@ -0,0 +1,649 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -2806,6 +3089,7 @@ index 00000000..99f28e02 +import net.raphimc.viabedrock.api.model.NativePlayerInventoryResult; +import net.raphimc.viabedrock.api.model.container.Container; +import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeCrafting; +import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; +import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; +import net.raphimc.viabedrock.api.resourcepack.ui.JsonUiProgram; @@ -2831,6 +3115,7 @@ index 00000000..99f28e02 +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; +import net.raphimc.viabedrock.protocol.model.recipe.ShapedRecipe; ++import net.raphimc.viabedrock.protocol.model.recipe.ShapelessRecipe; +import net.raphimc.viabedrock.protocol.rewriter.BlockStateRewriter; +import net.raphimc.viabedrock.protocol.rewriter.GameTypeRewriter; +import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; @@ -3065,15 +3350,15 @@ index 00000000..99f28e02 + return false; + } + final var owner = inventory.getCurrentContainer(); -+ 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.BULK) { ++ return this.placeSurvival(entry); ++ } + if (action.kind() == NativePlayerInventoryAction.Kind.SECONDARY && !state.creative()) { + return true; + } @@ -3106,6 +3391,60 @@ index 00000000..99f28e02 + && (this.context.workbench() || this.user().get(PlayerInventoryScreenTracker.class).acknowledgedContainer(this.opening) == inventory.getCurrentContainer()); + } + ++ private boolean placeSurvival(final CreativeCatalog.CollectionEntry entry) { ++ final var inventory = this.user().get(InventoryTracker.class); ++ final var owner = inventory.getCurrentContainer(); ++ final var recipes = this.user().get(CraftingDataTracker.class); ++ final var items = this.user().get(ItemRewriter.class); ++ for (final var data : NativeSort.sorted(recipes.outputCandidates(entry.item(), this.context.recipeTags(recipes)), ++ Comparator.comparingInt(candidate -> candidate.recipe().getPriority()))) { ++ final var rules = this.user().get(GameRulesStorage.class); ++ if (rules != null && Boolean.TRUE.equals(rules.getGameRule("recipesUnlock")) && !recipes.isRecipeUnlocked(data)) { ++ continue; ++ } ++ final var candidate = recipes.playerRecipeCandidate(data); ++ if (candidate.requiresCraftingTable() && !this.context.workbench()) { ++ continue; ++ } ++ final List results = switch (data.recipe()) { ++ case ShapedRecipe shaped -> shaped.getResults(); ++ case ShapelessRecipe shapeless -> shapeless.getResults(); ++ default -> List.of(); ++ }; ++ if (results.size() != 1) { ++ continue; ++ } ++ final var plan = NativeRecipeCrafting.plan(data.networkId(), candidate.recipe(), this.context.sources(false), ++ owner, inventory, results.getFirst(), items); ++ if (plan == null) { ++ continue; ++ } ++ final var requests = this.user().get(InventoryRequestTracker.class); ++ final int id = requests.nextRequestId(); ++ final var request = new ItemStackRequestInfo(id, plan.actions(id), List.of(), TextProcessingEventOrigin.unknown); ++ final var player = inventory.getInventoryContainer(); ++ requests.addRequest(new InventoryRequestStorage(request, 0, inventory.getHudContainer().copy(), ++ owner == player ? List.of(player.copy()) : List.of(owner.copy(), player.copy()))); ++ requests.setPredictingClick(true); ++ try { ++ plan.apply(); ++ if (owner instanceof CraftingTableContainer table) { ++ table.refreshOutput(); ++ } else if (owner instanceof InventoryContainer playerOwner) { ++ playerOwner.refreshCraftingOutput(); ++ } ++ } finally { ++ requests.setPredictingClick(false); ++ } ++ PacketFactory.sendBedrockInventoryRequest(this.user(), new ItemStackRequestInfo[] {request}); ++ PacketFactory.sendJavaContainerSetContent(this.user(), inventory.getHudContainer()); ++ PacketFactory.sendJavaContainerSetContent(this.user(), player); ++ PacketFactory.sendJavaContainerSetContent(this.user(), owner); ++ return true; ++ } ++ return false; ++ } ++ + /** 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); @@ -5996,5 +6335,318 @@ index 00000000..247e1993 + } + +} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.java +new file mode 100644 +index 00000000..dfe473a7 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.java +@@ -0,0 +1,307 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.storage; ++ ++import com.viaversion.nbt.tag.CompoundTag; ++import com.viaversion.viaversion.api.connection.UserConnection; ++import java.util.List; ++import net.raphimc.viabedrock.api.model.container.Container; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; ++import net.raphimc.viabedrock.api.model.container.NativeRecipeCrafting; ++import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.TextProcessingEventOrigin; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.NativeItemDefinitions; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; ++import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptor; ++import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeDescriptor; ++import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; ++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 static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class NativeRecipeCraftingTest { ++ ++ @Test ++ void craftsOneOutputStackAndPreservesAnUnrelatedCursorWhenInventoryHasRoom() { ++ final var f = new Fixture(); ++ f.set(0, item(1, 32, 10)); ++ final var held = item(3, 7, 90); ++ f.inventory.getHudContainer().setItem(0, held); ++ final var plan = f.plan(recipe(1), item(2, 4, null)); ++ final var actions = plan.actions(-3); ++ assertEquals(new ItemStackRequestAction.CraftRecipeAutoAction(3587, 16, ++ List.of(new ItemDescriptor.ItemNameDescriptor("minecraft:oak_log", 32767, 1))), actions.get(0)); ++ assertEquals(16, assertInstanceOf(ItemStackRequestAction.CraftResultsDeprecatedAction.class, actions.get(1)).timesCrafted()); ++ assertEquals(new ItemStackRequestAction.ConsumeAction(16, f.player().requestSlotInfo(0, 10)), actions.get(2)); ++ final var place = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.get(3)); ++ assertEquals(64, place.count()); ++ assertEquals(f.player().requestSlotInfo(1, 0), place.destination()); ++ f.apply(plan); ++ assertEquals(16, f.player().getItem(0).amount()); ++ assertEquals(64, f.player().getItem(1).amount()); ++ assertSame(held, f.inventory.getHudContainer().getItem(0)); ++ f.reject(); ++ assertEquals(32, f.player().getItem(0).amount()); ++ assertTrue(f.player().getItem(1).isEmpty()); ++ assertEquals(held, f.inventory.getHudContainer().getItem(0)); ++ } ++ ++ @Test ++ void mergesBeforeUsingConsumedSourceSlotAndRestoresAllChangesOnRejection() { ++ final var f = new Fixture(); ++ f.set(0, item(1, 2, 10)); ++ f.set(1, item(2, 63, 11)); ++ final var plan = f.plan(recipe(1), item(2, 4, null)); ++ final var actions = plan.actions(-3); ++ final var merge = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.get(3)); ++ final var freed = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.get(4)); ++ assertEquals(1, merge.count()); ++ assertEquals(f.player().requestSlotInfo(1, 11), merge.destination()); ++ assertEquals(7, freed.count()); ++ assertEquals(f.player().requestSlotInfo(0, -3), freed.destination()); ++ f.apply(plan); ++ assertEquals(7, f.player().getItem(0).amount()); ++ assertEquals(2, f.player().getItem(0).identifier()); ++ assertEquals(64, f.player().getItem(1).amount()); ++ f.reject(); ++ assertEquals(1, f.player().getItem(0).identifier()); ++ assertEquals(2, f.player().getItem(0).amount()); ++ assertEquals(63, f.player().getItem(1).amount()); ++ } ++ ++ @ParameterizedTest ++ @ValueSource(ints = {0, 2, 3}) ++ void partialInventoryAdmitsFullBatchAndReplacesAnyHeldStackWithOverflow(final int cursorId) { ++ final var f = new Fixture(); ++ f.fill(); ++ f.set(0, item(1, 32, 10)); ++ f.set(1, item(2, 63, 11)); ++ final var cursor = cursorId == 0 ? BedrockItem.empty() : item(cursorId, 64, 90); ++ f.inventory.getHudContainer().setItem(0, cursor); ++ final var plan = f.plan(recipe(1), item(2, 4, null)); ++ final var actions = plan.actions(-3); ++ assertEquals(16, assertInstanceOf(ItemStackRequestAction.CraftRecipeAutoAction.class, actions.getFirst()).numberOfRequestedCrafts()); ++ assertEquals(1, assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.get(3)).count()); ++ if (cursorId != 0) { ++ assertEquals(new ItemStackRequestAction.DropAction(64, f.inventory.getHudContainer().requestSlotInfo(0, 90), false), actions.get(4)); ++ } ++ final var take = assertInstanceOf(ItemStackRequestAction.TakeAction.class, actions.getLast()); ++ assertEquals(63, take.count()); ++ assertEquals(f.inventory.getHudContainer().requestSlotInfo(0, cursorId == 0 ? 0 : -3), take.destination()); ++ f.apply(plan); ++ assertEquals(16, f.player().getItem(0).amount()); ++ assertEquals(64, f.player().getItem(1).amount()); ++ assertEquals(63, f.inventory.getHudContainer().getItem(0).amount()); ++ assertEquals(2, f.inventory.getHudContainer().getItem(0).identifier()); ++ f.reject(); ++ assertEquals(cursor, f.inventory.getHudContainer().getItem(0)); ++ assertEquals(32, f.player().getItem(0).amount()); ++ } ++ ++ @Test ++ void checksCapacityBeforeConsumptionButUsesFreedSlotsAfterAnEmptyCursorAdmitsTheCraft() { ++ final var f = new Fixture(); ++ f.fill(); ++ f.set(0, item(1, 16, 10)); ++ f.inventory.getHudContainer().setItem(0, item(3, 64, 90)); ++ assertNull(f.plan(recipe(1), item(2, 4, null))); ++ f.inventory.getHudContainer().setItem(0, BedrockItem.empty()); ++ final var plan = f.plan(recipe(1), item(2, 4, null)); ++ final var place = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, plan.actions(-3).getLast()); ++ assertEquals(64, place.count()); ++ assertEquals(f.player().requestSlotInfo(0, -3), place.destination()); ++ f.apply(plan); ++ assertEquals(2, f.player().getItem(0).identifier()); ++ assertTrue(f.inventory.getHudContainer().getItem(0).isEmpty()); ++ } ++ ++ @Test ++ void capsNonstackableOutputAndUsesRequestIdForRepeatedConsumptionOfTheSameSource() { ++ final var f = new Fixture(); ++ when(f.items.maxStackSize(any())).thenReturn(1); ++ f.set(0, item(2, 64, 10)); ++ f.set(1, item(4, 32, 11)); ++ final var actions = f.plan(recipe(2, 2, 4), item(5, 1, null)).actions(-3); ++ assertEquals(1, assertInstanceOf(ItemStackRequestAction.CraftRecipeAutoAction.class, actions.getFirst()).numberOfRequestedCrafts()); ++ assertEquals(new ItemStackRequestAction.ConsumeAction(1, f.player().requestSlotInfo(0, 10)), actions.get(2)); ++ assertEquals(new ItemStackRequestAction.ConsumeAction(1, f.player().requestSlotInfo(0, -3)), actions.get(3)); ++ assertEquals(new ItemStackRequestAction.ConsumeAction(1, f.player().requestSlotInfo(1, 11)), actions.get(4)); ++ } ++ ++ @Test ++ void consumesTheGridBeforeInventoryAndNeverConsumesHeldIngredients() { ++ final var f = new Fixture(); ++ f.inventory.getHudContainer().setItem(28, item(1, 2, 9)); ++ f.set(0, item(1, 2, 10)); ++ f.inventory.getHudContainer().setItem(0, item(1, 32, 90)); ++ final var plan = f.plan(recipe(1), item(2, 4, null)); ++ final var actions = plan.actions(-3); ++ assertEquals(4, assertInstanceOf(ItemStackRequestAction.CraftRecipeAutoAction.class, actions.getFirst()).numberOfRequestedCrafts()); ++ assertEquals(new ItemStackRequestAction.ConsumeAction(2, f.inventory.getHudContainer().requestSlotInfo(28, 9)), actions.get(2)); ++ assertEquals(new ItemStackRequestAction.ConsumeAction(2, f.player().requestSlotInfo(0, 10)), actions.get(3)); ++ f.apply(plan); ++ assertTrue(f.inventory.getHudContainer().getItem(28).isEmpty()); ++ assertEquals(32, f.inventory.getHudContainer().getItem(0).amount()); ++ } ++ ++ @Test ++ void usesTheFirstDescriptorAllocationRoundInsteadOfTheCombinedDisplayedQuantity() { ++ final var f = new Fixture(); ++ f.set(0, item(2, 30, 10)); ++ f.set(1, item(6, 2, 11)); ++ final var recipe = new NativeRecipeCounter.Recipe() { ++ public int ingredientCount() { ++ return 9; ++ } ++ ++ public boolean emptyIngredient(final int index) { ++ return index == 4; ++ } ++ ++ public boolean matchesIngredient(final int index, final BedrockItem item) { ++ return index != 4 && (item.identifier() == 2 || item.identifier() == 6); ++ } ++ }; ++ final var sources = f.inventory.craftingSources(f.player(), false); ++ assertEquals(4, NativeRecipeCounter.count(recipe, sources, item -> item.identifier()).count()); ++ final var plan = f.plan(recipe, item(7, 1, null)); ++ assertEquals(3, assertInstanceOf(ItemStackRequestAction.CraftRecipeAutoAction.class, plan.actions(-3).getFirst()).numberOfRequestedCrafts()); ++ f.apply(plan); ++ assertEquals(6, f.player().getItem(0).amount()); ++ assertEquals(2, f.player().getItem(1).amount()); ++ } ++ ++ @Test ++ void mapsWorkbenchInputsToTheirRealSlotsAndRollsBackBothGridAndInventory() { ++ final var f = new Fixture(); ++ f.inventory.setCurrentContainerClosed(false); ++ final var table = new CraftingTableContainer(f.user, (byte) 5, null, null); ++ f.inventory.setCurrentContainer(table); ++ table.setItem(40, item(1, 2, 9)); ++ f.set(0, item(1, 2, 10)); ++ final var plan = NativeRecipeCrafting.plan(3587, recipe(1), f.inventory.craftingSources(table, false), ++ table, f.inventory, item(2, 4, null), f.items); ++ final var actions = plan.actions(-3); ++ assertEquals(new ItemStackRequestAction.ConsumeAction(2, table.requestSlotInfo(40, 9)), actions.get(2)); ++ final var request = new ItemStackRequestInfo(-3, actions, List.of(), TextProcessingEventOrigin.unknown); ++ f.requests.addRequest(new InventoryRequestStorage(request, 0, f.inventory.getHudContainer().copy(), List.of(table.copy(), f.player().copy()))); ++ plan.apply(); ++ f.requests.getRequest(-3).capturePrediction(f.inventory); ++ assertTrue(table.getItem(40).isEmpty()); ++ assertEquals(16, f.player().getItem(0).amount()); ++ f.reject(); ++ assertEquals(2, table.getItem(40).amount()); ++ assertEquals(9, table.getItem(40).netId()); ++ assertEquals(2, f.player().getItem(0).amount()); ++ assertEquals(1, f.player().getItem(0).identifier()); ++ } ++ ++ @ParameterizedTest ++ @ValueSource(ints = {0, 1, 2}) ++ void refusesToConsumeUnconfirmedOrLockedIngredients(final int lock) { ++ final var f = new Fixture(); ++ final var source = item(1, 32, lock == 0 ? null : 10); ++ if (lock != 0) { ++ final var tag = new CompoundTag(); ++ tag.putByte("minecraft:item_lock", (byte) lock); ++ source.setTag(tag); ++ } ++ f.set(0, source); ++ assertNull(f.plan(recipe(1), item(2, 4, null))); ++ assertSame(source, f.player().getItem(0)); ++ } ++ ++ private static NativeRecipeCounter.Recipe recipe(final int... ingredients) { ++ return new NativeRecipeCounter.Recipe() { ++ public int ingredientCount() { ++ return ingredients.length; ++ } ++ ++ public boolean emptyIngredient(final int index) { ++ return ingredients[index] == 0; ++ } ++ ++ public boolean matchesIngredient(final int index, final BedrockItem item) { ++ return item.identifier() == ingredients[index]; ++ } ++ }; ++ } ++ ++ 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 { ++ ++ final UserConnection user = mock(UserConnection.class); ++ final InventoryTracker inventory = new InventoryTracker(this.user); ++ final InventoryRequestTracker requests = new InventoryRequestTracker(this.user); ++ final ItemRewriter items = mock(ItemRewriter.class); ++ ++ 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); ++ this.requests.setPredictingClick(true); ++ this.inventory.setCurrentContainer(this.player()); ++ 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()); ++ when(this.items.recipeKey(any())).thenAnswer(call -> new ItemRecipeKey(((BedrockItem) call.getArgument(0)).identifier(), (short) 0)); ++ when(this.items.recipeDescriptor(any())).thenAnswer(call -> { ++ final int id = ((BedrockItem) call.getArgument(0)).identifier(); ++ final var definition = mock(NativeItemDefinitions.Definition.class); ++ when(definition.identifier()).thenReturn(switch (id) { ++ case 1 -> "minecraft:oak_log"; ++ case 2 -> "minecraft:oak_planks"; ++ case 4 -> "minecraft:stick"; ++ default -> "minecraft:stone"; ++ }); ++ return new ItemRecipeDescriptor(definition, (short) (id == 2 ? 0 : 32767)); ++ }); ++ } ++ ++ Container player() { ++ return this.inventory.getInventoryContainer(); ++ } ++ ++ void set(final int slot, final BedrockItem item) { ++ this.player().setItem(slot, item); ++ } ++ ++ void fill() { ++ for (int slot = 0; slot < 36; slot++) { ++ this.set(slot, item(3, 64, slot + 100)); ++ } ++ } ++ ++ NativeRecipeCrafting.Plan plan(final NativeRecipeCounter.Recipe recipe, final BedrockItem output) { ++ return NativeRecipeCrafting.plan(3587, recipe, this.inventory.craftingSources(this.player(), false), ++ this.player(), this.inventory, output, this.items); ++ } ++ ++ void apply(final NativeRecipeCrafting.Plan plan) { ++ final var request = new ItemStackRequestInfo(-3, plan.actions(-3), List.of(), TextProcessingEventOrigin.unknown); ++ this.requests.addRequest(new InventoryRequestStorage(request, 0, this.inventory.getHudContainer().copy(), List.of(this.player().copy()))); ++ plan.apply(); ++ this.requests.getRequest(-3).capturePrediction(this.inventory); ++ } ++ ++ void reject() { ++ this.requests.restoreRejectedRequest(this.requests.getRequest(-3)); ++ } ++ } ++ ++} -- 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 345cbd24..e189b28f 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 @@ -142,7 +142,12 @@ Installed direct and ViaProxy controls each complete seven server-accepted reque 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. +Survival catalog bulk actions now plan single-output shaped and shapeless requests. +The planner uses the first ingredient allocation round and caps each request at one output stack. +Native 1.26.60.5 and BDS 1.26.60.6 controls establish capacity admission before consumption, split placement, and cursor replacement. +With one free inventory item-space, native drops an incompatible or full held stack after partial placement and takes the remainder. +Request/state regressions cover descriptors, changed-slot references, grid sources, capacity, cursor preservation, and rejection rollback. +Multiple outputs, ingredient remainders, installed direct/proxy comparison, 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 0821234c15a6db5b4458afaea8ebd0a283aedc7c Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:17:16 +0100 Subject: [PATCH 05/47] fix(inventory): preserve recipe metadata in bulk requests Use retained resolved metadata for ordinary named ingredients and concrete source descriptors for tag and Molang ingredients. The official protocol2225 recipe table and requests preserve wildcard32767 for logs and sticks, while wooden-sword plank tags resolve to metadata0. Source-only construction lost that distinction even though BDS accepted the request. Fold the correction into the owning recipe patch and add a real registered recipe/item-registry regression. The full four-project build passes 3,260 tests with 325 skipped and no failures. Installed direct and ViaProxy controls each receive 11 successful responses, with equal normalized request structures. Three requests per route also match native fixtures after stack-ID normalization. Keep multiple outputs, remainders, broader recipes and live rejection open. --- docs/bedrock-coverage.md | 2 +- docs/native-player-inventory.md | 11 +- ...ain-bedrock-creative-catalog-entries.patch | 149 +++++++++++++++--- ...ain-bedrock-creative-catalog-entries.pr.md | 8 +- 4 files changed, 145 insertions(+), 25 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 93d3a48c..c30b9a44 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, Creative catalog actions, Survival bulk request planning, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, installed Survival bulk parity, recipe remainders, 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, Survival bulk actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, broader Survival recipes, ingredient remainders, 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 534a4059..a77a0344 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -109,7 +109,9 @@ A normal recipe click fills the grid. Taking its output sends a separate craft r A workbench recipe Shift-click uses `CraftRecipeAuto` with explicit ingredient descriptors, followed by results, consumption, and placement actions. Core plans this request for single-output shaped and shapeless recipes. The planner uses the first eligible ingredient allocation. -Requests retain concrete descriptors and use the request ID for later references to a changed slot. +Ordinary named ingredients retain their resolved recipe metadata, including wildcard auxiliary values. +Tag and Molang ingredients use the selected source descriptor. +Requests use the request ID for later references to a changed slot. Rejected requests restore the crafting grid, inventory, and cursor. Native 1.26.60.5 plank controls convert 16 of 32 logs into 64 planks per Shift-click. @@ -129,7 +131,12 @@ Native wooden-sword bulk crafting produces one sword despite ingredients for 32. Complete request fixtures preserve split placement, repeated source references, and cursor replacement. The matching server accepts these native requests. Planner tests cover these request and prediction boundaries, including rejection rollback. -Installed direct and ViaProxy comparisons remain unverified. +Installed direct and ViaProxy controls each complete 11 server-accepted requests. +Both routes match the captured split-placement, cursor-replacement, and freed-slot requests after stack-ID normalization. +Additional controls consume the player crafting grid and produce one wooden sword with repeated references to its plank source. +Reopening retains completed output and the returned cursor stack. +The incompatible-cursor control sends no craft request until the cursor is empty. +Rejected requests remain unit-tested. These live controls do not induce server rejection. Admit an authored player screen only when its required collections and controls are available. The shared UI renderer supports the local player portrait and separate body/head rotations. 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 ba281120..4230605f 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 6a315ebc19541295b23c8bee187f8f8b32eef044 Mon Sep 17 00:00:00 2001 +From 42aecdaf77ae0032d29fbe02bc1f94ce607bbc5a 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 @@ -326,12 +326,22 @@ Targeted request/state tests preserve these boundaries and rejection rollback. This planner handles single-output shaped/shapeless candidates. Multiple outputs, native ingredient remainder production and installed direct/proxy parity remain separate validation work; do not claim complete inventory parity. + +Build ordinary auto-craft ingredients from their retained resolved recipe +descriptors, including wildcard auxiliary32767. Resolve tag/Molang inputs +from the selected source instead. Native protocol2225 CraftingData declares +oak_log and stick as named wildcard ingredients, but wooden-sword planks +as tags. The official client emits32767 for the former and concrete0 for +planks. A direct installed control exposed source-only metadata0; BDS +accepted it, but acceptance did not establish native request parity. +A real registered-recipe/item-registry regression now preserves both paths. --- .../model/NativePlayerInventoryAction.java | 94 +++ .../model/NativePlayerInventoryMessage.java | 271 ++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ .../model/container/NativeRecipeCounter.java | 74 ++ - .../model/container/NativeRecipeCrafting.java | 238 +++++++ + .../model/container/NativeRecipeCrafting.java | 245 +++++++ + .../container/NativeRecipeDescriptor.java | 10 + .../block/CraftingTableContainer.java | 2 +- .../resourcepack/ui/PlayerInventoryUi.java | 397 +++++++++++ .../viabedrock/protocol/BedrockProtocol.java | 1 + @@ -342,7 +352,7 @@ parity remain separate validation work; do not claim complete inventory parity. .../protocol/packet/InventoryPackets.java | 213 ++++-- .../protocol/packet/MultiStatePackets.java | 22 + .../protocol/rewriter/ItemRewriter.java | 62 +- - .../protocol/storage/CraftingDataTracker.java | 129 +++- + .../protocol/storage/CraftingDataTracker.java | 134 +++- .../storage/InventoryRequestTracker.java | 24 + .../protocol/storage/InventoryTracker.java | 21 + .../storage/NativePlayerRecipeState.java | 649 ++++++++++++++++++ @@ -360,8 +370,8 @@ parity remain separate validation work; do not claim complete inventory parity. .../storage/InventoryRequestTrackerTest.java | 83 +++ .../NativePlayerCreativeAuxiliaryTest.java | 333 +++++++++ .../storage/NativePlayerCreativePickTest.java | 138 ++++ - .../storage/NativeRecipeCraftingTest.java | 307 +++++++++ - 34 files changed, 5442 insertions(+), 171 deletions(-) + .../storage/NativeRecipeCraftingTest.java | 371 ++++++++++ + 35 files changed, 5528 insertions(+), 171 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 @@ -906,10 +916,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/model/container/NativeRecipeCrafting.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCrafting.java new file mode 100644 -index 00000000..40fdd804 +index 00000000..8fae6f47 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCrafting.java -@@ -0,0 +1,238 @@ +@@ -0,0 +1,245 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -922,6 +932,7 @@ index 00000000..40fdd804 +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; ++import java.util.function.BiFunction; +import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; +import net.raphimc.viabedrock.protocol.model.BedrockItem; @@ -929,6 +940,7 @@ index 00000000..40fdd804 +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; +import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptor; ++import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeDescriptor; +import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; +import net.raphimc.viabedrock.protocol.storage.InventoryTracker; + @@ -939,7 +951,8 @@ index 00000000..40fdd804 + + public static Plan plan(final int recipeId, final NativeRecipeCounter.Recipe recipe, + final List sources, final Container owner, -+ final InventoryTracker inventory, final BedrockItem result, final ItemRewriter items) { ++ final InventoryTracker inventory, final BedrockItem result, final ItemRewriter items, ++ final BiFunction craftDescriptor) { + final int maximum = Math.min(255, items.maxStackSize(result)); + if (recipeId == 0 || recipe.ingredientCount() == 0 || result.isEmpty() || result.amount() > maximum) { + return null; @@ -995,7 +1008,11 @@ index 00000000..40fdd804 + return null; + } + final var first = source(ingredient.sources().getFirst(), owner, inventory).item(); -+ final var descriptor = items.recipeDescriptor(first); ++ final var sourceDescriptor = items.recipeDescriptor(first); ++ if (sourceDescriptor == null) { ++ return null; ++ } ++ final var descriptor = craftDescriptor.apply(index, sourceDescriptor); + if (descriptor == null) { + return null; + } @@ -1148,6 +1165,27 @@ index 00000000..40fdd804 + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptor.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptor.java +index 271d9fad..5aad3b96 100644 +--- a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptor.java ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptor.java +@@ -64,6 +64,16 @@ public sealed interface NativeRecipeDescriptor permits NativeRecipeDescriptor.Em + + boolean isEmpty(Registry registry); + ++ /** Ordinary descriptors retain recipe metadata; predicates choose a concrete source. */ ++ default ItemRecipeDescriptor craftDescriptor(final ItemRecipeDescriptor source, final Registry registry) { ++ return switch (this) { ++ case Empty _ -> null; ++ case Deferred deferred -> deferred.value(registry).craftDescriptor(source, registry); ++ case Ordinary ordinary -> new ItemRecipeDescriptor(ordinary.item, ordinary.auxiliary); ++ case ComplexAlias _, Tag _, Molang _ -> source; ++ }; ++ } ++ + /** Match the default concrete descriptor produced from a source stack after native source guards. */ + default boolean matchesSource(final BedrockItem source, final Registry registry, + final Function descriptor) { diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java b/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java index 1b9d7c05..eac4c053 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java @@ -2736,7 +2774,7 @@ index f0ff52af..2a48c2ad 100644 final ItemRecipeKey key = this.recipeKey(item); return key != null ? key.descriptorHash() : 0; diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java -index 1b68e72e..ed519b19 100644 +index 1b68e72e..2073bd20 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/CraftingDataTracker.java @@ -27,14 +27,15 @@ import net.raphimc.viabedrock.api.util.Fnv1; @@ -2815,7 +2853,7 @@ index 1b68e72e..ed519b19 100644 } private boolean requiresCraftingTable(final CraftingDataStorage data) { -@@ -185,28 +201,71 @@ public final class CraftingDataTracker extends StoredObject { +@@ -185,28 +201,76 @@ public final class CraftingDataTracker extends StoredObject { ? shaped.getWidth() >= 3 || shaped.getHeight() >= 3 : this.recipeIngredients(data).size() >= 5; } @@ -2904,10 +2942,15 @@ index 1b68e72e..ed519b19 100644 + return ingredients.get(index).amount(); + } + }); ++ } ++ ++ public ItemRecipeDescriptor craftDescriptor(final CraftingDataStorage data, final int index, ++ final ItemRecipeDescriptor source) { ++ return this.recipeIngredients(data).get(index).craftDescriptor(source, this.recipeRegistry()); } private static BedrockItem firstOutput(final Recipe recipe) { -@@ -864,6 +923,11 @@ public final class CraftingDataTracker extends StoredObject { +@@ -864,6 +928,11 @@ public final class CraftingDataTracker extends StoredObject { /** Bind both real item producers; the search preserves their native short-circuit order. */ public int availability(final BedrockItem output, final BedrockItem selectedOutput, final Container owner, final boolean filtering, final boolean includeCursor) { @@ -2919,7 +2962,7 @@ index 1b68e72e..ed519b19 100644 return this.availability(output, selectedOutput, owner, filtering, includeCursor, () -> NbtUtf8.encode(CraftingDataTracker.this.user().get(ItemRewriter.class).itemSecondaryDescription(output))); } -@@ -871,6 +935,11 @@ public final class CraftingDataTracker extends StoredObject { +@@ -871,6 +940,11 @@ public final class CraftingDataTracker extends StoredObject { /** Bind the connection's selected-definition name while leaving secondary description production lazy. */ public int availability(final BedrockItem output, final BedrockItem selectedOutput, final Container owner, final boolean filtering, final boolean includeCursor, final Supplier secondaryDescription) { @@ -2931,7 +2974,7 @@ index 1b68e72e..ed519b19 100644 return this.availability(output, selectedOutput, owner, filtering, includeCursor, () -> NbtUtf8.encode(CraftingDataTracker.this.user().get(ItemRewriter.class).itemPrimaryName(output)), secondaryDescription); } -@@ -879,6 +948,12 @@ public final class CraftingDataTracker extends StoredObject { +@@ -879,6 +953,12 @@ public final class CraftingDataTracker extends StoredObject { public int availability(final BedrockItem output, final BedrockItem selectedOutput, final Container owner, final boolean filtering, final boolean includeCursor, final Supplier primaryName, final Supplier secondaryDescription) { @@ -2944,7 +2987,7 @@ index 1b68e72e..ed519b19 100644 if (!this.instabuild && !this.unlocked(output, owner)) { final var rules = CraftingDataTracker.this.user().get(GameRulesStorage.class); if (rules != null && Boolean.TRUE.equals(rules.getGameRule("recipesUnlock")) -@@ -906,8 +981,8 @@ public final class CraftingDataTracker extends StoredObject { +@@ -906,8 +986,8 @@ public final class CraftingDataTracker extends StoredObject { return searchState == 0 || countState == 0 ? 0 : 1; } @@ -3052,7 +3095,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..df8363e1 +index 00000000..f910fdc6 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java @@ -0,0 +1,649 @@ @@ -3415,7 +3458,7 @@ index 00000000..df8363e1 + continue; + } + final var plan = NativeRecipeCrafting.plan(data.networkId(), candidate.recipe(), this.context.sources(false), -+ owner, inventory, results.getFirst(), items); ++ owner, inventory, results.getFirst(), items, (index, source) -> recipes.craftDescriptor(data, index, source)); + if (plan == null) { + continue; + } @@ -6337,10 +6380,10 @@ index 00000000..247e1993 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.java new file mode 100644 -index 00000000..dfe473a7 +index 00000000..dd813af3 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.java -@@ -0,0 +1,307 @@ +@@ -0,0 +1,371 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -6351,18 +6394,26 @@ index 00000000..dfe473a7 +import com.viaversion.nbt.tag.CompoundTag; +import com.viaversion.viaversion.api.connection.UserConnection; +import java.util.List; ++import java.util.UUID; +import net.raphimc.viabedrock.api.model.container.Container; +import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; +import net.raphimc.viabedrock.api.model.container.NativeRecipeCrafting; +import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.TextProcessingEventOrigin; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; +import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.BaseGameVersion; +import net.raphimc.viabedrock.protocol.model.NativeItemDefinitions; ++import net.raphimc.viabedrock.protocol.model.ItemEntry; ++import net.raphimc.viabedrock.protocol.model.ItemRegistry; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; +import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptor; +import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeDescriptor; +import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; ++import net.raphimc.viabedrock.protocol.model.recipe.RecipeType; ++import net.raphimc.viabedrock.protocol.model.recipe.ShapedRecipe; ++import net.raphimc.viabedrock.protocol.model.recipe.ShapelessRecipe; +import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; @@ -6374,6 +6425,62 @@ index 00000000..dfe473a7 +class NativeRecipeCraftingTest { + + @Test ++ void registeredRecipeDescriptorsKeepNamedWildcardsAndResolveTagIngredientsFromSource() throws Exception { ++ final var registry = new ItemRegistry(new ItemEntry[] { ++ new ItemEntry("minecraft:oak_log", 30001, false, ItemVersion.Legacy, null), ++ new ItemEntry("minecraft:oak_planks", 30002, false, ItemVersion.Legacy, null), ++ new ItemEntry("minecraft:stick", 30003, false, ItemVersion.Legacy, null), ++ new ItemEntry("minecraft:wooden_sword", 30004, false, ItemVersion.Legacy, null) ++ }, BaseGameVersion.parse("1.26.60")); ++ final var context = NativeRecipeOutputIndexTest.context(registry); ++ final var inventory = new InventoryTracker(context.user()); ++ final var requests = new InventoryRequestTracker(context.user()); ++ when(context.user().get(InventoryTracker.class)).thenReturn(inventory); ++ when(context.user().get(InventoryRequestTracker.class)).thenReturn(requests); ++ requests.setPredictingClick(true); ++ final var table = new CraftingTableContainer(context.user(), (byte) 5, null, null); ++ inventory.setCurrentContainer(table); ++ final var log = registry.createDefinitionItem(registry.definition("minecraft:oak_log"), 0, 32, (definition, metadata) -> null); ++ log.setNetId(10); ++ final var planks = registry.createDefinitionItem(registry.definition("minecraft:oak_planks"), 0, 4, (definition, metadata) -> null); ++ final var stick = registry.createDefinitionItem(registry.definition("minecraft:stick"), 0, 32, (definition, metadata) -> null); ++ stick.setNetId(11); ++ final var sword = registry.createDefinitionItem(registry.definition("minecraft:wooden_sword"), 0, 1, (definition, metadata) -> null); ++ doAnswer(call -> ((BedrockItem) call.getArgument(0)).identifier() == sword.identifier() ? 1 : 64) ++ .when(context.items()).maxStackSize(any()); ++ final var plankRecipe = new CraftingDataStorage(RecipeType.SHAPELESS, 3587, ++ new ShapelessRecipe("minecraft:oak_planks", new UUID(0, 1), "crafting_table", 0, ++ List.of(new ItemDescriptor.ItemNameDescriptor("minecraft:oak_log", 32767)), List.of(planks)), true); ++ final var swordRecipe = new CraftingDataStorage(RecipeType.SHAPED, 4603, ++ new ShapedRecipe("minecraft:wooden_sword", new UUID(0, 2), "crafting_table", 0, 1, 3, ++ List.of(new ItemDescriptor.ItemTagDescriptor("minecraft:planks"), ++ new ItemDescriptor.ItemTagDescriptor("minecraft:planks"), ++ new ItemDescriptor.ItemNameDescriptor("minecraft:stick", 32767)), List.of(sword), false), true); ++ context.tracker().updateCraftingDataList(List.of(plankRecipe, swordRecipe), true); ++ inventory.getInventoryContainer().setItem(0, log); ++ assertEquals(0, context.items().recipeDescriptor(log).metadata()); ++ final var plankPlan = NativeRecipeCrafting.plan(3587, context.tracker().playerRecipeCandidate(plankRecipe).recipe(), ++ inventory.craftingSources(table, false), table, inventory, planks, context.items(), ++ (index, source) -> context.tracker().craftDescriptor(plankRecipe, index, source)); ++ assertEquals(new ItemStackRequestAction.CraftRecipeAutoAction(3587, 16, ++ List.of(new ItemDescriptor.ItemNameDescriptor("minecraft:oak_log", 32767))), plankPlan.actions(-3).getFirst()); ++ final var sourcePlanks = planks.copy(); ++ sourcePlanks.setAmount(64); ++ sourcePlanks.setNetId(12); ++ inventory.getInventoryContainer().setItem(0, sourcePlanks); ++ inventory.getInventoryContainer().setItem(1, stick); ++ final var swordPlan = NativeRecipeCrafting.plan(4603, context.tracker().playerRecipeCandidate(swordRecipe).recipe(), ++ inventory.craftingSources(table, false), table, inventory, sword, context.items(), ++ (index, source) -> context.tracker().craftDescriptor(swordRecipe, index, source)); ++ assertNotNull(swordPlan, () -> "Source descriptor " + context.items().recipeDescriptor(sourcePlanks) ++ + ", tags " + registry.tags(sourcePlanks)); ++ assertEquals(new ItemStackRequestAction.CraftRecipeAutoAction(4603, 1, List.of( ++ new ItemDescriptor.ItemNameDescriptor("minecraft:oak_planks", 0), ++ new ItemDescriptor.ItemNameDescriptor("minecraft:oak_planks", 0), ++ new ItemDescriptor.ItemNameDescriptor("minecraft:stick", 32767))), swordPlan.actions(-3).getFirst()); ++ } ++ ++ @Test + void craftsOneOutputStackAndPreservesAnUnrelatedCursorWhenInventoryHasRoom() { + final var f = new Fixture(); + f.set(0, item(1, 32, 10)); @@ -6532,7 +6639,7 @@ index 00000000..dfe473a7 + table.setItem(40, item(1, 2, 9)); + f.set(0, item(1, 2, 10)); + final var plan = NativeRecipeCrafting.plan(3587, recipe(1), f.inventory.craftingSources(table, false), -+ table, f.inventory, item(2, 4, null), f.items); ++ table, f.inventory, item(2, 4, null), f.items, (index, source) -> source); + final var actions = plan.actions(-3); + assertEquals(new ItemStackRequestAction.ConsumeAction(2, table.requestSlotInfo(40, 9)), actions.get(2)); + final var request = new ItemStackRequestInfo(-3, actions, List.of(), TextProcessingEventOrigin.unknown); @@ -6632,7 +6739,7 @@ index 00000000..dfe473a7 + + NativeRecipeCrafting.Plan plan(final NativeRecipeCounter.Recipe recipe, final BedrockItem output) { + return NativeRecipeCrafting.plan(3587, recipe, this.inventory.craftingSources(this.player(), false), -+ this.player(), this.inventory, output, this.items); ++ this.player(), this.inventory, output, this.items, (index, source) -> source); + } + + void apply(final NativeRecipeCrafting.Plan plan) { 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 e189b28f..1fba35a0 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 @@ -147,7 +147,13 @@ The planner uses the first ingredient allocation round and caps each request at Native 1.26.60.5 and BDS 1.26.60.6 controls establish capacity admission before consumption, split placement, and cursor replacement. With one free inventory item-space, native drops an incompatible or full held stack after partial placement and takes the remainder. Request/state regressions cover descriptors, changed-slot references, grid sources, capacity, cursor preservation, and rejection rollback. -Multiple outputs, ingredient remainders, installed direct/proxy comparison, and complete native screen parity remain unfinished. +Ordinary named ingredients retain resolved recipe metadata. Tag and Molang ingredients use the selected source descriptor. +A real registry regression covers named wildcard logs and sticks alongside concrete tag-selected planks. +Installed direct and ViaProxy controls each complete 11 server-accepted requests with matching normalized structures across routes. +Three requests per route match the native fixtures field for field after stack-ID normalization. +Both routes also consume player grid slot 28, craft one wooden sword, and retain output after reopening. +Installed add-on and proxy hashes match the rebuilt artifacts. Both recordings and their owned cleanup finish normally. +Multiple outputs, ingredient remainders, broader recipe families, live rejection controls, 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 9903290be2309fcf0b6ec3ba1cd3d015ab5c874c Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:03:12 +0100 Subject: [PATCH 06/47] fix(inventory): return native recipe containers Read every shaped and shapeless output from CraftingData. Match native Create selection, returned-container chunks, repeated merging, and created-output drops without a Java remainder table. Preserve primary cursor overflow and rejection rollback. Native 1.26.60.5 cake and honey captures provide complete packet fixtures. The full build passes 3,265 tests; rebuilt direct and ViaProxy controls each accept six requests, including three complete native craft matches after stack-ID normalization. --- docs/bedrock-coverage.md | 2 +- docs/native-player-inventory.md | 14 +- ...ain-bedrock-creative-catalog-entries.patch | 315 ++++++++++++++---- ...ain-bedrock-creative-catalog-entries.pr.md | 11 +- ...ed-completed-break-inventory-actions.patch | 14 +- ...ne-bedrock-request-and-recipe-codecs.patch | 110 +++++- ...ne-bedrock-request-and-recipe-codecs.pr.md | 2 +- 7 files changed, 361 insertions(+), 107 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index c30b9a44..b14a2fe2 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, Creative catalog actions, Survival bulk actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, broader Survival recipes, ingredient remainders, 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, Survival bulk actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, broader Survival recipes 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 a77a0344..ff840f61 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -107,7 +107,7 @@ A fallback acknowledgment must not consume a newer menu's request. Native 1.26.60.5 classic-screen controls distinguish recipe selection from crafting. A normal recipe click fills the grid. Taking its output sends a separate craft request. A workbench recipe Shift-click uses `CraftRecipeAuto` with explicit ingredient descriptors, followed by results, consumption, and placement actions. -Core plans this request for single-output shaped and shapeless recipes. +Core plans this request for shaped and shapeless recipes from their server-supplied results. The planner uses the first eligible ingredient allocation. Ordinary named ingredients retain their resolved recipe metadata, including wildcard auxiliary values. Tag and Molang ingredients use the selected source descriptor. @@ -128,6 +128,13 @@ The initial capacity check precedes consumption. A full inventory and incompatible cursor prevent crafting even when consumption can free a source slot. An empty cursor admits that same craft. Output can then use the freed source slot. Native wooden-sword bulk crafting produces one sword despite ingredients for 32. +The primary output stack limit caps the craft count. Returned containers do not reduce it. +Recipe data includes returned buckets and bottles, so core does not need a separate remainder mapping. +For two-output recipes, native selects the secondary output before the primary output with `Create` actions. +Each created stack chunk starts another merge pass before using empty inventory slots. +Six cakes return 18 buckets as stacks of 16 and 2. +When inventory is full, returned bottles drop directly from created-output slot 50. +The primary honey blocks still use the cursor fallback. Complete request fixtures preserve split placement, repeated source references, and cursor replacement. The matching server accepts these native requests. Planner tests cover these request and prediction boundaries, including rejection rollback. @@ -136,6 +143,9 @@ Both routes match the captured split-placement, cursor-replacement, and freed-sl Additional controls consume the player crafting grid and produce one wooden sword with repeated references to its plank source. Reopening retains completed output and the returned cursor stack. The incompatible-cursor control sends no craft request until the cursor is empty. +Separate installed direct and ViaProxy controls verify bucket chunks, returned bottles using a freed source slot, and bottle drops with a full inventory. +The three craft requests per route match the native cake and honey captures after stack-ID normalization. +Authoritative responses confirm inventory and cursor quantities. Reopening retains completed output. Rejected requests remain unit-tested. These live controls do not induce server rejection. Admit an authored player screen only when its required collections and controls are available. @@ -192,7 +202,7 @@ 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. -Multiple recipe outputs, ingredient remainders, and complete installed native parity remain unfinished. +Recipe families beyond the captured cake and honey controls, recipes with more than two outputs, 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 4230605f..8bd0c900 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 42aecdaf77ae0032d29fbe02bc1f94ce607bbc5a Mon Sep 17 00:00:00 2001 +From 90aea9d9b603a5dab3e2446356d90ebd9788c300 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 @@ -323,10 +323,6 @@ incompatible cursor refuses even when consumption would free a source slot. An empty cursor admits that same craft and output uses the freed source slot. Targeted request/state tests preserve these boundaries and rejection rollback. -This planner handles single-output shaped/shapeless candidates. Multiple -outputs, native ingredient remainder production and installed direct/proxy -parity remain separate validation work; do not claim complete inventory parity. - Build ordinary auto-craft ingredients from their retained resolved recipe descriptors, including wildcard auxiliary32767. Resolve tag/Molang inputs from the selected source instead. Native protocol2225 CraftingData declares @@ -335,12 +331,32 @@ as tags. The official client emits32767 for the former and concrete0 for planks. A direct installed control exposed source-only metadata0; BDS accepted it, but acceptance did not establish native request parity. A real registered-recipe/item-registry regression now preserves both paths. + +Read all shaped/shapeless outputs from CraftingData, including returned +buckets and bottles, instead of maintaining Java remainder mappings. +Official 1.26.60.5/protocol2225 controls select secondary output1 before +primary0 with Create actions. Select each created stack chunk separately, +restart inventory merging for each chunk, and use the current request ID +for destinations changed by consumption or earlier output placement. + +Six cakes return18 buckets as stacks16+2 without reducing craft count. +With15 existing buckets, the second chunk merges into the first newly +created stack before using another freed slot. Honey-block controls show +returned bottles using the consumed source slot, while the primary output +falls back to the cursor. When all inventory slots remain occupied, drop +secondary output directly from CreatedOutput50 with randomly=false. +Preserve single-output requests without Create actions and snapshot every +predicted grid, inventory and cursor mutation for rejection rollback. + +Native capture and byte fixtures cover two-output cake and honey recipes. +Broader recipe families, more than two outputs, live rejection controls and +complete screen parity remain separate validation work. --- .../model/NativePlayerInventoryAction.java | 94 +++ .../model/NativePlayerInventoryMessage.java | 271 ++++++++ .../model/NativePlayerInventoryResult.java | 47 ++ .../model/container/NativeRecipeCounter.java | 74 ++ - .../model/container/NativeRecipeCrafting.java | 245 +++++++ + .../model/container/NativeRecipeCrafting.java | 276 ++++++++ .../container/NativeRecipeDescriptor.java | 10 + .../block/CraftingTableContainer.java | 2 +- .../resourcepack/ui/PlayerInventoryUi.java | 397 +++++++++++ @@ -370,8 +386,8 @@ A real registered-recipe/item-registry regression now preserves both paths. .../storage/InventoryRequestTrackerTest.java | 83 +++ .../NativePlayerCreativeAuxiliaryTest.java | 333 +++++++++ .../storage/NativePlayerCreativePickTest.java | 138 ++++ - .../storage/NativeRecipeCraftingTest.java | 371 ++++++++++ - 35 files changed, 5528 insertions(+), 171 deletions(-) + .../storage/NativeRecipeCraftingTest.java | 491 +++++++++++++ + 35 files changed, 5679 insertions(+), 171 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 @@ -916,10 +932,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/model/container/NativeRecipeCrafting.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCrafting.java new file mode 100644 -index 00000000..8fae6f47 +index 00000000..07562ce6 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCrafting.java -@@ -0,0 +1,245 @@ +@@ -0,0 +1,276 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -951,10 +967,19 @@ index 00000000..8fae6f47 + + public static Plan plan(final int recipeId, final NativeRecipeCounter.Recipe recipe, + final List sources, final Container owner, -+ final InventoryTracker inventory, final BedrockItem result, final ItemRewriter items, ++ final InventoryTracker inventory, final List results, final ItemRewriter items, + final BiFunction craftDescriptor) { ++ if (recipeId == 0 || recipe.ingredientCount() == 0 || results.isEmpty() || results.size() > 256) { ++ return null; ++ } ++ for (final var output : results) { ++ if (output.isEmpty() || items.maxStackSize(output) <= 0) { ++ return null; ++ } ++ } ++ final var result = results.getFirst(); + final int maximum = Math.min(255, items.maxStackSize(result)); -+ if (recipeId == 0 || recipe.ingredientCount() == 0 || result.isEmpty() || result.amount() > maximum) { ++ if (result.amount() > maximum) { + return null; + } + final var player = inventory.getInventoryContainer(); @@ -1042,41 +1067,55 @@ index 00000000..8fae6f47 + } + } + -+ final var placements = new ArrayList(); -+ int remaining = crafts * result.amount(); -+ for (final boolean merge : new boolean[] {true, false}) { -+ for (int index = 0; index < player.size() && remaining > 0; index++) { -+ final var slot = new Slot(player, index); -+ final var previous = changes.getOrDefault(slot, slot.item()); -+ if (merge ? previous.isEmpty() || previous.netId() == null || !items.canMerge(stack, previous) -+ : !previous.isEmpty()) { -+ continue; ++ final var outputs = new ArrayList(); ++ // Native 1.26.60 selects secondary results before the primary result. ++ for (int resultSlot = results.size() - 1; resultSlot >= 0; resultSlot--) { ++ final var output = results.get(resultSlot); ++ final int outputMaximum = Math.min(255, items.maxStackSize(output)); ++ final var outputStack = output.copy(); ++ outputStack.setAmount(outputMaximum); ++ int pending = crafts * output.amount(); ++ while (pending > 0) { ++ int remaining = Math.min(pending, outputMaximum); ++ pending -= remaining; ++ final var placements = new ArrayList(); ++ for (final boolean merge : new boolean[] {true, false}) { ++ for (int index = 0; index < player.size() && remaining > 0; index++) { ++ final var slot = new Slot(player, index); ++ final var previous = changes.getOrDefault(slot, slot.item()); ++ if (merge ? previous.isEmpty() || previous.netId() == null && !changes.containsKey(slot) ++ || !items.canMerge(outputStack, previous) : !previous.isEmpty()) { ++ continue; ++ } ++ final int count = Math.min(remaining, outputMaximum - previous.amount()); ++ if (count <= 0 || !player.canPlaceItem(index, outputStack, count)) { ++ continue; ++ } ++ final var next = previous.isEmpty() ? output.copy() : previous.copy(); ++ next.setAmount(previous.amount() + count); ++ changes.put(slot, next); ++ placements.add(new Transfer(slot, count)); ++ remaining -= count; ++ } + } -+ final int count = Math.min(remaining, maximum - previous.amount()); -+ if (count <= 0 || !player.canPlaceItem(index, stack, count)) { -+ continue; ++ if (resultSlot == 0 && remaining > 0) { ++ if (!cursor.isEmpty() && (cursor.netId() == null ++ || !cursorSlot.container.canDiscardItem(0, cursor.amount()))) { ++ return null; ++ } ++ if (!cursorSlot.container.canPlaceItem(0, outputStack, remaining)) { ++ return null; ++ } ++ final var next = output.copy(); ++ next.setAmount(remaining); ++ changes.put(cursorSlot, next); + } -+ final var next = previous.isEmpty() ? result.copy() : previous.copy(); -+ next.setAmount(previous.amount() + count); -+ changes.put(slot, next); -+ placements.add(new Transfer(slot, count)); -+ remaining -= count; ++ outputs.add(new Output(resultSlot, List.copyOf(placements), resultSlot == 0 ? 0 : remaining, ++ resultSlot == 0 ? remaining : 0)); + } + } -+ if (remaining > 0) { -+ if (!cursor.isEmpty() && (cursor.netId() == null -+ || !cursorSlot.container.canDiscardItem(0, cursor.amount()))) { -+ return null; -+ } -+ if (!cursorSlot.container.canPlaceItem(0, stack, remaining)) { -+ return null; -+ } -+ final var next = result.copy(); -+ next.setAmount(remaining); -+ changes.put(cursorSlot, next); -+ } -+ return new Plan(recipeId, crafts, result.copy(), List.copyOf(descriptors), List.copyOf(consumption), -+ List.copyOf(placements), cursorSlot, remaining, new LinkedHashMap<>(changes)); ++ return new Plan(recipeId, crafts, results.stream().map(BedrockItem::copy).toList(), List.copyOf(descriptors), ++ List.copyOf(consumption), List.copyOf(outputs), cursorSlot, new LinkedHashMap<>(changes)); + } + + private static Slot source(final NativeRecipeCounter.Location location, final Container owner, @@ -1102,30 +1141,30 @@ index 00000000..8fae6f47 + + private record Transfer(Slot slot, int count) { } + ++ private record Output(int resultSlot, List placements, int dropped, int cursorOutput) { } ++ + public static final class Plan { + + private final int recipeId; + private final int crafts; -+ private final BedrockItem result; ++ private final List results; + private final List ingredients; + private final List consumption; -+ private final List placements; ++ private final List outputs; + private final Slot cursor; -+ private final int cursorOutput; + private final Map changes; + -+ private Plan(final int recipeId, final int crafts, final BedrockItem result, ++ private Plan(final int recipeId, final int crafts, final List results, + final List ingredients, final List consumption, -+ final List placements, final Slot cursor, final int cursorOutput, ++ final List outputs, final Slot cursor, + final Map changes) { + this.recipeId = recipeId; + this.crafts = crafts; -+ this.result = result; ++ this.results = results; + this.ingredients = ingredients; + this.consumption = consumption; -+ this.placements = placements; ++ this.outputs = outputs; + this.cursor = cursor; -+ this.cursorOutput = cursorOutput; + this.changes = changes; + } + @@ -1133,7 +1172,7 @@ index 00000000..8fae6f47 + final var actions = new ArrayList(); + final var mutated = new HashSet(); + actions.add(new ItemStackRequestAction.CraftRecipeAutoAction(this.recipeId, this.crafts, this.ingredients)); -+ actions.add(new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(this.result.copy()), this.crafts)); ++ actions.add(new ItemStackRequestAction.CraftResultsDeprecatedAction(this.results.stream().map(BedrockItem::copy).toList(), this.crafts)); + for (final var transfer : this.consumption) { + final var slot = transfer.slot; + actions.add(new ItemStackRequestAction.ConsumeAction(transfer.count, @@ -1142,19 +1181,27 @@ index 00000000..8fae6f47 + } + final var output = new ItemStackRequestSlotInfo(new FullContainerName(ContainerEnumName.CreatedOutputContainer, null), + (byte) 50, requestId); -+ for (final var transfer : this.placements) { -+ final var slot = transfer.slot; -+ actions.add(new ItemStackRequestAction.PlaceAction(transfer.count, output, -+ slot.reference(mutated.contains(slot) ? requestId : slot.item().isEmpty() ? 0 : slot.item().netId()))); -+ mutated.add(slot); -+ } -+ if (this.cursorOutput != 0) { -+ final var previous = this.cursor.item(); -+ if (!previous.isEmpty()) { -+ actions.add(new ItemStackRequestAction.DropAction(previous.amount(), this.cursor.reference(previous.netId()), false)); ++ for (final var result : this.outputs) { ++ if (this.results.size() > 1) { ++ actions.add(new ItemStackRequestAction.CreateAction(result.resultSlot)); ++ } ++ for (final var transfer : result.placements) { ++ final var slot = transfer.slot; ++ actions.add(new ItemStackRequestAction.PlaceAction(transfer.count, output, ++ slot.reference(mutated.contains(slot) ? requestId : slot.item().isEmpty() ? 0 : slot.item().netId()))); ++ mutated.add(slot); ++ } ++ if (result.dropped != 0) { ++ actions.add(new ItemStackRequestAction.DropAction(result.dropped, output, false)); ++ } ++ if (result.cursorOutput != 0) { ++ final var previous = this.cursor.item(); ++ if (!previous.isEmpty()) { ++ actions.add(new ItemStackRequestAction.DropAction(previous.amount(), this.cursor.reference(previous.netId()), false)); ++ } ++ actions.add(new ItemStackRequestAction.TakeAction(result.cursorOutput, output, ++ this.cursor.reference(previous.isEmpty() ? 0 : requestId))); + } -+ actions.add(new ItemStackRequestAction.TakeAction(this.cursorOutput, output, -+ this.cursor.reference(previous.isEmpty() ? 0 : requestId))); + } + return List.copyOf(actions); + } @@ -3095,7 +3142,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..f910fdc6 +index 00000000..da892e2c --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java @@ -0,0 +1,649 @@ @@ -3454,11 +3501,11 @@ index 00000000..f910fdc6 + case ShapelessRecipe shapeless -> shapeless.getResults(); + default -> List.of(); + }; -+ if (results.size() != 1) { ++ if (results.isEmpty()) { + continue; + } + final var plan = NativeRecipeCrafting.plan(data.networkId(), candidate.recipe(), this.context.sources(false), -+ owner, inventory, results.getFirst(), items, (index, source) -> recipes.craftDescriptor(data, index, source)); ++ owner, inventory, results, items, (index, source) -> recipes.craftDescriptor(data, index, source)); + if (plan == null) { + continue; + } @@ -6380,10 +6427,10 @@ index 00000000..247e1993 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.java new file mode 100644 -index 00000000..dd813af3 +index 00000000..8879b595 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/NativeRecipeCraftingTest.java -@@ -0,0 +1,371 @@ +@@ -0,0 +1,491 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -6399,15 +6446,18 @@ index 00000000..dd813af3 +import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; +import net.raphimc.viabedrock.api.model.container.NativeRecipeCrafting; +import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.TextProcessingEventOrigin; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; +import net.raphimc.viabedrock.protocol.model.BedrockItem; +import net.raphimc.viabedrock.protocol.model.BaseGameVersion; ++import net.raphimc.viabedrock.protocol.model.FullContainerName; +import net.raphimc.viabedrock.protocol.model.NativeItemDefinitions; +import net.raphimc.viabedrock.protocol.model.ItemEntry; +import net.raphimc.viabedrock.protocol.model.ItemRegistry; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; +import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptor; +import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeDescriptor; +import net.raphimc.viabedrock.protocol.model.recipe.ItemRecipeKey; @@ -6460,7 +6510,7 @@ index 00000000..dd813af3 + inventory.getInventoryContainer().setItem(0, log); + assertEquals(0, context.items().recipeDescriptor(log).metadata()); + final var plankPlan = NativeRecipeCrafting.plan(3587, context.tracker().playerRecipeCandidate(plankRecipe).recipe(), -+ inventory.craftingSources(table, false), table, inventory, planks, context.items(), ++ inventory.craftingSources(table, false), table, inventory, List.of(planks), context.items(), + (index, source) -> context.tracker().craftDescriptor(plankRecipe, index, source)); + assertEquals(new ItemStackRequestAction.CraftRecipeAutoAction(3587, 16, + List.of(new ItemDescriptor.ItemNameDescriptor("minecraft:oak_log", 32767))), plankPlan.actions(-3).getFirst()); @@ -6470,7 +6520,7 @@ index 00000000..dd813af3 + inventory.getInventoryContainer().setItem(0, sourcePlanks); + inventory.getInventoryContainer().setItem(1, stick); + final var swordPlan = NativeRecipeCrafting.plan(4603, context.tracker().playerRecipeCandidate(swordRecipe).recipe(), -+ inventory.craftingSources(table, false), table, inventory, sword, context.items(), ++ inventory.craftingSources(table, false), table, inventory, List.of(sword), context.items(), + (index, source) -> context.tracker().craftDescriptor(swordRecipe, index, source)); + assertNotNull(swordPlan, () -> "Source descriptor " + context.items().recipeDescriptor(sourcePlanks) + + ", tags " + registry.tags(sourcePlanks)); @@ -6639,7 +6689,7 @@ index 00000000..dd813af3 + table.setItem(40, item(1, 2, 9)); + f.set(0, item(1, 2, 10)); + final var plan = NativeRecipeCrafting.plan(3587, recipe(1), f.inventory.craftingSources(table, false), -+ table, f.inventory, item(2, 4, null), f.items, (index, source) -> source); ++ table, f.inventory, List.of(item(2, 4, null)), f.items, (index, source) -> source); + final var actions = plan.actions(-3); + assertEquals(new ItemStackRequestAction.ConsumeAction(2, table.requestSlotInfo(40, 9)), actions.get(2)); + final var request = new ItemStackRequestInfo(-3, actions, List.of(), TextProcessingEventOrigin.unknown); @@ -6670,6 +6720,113 @@ index 00000000..dd813af3 + assertSame(source, f.player().getItem(0)); + } + ++ @Test ++ void returnsSixCakesWorthOfBucketsInChunksAndMergesAgainBeforeUsingAnotherFreedSlot() { ++ final var f = new Fixture(); ++ when(f.items.maxStackSize(any())).thenAnswer(call -> ((BedrockItem) call.getArgument(0)).identifier() == 13 ? 16 : 64); ++ for (int slot = 0; slot < 18; slot++) { ++ f.set(slot, item(8, 1, slot + 100)); ++ } ++ f.set(18, item(9, 12, 118)); ++ f.set(19, item(10, 6, 119)); ++ f.set(20, item(11, 18, 120)); ++ f.set(21, item(13, 15, 121)); ++ final var results = List.of(item(12, 1, null), item(13, 3, null)); ++ final var plan = NativeRecipeCrafting.plan(3415, recipe(8, 8, 8, 9, 10, 9, 11, 11, 11), ++ f.inventory.craftingSources(f.player(), false), f.player(), f.inventory, results, f.items, (index, source) -> source); ++ final var actions = plan.actions(-3); ++ assertEquals(6, assertInstanceOf(ItemStackRequestAction.CraftRecipeAutoAction.class, actions.getFirst()).numberOfRequestedCrafts()); ++ assertEquals(new ItemStackRequestAction.CraftResultsDeprecatedAction(results, 6), actions.get(1)); ++ final var created = createdOutput(-3); ++ assertEquals(List.of( ++ new ItemStackRequestAction.CreateAction(1), ++ new ItemStackRequestAction.PlaceAction(1, created, f.player().requestSlotInfo(21, 121)), ++ new ItemStackRequestAction.PlaceAction(15, created, f.player().requestSlotInfo(0, -3)), ++ new ItemStackRequestAction.CreateAction(1), ++ new ItemStackRequestAction.PlaceAction(1, created, f.player().requestSlotInfo(0, -3)), ++ new ItemStackRequestAction.PlaceAction(1, created, f.player().requestSlotInfo(1, -3)), ++ new ItemStackRequestAction.CreateAction(0), ++ new ItemStackRequestAction.PlaceAction(6, created, f.player().requestSlotInfo(2, -3))), ++ actions.subList(actions.size() - 8, actions.size())); ++ f.apply(plan); ++ assertEquals(item(13, 16, null), f.player().getItem(0)); ++ assertEquals(item(13, 1, null), f.player().getItem(1)); ++ assertEquals(item(12, 6, null), f.player().getItem(2)); ++ assertEquals(16, f.player().getItem(21).amount()); ++ f.reject(); ++ for (int slot = 0; slot < 18; slot++) { ++ assertEquals(item(8, 1, slot + 100), f.player().getItem(slot)); ++ } ++ assertEquals(12, f.player().getItem(18).amount()); ++ assertEquals(6, f.player().getItem(19).amount()); ++ assertEquals(18, f.player().getItem(20).amount()); ++ assertEquals(item(13, 15, 121), f.player().getItem(21)); ++ } ++ ++ @Test ++ void dropsReturnedBottlesFromCreatedOutputWhenInventoryIsFullAndTakesOnlyHoneyBlocksOnCursor() { ++ final var f = new Fixture(); ++ f.fill(); ++ f.inventory.setCurrentContainerClosed(false); ++ final var table = new CraftingTableContainer(f.user, (byte) 5, null, null); ++ f.inventory.setCurrentContainer(table); ++ for (final int slot : new int[] {32, 33, 35, 36}) { ++ table.setItem(slot, item(14, 4, slot + 100)); ++ } ++ final var plan = NativeRecipeCrafting.plan(4239, recipe(14, 14, 14, 14), ++ f.inventory.craftingSources(table, false), table, f.inventory, ++ List.of(item(15, 1, null), item(16, 4, null)), f.items, (index, source) -> source); ++ final var actions = plan.actions(-3); ++ final var created = createdOutput(-3); ++ assertEquals(List.of( ++ new ItemStackRequestAction.CreateAction(1), ++ new ItemStackRequestAction.DropAction(16, created, false), ++ new ItemStackRequestAction.CreateAction(0), ++ new ItemStackRequestAction.TakeAction(4, created, f.inventory.getHudContainer().requestSlotInfo(0, 0))), ++ actions.subList(actions.size() - 4, actions.size())); ++ f.apply(plan, table); ++ for (final int slot : new int[] {32, 33, 35, 36}) { ++ assertTrue(table.getItem(slot).isEmpty()); ++ } ++ for (int slot = 0; slot < 36; slot++) { ++ assertEquals(item(3, 64, slot + 100), f.player().getItem(slot)); ++ } ++ assertEquals(item(15, 4, null), f.inventory.getHudContainer().getItem(0)); ++ f.reject(); ++ for (final int slot : new int[] {32, 33, 35, 36}) { ++ assertEquals(item(14, 4, slot + 100), table.getItem(slot)); ++ } ++ assertTrue(f.inventory.getHudContainer().getItem(0).isEmpty()); ++ } ++ ++ @Test ++ void returnedBottlesUseTheConsumedInventorySlotBeforeHoneyBlocksOverflowOntoCursor() { ++ final var f = new Fixture(); ++ f.fill(); ++ f.set(0, item(14, 16, 10)); ++ final var plan = NativeRecipeCrafting.plan(4239, recipe(14, 14, 14, 14), ++ f.inventory.craftingSources(f.player(), false), f.player(), f.inventory, ++ List.of(item(15, 1, null), item(16, 4, null)), f.items, (index, source) -> source); ++ final var actions = plan.actions(-3); ++ final var created = createdOutput(-3); ++ assertEquals(List.of( ++ new ItemStackRequestAction.CreateAction(1), ++ new ItemStackRequestAction.PlaceAction(16, created, f.player().requestSlotInfo(0, -3)), ++ new ItemStackRequestAction.CreateAction(0), ++ new ItemStackRequestAction.TakeAction(4, created, f.inventory.getHudContainer().requestSlotInfo(0, 0))), ++ actions.subList(actions.size() - 4, actions.size())); ++ f.apply(plan); ++ assertEquals(item(16, 16, null), f.player().getItem(0)); ++ assertEquals(item(15, 4, null), f.inventory.getHudContainer().getItem(0)); ++ f.reject(); ++ assertEquals(item(14, 16, 10), f.player().getItem(0)); ++ assertTrue(f.inventory.getHudContainer().getItem(0).isEmpty()); ++ } ++ ++ private static ItemStackRequestSlotInfo createdOutput(final int id) { ++ return new ItemStackRequestSlotInfo(new FullContainerName(ContainerEnumName.CreatedOutputContainer, null), (byte) 50, id); ++ } ++ + private static NativeRecipeCounter.Recipe recipe(final int... ingredients) { + return new NativeRecipeCounter.Recipe() { + public int ingredientCount() { @@ -6717,6 +6874,11 @@ index 00000000..dd813af3 + case 1 -> "minecraft:oak_log"; + case 2 -> "minecraft:oak_planks"; + case 4 -> "minecraft:stick"; ++ case 8 -> "minecraft:milk_bucket"; ++ case 9 -> "minecraft:sugar"; ++ case 10 -> "minecraft:egg"; ++ case 11 -> "minecraft:wheat"; ++ case 14 -> "minecraft:honey_bottle"; + default -> "minecraft:stone"; + }); + return new ItemRecipeDescriptor(definition, (short) (id == 2 ? 0 : 32767)); @@ -6739,12 +6901,17 @@ index 00000000..dd813af3 + + NativeRecipeCrafting.Plan plan(final NativeRecipeCounter.Recipe recipe, final BedrockItem output) { + return NativeRecipeCrafting.plan(3587, recipe, this.inventory.craftingSources(this.player(), false), -+ this.player(), this.inventory, output, this.items, (index, source) -> source); ++ this.player(), this.inventory, List.of(output), this.items, (index, source) -> source); + } + + void apply(final NativeRecipeCrafting.Plan plan) { ++ this.apply(plan, this.player()); ++ } ++ ++ void apply(final NativeRecipeCrafting.Plan plan, final Container owner) { + final var request = new ItemStackRequestInfo(-3, plan.actions(-3), List.of(), TextProcessingEventOrigin.unknown); -+ this.requests.addRequest(new InventoryRequestStorage(request, 0, this.inventory.getHudContainer().copy(), List.of(this.player().copy()))); ++ this.requests.addRequest(new InventoryRequestStorage(request, 0, this.inventory.getHudContainer().copy(), ++ owner == this.player() ? List.of(this.player().copy()) : List.of(owner.copy(), this.player().copy()))); + plan.apply(); + this.requests.getRequest(-3).capturePrediction(this.inventory); + } 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 1fba35a0..171e2686 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 Survival recipe selection and complete installed action parity remain unfinished. +Complete installed action and screen parity remain unfinished. The Survival bulk validation below covers selected recipe boundaries. 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. @@ -142,8 +142,8 @@ Installed direct and ViaProxy controls each complete seven server-accepted reque 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. -Survival catalog bulk actions now plan single-output shaped and shapeless requests. -The planner uses the first ingredient allocation round and caps each request at one output stack. +Survival catalog bulk actions plan shaped and shapeless requests from the server recipe results, including returned containers. +The planner uses the first ingredient allocation round and caps each request at one primary output stack. Native 1.26.60.5 and BDS 1.26.60.6 controls establish capacity admission before consumption, split placement, and cursor replacement. With one free inventory item-space, native drops an incompatible or full held stack after partial placement and takes the remainder. Request/state regressions cover descriptors, changed-slot references, grid sources, capacity, cursor preservation, and rejection rollback. @@ -153,7 +153,10 @@ Installed direct and ViaProxy controls each complete 11 server-accepted requests Three requests per route match the native fixtures field for field after stack-ID normalization. Both routes also consume player grid slot 28, craft one wooden sword, and retain output after reopening. Installed add-on and proxy hashes match the rebuilt artifacts. Both recordings and their owned cleanup finish normally. -Multiple outputs, ingredient remainders, broader recipe families, live rejection controls, and complete native screen parity remain unfinished. +Native 1.26.60.5 controls cover cake and honey recipes with two outputs. Secondary outputs are selected before the primary output, split into stack-sized chunks, and merged before using empty slots for each chunk. Returned bottles drop directly from created-output slot 50 when inventory is full. Primary output keeps the cursor fallback. +Request/state regressions cover six cakes returning 18 buckets, merging across bucket chunks, bottle overflow, and rejection rollback. +Installed direct and ViaProxy remainder controls each complete six server-accepted requests. Three craft requests per route match the native cake and honey captures field for field after stack-ID normalization. Authoritative responses confirm returned-container placement and primary cursor output. Both routes retain the completed output after reopening. Installed artifact hashes match the full rebuild. +Broader recipe families, more than two outputs, live rejection controls, 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/0013-embed-completed-break-inventory-actions.patch b/patches/viabedrock/deferred/0013-embed-completed-break-inventory-actions.patch index 06564635..1db6166c 100644 --- a/patches/viabedrock/deferred/0013-embed-completed-break-inventory-actions.patch +++ b/patches/viabedrock/deferred/0013-embed-completed-break-inventory-actions.patch @@ -1,4 +1,4 @@ -From 1b9779d249571cdbc392218126da68d1ed4d64bf Mon Sep 17 00:00:00 2001 +From c33618a3cf63136dd70725871c20df246cbb4926 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(mining): reconcile completed and creative block breaks @@ -74,19 +74,19 @@ index b1e8fa34..b83bbd7e 100644 this.requestedDismount = requestedDismount; } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestAction.java b/src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestAction.java -index 93f73370..41edddc6 100644 +index ecaac963..8d0d5ad2 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestAction.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestAction.java @@ -15,7 +15,7 @@ import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptor; import java.util.List; --public sealed interface ItemStackRequestAction permits ItemStackRequestAction.TakeAction, ItemStackRequestAction.PlaceAction, ItemStackRequestAction.SwapAction, ItemStackRequestAction.DropAction, ItemStackRequestAction.DestroyAction, ItemStackRequestAction.ConsumeAction, ItemStackRequestAction.BeaconPaymentAction, ItemStackRequestAction.CraftRecipeAction, ItemStackRequestAction.CraftRecipeAutoAction, ItemStackRequestAction.CraftCreativeAction, ItemStackRequestAction.CraftRecipeOptionalAction, ItemStackRequestAction.CraftResultsDeprecatedAction, ItemStackRequestAction.CraftLoomAction { -+public sealed interface ItemStackRequestAction permits ItemStackRequestAction.TakeAction, ItemStackRequestAction.PlaceAction, ItemStackRequestAction.SwapAction, ItemStackRequestAction.DropAction, ItemStackRequestAction.DestroyAction, ItemStackRequestAction.ConsumeAction, ItemStackRequestAction.BeaconPaymentAction, ItemStackRequestAction.MineBlockAction, ItemStackRequestAction.CraftRecipeAction, ItemStackRequestAction.CraftRecipeAutoAction, ItemStackRequestAction.CraftCreativeAction, ItemStackRequestAction.CraftRecipeOptionalAction, ItemStackRequestAction.CraftResultsDeprecatedAction, ItemStackRequestAction.CraftLoomAction { +-public sealed interface ItemStackRequestAction permits ItemStackRequestAction.TakeAction, ItemStackRequestAction.PlaceAction, ItemStackRequestAction.SwapAction, ItemStackRequestAction.DropAction, ItemStackRequestAction.DestroyAction, ItemStackRequestAction.ConsumeAction, ItemStackRequestAction.CreateAction, ItemStackRequestAction.BeaconPaymentAction, ItemStackRequestAction.CraftRecipeAction, ItemStackRequestAction.CraftRecipeAutoAction, ItemStackRequestAction.CraftCreativeAction, ItemStackRequestAction.CraftRecipeOptionalAction, ItemStackRequestAction.CraftResultsDeprecatedAction, ItemStackRequestAction.CraftLoomAction { ++public sealed interface ItemStackRequestAction permits ItemStackRequestAction.TakeAction, ItemStackRequestAction.PlaceAction, ItemStackRequestAction.SwapAction, ItemStackRequestAction.DropAction, ItemStackRequestAction.DestroyAction, ItemStackRequestAction.ConsumeAction, ItemStackRequestAction.CreateAction, ItemStackRequestAction.BeaconPaymentAction, ItemStackRequestAction.MineBlockAction, ItemStackRequestAction.CraftRecipeAction, ItemStackRequestAction.CraftRecipeAutoAction, ItemStackRequestAction.CraftCreativeAction, ItemStackRequestAction.CraftRecipeOptionalAction, ItemStackRequestAction.CraftResultsDeprecatedAction, ItemStackRequestAction.CraftLoomAction { ItemStackRequestActionType getType(); -@@ -77,6 +77,14 @@ public sealed interface ItemStackRequestAction permits ItemStackRequestAction.Ta +@@ -90,6 +90,14 @@ public sealed interface ItemStackRequestAction permits ItemStackRequestAction.Ta } } @@ -501,10 +501,10 @@ index 9e2d8d72..9f149ccb 100644 private final ArrayDeque clicks = new ArrayDeque<>(); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackActionType.java b/src/main/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackActionType.java -index 17d19209..b4028f5c 100644 +index cf1bdeee..49bcea9e 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackActionType.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackActionType.java -@@ -66,6 +66,10 @@ public final class ItemStackActionType extends Type { +@@ -68,6 +68,10 @@ public final class ItemStackActionType extends Type { } else if (value instanceof ItemStackRequestAction.BeaconPaymentAction action) { BedrockTypes.VAR_INT.write(buffer, action.primaryEvent()); BedrockTypes.VAR_INT.write(buffer, action.secondaryEvent()); diff --git a/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.patch b/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.patch index 7e0ab1ad..02805eea 100644 --- a/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.patch +++ b/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.patch @@ -1,4 +1,4 @@ -From 3d993f6d45704cffd586c22db30319b8abb70b7c Mon Sep 17 00:00:00 2001 +From 85c60aa5f712170f2514e4607f041ab09acc8738 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:08:16 +0100 Subject: [PATCH] feat(inventory): define Bedrock request and recipe codecs @@ -70,9 +70,17 @@ and references the request ID after its first consumption from a shared source slot. Keep wildcard ingredient metadata and created-output IDs exactly as captured. These codec fixtures do not implement bulk planning or establish complete installed recipe-action parity. + +Encode protocol2225 Create actions with an unsigned-byte result index. +Official1.26.60.5 cake and honey controls select secondary outputs before +primary output through CreatedOutputContainer slot50. Each output stack +starts with Create, including repeated selections when returned buckets +exceed their16-item stack limit. A complete accepted honey-grid request +fixture preserves both results, Create1/Drop16 and Create0/Take4. +Keep this typed action in the codec foundation for all inventory consumers. --- - .../inventory/ItemStackRequestAction.java | 149 +++++++++++ - .../model/inventory/ItemStackRequestInfo.java | 28 +++ + .../inventory/ItemStackRequestAction.java | 162 ++++++++++++ + .../model/inventory/ItemStackRequestInfo.java | 28 ++ .../inventory/ItemStackRequestSlotInfo.java | 23 ++ .../ItemStackResponseContainerInfo.java | 25 ++ .../inventory/ItemStackResponseInfo.java | 27 ++ @@ -84,12 +92,12 @@ or establish complete installed recipe-action parity. .../protocol/model/recipe/Recipe.java | 68 +++++ .../protocol/model/recipe/RecipeType.java | 85 +++++++ .../protocol/model/recipe/ShapedRecipe.java | 71 ++++++ - .../model/recipe/ShapelessRecipe.java | 70 ++++++ + .../model/recipe/ShapelessRecipe.java | 70 +++++ .../protocol/model/recipe/SmithingRecipe.java | 78 ++++++ .../protocol/storage/CraftingDataStorage.java | 39 +++ .../protocol/types/InventoryTypes.java | 23 +- - .../types/inventory/ItemStackActionType.java | 118 +++++++++ - .../ItemStackContainerResponseType.java | 56 +++++ + .../types/inventory/ItemStackActionType.java | 120 +++++++++ + .../ItemStackContainerResponseType.java | 56 ++++ .../inventory/ItemStackRequestResultType.java | 74 ++++++ .../types/inventory/ItemStackRequestType.java | 45 ++++ .../inventory/ItemStackResponseType.java | 75 ++++++ @@ -99,13 +107,13 @@ or establish complete installed recipe-action parity. .../item/NetworkItemStackDescriptorType.java | 2 +- .../types/recipe/CraftingDataReader.java | 175 +++++++++++++ .../recipe/NetworkItemDescriptorType.java | 101 ++++++++ - .../inventory/ItemStackAutoCraftTypeTest.java | 188 ++++++++++++++ - .../types/inventory/ItemStackCodecTest.java | 232 ++++++++++++++++++ - .../ItemStackRequestResultTypeTest.java | 55 +++++ + .../inventory/ItemStackAutoCraftTypeTest.java | 239 ++++++++++++++++++ + .../types/inventory/ItemStackCodecTest.java | 232 +++++++++++++++++ + .../ItemStackRequestResultTypeTest.java | 55 ++++ .../types/item/NetworkItemCountTest.java | 87 +++++++ .../types/recipe/CraftingDataReaderTest.java | 85 +++++++ .../recipe/NetworkItemDescriptorTypeTest.java | 144 +++++++++++ - 34 files changed, 2536 insertions(+), 5 deletions(-) + 34 files changed, 2602 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestAction.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestInfo.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestSlotInfo.java @@ -140,10 +148,10 @@ or establish complete installed recipe-action parity. diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestAction.java b/src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestAction.java new file mode 100644 -index 00000000..93f73370 +index 00000000..ecaac963 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/inventory/ItemStackRequestAction.java -@@ -0,0 +1,149 @@ +@@ -0,0 +1,162 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors @@ -161,7 +169,7 @@ index 00000000..93f73370 + +import java.util.List; + -+public sealed interface ItemStackRequestAction permits ItemStackRequestAction.TakeAction, ItemStackRequestAction.PlaceAction, ItemStackRequestAction.SwapAction, ItemStackRequestAction.DropAction, ItemStackRequestAction.DestroyAction, ItemStackRequestAction.ConsumeAction, ItemStackRequestAction.BeaconPaymentAction, ItemStackRequestAction.CraftRecipeAction, ItemStackRequestAction.CraftRecipeAutoAction, ItemStackRequestAction.CraftCreativeAction, ItemStackRequestAction.CraftRecipeOptionalAction, ItemStackRequestAction.CraftResultsDeprecatedAction, ItemStackRequestAction.CraftLoomAction { ++public sealed interface ItemStackRequestAction permits ItemStackRequestAction.TakeAction, ItemStackRequestAction.PlaceAction, ItemStackRequestAction.SwapAction, ItemStackRequestAction.DropAction, ItemStackRequestAction.DestroyAction, ItemStackRequestAction.ConsumeAction, ItemStackRequestAction.CreateAction, ItemStackRequestAction.BeaconPaymentAction, ItemStackRequestAction.CraftRecipeAction, ItemStackRequestAction.CraftRecipeAutoAction, ItemStackRequestAction.CraftCreativeAction, ItemStackRequestAction.CraftRecipeOptionalAction, ItemStackRequestAction.CraftResultsDeprecatedAction, ItemStackRequestAction.CraftLoomAction { + + ItemStackRequestActionType getType(); + @@ -215,6 +223,19 @@ index 00000000..93f73370 + } + } + ++ record CreateAction(int resultSlot) implements ItemStackRequestAction { ++ public CreateAction { ++ if (resultSlot < 0 || resultSlot > 255) { ++ throw new IllegalArgumentException("Created result index is outside the unsigned byte range"); ++ } ++ } ++ ++ @Override ++ public ItemStackRequestActionType getType() { ++ return ItemStackRequestActionType.Create; ++ } ++ } ++ + record BeaconPaymentAction(int primaryEvent, int secondaryEvent) + implements ItemStackRequestAction { + @Override @@ -1243,10 +1264,10 @@ index e37cc63f..0a587c29 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackActionType.java b/src/main/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackActionType.java new file mode 100644 -index 00000000..17d19209 +index 00000000..cf1bdeee --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackActionType.java -@@ -0,0 +1,118 @@ +@@ -0,0 +1,120 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors @@ -1312,6 +1333,8 @@ index 00000000..17d19209 + } else if (value instanceof ItemStackRequestAction.ConsumeAction action) { + buffer.writeByte(action.count()); + InventoryTypes.ITEM_STACK_REQUEST_SLOT.write(buffer, action.item()); ++ } else if (value instanceof ItemStackRequestAction.CreateAction action) { ++ buffer.writeByte(action.resultSlot()); + } else if (value instanceof ItemStackRequestAction.BeaconPaymentAction action) { + BedrockTypes.VAR_INT.write(buffer, action.primaryEvent()); + BedrockTypes.VAR_INT.write(buffer, action.secondaryEvent()); @@ -2082,10 +2105,10 @@ index 00000000..a485cb82 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackAutoCraftTypeTest.java b/src/test/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackAutoCraftTypeTest.java new file mode 100644 -index 00000000..a7f7ad20 +index 00000000..1501e84b --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/types/inventory/ItemStackAutoCraftTypeTest.java -@@ -0,0 +1,188 @@ +@@ -0,0 +1,239 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -2113,6 +2136,7 @@ index 00000000..a7f7ad20 +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.List; ++import java.util.function.IntFunction; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; @@ -2121,6 +2145,52 @@ index 00000000..a7f7ad20 +class ItemStackAutoCraftTypeTest { + + @Test ++ void multipleOutputsMatchNativeSelectionByproductDropAndPrimaryCursorTransfer() { ++ // Accepted native protocol-2225 request -21 consumes four grid stacks with a full inventory. ++ final var honey = new BedrockItem(42); ++ honey.setBlockRuntimeId(1517479843); ++ final var bottles = new BedrockItem(43, (short) 0, (byte) 4); ++ final var input = new ItemDescriptor.ItemNameDescriptor("minecraft:honey_bottle", 32767); ++ final var output = slot(ContainerEnumName.CreatedOutputContainer, 50, -21); ++ final var request = new ItemStackRequestInfo(-21, List.of( ++ new ItemStackRequestAction.CraftRecipeAutoAction(4239, 4, List.of(input, input, input, input)), ++ new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(honey, bottles), 4), ++ new ItemStackRequestAction.ConsumeAction(4, slot(ContainerEnumName.CraftingInputContainer, 32, 141)), ++ new ItemStackRequestAction.ConsumeAction(4, slot(ContainerEnumName.CraftingInputContainer, 33, 142)), ++ new ItemStackRequestAction.ConsumeAction(4, slot(ContainerEnumName.CraftingInputContainer, 35, 143)), ++ new ItemStackRequestAction.ConsumeAction(4, slot(ContainerEnumName.CraftingInputContainer, 36, 140)), ++ new ItemStackRequestAction.CreateAction(1), ++ new ItemStackRequestAction.DropAction(16, output, false), ++ new ItemStackRequestAction.CreateAction(0), ++ new ItemStackRequestAction.TakeAction(4, output, slot(ContainerEnumName.CursorContainer, 0, 0))), ++ List.of(), TextProcessingEventOrigin.unknown); ++ assertNativePacket(request, id -> id == 42 ? "minecraft:honey_block" : id == 43 ? "minecraft:glass_bottle" : null, """ ++ 01290a0b0d8f2104040101166d696e6563726166743a686f6e65795f626f74746c65feff0301000101166d696e6563726166 ++ 743a686f6e65795f626f74746c65feff0301000101166d696e6563726166743a686f6e65795f626f74746c65feff03010001 ++ 01166d696e6563726166743a686f6e65795f626f74746c65feff0301001214020101156d696e6563726166743a686f6e6579 ++ 5f626c6f636b000100a3cfcbd3050a000000000000000000000101166d696e6563726166743a676c6173735f626f74746c65 ++ 000400000a00000000000000000000040505040d00208d0000000505040d00218e0000000505040d00238f0000000505040d ++ 00248c0000000606010303103c0032ebffffff000606000000043c0032ebffffff3b00000000000000ffffffff ++ """); ++ } ++ ++ @Test ++ void resultSelectionRetainsTheUnsignedByteIndexAndRejectsTruncation() { ++ final ByteBuf buffer = Unpooled.buffer(); ++ try { ++ new ItemStackActionType(new ItemStackRequestResultType(id -> null, 0)) ++ .write(buffer, new ItemStackRequestAction.CreateAction(255)); ++ assertEquals(6, buffer.readUnsignedByte()); ++ assertEquals(6, buffer.readUnsignedByte()); ++ assertEquals(255, buffer.readUnsignedByte()); ++ assertThrows(IllegalArgumentException.class, () -> new ItemStackRequestAction.CreateAction(-1)); ++ assertThrows(IllegalArgumentException.class, () -> new ItemStackRequestAction.CreateAction(256)); ++ } finally { ++ buffer.release(); ++ } ++ } ++ ++ @Test + void bulkOverflowPreservesNativeDropBeforeCursorReplacement() { + // Official 1.26.60.5 -> BDS 1.26.60.6, protocol 2225, accepted request -7. + // Full inventory, 32 logs, and 60 held planks: drop the held stack, then take 64. @@ -2198,10 +2268,14 @@ index 00000000..a7f7ad20 + } + + private static void assertNativePacket(final ItemStackRequestInfo request, final String resultName, final String nativePayload) { ++ assertNativePacket(request, id -> id == 42 ? resultName : null, nativePayload); ++ } ++ ++ private static void assertNativePacket(final ItemStackRequestInfo request, final IntFunction resultName, final String nativePayload) { + final ByteBuf buffer = Unpooled.buffer(); + try { + BedrockTypes.UNSIGNED_VAR_INT.write(buffer, 1); -+ new ItemStackRequestType(new ItemStackRequestResultType(id -> id == 42 ? resultName : null, 0)).write(buffer, request); ++ new ItemStackRequestType(new ItemStackRequestResultType(resultName, 0)).write(buffer, request); + final byte[] actual = new byte[buffer.readableBytes()]; + buffer.readBytes(actual); + assertArrayEquals(HexFormat.of().parseHex(nativePayload.replaceAll("\\s", "")), actual); diff --git a/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.pr.md b/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.pr.md index 22e64b8d..6c5bebb0 100644 --- a/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.pr.md +++ b/patches/viabedrock/deferred/0022-define-bedrock-request-and-recipe-codecs.pr.md @@ -13,7 +13,7 @@ Separate semantic enum values from Cereal variant indexes. Use protocol-2225 exp Regression coverage includes `ItemStackCodecTest`, `ItemStackAutoCraftTypeTest`, `ItemStackRequestResultTypeTest`, `NetworkItemCountTest`, `CraftingDataReaderTest`. An official 1.26.60.5 capture verifies the automatic-craft action from a physical workbench Shift-click. -The regression matches its complete ingredient payload. Separate schema vectors cover descriptor variants and field widths. +The regression matches its complete ingredient payload. A second complete native packet selects returned bottles with Create, drops them from created-output slot 50, then selects honey blocks and takes them onto the cursor. Separate schema vectors cover descriptor variants, the unsigned result index, and field widths. Independent wire vectors and malformed-input tests protect codec boundaries. Encoding does not establish installed recipe actions or complete inventory parity. From 97a7861f74e807db9d5df211f8f0ea8f3fc7664b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:18:02 +0100 Subject: [PATCH 07/47] fix(inventory): restore search focus after recipe clicks Clear the parent GUI focus owner when the native search editor loses focus. Clearing only the editor flag leaves a stale focused child, so another click cannot restore text input and typing the inventory key can close the menu. A real-editor screen regression reproduces lost input before the fix. The full build passes 3,266 tests; installed direct and ViaProxy controls accept repeated query changes in the same opening and retain crafted output after reopening. --- docs/bedrock-coverage.md | 2 +- docs/native-player-inventory.md | 3 +- ...9-present-authored-native-containers.patch | 94 +++++++++++++++++-- ...9-present-authored-native-containers.pr.md | 3 + 4 files changed, 91 insertions(+), 11 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index b14a2fe2..381404d1 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, Creative catalog actions, Survival bulk actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, broader Survival recipes 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, Survival bulk actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, manual crafting remainders, broader Survival recipes, 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 ff840f61..3d0f3592 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -166,6 +166,7 @@ Use the [source guide](bedrock-development-sources.md) for versioned native refe Core publishes the retained player/workbench rows and full item previews in compressed snapshots. Commands bind to the opening, container generation, snapshot revision, and request sequence. The add-on buffers search edits while a command awaits its reply. +Returning to search after a recipe click restores text input without closing the inventory. The default player and workbench graphs resolve real slots, recipe rows, and local Help views. Survival Help opens Your First Craft. Creative Help opens the Encyclopedia at Creative Mode. @@ -202,7 +203,7 @@ 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. -Recipe families beyond the captured cake and honey controls, recipes with more than two outputs, and complete installed native parity remain unfinished. +Manual output-slot crafting with returned containers, recipe families beyond the captured bulk cake and honey controls, recipes with more than two outputs, 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/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.patch b/patches/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.patch index c568d549..cfafb445 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 a83ca770d78b82f6acc0214688e5838e2b6e0493 Mon Sep 17 00:00:00 2001 +From a47637bf85996556ce609de662cded209c5cc817 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 @@ -678,6 +678,18 @@ 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. + +Clear the parent GUI focus owner when the player search editor loses +focus on an outside click, disappearance or screen exit. Clearing only the +editor flag leaves it registered as the focused child. Java26.3 skips +reselecting that same child, so a later click cannot restore text input +and the inventory key can close the screen while typing a new query. + +Installed direct controls exposed this after a recipe click. A real +EditBox screen regression reproduces lost text input before the fix, then +accepts another query containing the inventory key after refocusing. +This fixes Java focus bookkeeping; it does not change native search or +inventory action semantics. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -757,9 +769,9 @@ Occupied-cursor regressions cover inventory-only and recipe-book layouts. .../NativeInventoryProgressTest.java | 83 + .../container/NativePlayerHelpTextTest.java | 34 + .../NativePlayerPresentationTest.java | 153 ++ - .../container/NativePlayerScreenTest.java | 507 ++++++ + .../container/NativePlayerScreenTest.java | 571 +++++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 80 files changed, 10715 insertions(+), 62 deletions(-) + 80 files changed, 10779 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 @@ -1301,7 +1313,7 @@ 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..5deb3b1 +index 0000000..15de47d --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java @@ -0,0 +1,1330 @@ @@ -1685,7 +1697,7 @@ index 0000000..5deb3b1 + if (this.lifecycle != null && this.lifecycle.requestExit(this.animationTime())) { + this.setDragging(false); + this.scroll.release(); -+ if (this.search != null) this.search.setFocused(false); ++ this.setFocused(null); + final var press = this.recipePress; + this.recipePress = null; + if (press != null) BedrockNativeCooking.action(this.owner, this.menu, press.recipe, press.mode, 2); @@ -1750,7 +1762,7 @@ index 0000000..5deb3b1 + } + if (this.search != null && this.searchBox == null) { + this.search.visible = false; -+ this.search.setFocused(false); ++ this.setFocused(null); + } + } + @@ -2188,7 +2200,7 @@ index 0000000..5deb3b1 + this.setFocused(this.search); + return true; + } -+ if (this.search != null) this.search.setFocused(false); ++ this.setFocused(null); + final String componentSource = this.presentation instanceof NativePlayerPresentation && event.hasShiftDown() + && (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) @@ -11536,10 +11548,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..400b180 +index 0000000..ff0af89 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java -@@ -0,0 +1,507 @@ +@@ -0,0 +1,571 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -11568,11 +11580,13 @@ index 0000000..400b180 +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Options; ++import net.minecraft.client.StringSplitter; +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.input.KeyEvent; ++import net.minecraft.client.input.CharacterEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.input.MouseButtonInfo; +import net.minecraft.client.multiplayer.ClientPacketListener; @@ -11616,6 +11630,68 @@ index 0000000..400b180 + return IntStream.of(InputConstants.MOUSE_BUTTON_LEFT, InputConstants.MOUSE_BUTTON_RIGHT); + } + ++ @Test ++ void searchAcceptsTextAgainAfterClickingARecipeWithoutClosingTheInventory() 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)); ++ when(options.keyInventory.matches(any(KeyEvent.class))).thenAnswer(call -> ((KeyEvent) call.getArgument(0)).key() == InputConstants.KEY_E); ++ when(font.getSplitter()).thenReturn(new StringSplitter((c, style) -> 6)); ++ when(font.plainSubstrByWidth(anyString(), anyInt())).thenAnswer(call -> call.getArgument(0)); ++ when(font.plainSubstrByWidth(anyString(), anyInt(), anyBoolean())).thenAnswer(call -> call.getArgument(0)); ++ when(font.width(anyString())).thenAnswer(call -> ((String) call.getArgument(0)).length() * 6); ++ 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); ++ var search = pointer(screen, draw -> draw.box().node().type().equals("edit_box")); ++ assertTrue(screen.mouseClicked(search, false)); ++ screen.mouseReleased(search); ++ assertTrue(screen.charTyped(new CharacterEvent('a'))); ++ network.verify(() -> BedrockNativePlayerInventory.query(owner, menu, "a")); ++ final var recipe = pointer(screen, draw -> draw.box().node().properties().has("recipe_actions")); ++ assertTrue(screen.mouseClicked(recipe, false)); ++ assertTrue(screen.mouseReleased(recipe)); ++ search = pointer(screen, draw -> draw.box().node().type().equals("edit_box")); ++ assertTrue(screen.mouseClicked(search, false)); ++ screen.mouseReleased(search); ++ assertTrue(screen.keyPressed(new KeyEvent(InputConstants.KEY_E, 0, 0))); ++ assertTrue(screen.charTyped(new CharacterEvent('e'))); ++ network.verify(() -> BedrockNativePlayerInventory.query(owner, menu, "ae")); ++ assertFalse(screen.closing()); ++ verifyNoInteractions(game.gameMode); ++ } finally { screen.discard(); } ++ } ++ } ++ + @ParameterizedTest + @MethodSource("catalogButtons") + void shiftCatalogClicksUseBulkAndReleaseTheirOriginalInputAfterShiftChanges(final int button) throws Exception { 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 0d4ee519..d4337c12 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 @@ -32,6 +32,9 @@ The consumer regression keeps raw binding quantities and genuine quickcraft prev Player and workbench snapshots retain actual menu slots, recipe identities, and full item previews. Commands carry the opening, generation, snapshot revision, and request sequence. Buffered search edits survive an acknowledgment that advances the container generation. +Search focus clears through the parent GUI owner when clicking elsewhere, hiding the editor, or exiting. +A real-editor screen regression clicks a recipe, returns to search, and types the inventory key without closing the screen. +Installed direct and ViaProxy controls craft once, enter three replacement queries in the same opening, and retain completed output after reopening. Each route records one server-accepted craft request. Late replies and hover exits cannot affect a replacement opening or selection. Default player and workbench graphs retain all 46 menu slots and real recipe rows. From 39a461aca6922ab592872daf6f7a3022de4a75c7 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 18:42:25 +0100 Subject: [PATCH 08/47] fix(crafting): match native manual output actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use server-supplied results for primary output and returned containers in both crafting grids. Keep manual pickup and catalog crafting behavior separate. Queue one Shift-click’s linked requests in one packet and reuse ordered batch reconciliation for confirmation and rejection. Fold the handler and request admission into their owning patches, remove the old single-output planner, and replay the full stack. Native 1.26.60 captures and installed direct/proxy controls establish the selected plank, cake, and honey behavior. Keep broader recipes, screen lifetime, and the original rendering reports open. --- docs/bedrock-coverage.md | 2 +- docs/native-player-inventory.md | 14 +- ...erver-authoritative-bedrock-requests.patch | 220 ++- ...erver-authoritative-bedrock-requests.pr.md | 5 +- ...edrock-recipes-and-place-ingredients.patch | 1201 +++++++++-------- ...edrock-recipes-and-place-ingredients.pr.md | 9 +- ...ain-bedrock-creative-catalog-entries.patch | 12 +- ...ed-completed-break-inventory-actions.patch | 16 +- ...bedrock-merchant-inventories-in-core.patch | 12 +- ...1-confirm-survival-offhand-transfers.patch | 28 +- 10 files changed, 906 insertions(+), 613 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 381404d1..2e180caf 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, Creative catalog actions, Survival bulk actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, manual crafting remainders, broader Survival recipes, 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, Survival bulk and manual crafting actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, search retention across openings, broader Survival recipes, 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 3d0f3592..3275f5f3 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -130,7 +130,7 @@ An empty cursor admits that same craft. Output can then use the freed source slo Native wooden-sword bulk crafting produces one sword despite ingredients for 32. The primary output stack limit caps the craft count. Returned containers do not reduce it. Recipe data includes returned buckets and bottles, so core does not need a separate remainder mapping. -For two-output recipes, native selects the secondary output before the primary output with `Create` actions. +For two-output catalog recipes, native selects the secondary output before the primary output with `Create` actions. Each created stack chunk starts another merge pass before using empty inventory slots. Six cakes return 18 buckets as stacks of 16 and 2. When inventory is full, returned bottles drop directly from created-output slot 50. @@ -148,6 +148,15 @@ The three craft requests per route match the native cake and honey captures afte Authoritative responses confirm inventory and cursor quantities. Reopening retains completed output. Rejected requests remain unit-tested. These live controls do not induce server rejection. +Direct output-slot clicks craft once onto the cursor, including right-click. +Manual crafting selects the primary output before returned containers and places returns in inventory, with overflow dropped from created output. +Manual Shift-click leaves the cursor unchanged and uses combined inventory capacity. +It sends the remaining stack-sized craft requests in one packet, with each request referring to earlier predicted stack identities. +The native 20-millisecond click control crafts 128 planks through two requests of 64. +State regressions cover both grids, returned containers, capacity across partial stacks, linked requests, and partial rejection. +Installed direct and ViaProxy controls each match nine native craft requests and their authoritative response quantities across eight player/workbench cases. +The controls cover right-click, linked Shift-click requests, returned containers, full inventory, partial bottle stacks, and retained output after reopening. + Admit an authored player screen only when its required collections and controls are available. The shared UI renderer supports the local player portrait and separate body/head rotations. Portrait rendering alone does not establish a complete native inventory. @@ -203,7 +212,8 @@ 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. -Manual output-slot crafting with returned containers, recipe families beyond the captured bulk cake and honey controls, recipes with more than two outputs, and complete installed native parity remain unfinished. +Recipe families beyond the captured plank, cake, and honey controls, recipes with more than two outputs, and complete installed native parity remain unfinished. +Search text does not yet persist between player and workbench openings as it does in the native client. 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.patch b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch index 9da94b15..fdb45743 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 2711c081495b3bce94e0efbd1c2d431d71e24478 Mon Sep 17 00:00:00 2001 +From dc5b3eec04252a2e57c325018f4e2785cf7b6481 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 @@ -749,6 +749,19 @@ 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. + +Admit manual crafting batches separately from ordinary transfer batches. +A Bedrock 1.26.60.5 control on protocol 2225 with a 20 ms Shift-click emits two stack-sized +CraftRecipe requests in one packet and refers to the first request's +predicted grid/output identities from the second. Retain the existing +ordered batch reconciliation, intermediate snapshots and authoritative +revision guards rather than registering these requests independently. + +Require a CraftRecipe/CraftResults preamble, crafting-grid consumption and +created-output transfers for crafting batches. Ordinary transfer admission +continues to reject crafting actions. Native-shaped regression requests +cover reordered success responses, a rejected later craft, whole-batch +rejection and refusal of unrelated destructive actions. --- build.gradle | 3 +- .../api/model/PlayerInventoryScreenClose.java | 46 + @@ -814,7 +827,7 @@ open packet and exported native presentation. .../protocol/storage/CraftingDataTracker.java | 206 + .../storage/InventoryRequestRecovery.java | 80 + .../storage/InventoryRequestStorage.java | 232 + - .../storage/InventoryRequestTracker.java | 728 ++ + .../storage/InventoryRequestTracker.java | 795 +++ .../protocol/storage/InventoryTracker.java | 282 +- .../storage/PlayerInventoryScreenTracker.java | 202 + .../protocol/types/item/BedrockItemType.java | 68 +- @@ -877,7 +890,7 @@ open packet and exported native presentation. .../InventoryRequestTrackerQueueTest.java | 136 + .../storage/InventoryRequestTrackerTest.java | 385 + .../storage/InventoryTrackerTest.java | 124 + - .../storage/InventoryTransferBatchTest.java | 258 + + .../storage/InventoryTransferBatchTest.java | 374 + .../NativeAutoPlacementRecoveryTest.java | 126 + .../NativeContainerActionBackendTest.java | 95 + .../storage/NativeContainerResponseTest.java | 287 + @@ -888,7 +901,7 @@ open packet and exported native presentation. .../item/NetworkItemConstructionTest.java | 171 + .../types/item/NetworkItemCountTest.java | 35 +- .../types/item/NetworkItemStackFixtures.java | 69 + - 138 files changed, 28650 insertions(+), 2047 deletions(-) + 138 files changed, 28833 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 @@ -12706,10 +12719,10 @@ index 00000000..5fddc92e +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java new file mode 100644 -index 00000000..b8929f8b +index 00000000..2c792573 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java -@@ -0,0 +1,728 @@ +@@ -0,0 +1,795 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2023-2025 RK_01/RaphiMC and contributors @@ -12747,6 +12760,7 @@ index 00000000..b8929f8b +import net.raphimc.viabedrock.protocol.model.inventory.InventoryTransactionData; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; +import net.raphimc.viabedrock.protocol.rewriter.InventoryTransactionRewriter; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.InteractPacketPayload_Action; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; @@ -12806,7 +12820,16 @@ index 00000000..b8929f8b + + /** Send one input's separately scoped transfers, each with its intermediate prediction captured. */ + public void sendTransferBatch(final List batch) { -+ this.addTransferBatch(batch); ++ this.sendBatch(batch, false); ++ } ++ ++ /** Send the separately predicted stack-sized requests from one manual craft input. */ ++ public void sendCraftingBatch(final List batch) { ++ this.sendBatch(batch, true); ++ } ++ ++ private void sendBatch(final List batch, final boolean crafting) { ++ this.addBatch(batch, crafting); + try { + PacketFactory.sendBedrockInventoryRequest(this.user(), batch.stream() + .map(InventoryRequestStorage::requestInfo) @@ -12830,16 +12853,25 @@ index 00000000..b8929f8b + } + + void addTransferBatch(final List batch) { ++ this.addBatch(batch, false); ++ } ++ ++ void addCraftingBatch(final List batch) { ++ this.addBatch(batch, true); ++ } ++ ++ private void addBatch(final List batch, final boolean crafting) { + if (this.hasPending() || batch.isEmpty()) { -+ throw new IllegalStateException("An inventory transfer batch cannot start"); ++ throw new IllegalStateException("An inventory request batch cannot start"); + } + final Set ids = new HashSet<>(); + for (final var request : batch) { + if (!request.hasPrediction() || !request.hasCurrentMenu(this.user().get(InventoryTracker.class)) + || !ids.add(request.requestInfo().requestId()) + || request.requestInfo().actions().isEmpty() -+ || !request.requestInfo().actions().stream().allMatch(InventoryRequestTracker::ordinaryTransfer)) { -+ throw new IllegalArgumentException("A transfer batch requires distinct, predicted ordinary requests"); ++ || !(crafting ? craftingRequest(request.requestInfo()) ++ : request.requestInfo().actions().stream().allMatch(InventoryRequestTracker::ordinaryTransfer))) { ++ throw new IllegalArgumentException("A request batch requires distinct predictions and a supported action sequence"); + } + } + this.transferBatch = new TransferBatch(List.copyOf(batch), new Int2ObjectOpenHashMap<>()); @@ -12850,6 +12882,54 @@ index 00000000..b8929f8b + this.recovery.reset(); + } + ++ private static boolean craftingRequest(final ItemStackRequestInfo request) { ++ final var actions = request.actions(); ++ if (actions.size() < 4 || !(actions.getFirst() instanceof ItemStackRequestAction.CraftRecipeAction) ++ || !(actions.get(1) instanceof ItemStackRequestAction.CraftResultsDeprecatedAction results) ++ || results.resultItems().isEmpty()) { ++ return false; ++ } ++ boolean consumed = false; ++ boolean transferred = false; ++ for (int index = 2; index < actions.size(); index++) { ++ final var action = actions.get(index); ++ if (action instanceof ItemStackRequestAction.ConsumeAction consume) { ++ if (consume.item().container().name() != ContainerEnumName.CraftingInputContainer) { ++ return false; ++ } ++ consumed = true; ++ continue; ++ } ++ if (action instanceof ItemStackRequestAction.CreateAction create) { ++ if (create.resultSlot() >= results.resultItems().size()) { ++ return false; ++ } ++ continue; ++ } ++ final ItemStackRequestSlotInfo source; ++ if (action instanceof ItemStackRequestAction.TakeAction take) { ++ source = take.source(); ++ if (take.destination().container().name() == ContainerEnumName.CreatedOutputContainer) { ++ return false; ++ } ++ } else if (action instanceof ItemStackRequestAction.PlaceAction place) { ++ source = place.source(); ++ if (place.destination().container().name() == ContainerEnumName.CreatedOutputContainer) { ++ return false; ++ } ++ } else if (action instanceof ItemStackRequestAction.DropAction drop) { ++ source = drop.item(); ++ } else { ++ return false; ++ } ++ if (source.container().name() != ContainerEnumName.CreatedOutputContainer) { ++ return false; ++ } ++ transferred = true; ++ } ++ return consumed && transferred; ++ } ++ + private static boolean ordinaryTransfer(final ItemStackRequestAction action) { + if (action instanceof ItemStackRequestAction.TakeAction take) { + return take.source().container().name() != ContainerEnumName.CreatedOutputContainer @@ -39081,10 +39161,10 @@ index 00000000..9574382a +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/InventoryTransferBatchTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/InventoryTransferBatchTest.java new file mode 100644 -index 00000000..56734494 +index 00000000..b26a628e --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/InventoryTransferBatchTest.java -@@ -0,0 +1,258 @@ +@@ -0,0 +1,374 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -39099,10 +39179,13 @@ index 00000000..56734494 +import net.raphimc.viabedrock.api.util.PacketFactory; +import net.raphimc.viabedrock.platform.ViaBedrockPlatform; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemStackNetResult; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.TextProcessingEventOrigin; +import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.FullContainerName; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseContainerInfo; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseInfo; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseSlotInfo; @@ -39270,6 +39353,68 @@ index 00000000..56734494 + } + } + ++ @Test ++ void craftingBatchReconcilesCreatedOutputAfterOutOfOrderResponses() { ++ try (var fixture = new Fixture()) { ++ final var batch = fixture.prepareCrafting(); ++ fixture.requests.sendCraftingBatch(batch); ++ fixture.requests.handleResponse(fixture.craftingResponse(batch.getLast(), true)); ++ assertTrue(fixture.requests.hasPending()); ++ fixture.requests.handleResponse(fixture.craftingResponse(batch.getFirst(), false)); ++ assertFalse(fixture.requests.hasPending()); ++ assertTrue(fixture.inventory.getHudContainer().getItem(28).isEmpty()); ++ assertEquals(item(64, 208), fixture.inventory.getInventoryContainer().getItem(0)); ++ assertEquals(item(64, 209), fixture.inventory.getInventoryContainer().getItem(1)); ++ } ++ } ++ ++ @Test ++ void rejectedLaterCraftKeepsTheAcceptedGridAndOutputCounts() { ++ try (var fixture = new Fixture()) { ++ final var batch = fixture.prepareCrafting(); ++ fixture.requests.addCraftingBatch(batch); ++ fixture.requests.handleResponse(rejected(batch.getLast())); ++ fixture.requests.handleResponse(fixture.craftingResponse(batch.getFirst(), false)); ++ assertFalse(fixture.requests.hasPending()); ++ assertEquals(15, fixture.inventory.getHudContainer().getItem(28).amount()); ++ assertEquals(207, fixture.inventory.getHudContainer().getItem(28).netId()); ++ assertEquals(item(64, 208), fixture.inventory.getInventoryContainer().getItem(0)); ++ assertEquals(item(4, 209), fixture.inventory.getInventoryContainer().getItem(1)); ++ } ++ } ++ ++ @Test ++ void rejectedCraftingBatchRestoresTheGridAndOutputBeforeAnyCraft() { ++ try (var fixture = new Fixture()) { ++ final var batch = fixture.prepareCrafting(); ++ fixture.requests.addCraftingBatch(batch); ++ fixture.requests.handleResponse(rejected(batch.getFirst())); ++ fixture.requests.handleResponse(rejected(batch.getLast())); ++ assertFalse(fixture.requests.hasPending()); ++ assertEquals(31, fixture.inventory.getHudContainer().getItem(28).amount()); ++ assertEquals(207, fixture.inventory.getHudContainer().getItem(28).netId()); ++ assertEquals(item(4, 208), fixture.inventory.getInventoryContainer().getItem(0)); ++ assertTrue(fixture.inventory.getInventoryContainer().getItem(1).isEmpty()); ++ } ++ } ++ ++ @Test ++ void batchAdmissionKeepsOrdinaryTransfersAndCraftingOperationsSeparate() { ++ try (var fixture = new Fixture()) { ++ final var batch = fixture.prepareCrafting(); ++ assertThrows(IllegalArgumentException.class, () -> fixture.requests.addTransferBatch(batch)); ++ final var original = batch.getLast(); ++ final var actions = new ArrayList<>(original.requestInfo().actions()); ++ actions.add(new ItemStackRequestAction.DestroyAction(1, fixture.inventory.getInventoryContainer().requestSlotInfo(0, 208))); ++ final var invalid = new InventoryRequestStorage(new ItemStackRequestInfo(original.requestInfo().requestId(), ++ actions, List.of(), TextProcessingEventOrigin.unknown), 0, original.prevCursorContainer(), original.prevContainers()); ++ invalid.capturePrediction(fixture.inventory); ++ assertThrows(IllegalArgumentException.class, () -> fixture.requests.addCraftingBatch(List.of(invalid))); ++ assertFalse(fixture.requests.hasPending()); ++ fixture.packets.verifyNoInteractions(); ++ } ++ } ++ + private static ItemStackResponseInfo rejected(final InventoryRequestStorage request) { + return new ItemStackResponseInfo(ItemStackNetResult.Error, request.requestInfo().requestId(), List.of()); + } @@ -39326,6 +39471,57 @@ index 00000000..56734494 + return request; + } + ++ private List prepareCrafting() { ++ final var player = this.inventory.getInventoryContainer(); ++ final var hud = this.inventory.getHudContainer(); ++ this.inventory.setCurrentContainerClosed(false); ++ this.inventory.setCurrentContainer(player); ++ final var log = new BedrockItem(1); ++ log.setAmount(31); ++ log.setNetId(207); ++ hud.setItem(28, log); ++ player.setItem(0, item(4, 208)); ++ final var firstOutput = new ItemStackRequestSlotInfo(new FullContainerName(ContainerEnumName.CreatedOutputContainer, null), (byte) 50, -3); ++ final var first = new InventoryRequestStorage(new ItemStackRequestInfo(-3, List.of( ++ new ItemStackRequestAction.CraftRecipeAction(3587, 16), ++ new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(item(4, 0)), 1), ++ new ItemStackRequestAction.ConsumeAction(16, hud.requestSlotInfo(28, 207)), ++ new ItemStackRequestAction.PlaceAction(60, firstOutput, player.requestSlotInfo(0, 208)), ++ new ItemStackRequestAction.PlaceAction(4, firstOutput, player.requestSlotInfo(1, 0))), ++ List.of(), TextProcessingEventOrigin.unknown), 0, hud.copy(), List.of(player.copy())); ++ final var remaining = log.copy(); ++ remaining.setAmount(15); ++ remaining.setNetId(-3); ++ hud.setItem(28, remaining); ++ player.setItem(0, item(64, -3)); ++ player.setItem(1, item(4, -3)); ++ first.capturePrediction(this.inventory); ++ final var lastOutput = new ItemStackRequestSlotInfo(new FullContainerName(ContainerEnumName.CreatedOutputContainer, null), (byte) 50, -5); ++ final var last = new InventoryRequestStorage(new ItemStackRequestInfo(-5, List.of( ++ new ItemStackRequestAction.CraftRecipeAction(3587, 15), ++ new ItemStackRequestAction.CraftResultsDeprecatedAction(List.of(item(4, 0)), 1), ++ new ItemStackRequestAction.ConsumeAction(15, hud.requestSlotInfo(28, -3)), ++ new ItemStackRequestAction.PlaceAction(60, lastOutput, player.requestSlotInfo(1, -3))), ++ List.of(), TextProcessingEventOrigin.unknown), 0, hud.copy(), List.of(player.copy())); ++ hud.setItem(28, BedrockItem.empty()); ++ player.setItem(1, item(64, -5)); ++ last.capturePrediction(this.inventory); ++ return List.of(first, last); ++ } ++ ++ private ItemStackResponseInfo craftingResponse(final InventoryRequestStorage request, final boolean last) { ++ final var player = this.inventory.getInventoryContainer(); ++ final var hud = this.inventory.getHudContainer(); ++ final var output = last ++ ? List.of(new ItemStackResponseSlotInfo((byte) 1, (byte) 1, (byte) 64, 209, "", null, 0)) ++ : List.of(new ItemStackResponseSlotInfo((byte) 0, (byte) 0, (byte) 64, 208, "", null, 0), ++ new ItemStackResponseSlotInfo((byte) 1, (byte) 1, (byte) 4, 209, "", null, 0)); ++ return new ItemStackResponseInfo(ItemStackNetResult.Success, request.requestInfo().requestId(), List.of( ++ new ItemStackResponseContainerInfo(hud.getFullContainerName(28), List.of( ++ new ItemStackResponseSlotInfo((byte) 28, (byte) 28, (byte) (last ? 0 : 15), last ? 0 : 207, "", null, 0))), ++ new ItemStackResponseContainerInfo(player.getFullContainerName(0), output))); ++ } ++ + private ItemStackResponseInfo response(final InventoryRequestStorage request, final int source, + final int amount, final int id) { + return new ItemStackResponseInfo(ItemStackNetResult.Success, request.requestInfo().requestId(), List.of( 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 b05d54d0..a8a417a6 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 @@ -10,9 +10,10 @@ Own predictions and responses by request, container object, and opening lifetime ## Validation and remaining limits -Regression coverage includes `ContainerTest`, `HorseContainerTest`, `NativeArmorPermissionsTest`, `NativeContainerActionSessionTest`. +Regression coverage includes `ContainerTest`, `HorseContainerTest`, `NativeArmorPermissionsTest`, `NativeContainerActionSessionTest`, and `InventoryTransferBatchTest`. -Late replies and replacement menus have targeted regressions. Complete native response-manager behavior, live transactions, and every container remain unfinished. +Late replies and replacement menus have targeted regressions. +Scoped manual crafting batches reuse ordered prediction and response reconciliation. Native-shaped regressions cover reordered responses, rejection of a later craft, whole-batch rejection, and refusal of unrelated operations. 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. 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 83739ed2..628a6a6c 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 71ecc23b682645014a48aae03a2f3c8e913b6b1f Mon Sep 17 00:00:00 2001 +From 7a67755200518dbbbb925e488f174fbdbc7bb45e 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 @@ -11,7 +11,12 @@ Treat wildcard ingredient metadata as a wildcard when building Java recipe displ Use the same placement logic for the player 2x2 grid so recipe book clicks work in both the player inventory and crafting table. -Batch shift crafting into one compatible inventory stack per request. Cap the recipe count by ingredient availability, stack capacity, and Bedrock action limits, then predict the same quantity locally until the server responds. +Plan manual output crafting through one shared player/workbench handler. +Each request produces at most one primary output stack, merges compatible +inventory stacks before empty hotbar/main-inventory slots, and can split +one craft across multiple destinations. A Shift-click queues the remaining +stack-sized requests in the same packet, using prior request IDs for grid +and destination identities changed by earlier predictions. Include CraftResults immediately after CraftRecipe for both crafting-table and player-grid requests. Geyser's InventoryTranslator requires this @@ -847,22 +852,51 @@ 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. + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 captures on protocol 2225 establish +manual output behavior independently of catalog CraftRecipeAuto. Preserve +CraftResults times 1 even when CraftRecipe requests several crafts. Select +primary output 0 before returned containers, use Take for ordinary cursor +pickup, and support both pointer buttons. Read all results, including +buckets and bottles, from CraftingData rather than a Java remainder list. + +Manual Shift-click uses only inventory capacity and leaves the cursor +unchanged. Normal pickup can craft with full inventory and drops returned +containers directly from created output 50 when they do not fit. Sum +compatible inventory capacity before choosing the craft quantity, then +place each created chunk in native hotbar/main-inventory order. Preserve +per-craft output quantities when room is spread across partial stacks. + +A 20 ms player-grid click produces two 16-craft requests from 32 logs in one +packet. A workbench 31-log control produces 16+15 with changed-slot aliases +in the second request. Both omit Create for single-output recipes; cake +and honey controls select Create 0 then Create 1. Player inputs use 28..31 +and table inputs 32..40. Native queries also survive closing the workbench +and opening the player grid; that screen-lifetime gap remains separate. + +Capture each intermediate grid, inventory, cursor and output-preview +prediction before batch submission. The shared request tracker buffers +responses, rolls back to the oldest snapshots and replays accepted steps +in request order. Regressions cover native-shaped request batches, +partial rejection, aggregate capacity, returned-container overflow, +right-button pickup and both crafting grids. Complete inventory parity, +arbitrary recipe families and original reports remain separate work. --- .../api/model/NativeFurnaceOptions.java | 44 + .../container/CookingRecipeAvailability.java | 30 + .../model/container/CookingRecipeCatalog.java | 81 ++ .../model/container/CookingRecipeInputs.java | 67 ++ .../model/container/CookingRecipeSources.java | 80 ++ - .../model/container/CraftingOutputPlan.java | 109 +++ .../model/container/CraftingPlacement.java | 229 +++++ .../container/FurnaceRecipePlacement.java | 221 +++++ .../NativeCookingRecipeSelection.java | 205 +++++ + .../model/container/NativeCraftingOutput.java | 271 ++++++ .../model/container/NativeRecipeCounter.java | 202 ++++ .../container/NativeRecipeDescriptor.java | 60 +- - .../block/CraftingTableContainer.java | 100 +- + .../block/CraftingTableContainer.java | 110 +-- .../container/block/FurnaceContainer.java | 209 +++++ .../model/container/player/HudContainer.java | 3 + - .../container/player/InventoryContainer.java | 190 +++- + .../container/player/InventoryContainer.java | 114 ++- .../net/raphimc/viabedrock/api/util/Fnv1.java | 9 + .../viabedrock/api/util/PacketFactory.java | 2 + .../viabedrock/protocol/BedrockProtocol.java | 2 + @@ -899,13 +933,11 @@ including explicit zeros and unknown saved values, through the existing path. .../CookingRecipeAvailabilityTest.java | 94 ++ .../container/CookingRecipeInputsTest.java | 229 +++++ .../container/CookingRecipeSourcesTest.java | 146 +++ - .../container/CraftingOutputPlanTest.java | 84 ++ .../container/CraftingPlacementTest.java | 161 ++++ .../container/FurnaceRecipePlacementTest.java | 418 +++++++++ .../NativeRecipeCounterFixtureTest.java | 132 +++ .../container/NativeRecipeCounterTest.java | 66 ++ .../container/NativeRecipeDescriptorTest.java | 356 ++++++++ - .../player/InventoryContainerTest.java | 131 +++ .../protocol/model/ItemPrimarySearchTest.java | 173 ++++ .../protocol/model/ItemRegistryTagsTest.java | 20 +- .../model/ItemSecondaryDescriptionTest.java | 179 ++++ @@ -924,7 +956,7 @@ including explicit zeros and unknown saved values, through the existing path. .../RegisteredPlayerRecipeSearchTest.java | 268 ++++++ .../ResolvedRecipeIngredientTest.java | 283 ++++++ .../storage/CookingRecipeBookTest.java | 194 ++++ - .../protocol/storage/CraftingCursorTest.java | 158 ++++ + .../protocol/storage/CraftingCursorTest.java | 363 ++++++++ .../storage/CraftingDataTrackerTest.java | 407 +++++++++ .../storage/CraftingRecipeSourcesTest.java | 128 +++ .../storage/FurnaceRecipeRequestTest.java | 797 ++++++++++++++++ @@ -940,16 +972,16 @@ including explicit zeros and unknown saved values, through the existing path. .../types/model/FurnaceOptionsTypeTest.java | 75 ++ .../types/model/InventoryOptionsTypeTest.java | 46 + .../types/recipe/CraftingDataReaderTest.java | 84 +- - 92 files changed, 12984 insertions(+), 189 deletions(-) + 90 files changed, 13026 insertions(+), 233 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 create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeInputs.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CookingRecipeSources.java - create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CraftingOutputPlan.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/CraftingPlacement.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/FurnaceRecipePlacement.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeCookingRecipeSelection.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeCraftingOutput.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/FurnaceOptions.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/InventoryOptions.java @@ -966,13 +998,11 @@ including explicit zeros and unknown saved values, through the existing path. create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/CookingRecipeAvailabilityTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/CookingRecipeInputsTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/CookingRecipeSourcesTest.java - create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/CraftingOutputPlanTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/CraftingPlacementTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/FurnaceRecipePlacementTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterFixtureTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounterTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/NativeRecipeDescriptorTest.java - create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/ItemPrimarySearchTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/ItemSecondaryDescriptionTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/NativeCraftingContextTest.java @@ -1337,121 +1367,6 @@ index 00000000..c531ebe6 + } + +} -diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/CraftingOutputPlan.java b/src/main/java/net/raphimc/viabedrock/api/model/container/CraftingOutputPlan.java -new file mode 100644 -index 00000000..588fe153 ---- /dev/null -+++ b/src/main/java/net/raphimc/viabedrock/api/model/container/CraftingOutputPlan.java -@@ -0,0 +1,109 @@ -+/* -+ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock -+ * Copyright (C) 2026 RK_01/RaphiMC and contributors -+ * -+ * This program is free software: you can redistribute it and/or modify -+ * it under the terms of the GNU General Public License as published by -+ * the Free Software Foundation, either version 3 of the License, or -+ * (at your option) any later version. -+ * -+ * This program is distributed in the hope that it will be useful, -+ * but WITHOUT ANY WARRANTY; without even the implied warranty of -+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ * GNU General Public License for more details. -+ * -+ * You should have received a copy of the GNU General Public License -+ * along with this program. If not, see . -+ */ -+package net.raphimc.viabedrock.api.model.container; -+ -+import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; -+import net.raphimc.viabedrock.protocol.model.BedrockItem; -+ -+import java.util.Arrays; -+ -+/** One server-authoritative craft request whose output fits in one inventory or cursor slot. */ -+public record CraftingOutputPlan(int destination, int crafts, int outputAmount, -+ int destinationNetId) { -+ -+ public static CraftingOutputPlan plan(final InventoryContainer inventory, -+ final BedrockItem result, final Container grid, final int firstGridSlot, -+ final int[] amountsPerCraft, final int maxStackSize) { -+ if (result.isEmpty() || amountsPerCraft.length == 0 || maxStackSize < result.amount()) { -+ return null; -+ } -+ int maxCrafts = 255; -+ boolean hasIngredient = false; -+ for (int index = 0; index < amountsPerCraft.length; index++) { -+ final int required = amountsPerCraft[index]; -+ if (required <= 0) { -+ continue; -+ } -+ hasIngredient = true; -+ final BedrockItem ingredient = grid.getItem(firstGridSlot + index); -+ if (ingredient.isEmpty() || ingredient.netId() == null) { -+ return null; -+ } -+ maxCrafts = Math.min(maxCrafts, ingredient.amount() / required); -+ } -+ if (!hasIngredient || maxCrafts == 0) { -+ return null; -+ } -+ -+ for (int pass = 0; pass < 2; pass++) { -+ for (int slot = 9; slot < inventory.size() + 9; slot++) { -+ final int destination = slot % inventory.size(); -+ final BedrockItem existing = inventory.getItem(destination); -+ final int networkId; -+ final int freeSpace; -+ if (pass == 0) { -+ if (existing.isEmpty() || !canStack(existing, result) -+ || existing.netId() == null) { -+ continue; -+ } -+ networkId = existing.netId(); -+ freeSpace = Math.min(255, maxStackSize - existing.amount()); -+ } else { -+ if (!existing.isEmpty()) { -+ continue; -+ } -+ networkId = 0; -+ freeSpace = Math.min(255, maxStackSize); -+ } -+ final int crafts = Math.min(maxCrafts, freeSpace / result.amount()); -+ if (crafts > 0) { -+ return new CraftingOutputPlan(destination, crafts, -+ crafts * result.amount(), networkId); -+ } -+ } -+ } -+ return null; -+ } -+ -+ public static CraftingOutputPlan pickup(final Container hud, final BedrockItem result, -+ final int maxStackSize) { -+ final BedrockItem cursor = hud.getItem(0); -+ if (result.isEmpty() || result.amount() > Math.min(255, maxStackSize) - cursor.amount()) { -+ return null; -+ } -+ if (!cursor.isEmpty() && (!canStack(cursor, result) || cursor.netId() == null)) { -+ return null; -+ } -+ return new CraftingOutputPlan(0, 1, result.amount(), cursor.isEmpty() ? 0 : cursor.netId()); -+ } -+ -+ private static boolean canStack(final BedrockItem existing, final BedrockItem result) { -+ return !existing.isDifferent(result) -+ && Arrays.equals(existing.canPlace(), result.canPlace()) -+ && Arrays.equals(existing.canBreak(), result.canBreak()) -+ && existing.blockingTicks() == result.blockingTicks(); -+ } -+ -+ public void apply(final Container inventory, final BedrockItem result) { -+ final BedrockItem existing = inventory.getItem(this.destination); -+ final BedrockItem updated = existing.isEmpty() ? result.copy() : existing.copy(); -+ updated.setAmount(existing.amount() + this.outputAmount); -+ inventory.setItem(this.destination, updated); -+ } -+ -+} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/CraftingPlacement.java b/src/main/java/net/raphimc/viabedrock/api/model/container/CraftingPlacement.java new file mode 100644 index 00000000..a6c19807 @@ -2125,6 +2040,283 @@ index 00000000..acfb4b9b + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeCraftingOutput.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeCraftingOutput.java +new file mode 100644 +index 00000000..f05cec35 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeCraftingOutput.java +@@ -0,0 +1,271 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2026 RK_01/RaphiMC and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model.container; ++ ++import com.viaversion.viaversion.api.connection.UserConnection; ++import java.util.ArrayList; ++import java.util.HashSet; ++import java.util.LinkedHashMap; ++import java.util.List; ++import java.util.Map; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.TextProcessingEventOrigin; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.FullContainerName; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; ++import net.raphimc.viabedrock.protocol.model.recipe.ShapedRecipe; ++import net.raphimc.viabedrock.protocol.model.recipe.ShapelessRecipe; ++import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import net.raphimc.viabedrock.protocol.storage.CraftingDataTracker.RecipeMatch; ++import net.raphimc.viabedrock.protocol.storage.InventoryRequestStorage; ++import net.raphimc.viabedrock.protocol.storage.InventoryRequestTracker; ++import net.raphimc.viabedrock.protocol.storage.InventoryTracker; ++ ++/** Manual output pickup and the separately scoped requests from one native Shift-click. */ ++public final class NativeCraftingOutput { ++ ++ private NativeCraftingOutput() { } ++ ++ public static boolean handle(final UserConnection user, final Container owner, final Container grid, ++ final int firstGridSlot, final RecipeMatch match, final int revision, ++ final boolean toInventory, final Runnable refreshOutput) { ++ final var requests = user.get(InventoryRequestTracker.class); ++ if (!requests.canStartRecipePlacement()) { ++ return false; ++ } ++ final List results = switch (match.data().recipe()) { ++ case ShapedRecipe shaped -> shaped.getResults(); ++ case ShapelessRecipe shapeless -> shapeless.getResults(); ++ default -> List.of(); ++ }; ++ final var inventory = user.get(InventoryTracker.class); ++ final var items = user.get(ItemRewriter.class); ++ var step = plan(results, grid, firstGridSlot, match.amounts(), inventory, items, toInventory); ++ if (step == null) { ++ return false; ++ } ++ final var hud = inventory.getHudContainer(); ++ final var player = inventory.getInventoryContainer(); ++ final var beforeHud = hud.copy(); ++ final var beforePlayer = player.copy(); ++ final var beforeOwner = owner == player ? null : owner.copy(); ++ final var batch = new ArrayList(); ++ final boolean wasPredicting = requests.isPredictingClick(); ++ requests.setPredictingClick(true); ++ try { ++ do { ++ final int id = requests.nextRequestId(); ++ final var request = new ItemStackRequestInfo(id, step.actions(match.data().networkId(), results, id), ++ List.of(), TextProcessingEventOrigin.unknown); ++ final var storage = new InventoryRequestStorage(request, revision, hud.copy(), ++ owner == player ? List.of(player.copy()) : List.of(owner.copy(), player.copy())); ++ step.apply(id); ++ refreshOutput.run(); ++ storage.capturePrediction(inventory); ++ batch.add(storage); ++ // Native 1.26.60 queues the remaining stack-sized crafts before sending any response. ++ step = toInventory ? plan(results, grid, firstGridSlot, match.amounts(), inventory, items, true) : null; ++ } while (step != null); ++ requests.sendCraftingBatch(batch); ++ } catch (final RuntimeException error) { ++ hud.setItems(beforeHud.getItems()); ++ player.setItems(beforePlayer.getItems()); ++ if (beforeOwner != null) { ++ owner.setItems(beforeOwner.getItems()); ++ } ++ refreshOutput.run(); ++ throw error; ++ } finally { ++ requests.setPredictingClick(wasPredicting); ++ } ++ return true; ++ } ++ ++ private static Step plan(final List results, final Container grid, final int firstGridSlot, ++ final int[] amounts, final InventoryTracker inventory, final ItemRewriter items, ++ final boolean toInventory) { ++ if (results.isEmpty() || results.size() > 256 || amounts.length == 0) { ++ return null; ++ } ++ for (final var result : results) { ++ if (result.isEmpty() || items.maxStackSize(result) <= 0) { ++ return null; ++ } ++ } ++ final var primary = results.getFirst(); ++ final int maximum = Math.min(255, items.maxStackSize(primary)); ++ if (primary.amount() > maximum) { ++ return null; ++ } ++ int crafts = toInventory ? maximum / primary.amount() : 1; ++ boolean ingredient = false; ++ for (int index = 0; index < amounts.length; index++) { ++ final int required = amounts[index]; ++ if (required < 0) { ++ return null; ++ } ++ if (required != 0) { ++ ingredient = true; ++ final var item = grid.getItem(firstGridSlot + index); ++ if (item.isEmpty() || item.netId() == null) { ++ return null; ++ } ++ crafts = Math.min(crafts, item.amount() / required); ++ } ++ } ++ if (!ingredient || crafts == 0) { ++ return null; ++ } ++ final var player = inventory.getInventoryContainer(); ++ final var primaryStack = primary.copy(); ++ primaryStack.setAmount(maximum); ++ final var cursor = new Slot(inventory.getHudContainer(), 0); ++ if (toInventory) { ++ int capacity = 0; ++ for (int index = 0; index < player.size(); index++) { ++ final var previous = player.getItem(index); ++ final int room = previous.isEmpty() ? maximum ++ : previous.netId() != null && items.canMerge(primary, previous) ? maximum - previous.amount() : 0; ++ if (room > 0 && player.canPlaceItem(index, primaryStack, room)) { ++ capacity += room; ++ } ++ } ++ crafts = Math.min(crafts, capacity / primary.amount()); ++ if (crafts == 0) { ++ return null; ++ } ++ } else { ++ final var previous = cursor.item(); ++ if (primary.amount() > maximum - previous.amount() || !previous.isEmpty() ++ && (previous.netId() == null || !items.canMerge(primary, previous)) ++ || !cursor.container.canPlaceItem(0, primary, primary.amount())) { ++ return null; ++ } ++ } ++ final var changes = new LinkedHashMap(); ++ final var consumption = new ArrayList(); ++ for (int index = 0; index < amounts.length; index++) { ++ if (amounts[index] == 0) { ++ continue; ++ } ++ final var slot = new Slot(grid, firstGridSlot + index); ++ final int count = amounts[index] * crafts; ++ if (!grid.canDiscardItem(slot.index, count)) { ++ return null; ++ } ++ consumption.add(new Transfer(slot, count)); ++ final var next = slot.item().copy(); ++ next.setAmount(next.amount() - count); ++ changes.put(slot, next.isEmpty() ? BedrockItem.empty() : next); ++ } ++ final var outputs = new ArrayList(); ++ for (int resultSlot = 0; resultSlot < results.size(); resultSlot++) { ++ final var result = results.get(resultSlot); ++ if (resultSlot == 0 && !toInventory) { ++ final var previous = cursor.item(); ++ final var next = previous.isEmpty() ? result.copy() : previous.copy(); ++ next.setAmount(previous.amount() + result.amount()); ++ changes.put(cursor, next); ++ outputs.add(new Output(0, List.of(new Transfer(cursor, result.amount())), true, 0)); ++ continue; ++ } ++ final int resultMaximum = Math.min(255, items.maxStackSize(result)); ++ final var stack = result.copy(); ++ stack.setAmount(resultMaximum); ++ int pending = result.amount() * crafts; ++ while (pending > 0) { ++ int remaining = Math.min(pending, resultMaximum); ++ pending -= remaining; ++ final var placements = new ArrayList(); ++ for (final boolean merge : new boolean[] {true, false}) { ++ for (int index = 0; index < player.size() && remaining > 0; index++) { ++ final var slot = new Slot(player, index); ++ final var previous = changes.getOrDefault(slot, slot.item()); ++ if (merge ? previous.isEmpty() || previous.netId() == null && !changes.containsKey(slot) ++ || !items.canMerge(result, previous) : !previous.isEmpty()) { ++ continue; ++ } ++ final int count = Math.min(remaining, resultMaximum - previous.amount()); ++ if (count <= 0 || !player.canPlaceItem(index, stack, count)) { ++ continue; ++ } ++ final var next = previous.isEmpty() ? result.copy() : previous.copy(); ++ next.setAmount(previous.amount() + count); ++ changes.put(slot, next); ++ placements.add(new Transfer(slot, count)); ++ remaining -= count; ++ } ++ } ++ if (resultSlot == 0 && remaining != 0) { ++ return null; ++ } ++ outputs.add(new Output(resultSlot, List.copyOf(placements), false, remaining)); ++ } ++ } ++ return new Step(crafts, List.copyOf(consumption), List.copyOf(outputs), changes); ++ } ++ ++ private record Slot(Container container, int index) { ++ ++ BedrockItem item() { ++ return this.container.getItem(this.index); ++ } ++ ++ ItemStackRequestSlotInfo reference(final int networkId) { ++ return this.container.requestSlotInfo(this.index, networkId); ++ } ++ } ++ ++ private record Transfer(Slot slot, int count) { } ++ ++ private record Output(int resultSlot, List placements, boolean cursor, int dropped) { } ++ ++ private record Step(int crafts, List consumption, List outputs, ++ Map changes) { ++ ++ List actions(final int recipeId, final List results, final int id) { ++ final var actions = new ArrayList(); ++ final var mutated = new HashSet(); ++ actions.add(new ItemStackRequestAction.CraftRecipeAction(recipeId, this.crafts)); ++ // Unlike catalog CraftRecipeAuto, manual CraftRecipe retains one here for a batch. ++ actions.add(new ItemStackRequestAction.CraftResultsDeprecatedAction(results.stream().map(BedrockItem::copy).toList(), 1)); ++ for (final var transfer : this.consumption) { ++ actions.add(new ItemStackRequestAction.ConsumeAction(transfer.count, transfer.slot.reference(transfer.slot.item().netId()))); ++ mutated.add(transfer.slot); ++ } ++ final var created = new ItemStackRequestSlotInfo(new FullContainerName(ContainerEnumName.CreatedOutputContainer, null), (byte) 50, id); ++ for (final var output : this.outputs) { ++ if (results.size() > 1) { ++ actions.add(new ItemStackRequestAction.CreateAction(output.resultSlot)); ++ } ++ for (final var transfer : output.placements) { ++ final var slot = transfer.slot; ++ final var destination = slot.reference(mutated.contains(slot) ? id : slot.item().isEmpty() ? 0 : slot.item().netId()); ++ actions.add(output.cursor ? new ItemStackRequestAction.TakeAction(transfer.count, created, destination) ++ : new ItemStackRequestAction.PlaceAction(transfer.count, created, destination)); ++ mutated.add(slot); ++ } ++ if (output.dropped != 0) { ++ actions.add(new ItemStackRequestAction.DropAction(output.dropped, created, false)); ++ } ++ } ++ return List.copyOf(actions); ++ } ++ ++ void apply(final int id) { ++ this.changes.forEach((slot, item) -> { ++ final var next = item.copy(); ++ if (!next.isEmpty()) { ++ next.setNetId(id); ++ } ++ slot.container.setItem(slot.index, next); ++ }); ++ } ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java b/src/main/java/net/raphimc/viabedrock/api/model/container/NativeRecipeCounter.java new file mode 100644 index 00000000..fe874172 @@ -2440,7 +2632,7 @@ index a24c0fe6..271d9fad 100644 record ComplexAlias(String name, NativeItemDefinitions.Definition representative) implements NativeRecipeDescriptor { public ComplexAlias { diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java b/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java -index fec81fe6..1b9d7c05 100644 +index fec81fe6..a90cf752 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java @@ -22,6 +22,8 @@ import com.viaversion.viaversion.api.minecraft.BlockPosition; @@ -2448,12 +2640,17 @@ index fec81fe6..1b9d7c05 100644 import net.raphimc.viabedrock.api.model.container.Container; +import net.raphimc.viabedrock.api.model.container.CraftingPlacement; -+import net.raphimc.viabedrock.api.model.container.CraftingOutputPlan; ++import net.raphimc.viabedrock.api.model.container.NativeCraftingOutput; import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; import net.raphimc.viabedrock.api.util.PacketFactory; import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; -@@ -36,8 +38,10 @@ import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; - import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; +@@ -31,18 +33,17 @@ import net.raphimc.viabedrock.protocol.data.enums.java.generated.ContainerInput; + import net.raphimc.viabedrock.protocol.data.generated.bedrock.CustomBlockTags; + import net.raphimc.viabedrock.protocol.model.BedrockItem; + import net.raphimc.viabedrock.protocol.model.FullContainerName; +-import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; + import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; +-import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; import net.raphimc.viabedrock.protocol.model.recipe.ShapedRecipe; import net.raphimc.viabedrock.protocol.model.recipe.ShapelessRecipe; +import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; @@ -2463,128 +2660,108 @@ index fec81fe6..1b9d7c05 100644 import net.raphimc.viabedrock.protocol.storage.InventoryRequestStorage; import net.raphimc.viabedrock.protocol.storage.InventoryRequestTracker; import net.raphimc.viabedrock.protocol.storage.InventoryTracker; -@@ -108,41 +112,48 @@ public final class CraftingTableContainer extends Container { + +-import java.util.ArrayList; + import java.util.List; + + public final class CraftingTableContainer extends Container { +@@ -104,83 +105,42 @@ public final class CraftingTableContainer extends Container { + if (action != ContainerInput.PICKUP && action != ContainerInput.QUICK_MOVE) { + return false; + } +- if (action == ContainerInput.PICKUP && button != 0) { ++ if (action == ContainerInput.PICKUP && (button < 0 || button > 1)) { return false; } - final CraftingDataStorage recipe = - this.user.get(CraftingDataTracker.class).getRecipeData(this, "crafting_table"); - if (recipe == null) { +- return false; +- } +- final BedrockItem result = result(recipe); +- if (result.isEmpty()) { + final RecipeMatch match = + this.user.get(CraftingDataTracker.class).getRecipeMatch(this); + if (match == null) { return false; } -- final BedrockItem result = result(recipe); -+ final BedrockItem result = result(match.data()); - if (result.isEmpty()) { - return false; - } ++ return NativeCraftingOutput.handle(this.user, this, this, 32, match, ++ revision, action == ContainerInput.QUICK_MOVE, this::refreshOutput); ++ } ++ ++ public boolean handleRecipePlacement(final CraftingDataStorage recipe, ++ final boolean maxItems) { final InventoryTracker inventory = this.user.get(InventoryTracker.class); - final InventoryContainer player = inventory.getInventoryContainer(); - final boolean toInventory = action == ContainerInput.QUICK_MOVE; +- final InventoryContainer player = inventory.getInventoryContainer(); +- final boolean toInventory = action == ContainerInput.QUICK_MOVE; - if (!toInventory && !inventory.getHudContainer().getItem(0).isEmpty()) { -- return false; -- } ++ final InventoryRequestTracker requests = this.user.get(InventoryRequestTracker.class); ++ if (!requests.canStartRecipePlacement()) { + return false; + } - final int destination = toInventory ? firstEmptySlot(player) : 0; - if (destination < 0) { -+ final int maxStackSize = this.user.get(ItemRewriter.class).maxStackSize(result); -+ final CraftingOutputPlan plan = toInventory -+ ? CraftingOutputPlan.plan(player, result, this, 32, match.amounts(), maxStackSize) -+ : CraftingOutputPlan.pickup(inventory.getHudContainer(), result, maxStackSize); -+ if (plan == null) { ++ final InventoryContainer player = inventory.getInventoryContainer(); ++ final CraftingPlacement.Placement placement = CraftingPlacement.plan(recipe, 3, 32, ++ this, player, maxItems, this.user.get(ItemRewriter.class)::maxStackSize, ++ (descriptor, item) -> descriptor.matchesItem(this.user, item)); ++ if (placement == null) { return false; } -+ final int destination = plan.destination(); -+ final int crafts = plan.crafts(); -+ final int outputAmount = plan.outputAmount(); -+ final int destinationNetId = plan.destinationNetId(); - final InventoryRequestTracker requests = this.user.get(InventoryRequestTracker.class); - final int requestId = requests.nextRequestId(); - final List actions = new ArrayList<>(); +- final InventoryRequestTracker requests = this.user.get(InventoryRequestTracker.class); +- final int requestId = requests.nextRequestId(); +- final List actions = new ArrayList<>(); - actions.add(new ItemStackRequestAction.CraftRecipeAction(recipe.networkId(), 1)); - for (int slot = 32; slot <= 40; slot++) { - final BedrockItem ingredient = this.getItem(slot); - if (ingredient.isEmpty()) { -+ actions.add(new ItemStackRequestAction.CraftRecipeAction(match.data().networkId(), crafts)); -+ actions.add(new ItemStackRequestAction.CraftResultsDeprecatedAction( -+ List.of(result.copy()), crafts)); -+ for (int index = 0; index < match.amounts().length; index++) { -+ if (match.amounts()[index] == 0) { - continue; - } -+ final int slot = index + 32; -+ final BedrockItem ingredient = this.getItem(slot); - if (ingredient.netId() == null) { - return false; - } - actions.add( - new ItemStackRequestAction.ConsumeAction( +- continue; +- } +- if (ingredient.netId() == null) { +- return false; +- } +- actions.add( +- new ItemStackRequestAction.ConsumeAction( - 1, -+ match.amounts()[index] * crafts, - new ItemStackRequestSlotInfo( - this.getFullContainerName(slot), - (byte) slot, -@@ -154,13 +165,14 @@ public final class CraftingTableContainer extends Container { - : inventory.getHudContainer().getFullContainerName(0); - actions.add( - new ItemStackRequestAction.PlaceAction( +- new ItemStackRequestSlotInfo( +- this.getFullContainerName(slot), +- (byte) slot, +- ingredient.netId()))); +- } +- final FullContainerName targetName = +- toInventory +- ? player.getFullContainerName(destination) +- : inventory.getHudContainer().getFullContainerName(0); +- actions.add( +- new ItemStackRequestAction.PlaceAction( - result.amount(), -+ outputAmount, - new ItemStackRequestSlotInfo( - new FullContainerName( - ContainerEnumName.CreatedOutputContainer, null), - (byte) 50, - requestId), +- new ItemStackRequestSlotInfo( +- new FullContainerName( +- ContainerEnumName.CreatedOutputContainer, null), +- (byte) 50, +- requestId), - new ItemStackRequestSlotInfo(targetName, (byte) destination, 0))); -+ new ItemStackRequestSlotInfo(targetName, (byte) destination, -+ destinationNetId))); - final ItemStackRequestInfo request = - new ItemStackRequestInfo( - requestId, actions, List.of(), TextProcessingEventOrigin.unknown); -@@ -169,18 +181,44 @@ public final class CraftingTableContainer extends Container { - requests.addRequest( - new InventoryRequestStorage( - request, revision, inventory.getHudContainer().copy(), snapshots)); +- final ItemStackRequestInfo request = +- new ItemStackRequestInfo( +- requestId, actions, List.of(), TextProcessingEventOrigin.unknown); +- final List snapshots = +- toInventory ? List.of(this.copy(), player.copy()) : List.of(this.copy()); +- requests.addRequest( +- new InventoryRequestStorage( +- request, revision, inventory.getHudContainer().copy(), snapshots)); - for (int slot = 32; slot <= 40; slot++) { - final BedrockItem ingredient = this.getItem(slot); - if (!ingredient.isEmpty()) { - this.setItem(slot, this.itemAfterRemovingAmount(ingredient, 1)); -+ for (int index = 0; index < match.amounts().length; index++) { -+ if (match.amounts()[index] == 0) { -+ continue; - } -+ final int slot = index + 32; -+ final BedrockItem ingredient = this.getItem(slot); -+ this.setItem(slot, this.itemAfterRemovingAmount( -+ ingredient, match.amounts()[index] * crafts)); - } +- } +- } - if (toInventory) { - player.setItem(destination, result.copy()); - } else { - inventory.getHudContainer().setItem(0, result.copy()); -+ plan.apply(toInventory ? player : inventory.getHudContainer(), result); -+ this.refreshOutput(); -+ PacketFactory.sendBedrockInventoryRequest(this.user, new ItemStackRequestInfo[] {request}); -+ return true; -+ } -+ -+ public boolean handleRecipePlacement(final CraftingDataStorage recipe, -+ final boolean maxItems) { -+ final InventoryTracker inventory = this.user.get(InventoryTracker.class); -+ final InventoryRequestTracker requests = this.user.get(InventoryRequestTracker.class); -+ if (!requests.canStartRecipePlacement()) { -+ return false; - } -+ final InventoryContainer player = inventory.getInventoryContainer(); -+ final CraftingPlacement.Placement placement = CraftingPlacement.plan(recipe, 3, 32, -+ this, player, maxItems, this.user.get(ItemRewriter.class)::maxStackSize, -+ (descriptor, item) -> descriptor.matchesItem(this.user, item)); -+ if (placement == null) { -+ return false; -+ } -+ +- } + final ItemStackRequestInfo request = new ItemStackRequestInfo( + requests.nextRequestId(), CraftingPlacement.actions(placement, this, player), + List.of(), TextProcessingEventOrigin.unknown); @@ -2596,7 +2773,7 @@ index fec81fe6..1b9d7c05 100644 PacketFactory.sendBedrockInventoryRequest(this.user, new ItemStackRequestInfo[] {request}); return true; } -@@ -202,18 +240,4 @@ public final class CraftingTableContainer extends Container { +@@ -202,18 +162,4 @@ public final class CraftingTableContainer extends Container { return BedrockItem.empty(); } @@ -2869,15 +3046,15 @@ index 47bd4d6b..72ba1793 100644 return slot - 27; } else { diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainer.java b/src/main/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainer.java -index 892d6fac..c4478b28 100644 +index 892d6fac..465ada34 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainer.java +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainer.java -@@ -26,23 +26,40 @@ import com.viaversion.viaversion.api.type.Types; +@@ -26,23 +26,37 @@ import com.viaversion.viaversion.api.type.Types; import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets26_3; import net.raphimc.viabedrock.api.model.container.Container; +import net.raphimc.viabedrock.api.model.container.CraftingPlacement; -+import net.raphimc.viabedrock.api.model.container.CraftingOutputPlan; ++import net.raphimc.viabedrock.api.model.container.NativeCraftingOutput; +import net.raphimc.viabedrock.api.util.PacketFactory; import net.raphimc.viabedrock.protocol.BedrockProtocol; import net.raphimc.viabedrock.protocol.ServerboundBedrockPackets; @@ -2889,9 +3066,7 @@ index 892d6fac..c4478b28 100644 +import net.raphimc.viabedrock.protocol.data.enums.java.generated.ContainerInput; import net.raphimc.viabedrock.protocol.model.BedrockItem; import net.raphimc.viabedrock.protocol.model.FullContainerName; -+import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; -+import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestSlotInfo; +import net.raphimc.viabedrock.protocol.model.recipe.ShapedRecipe; +import net.raphimc.viabedrock.protocol.model.recipe.ShapelessRecipe; import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; @@ -2904,7 +3079,6 @@ index 892d6fac..c4478b28 100644 +import net.raphimc.viabedrock.protocol.storage.InventoryTracker; import net.raphimc.viabedrock.protocol.types.BedrockTypes; -+import java.util.ArrayList; +import java.util.List; + public class InventoryContainer extends Container { @@ -2914,7 +3088,7 @@ index 892d6fac..c4478b28 100644 public InventoryContainer(final UserConnection user) { super( -@@ -68,6 +85,7 @@ public class InventoryContainer extends Container { +@@ -68,6 +82,7 @@ public class InventoryContainer extends Container { inventoryContainer.items, inventoryContainer.validBlockTags); this.selectedHotbarSlot = inventoryContainer.selectedHotbarSlot; @@ -2922,7 +3096,7 @@ index 892d6fac..c4478b28 100644 } @Override -@@ -91,6 +109,29 @@ public class InventoryContainer extends Container { +@@ -91,6 +106,29 @@ public class InventoryContainer extends Container { return javaSlot >= 36 && javaSlot < 45 ? javaSlot - 36 : javaSlot; } @@ -2952,7 +3126,7 @@ index 892d6fac..c4478b28 100644 @Override public Item[] getJavaItems() { final InventoryTracker inventoryTracker = this.user.get(InventoryTracker.class); -@@ -100,6 +141,8 @@ public class InventoryContainer extends Container { +@@ -100,6 +138,8 @@ public class InventoryContainer extends Container { final Container hudContainer = inventoryTracker.getHudContainer(); final Item[] combinedItems = StructuredItem.emptyArray(46); @@ -2961,7 +3135,7 @@ index 892d6fac..c4478b28 100644 System.arraycopy(armorItems, 0, combinedItems, 5, armorItems.length); System.arraycopy(inventoryItems, 9, combinedItems, 9, 27); System.arraycopy(inventoryItems, 0, combinedItems, 36, 9); -@@ -134,6 +177,151 @@ public class InventoryContainer extends Container { +@@ -134,6 +174,78 @@ public class InventoryContainer extends Container { return (byte) ContainerID.CONTAINER_ID_INVENTORY.getValue(); } @@ -3014,7 +3188,7 @@ index 892d6fac..c4478b28 100644 + if (action != ContainerInput.PICKUP && action != ContainerInput.QUICK_MOVE) { + return false; + } -+ if (action == ContainerInput.PICKUP && button != 0) { ++ if (action == ContainerInput.PICKUP && (button < 0 || button > 1)) { + return false; + } + final CraftingDataTracker.RecipeMatch match = this.user.get(CraftingDataTracker.class) @@ -3022,82 +3196,9 @@ index 892d6fac..c4478b28 100644 + if (match == null) { + return false; + } -+ final BedrockItem result = craftingResult(match.data()); -+ if (result.isEmpty()) { -+ return false; -+ } -+ final InventoryTracker inventory = this.user.get(InventoryTracker.class); -+ final HudContainer hud = inventory.getHudContainer(); -+ final boolean toInventory = action == ContainerInput.QUICK_MOVE; -+ final int maxStackSize = this.user.get(ItemRewriter.class).maxStackSize(result); -+ final CraftingOutputPlan plan = toInventory -+ ? CraftingOutputPlan.plan(this, result, hud, 28, match.amounts(), maxStackSize) -+ : CraftingOutputPlan.pickup(hud, result, maxStackSize); -+ if (plan == null) { -+ return false; -+ } -+ final int destination = plan.destination(); -+ final int crafts = plan.crafts(); -+ final int outputAmount = plan.outputAmount(); -+ final int destinationNetId = plan.destinationNetId(); -+ final InventoryRequestTracker requests = this.user.get(InventoryRequestTracker.class); -+ final int requestId = requests.nextRequestId(); -+ final List actions = craftingOutputActions(match, hud, -+ this, destination, toInventory, requestId, crafts, outputAmount, -+ destinationNetId); -+ if (actions == null) { -+ return false; -+ } -+ final ItemStackRequestInfo request = new ItemStackRequestInfo(requestId, actions, -+ List.of(), TextProcessingEventOrigin.unknown); -+ requests.addRequest(new InventoryRequestStorage(request, revision, hud.copy(), -+ List.of(this.copy()))); -+ for (int index = 0; index < match.amounts().length; index++) { -+ final int amount = match.amounts()[index]; -+ if (amount == 0) { -+ continue; -+ } -+ final int slot = 28 + index; -+ final BedrockItem ingredient = hud.getItem(slot); -+ hud.setItem(slot, this.itemAfterRemovingAmount(ingredient, amount * crafts)); -+ } -+ plan.apply(toInventory ? this : hud, result); -+ this.refreshCraftingOutput(); -+ PacketFactory.sendBedrockInventoryRequest(this.user, new ItemStackRequestInfo[] {request}); -+ return true; -+ } -+ -+ static List craftingOutputActions( -+ final CraftingDataTracker.RecipeMatch match, final HudContainer hud, -+ final InventoryContainer inventory, -+ final int destination, final boolean toInventory, final int requestId, -+ final int crafts, final int outputAmount, final int destinationNetId) { -+ final List actions = new ArrayList<>(); -+ actions.add(new ItemStackRequestAction.CraftRecipeAction(match.data().networkId(), crafts)); -+ actions.add(new ItemStackRequestAction.CraftResultsDeprecatedAction( -+ List.of(craftingResult(match.data()).copy()), crafts)); -+ for (int index = 0; index < match.amounts().length; index++) { -+ final int amount = match.amounts()[index]; -+ if (amount == 0) { -+ continue; -+ } -+ final int slot = 28 + index; -+ final BedrockItem ingredient = hud.getItem(slot); -+ if (ingredient.netId() == null) { -+ return null; -+ } -+ actions.add(new ItemStackRequestAction.ConsumeAction(amount * crafts, -+ new ItemStackRequestSlotInfo(hud.getFullContainerName(slot), -+ (byte) slot, ingredient.netId()))); -+ } -+ actions.add(new ItemStackRequestAction.PlaceAction(outputAmount, -+ new ItemStackRequestSlotInfo( -+ new FullContainerName(ContainerEnumName.CreatedOutputContainer, null), -+ (byte) 50, requestId), -+ new ItemStackRequestSlotInfo(toInventory -+ ? inventory.getFullContainerName(destination) : hud.getFullContainerName(0), -+ (byte) destination, destinationNetId))); -+ return actions; ++ final var hud = this.user.get(InventoryTracker.class).getHudContainer(); ++ return NativeCraftingOutput.handle(this.user, this, hud, 28, match, ++ revision, action == ContainerInput.QUICK_MOVE, this::refreshCraftingOutput); + } + + private static BedrockItem craftingResult(final CraftingDataStorage data) { @@ -5701,10 +5802,10 @@ index 00000000..f13adecf + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java -index b8929f8b..9e2d8d72 100644 +index 2c792573..c4213b97 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java -@@ -156,6 +156,10 @@ public class InventoryRequestTracker extends StoredObject { +@@ -223,6 +223,10 @@ public class InventoryRequestTracker extends StoredObject { return !this.requests.isEmpty() || this.craftedItemSynchronization != null || this.batchChanges != null; } @@ -5715,7 +5816,7 @@ index b8929f8b..9e2d8d72 100644 public boolean isPredictingClick() { return this.predictingClick; } -@@ -368,6 +372,7 @@ public class InventoryRequestTracker extends StoredObject { +@@ -435,6 +439,7 @@ public class InventoryRequestTracker extends StoredObject { this.transferBatch = null; this.craftedItemSynchronization = null; this.resetRecovery(); @@ -5723,7 +5824,7 @@ index b8929f8b..9e2d8d72 100644 PacketFactory.sendJavaContainerSetContent(this.user(), tracker.getInventoryContainer()); } } -@@ -425,6 +430,7 @@ public class InventoryRequestTracker extends StoredObject { +@@ -492,6 +497,7 @@ public class InventoryRequestTracker extends StoredObject { this.batchChanges.addAll(changed); return; } @@ -5731,7 +5832,7 @@ index b8929f8b..9e2d8d72 100644 if (response.result() == ItemStackNetResult.Success) { this.drainQueuedInteractions(); } -@@ -461,6 +467,7 @@ public class InventoryRequestTracker extends StoredObject { +@@ -528,6 +534,7 @@ public class InventoryRequestTracker extends StoredObject { this.predictingClick = wasPredicting; this.batchChanges = null; } @@ -7210,96 +7311,6 @@ index 00000000..4bfe26b7 + } + +} -diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/CraftingOutputPlanTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/CraftingOutputPlanTest.java -new file mode 100644 -index 00000000..544fbd37 ---- /dev/null -+++ b/src/test/java/net/raphimc/viabedrock/api/model/container/CraftingOutputPlanTest.java -@@ -0,0 +1,84 @@ -+package net.raphimc.viabedrock.api.model.container; -+ -+import com.viaversion.viaversion.api.connection.UserConnection; -+import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; -+import net.raphimc.viabedrock.api.model.container.player.HudContainer; -+import net.raphimc.viabedrock.protocol.model.BedrockItem; -+import org.junit.jupiter.api.Test; -+ -+import java.lang.reflect.Proxy; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+import static org.junit.jupiter.api.Assertions.assertNull; -+ -+class CraftingOutputPlanTest { -+ -+ @Test -+ void shiftCraftMergesIntoAnOccupiedStackAndCraftsAsManyAsFit() { -+ final UserConnection user = (UserConnection) Proxy.newProxyInstance( -+ UserConnection.class.getClassLoader(), new Class[] {UserConnection.class}, -+ (proxy, method, args) -> null); -+ final InventoryContainer inventory = new InventoryContainer(user); -+ final BedrockItem existing = new BedrockItem(2); -+ existing.setAmount(60); -+ existing.setNetId(19); -+ inventory.setItem(9, existing); -+ final BedrockItem result = new BedrockItem(2); -+ result.setAmount(2); -+ final HudContainer grid = new HudContainer(user); -+ final BedrockItem first = new BedrockItem(1); -+ first.setAmount(7); -+ first.setNetId(21); -+ grid.setItem(28, first); -+ final BedrockItem second = new BedrockItem(1); -+ second.setAmount(5); -+ second.setNetId(22); -+ grid.setItem(29, second); -+ -+ final CraftingOutputPlan plan = CraftingOutputPlan.plan( -+ inventory, result, grid, 28, new int[] {1, 1}, 64); -+ -+ assertEquals(9, plan.destination()); -+ assertEquals(2, plan.crafts()); -+ assertEquals(4, plan.outputAmount()); -+ assertEquals(19, plan.destinationNetId()); -+ } -+ -+ @Test -+ void shiftCraftDoesNotMergeItemsWithDifferentUserData() { -+ final UserConnection user = (UserConnection) Proxy.newProxyInstance( -+ UserConnection.class.getClassLoader(), new Class[] {UserConnection.class}, -+ (proxy, method, args) -> null); -+ final InventoryContainer inventory = new InventoryContainer(user); -+ final BedrockItem restricted = new BedrockItem(2); -+ restricted.setAmount(60); -+ restricted.setNetId(19); -+ restricted.setCanPlace(new String[] {"minecraft:stone"}); -+ inventory.setItem(9, restricted); -+ final HudContainer grid = new HudContainer(user); -+ final BedrockItem ingredient = new BedrockItem(1); -+ ingredient.setAmount(5); -+ ingredient.setNetId(21); -+ grid.setItem(28, ingredient); -+ -+ final CraftingOutputPlan plan = CraftingOutputPlan.plan( -+ inventory, new BedrockItem(2), grid, 28, new int[] {1}, 64); -+ -+ assertEquals(10, plan.destination()); -+ assertEquals(5, plan.crafts()); -+ assertEquals(0, plan.destinationNetId()); -+ } -+ -+ @Test -+ void shiftCraftRejectsRecipesWithoutIngredients() { -+ final UserConnection user = (UserConnection) Proxy.newProxyInstance( -+ UserConnection.class.getClassLoader(), new Class[] {UserConnection.class}, -+ (proxy, method, args) -> null); -+ final InventoryContainer inventory = new InventoryContainer(user); -+ final HudContainer grid = new HudContainer(user); -+ -+ assertNull(CraftingOutputPlan.plan(inventory, new BedrockItem(2), -+ grid, 28, new int[] {0, 0, 0, 0}, 64)); -+ } -+ -+} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/CraftingPlacementTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/CraftingPlacementTest.java new file mode 100644 index 00000000..bb3b16e1 @@ -8463,143 +8474,6 @@ index 00000000..14d72266 + } + +} -diff --git a/src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java b/src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java -new file mode 100644 -index 00000000..fbc7a40e ---- /dev/null -+++ b/src/test/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainerTest.java -@@ -0,0 +1,131 @@ -+/* -+ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock -+ * Copyright (C) 2026 RK_01/RaphiMC and contributors -+ * -+ * This program is free software: you can redistribute it and/or modify -+ * it under the terms of the GNU General Public License as published by -+ * the Free Software Foundation, either version 3 of the License, or -+ * (at your option) any later version. -+ */ -+package net.raphimc.viabedrock.api.model.container.player; -+ -+import com.viaversion.viaversion.api.connection.UserConnection; -+ -+import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; -+import net.raphimc.viabedrock.protocol.model.BedrockItem; -+import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; -+import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptor; -+import net.raphimc.viabedrock.protocol.model.recipe.ItemDescriptorType; -+import net.raphimc.viabedrock.protocol.model.recipe.RecipeType; -+import net.raphimc.viabedrock.protocol.model.recipe.ShapedRecipe; -+import net.raphimc.viabedrock.protocol.storage.CraftingDataStorage; -+import net.raphimc.viabedrock.protocol.storage.CraftingDataTracker; -+ -+import org.junit.jupiter.api.Test; -+ -+import java.lang.reflect.Proxy; -+import java.util.List; -+import java.util.UUID; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+import static org.junit.jupiter.api.Assertions.assertInstanceOf; -+ -+class InventoryContainerTest { -+ -+ @Test -+ void playerCraftRequestConsumesHudInputsAndPlacesCreatedOutputOnCursor() { -+ final UserConnection user = (UserConnection) Proxy.newProxyInstance( -+ UserConnection.class.getClassLoader(), new Class[] {UserConnection.class}, -+ (proxy, method, args) -> null); -+ final HudContainer hud = new HudContainer(user); -+ final BedrockItem ingredient = new BedrockItem(1); -+ ingredient.setNetId(19); -+ hud.setItem(30, ingredient); -+ final InventoryContainer inventory = new InventoryContainer(user); -+ final ShapedRecipe recipe = new ShapedRecipe("test", UUID.randomUUID(), -+ "crafting_table", 0, -+ new ItemDescriptor[][] {{new TestDescriptor()}}, -+ List.of(new BedrockItem(2)), false); -+ final CraftingDataTracker.RecipeMatch match = new CraftingDataTracker.RecipeMatch( -+ new CraftingDataStorage(RecipeType.SHAPED, 37, recipe, true), -+ new int[] {0, 0, 1, 0}); -+ -+ final List actions = InventoryContainer.craftingOutputActions( -+ match, hud, inventory, 0, false, -3, 1, 1, 0); -+ final ItemStackRequestAction.CraftRecipeAction craft = assertInstanceOf( -+ ItemStackRequestAction.CraftRecipeAction.class, actions.get(0)); -+ assertEquals(37, craft.recipeNetworkId()); -+ final ItemStackRequestAction.CraftResultsDeprecatedAction results = assertInstanceOf( -+ ItemStackRequestAction.CraftResultsDeprecatedAction.class, actions.get(1)); -+ assertEquals(1, results.timesCrafted()); -+ assertEquals(1, results.resultItems().size()); -+ assertEquals(2, results.resultItems().get(0).identifier()); -+ assertEquals(1, results.resultItems().get(0).amount()); -+ final ItemStackRequestAction.ConsumeAction consume = assertInstanceOf( -+ ItemStackRequestAction.ConsumeAction.class, actions.get(2)); -+ assertEquals(30, consume.item().slot()); -+ assertEquals(19, consume.item().stackNetworkId()); -+ final ItemStackRequestAction.PlaceAction place = assertInstanceOf( -+ ItemStackRequestAction.PlaceAction.class, actions.get(3)); -+ assertEquals(ContainerEnumName.CreatedOutputContainer, place.source().container().name()); -+ assertEquals(50, place.source().slot()); -+ assertEquals(-3, place.source().stackNetworkId()); -+ assertEquals(ContainerEnumName.CursorContainer, place.destination().container().name()); -+ assertEquals(0, place.destination().slot()); -+ } -+ -+ @Test -+ void playerShiftCraftRequestBatchesConsumptionAndMergesOutput() { -+ final UserConnection user = (UserConnection) Proxy.newProxyInstance( -+ UserConnection.class.getClassLoader(), new Class[] {UserConnection.class}, -+ (proxy, method, args) -> null); -+ final HudContainer hud = new HudContainer(user); -+ final BedrockItem ingredient = new BedrockItem(1); -+ ingredient.setAmount(5); -+ ingredient.setNetId(19); -+ hud.setItem(28, ingredient); -+ final InventoryContainer inventory = new InventoryContainer(user); -+ final ShapedRecipe recipe = new ShapedRecipe("test", UUID.randomUUID(), -+ "crafting_table", 0, new ItemDescriptor[][] {{new TestDescriptor()}}, -+ List.of(new BedrockItem(2)), false); -+ final CraftingDataTracker.RecipeMatch match = new CraftingDataTracker.RecipeMatch( -+ new CraftingDataStorage(RecipeType.SHAPED, 37, recipe, true), -+ new int[] {1, 0, 0, 0}); -+ -+ final List actions = InventoryContainer.craftingOutputActions( -+ match, hud, inventory, 9, true, -3, 4, 4, 23); -+ -+ final ItemStackRequestAction.CraftRecipeAction craft = assertInstanceOf( -+ ItemStackRequestAction.CraftRecipeAction.class, actions.get(0)); -+ assertEquals(4, craft.numberOfRequestedCrafts()); -+ final ItemStackRequestAction.CraftResultsDeprecatedAction results = assertInstanceOf( -+ ItemStackRequestAction.CraftResultsDeprecatedAction.class, actions.get(1)); -+ assertEquals(4, results.timesCrafted()); -+ assertEquals(1, results.resultItems().get(0).amount()); -+ final ItemStackRequestAction.ConsumeAction consume = assertInstanceOf( -+ ItemStackRequestAction.ConsumeAction.class, actions.get(2)); -+ assertEquals(4, consume.count()); -+ final ItemStackRequestAction.PlaceAction place = assertInstanceOf( -+ ItemStackRequestAction.PlaceAction.class, actions.get(3)); -+ assertEquals(4, place.count()); -+ assertEquals(23, place.destination().stackNetworkId()); -+ } -+ -+ private record TestDescriptor() implements ItemDescriptor { -+ @Override -+ public ItemDescriptorType getType() { -+ return ItemDescriptorType.ITEM_NAME; -+ } -+ -+ @Override -+ public boolean matchesItem(final UserConnection user, final BedrockItem item) { -+ return !item.isEmpty(); -+ } -+ -+ @Override -+ public ItemDescriptor withAmount(final int amount) { -+ return this; -+ } -+ } -+ -+} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/ItemPrimarySearchTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/ItemPrimarySearchTest.java new file mode 100644 index 00000000..245341a6 @@ -11936,10 +11810,10 @@ index 00000000..d64a324d +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingCursorTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingCursorTest.java new file mode 100644 -index 00000000..636ed8a4 +index 00000000..071c2cfe --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/CraftingCursorTest.java -@@ -0,0 +1,158 @@ +@@ -0,0 +1,363 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -11953,17 +11827,24 @@ index 00000000..636ed8a4 +import net.raphimc.viabedrock.api.model.container.block.CraftingTableContainer; +import net.raphimc.viabedrock.api.util.PacketFactory; +import net.raphimc.viabedrock.protocol.data.enums.java.generated.ContainerInput; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemStackNetResult; +import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseInfo; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseContainerInfo; ++import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseSlotInfo; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestAction; +import net.raphimc.viabedrock.protocol.model.inventory.ItemStackRequestInfo; +import net.raphimc.viabedrock.protocol.model.recipe.RecipeType; +import net.raphimc.viabedrock.protocol.model.recipe.ShapelessRecipe; +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 java.lang.reflect.Proxy; +import java.util.ArrayList; ++import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @@ -11985,15 +11866,13 @@ index 00000000..636ed8a4 + .thenAnswer(call -> { + final var request = ((ItemStackRequestInfo[]) call.getArgument(1))[0]; + sent.add(request); -+ fixture.requests.getRequest(request.requestId()).capturePrediction(fixture.inventory); + return null; + }); + assertTrue(fixture.output.handleClick(0, (short) 0, (byte) 0, ContainerInput.PICKUP)); + assertEquals(4, fixture.cursor().amount()); + assertEquals(1, fixture.ingredient().amount()); -+ fixture.cursor().setNetId(23); -+ fixture.ingredient().setNetId(18); -+ fixture.requests.removeRequest(sent.getFirst().requestId()); ++ fixture.confirm(sent.getFirst(), 4, 23, 1, 18); ++ assertFalse(fixture.requests.hasPending()); + + assertTrue(fixture.output.handleClick(1, (short) 0, (byte) 0, ContainerInput.PICKUP)); + assertEquals(8, fixture.cursor().amount()); @@ -12001,7 +11880,7 @@ index 00000000..636ed8a4 + final var request = sent.getLast(); + final var craft = assertInstanceOf(ItemStackRequestAction.CraftRecipeAction.class, request.actions().get(0)); + final var consume = assertInstanceOf(ItemStackRequestAction.ConsumeAction.class, request.actions().get(2)); -+ final var place = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, request.actions().get(3)); ++ final var place = assertInstanceOf(ItemStackRequestAction.TakeAction.class, request.actions().get(3)); + assertEquals(1, craft.numberOfRequestedCrafts()); + assertEquals(1, consume.count()); + assertEquals(18, consume.item().stackNetworkId()); @@ -12037,9 +11916,182 @@ index 00000000..636ed8a4 + final Fixture fixture = new Fixture(table); + try (var packets = mockStatic(PacketFactory.class)) { + assertTrue(fixture.output.handleClick(0, (short) 0, (byte) 0, ContainerInput.QUICK_MOVE)); -+ assertEquals(8, fixture.inventory.getInventoryContainer().getItem(9).amount()); ++ assertEquals(8, fixture.inventory.getInventoryContainer().getItem(0).amount()); ++ assertTrue(fixture.cursor().isEmpty()); ++ assertTrue(fixture.ingredient().isEmpty()); ++ } ++ } ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void rightPickupSelectsPrimaryBeforeReturningBottlesAndRestoresEveryCollection(final boolean table) { ++ final var fixture = Fixture.honey(table); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ final var sent = new ArrayList(); ++ packets.when(() -> PacketFactory.sendBedrockInventoryRequest(eq(fixture.user), any())) ++ .thenAnswer(call -> { ++ sent.addAll(Arrays.asList((ItemStackRequestInfo[]) call.getArgument(1))); ++ return null; ++ }); ++ assertTrue(fixture.output.handleClick(0, (short) 0, (byte) 1, ContainerInput.PICKUP)); ++ final var request = sent.getFirst(); ++ final var tail = request.actions().subList(request.actions().size() - 4, request.actions().size()); ++ assertEquals(new ItemStackRequestAction.CreateAction(0), tail.get(0)); ++ final var take = assertInstanceOf(ItemStackRequestAction.TakeAction.class, tail.get(1)); ++ assertEquals(1, take.count()); ++ assertEquals(ContainerEnumName.CreatedOutputContainer, take.source().container().name()); ++ assertEquals(fixture.inventory.getHudContainer().requestSlotInfo(0, 0), take.destination()); ++ assertEquals(new ItemStackRequestAction.CreateAction(1), tail.get(2)); ++ final var bottles = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, tail.get(3)); ++ assertEquals(4, bottles.count()); ++ assertEquals(fixture.inventory.getInventoryContainer().requestSlotInfo(0, 0), bottles.destination()); ++ assertEquals(1, fixture.cursor().amount()); ++ assertEquals(4, fixture.inventory.getInventoryContainer().getItem(0).amount()); ++ assertEquals(3, fixture.ingredient().amount()); ++ fixture.requests.restoreRejectedRequest(fixture.requests.getRequest(request.requestId())); + assertTrue(fixture.cursor().isEmpty()); ++ assertTrue(fixture.inventory.getInventoryContainer().getItem(0).isEmpty()); ++ assertEquals(4, fixture.ingredient().amount()); ++ } ++ } ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void shiftCraftsAllInputsInOnePacketWithLinkedStackSizedRequests(final boolean table) { ++ final var fixture = new Fixture(table); ++ fixture.grid.setItem(fixture.gridSlot, item(1, 31, 19)); ++ fixture.inventory.getInventoryContainer().setItem(0, item(2, 4, 23)); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ final var sent = new ArrayList(); ++ packets.when(() -> PacketFactory.sendBedrockInventoryRequest(eq(fixture.user), any())) ++ .thenAnswer(call -> { ++ sent.addAll(Arrays.asList((ItemStackRequestInfo[]) call.getArgument(1))); ++ return null; ++ }); ++ assertTrue(fixture.output.handleClick(0, (short) 0, (byte) 0, ContainerInput.QUICK_MOVE)); ++ assertEquals(2, sent.size()); ++ packets.verify(() -> PacketFactory.sendBedrockInventoryRequest(eq(fixture.user), any()), times(1)); ++ assertEquals(16, assertInstanceOf(ItemStackRequestAction.CraftRecipeAction.class, sent.getFirst().actions().getFirst()).numberOfRequestedCrafts()); ++ assertEquals(15, assertInstanceOf(ItemStackRequestAction.CraftRecipeAction.class, sent.getLast().actions().getFirst()).numberOfRequestedCrafts()); ++ for (final var request : sent) { ++ assertEquals(1, assertInstanceOf(ItemStackRequestAction.CraftResultsDeprecatedAction.class, request.actions().get(1)).timesCrafted()); ++ } ++ final var first = sent.getFirst(); ++ assertEquals(60, assertInstanceOf(ItemStackRequestAction.PlaceAction.class, first.actions().get(3)).count()); ++ assertEquals(4, assertInstanceOf(ItemStackRequestAction.PlaceAction.class, first.actions().get(4)).count()); ++ final var last = sent.getLast(); ++ assertEquals(first.requestId(), assertInstanceOf(ItemStackRequestAction.ConsumeAction.class, last.actions().get(2)).item().stackNetworkId()); ++ assertEquals(first.requestId(), assertInstanceOf(ItemStackRequestAction.PlaceAction.class, last.actions().get(3)).destination().stackNetworkId()); + assertTrue(fixture.ingredient().isEmpty()); ++ assertEquals(64, fixture.inventory.getInventoryContainer().getItem(0).amount()); ++ assertEquals(64, fixture.inventory.getInventoryContainer().getItem(1).amount()); ++ assertTrue(fixture.cursor().isEmpty()); ++ } ++ } ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void shiftCraftReturnsEveryBottleFromTheWholeGrid(final boolean table) { ++ final var fixture = Fixture.honey(table); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ final var sent = new ArrayList(); ++ packets.when(() -> PacketFactory.sendBedrockInventoryRequest(eq(fixture.user), any())) ++ .thenAnswer(call -> { ++ sent.addAll(Arrays.asList((ItemStackRequestInfo[]) call.getArgument(1))); ++ return null; ++ }); ++ assertTrue(fixture.output.handleClick(0, (short) 0, (byte) 0, ContainerInput.QUICK_MOVE)); ++ assertEquals(1, sent.size()); ++ final var actions = sent.getFirst().actions(); ++ assertEquals(4, assertInstanceOf(ItemStackRequestAction.CraftRecipeAction.class, actions.getFirst()).numberOfRequestedCrafts()); ++ assertEquals(1, assertInstanceOf(ItemStackRequestAction.CraftResultsDeprecatedAction.class, actions.get(1)).timesCrafted()); ++ assertEquals(16, assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.getLast()).count()); ++ assertEquals(item(2, 4, sent.getFirst().requestId()), fixture.inventory.getInventoryContainer().getItem(0)); ++ assertEquals(item(4, 16, sent.getFirst().requestId()), fixture.inventory.getInventoryContainer().getItem(1)); ++ assertTrue(fixture.cursor().isEmpty()); ++ assertTrue(fixture.ingredient().isEmpty()); ++ } ++ } ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void shiftCraftUsesCombinedPartialStackCapacityAndPreservesUnrelatedCursor(final boolean table) { ++ final var fixture = Fixture.honey(table); ++ fixture.fillInventory(); ++ final var player = fixture.inventory.getInventoryContainer(); ++ player.setItem(0, item(2, 63, 80)); ++ player.setItem(1, item(2, 63, 81)); ++ final var held = item(3, 7, 90); ++ fixture.inventory.getHudContainer().setItem(0, held); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ final var sent = new ArrayList(); ++ packets.when(() -> PacketFactory.sendBedrockInventoryRequest(eq(fixture.user), any())) ++ .thenAnswer(call -> { ++ sent.addAll(Arrays.asList((ItemStackRequestInfo[]) call.getArgument(1))); ++ return null; ++ }); ++ assertTrue(fixture.output.handleClick(0, (short) 0, (byte) 0, ContainerInput.QUICK_MOVE)); ++ assertEquals(1, sent.size()); ++ final var actions = sent.getFirst().actions(); ++ assertEquals(2, assertInstanceOf(ItemStackRequestAction.CraftRecipeAction.class, actions.getFirst()).numberOfRequestedCrafts()); ++ final var first = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.get(actions.size() - 4)); ++ final var second = assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.get(actions.size() - 3)); ++ assertEquals(player.requestSlotInfo(0, 80), first.destination()); ++ assertEquals(player.requestSlotInfo(1, 81), second.destination()); ++ assertEquals(1, first.count()); ++ assertEquals(1, second.count()); ++ final var drop = assertInstanceOf(ItemStackRequestAction.DropAction.class, actions.getLast()); ++ assertEquals(8, drop.count()); ++ assertEquals(ContainerEnumName.CreatedOutputContainer, drop.item().container().name()); ++ assertFalse(drop.randomly()); ++ assertEquals(2, fixture.ingredient().amount()); ++ assertSame(held, fixture.cursor()); ++ } ++ } ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void pickupFillsPartialReturnedBottleStackBeforeDroppingTheRemainder(final boolean table) { ++ final var fixture = Fixture.honey(table); ++ fixture.fillInventory(); ++ fixture.inventory.getInventoryContainer().setItem(0, item(4, 63, 80)); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ final var sent = new ArrayList(); ++ packets.when(() -> PacketFactory.sendBedrockInventoryRequest(eq(fixture.user), any())) ++ .thenAnswer(call -> { ++ sent.addAll(Arrays.asList((ItemStackRequestInfo[]) call.getArgument(1))); ++ return null; ++ }); ++ assertTrue(fixture.output.handleClick(0, (short) 0, (byte) 0, ContainerInput.PICKUP)); ++ final var actions = sent.getFirst().actions(); ++ assertEquals(1, assertInstanceOf(ItemStackRequestAction.PlaceAction.class, actions.get(actions.size() - 2)).count()); ++ assertEquals(3, assertInstanceOf(ItemStackRequestAction.DropAction.class, actions.getLast()).count()); ++ assertEquals(64, fixture.inventory.getInventoryContainer().getItem(0).amount()); ++ assertEquals(1, fixture.cursor().amount()); ++ } ++ } ++ ++ @Test ++ void shiftCraftKeepsStacksRejectedByNativeItemCompatibility() { ++ final var fixture = new Fixture(false); ++ final var restricted = item(2, 60, 80); ++ restricted.setCanPlace(new String[] {"minecraft:stone"}); ++ fixture.inventory.getInventoryContainer().setItem(0, restricted); ++ doReturn(false).when(fixture.items).canMerge(any(), same(restricted)); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ assertTrue(fixture.output.handleClick(0, (short) 0, (byte) 0, ContainerInput.QUICK_MOVE)); ++ assertSame(restricted, fixture.inventory.getInventoryContainer().getItem(0)); ++ assertEquals(8, fixture.inventory.getInventoryContainer().getItem(1).amount()); ++ } ++ } ++ ++ @Test ++ void shiftCraftRefusesRecipesWithoutConsumedInputs() { ++ final var fixture = new Fixture(false, List.of(item(2, 1, null)), new int[4], new int[4]); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ assertFalse(fixture.output.handleClick(0, (short) 0, (byte) 0, ContainerInput.QUICK_MOVE)); ++ assertFalse(fixture.requests.hasPending()); ++ packets.verifyNoInteractions(); + } + } + @@ -12057,34 +12109,61 @@ index 00000000..636ed8a4 + (proxy, method, args) -> method.getName().equals("get") ? this.storage.get(args[0]) : null); + 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 Container output; + private final Container grid; + private final int gridSlot; + + private Fixture(final boolean table) { ++ this(table, List.of(item(2, 4, null)), new int[] {1, 0, 0, 0}, new int[] {2, 0, 0, 0}); ++ } ++ ++ private Fixture(final boolean table, final List results, final int[] amounts, final int[] counts) { + this.output = table ? new CraftingTableContainer(this.user, (byte) 5, null, null) + : this.inventory.getInventoryContainer(); + this.grid = table ? this.output : this.inventory.getHudContainer(); + this.gridSlot = table ? 32 : 28; -+ if (table) { -+ this.inventory.setCurrentContainer(this.output); -+ } + this.storage.put(InventoryTracker.class, this.inventory); + this.storage.put(InventoryRequestTracker.class, this.requests); -+ final ItemRewriter items = mock(ItemRewriter.class); -+ when(items.maxStackSize(any())).thenReturn(64); -+ this.storage.put(ItemRewriter.class, items); ++ this.requests.setPredictingClick(true); ++ this.inventory.setCurrentContainer(this.output); ++ when(this.items.maxStackSize(any())).thenReturn(64); ++ when(this.items.canMerge(any(), any())).thenAnswer(call -> ++ !((BedrockItem) call.getArgument(0)).isDifferent(call.getArgument(1))); ++ this.storage.put(ItemRewriter.class, this.items); + final CraftingDataTracker recipes = mock(CraftingDataTracker.class); + this.storage.put(CraftingDataTracker.class, recipes); -+ final var recipe = new ShapelessRecipe("test", UUID.randomUUID(), "crafting_table", 0, -+ List.of(), List.of(item(2, 4, null))); -+ final int[] amounts = new int[table ? 9 : 4]; -+ amounts[0] = 1; ++ final var recipe = new ShapelessRecipe("test", UUID.randomUUID(), "crafting_table", 0, List.of(), results); + final var match = new CraftingDataTracker.RecipeMatch( + new CraftingDataStorage(RecipeType.SHAPELESS, 37, recipe, true), amounts); -+ when(recipes.getRecipeMatch(any())) -+ .thenAnswer(_ -> this.ingredient().isEmpty() ? null : match); -+ this.grid.setItem(this.gridSlot, item(1, 2, 19)); ++ when(recipes.getRecipeMatch(any())).thenReturn(match); ++ for (int index = 0; index < counts.length; index++) { ++ if (counts[index] > 0) { ++ this.grid.setItem(this.gridSlot + index, item(1, counts[index], 19 + index)); ++ } ++ } ++ } ++ ++ private static Fixture honey(final boolean table) { ++ final int[] amounts = table ? new int[] {1, 1, 0, 1, 1, 0, 0, 0, 0} : new int[] {1, 1, 1, 1}; ++ final int[] counts = Arrays.stream(amounts).map(amount -> amount * 4).toArray(); ++ return new Fixture(table, List.of(item(2, 1, null), item(4, 4, null)), amounts, counts); ++ } ++ ++ private void fillInventory() { ++ for (int index = 0; index < 36; index++) { ++ this.inventory.getInventoryContainer().setItem(index, item(3, 64, 100 + index)); ++ } ++ } ++ ++ private void confirm(final ItemStackRequestInfo request, final int cursorCount, final int cursorId, ++ final int ingredientCount, final int ingredientId) { ++ this.requests.handleResponse(new ItemStackResponseInfo(ItemStackNetResult.Success, request.requestId(), List.of( ++ new ItemStackResponseContainerInfo(this.inventory.getHudContainer().getFullContainerName(0), List.of( ++ new ItemStackResponseSlotInfo((byte) 0, (byte) 0, (byte) cursorCount, cursorId, "", null, 0))), ++ new ItemStackResponseContainerInfo(this.grid.getFullContainerName(this.gridSlot), List.of( ++ new ItemStackResponseSlotInfo((byte) this.gridSlot, (byte) this.gridSlot, ++ (byte) ingredientCount, ingredientId, "", null, 0)))))); + } + + private BedrockItem cursor() { 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 31d19528..846aa753 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 @@ -10,7 +10,14 @@ Resolve native ingredient descriptors and connection-owned item definitions befo ## Validation and remaining limits -Regression coverage includes `NativeFurnaceOptionsTest`, `CookingRecipeAvailabilityTest`, `CookingRecipeInputsTest`, `CookingRecipeSourcesTest`. +Regression coverage includes `NativeFurnaceOptionsTest`, `CookingRecipeAvailabilityTest`, `CookingRecipeInputsTest`, `CookingRecipeSourcesTest`, `CraftingCursorTest`, and `InventoryTransferBatchTest`. + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 captures establish manual output pickup and Shift-click behavior for planks, cake, and honey blocks. +Manual requests keep CraftResults times one, select primary output before returned containers, and use Take for cursor output. +Right-click crafts once. Shift-click fills inventory through linked stack-sized requests in one packet. +Capacity sums compatible inventory stacks. Returned containers merge before empty slots and drop from created output when they do not fit. +Player and workbench tests exercise the shared handler, predicted identities, returned-container placement, cursor preservation, and rollback. +Installed direct and ViaProxy checks match the native manual requests and authoritative quantities for the [documented controls](../../../docs/native-player-inventory.md). Controlled native recipe and naming references cover their declared boundaries. Complete installed player/cooking controllers and arbitrary recipe admission remain unfinished. 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 8bd0c900..b145655c 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 90aea9d9b603a5dab3e2446356d90ebd9788c300 Mon Sep 17 00:00:00 2001 +From 3e23298b27e16a05aceec84eebf13dae465c7982 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 @@ -1234,10 +1234,10 @@ index 271d9fad..5aad3b96 100644 default boolean matchesSource(final BedrockItem source, final Registry registry, final Function descriptor) { diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java b/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java -index 1b9d7c05..eac4c053 100644 +index a90cf752..b8efd50e 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/block/CraftingTableContainer.java -@@ -223,7 +223,7 @@ public final class CraftingTableContainer extends Container { +@@ -145,7 +145,7 @@ public final class CraftingTableContainer extends Container { return true; } @@ -3046,7 +3046,7 @@ index 1b68e72e..2073bd20 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java -index 06d9fd00..0f1aede7 100644 +index 3dcb9b50..cb23fe56 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java @@ -20,6 +20,7 @@ package net.raphimc.viabedrock.protocol.storage; @@ -3057,7 +3057,7 @@ index 06d9fd00..0f1aede7 100644 import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; import com.viaversion.viaversion.api.type.Types; import com.viaversion.nbt.tag.IntTag; -@@ -345,6 +346,10 @@ public class InventoryRequestTracker extends StoredObject { +@@ -412,6 +413,10 @@ public class InventoryRequestTracker extends StoredObject { return this.queueInteraction(new QueuedButtonClick(container, button)); } @@ -3068,7 +3068,7 @@ index 06d9fd00..0f1aede7 100644 boolean queueInteraction(final QueuedInteraction interaction) { if (this.recovery.isActive() || this.queuedInteractions.size() >= MAX_QUEUED_INTERACTIONS) { this.queuedInteractions.clear(); -@@ -930,6 +935,25 @@ public class InventoryRequestTracker extends StoredObject { +@@ -997,6 +1002,25 @@ public class InventoryRequestTracker extends StoredObject { boolean apply(); } diff --git a/patches/viabedrock/deferred/0013-embed-completed-break-inventory-actions.patch b/patches/viabedrock/deferred/0013-embed-completed-break-inventory-actions.patch index 1db6166c..74b4af84 100644 --- a/patches/viabedrock/deferred/0013-embed-completed-break-inventory-actions.patch +++ b/patches/viabedrock/deferred/0013-embed-completed-break-inventory-actions.patch @@ -1,4 +1,4 @@ -From c33618a3cf63136dd70725871c20df246cbb4926 Mon Sep 17 00:00:00 2001 +From 2562fd76203cdbd96b4dc1f1efef21d1e6f96646 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(mining): reconcile completed and creative block breaks @@ -368,7 +368,7 @@ index 16803a78..531a753c 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java -index 9e2d8d72..9f149ccb 100644 +index c4213b97..1e40221d 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java @@ -22,6 +22,7 @@ import com.viaversion.viaversion.api.connection.StoredObject; @@ -379,7 +379,7 @@ index 9e2d8d72..9f149ccb 100644 import com.viaversion.viaversion.libs.fastutil.ints.Int2ObjectOpenHashMap; import net.raphimc.viabedrock.ViaBedrock; -@@ -74,6 +75,7 @@ public class InventoryRequestTracker extends StoredObject { +@@ -75,6 +76,7 @@ public class InventoryRequestTracker extends StoredObject { private CraftedItemSynchronization craftedItemSynchronization; private TransferBatch transferBatch; private Set batchChanges; @@ -387,7 +387,7 @@ index 9e2d8d72..9f149ccb 100644 public InventoryRequestTracker(final UserConnection user) { super(user); -@@ -168,6 +170,19 @@ public class InventoryRequestTracker extends StoredObject { +@@ -235,6 +237,19 @@ public class InventoryRequestTracker extends StoredObject { this.predictingClick = predictingClick; } @@ -407,7 +407,7 @@ index 9e2d8d72..9f149ccb 100644 public boolean queueClick(final Container container, final int revision, final short slot, final byte button, final ContainerInput action) { return this.queueInteraction(new QueuedClick(container, revision, slot, button, action)); -@@ -304,6 +319,11 @@ public class InventoryRequestTracker extends StoredObject { +@@ -371,6 +386,11 @@ public class InventoryRequestTracker extends StoredObject { PacketFactory.sendJavaContainerSetContent(this.user(), this.user().get(InventoryTracker.class).getInventoryContainer()); } @@ -419,7 +419,7 @@ index 9e2d8d72..9f149ccb 100644 if (!this.hasPending()) { return; } -@@ -383,6 +403,13 @@ public class InventoryRequestTracker extends StoredObject { +@@ -450,6 +470,13 @@ public class InventoryRequestTracker extends StoredObject { } public void handleResponse(final ItemStackResponseInfo response) { @@ -433,7 +433,7 @@ index 9e2d8d72..9f149ccb 100644 if (this.recovery.isActive() && this.requests.containsKey(response.requestId())) { // The original screen has been closed. Its response cannot restore a complete // player inventory or cursor, and its slot IDs may now refer to another screen. -@@ -606,6 +633,56 @@ public class InventoryRequestTracker extends StoredObject { +@@ -673,6 +700,56 @@ public class InventoryRequestTracker extends StoredObject { return item; } @@ -490,7 +490,7 @@ index 9e2d8d72..9f149ccb 100644 void drainQueuedInteractions() { final InventoryTracker tracker = this.user().get(InventoryTracker.class); while (!this.hasPending() && tracker.getPendingCloseContainer() == null -@@ -622,6 +699,9 @@ public class InventoryRequestTracker extends StoredObject { +@@ -689,6 +766,9 @@ public class InventoryRequestTracker extends StoredObject { } } 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 add84cae..1ecdac3c 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 af9037997d896db085045ce995bb7d5a10d02a69 Mon Sep 17 00:00:00 2001 +From 4b34da3257b13a83590bffd65e010560056850d9 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 @@ -1090,7 +1090,7 @@ index 00000000..cc516ae8 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java -index 0f1aede7..60e2ee7c 100644 +index cb23fe56..358a8f3a 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java @@ -28,6 +28,8 @@ import com.viaversion.viaversion.libs.fastutil.ints.Int2ObjectOpenHashMap; @@ -1102,7 +1102,7 @@ index 0f1aede7..60e2ee7c 100644 import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; import net.raphimc.viabedrock.api.util.PacketFactory; import net.raphimc.viabedrock.protocol.BedrockProtocol; -@@ -47,6 +49,7 @@ import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseContaine +@@ -48,6 +50,7 @@ import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseContaine import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseInfo; import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseSlotInfo; import net.raphimc.viabedrock.protocol.packet.InventoryPackets; @@ -1110,7 +1110,7 @@ index 0f1aede7..60e2ee7c 100644 import net.raphimc.viabedrock.protocol.types.BedrockTypes; import java.util.ArrayDeque; -@@ -342,6 +345,14 @@ public class InventoryRequestTracker extends StoredObject { +@@ -409,6 +412,14 @@ public class InventoryRequestTracker extends StoredObject { }); } @@ -1125,7 +1125,7 @@ index 0f1aede7..60e2ee7c 100644 public boolean queueButtonClick(final Container container, final int button) { return this.queueInteraction(new QueuedButtonClick(container, button)); } -@@ -569,6 +580,16 @@ public class InventoryRequestTracker extends StoredObject { +@@ -636,6 +647,16 @@ public class InventoryRequestTracker extends StoredObject { } } @@ -1142,7 +1142,7 @@ index 0f1aede7..60e2ee7c 100644 if (this.batchChanges != null) { this.batchChanges.addAll(changed); return; -@@ -965,6 +986,22 @@ public class InventoryRequestTracker extends StoredObject { +@@ -1032,6 +1053,22 @@ public class InventoryRequestTracker extends StoredObject { } } diff --git a/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch b/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch index 5285e0f4..ed1f4c6d 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 0fc8b84410513e50392eb681bf9fe975cbf5bae0 Mon Sep 17 00:00:00 2001 +From cc017567c3740e16afd9b84f20730e887ec64ec6 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 @@ -313,10 +313,10 @@ index f5e0d204..f5cbbaf0 100644 final var status = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, wrapper.user()); status.write(Types.STRING, NativeContainerStatus.CHANNEL); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java -index 9f149ccb..06d9fd00 100644 +index 1e40221d..3dcb9b50 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/InventoryRequestTracker.java -@@ -45,6 +45,7 @@ import net.raphimc.viabedrock.protocol.model.BedrockItem; +@@ -46,6 +46,7 @@ import net.raphimc.viabedrock.protocol.model.BedrockItem; import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseContainerInfo; import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseInfo; import net.raphimc.viabedrock.protocol.model.inventory.ItemStackResponseSlotInfo; @@ -324,7 +324,7 @@ index 9f149ccb..06d9fd00 100644 import net.raphimc.viabedrock.protocol.types.BedrockTypes; import java.util.ArrayDeque; -@@ -76,6 +77,10 @@ public class InventoryRequestTracker extends StoredObject { +@@ -77,6 +78,10 @@ public class InventoryRequestTracker extends StoredObject { private TransferBatch transferBatch; private Set batchChanges; private MineBlockRequest mineBlockRequest; @@ -335,7 +335,7 @@ index 9f149ccb..06d9fd00 100644 public InventoryRequestTracker(final UserConnection user) { super(user); -@@ -159,13 +164,85 @@ public class InventoryRequestTracker extends StoredObject { +@@ -226,13 +231,85 @@ public class InventoryRequestTracker extends StoredObject { } public boolean canStartRecipePlacement() { @@ -422,7 +422,7 @@ index 9f149ccb..06d9fd00 100644 public void setPredictingClick(final boolean predictingClick) { this.predictingClick = predictingClick; } -@@ -185,7 +262,7 @@ public class InventoryRequestTracker extends StoredObject { +@@ -252,7 +329,7 @@ public class InventoryRequestTracker extends StoredObject { public boolean queueClick(final Container container, final int revision, final short slot, final byte button, final ContainerInput action) { @@ -431,7 +431,7 @@ index 9f149ccb..06d9fd00 100644 } public boolean isWaitingForInventoryOpen() { -@@ -231,15 +308,17 @@ public class InventoryRequestTracker extends StoredObject { +@@ -298,15 +375,17 @@ public class InventoryRequestTracker extends StoredObject { public void handleInventoryOpen(final Container container) { for (final InventoryOpenClick click : this.pendingInventoryOpenClicks.drain()) { @@ -452,7 +452,7 @@ index 9f149ccb..06d9fd00 100644 PacketFactory.sendJavaContainerSetContent(this.user(), container); } } -@@ -313,6 +392,31 @@ public class InventoryRequestTracker extends StoredObject { +@@ -380,6 +459,31 @@ public class InventoryRequestTracker extends StoredObject { } public void tick() { @@ -484,7 +484,7 @@ index 9f149ccb..06d9fd00 100644 if (this.pendingInventoryOpenClicks.expire( System.nanoTime(), RESPONSE_TIMEOUT_NANOS)) { this.clearQueuedInteractions(); -@@ -325,6 +429,11 @@ public class InventoryRequestTracker extends StoredObject { +@@ -392,6 +496,11 @@ public class InventoryRequestTracker extends StoredObject { this.mineBlockRequest = null; } if (!this.hasPending()) { @@ -496,7 +496,7 @@ index 9f149ccb..06d9fd00 100644 return; } final long now = System.nanoTime(); -@@ -383,7 +492,7 @@ public class InventoryRequestTracker extends StoredObject { +@@ -450,7 +559,7 @@ public class InventoryRequestTracker extends StoredObject { this.closeTemporaryInventoryIfIdle(); } } @@ -505,7 +505,7 @@ index 9f149ccb..06d9fd00 100644 return; } final InventoryTracker tracker = this.user().get(InventoryTracker.class); -@@ -391,6 +500,8 @@ public class InventoryRequestTracker extends StoredObject { +@@ -458,6 +567,8 @@ public class InventoryRequestTracker extends StoredObject { this.requests.clear(); this.transferBatch = null; this.craftedItemSynchronization = null; @@ -514,7 +514,7 @@ index 9f149ccb..06d9fd00 100644 this.resetRecovery(); tracker.getInventoryContainer().updateCraftingOutput(); PacketFactory.sendJavaContainerSetContent(this.user(), tracker.getInventoryContainer()); -@@ -685,7 +796,8 @@ public class InventoryRequestTracker extends StoredObject { +@@ -752,7 +863,8 @@ public class InventoryRequestTracker extends StoredObject { void drainQueuedInteractions() { final InventoryTracker tracker = this.user().get(InventoryTracker.class); @@ -524,7 +524,7 @@ index 9f149ccb..06d9fd00 100644 && !this.isWaitingForInventoryOpen() && !this.queuedInteractions.isEmpty()) { final QueuedInteraction interaction = this.queuedInteractions.removeFirst(); -@@ -702,6 +814,29 @@ public class InventoryRequestTracker extends StoredObject { +@@ -769,6 +881,29 @@ public class InventoryRequestTracker extends StoredObject { private record MineBlockRequest(int requestId, int hotbarSlot, int stackNetworkId, long sentAtNanos) { } @@ -554,7 +554,7 @@ index 9f149ccb..06d9fd00 100644 static final class PendingInventoryOpenClicks { private final ArrayDeque clicks = new ArrayDeque<>(); -@@ -795,12 +930,14 @@ public class InventoryRequestTracker extends StoredObject { +@@ -862,12 +997,14 @@ public class InventoryRequestTracker extends StoredObject { boolean apply(); } From c9c6afe5e36e6f23c8214f0f799c0dd6eed4bf9d Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 18:59:11 +0100 Subject: [PATCH 09/47] fix(inventory): retain recipe queries across openings Carry the raw query into a fresh player or workbench model when the prior opening retires. Preserve explicit clearing while keeping selection, collections, generations, and item actions scoped to each opening. Fold the change into the owning catalog patch. Native 1.26.60 captures establish workbench reopening and workbench-to-player query retention. Lifecycle regressions reproduce the old loss, and installed direct/proxy controls cover settled text, matching rows, clearing, and a successful craft. Native disconnect/world lifetime and close-before-response timing remain unverified. --- docs/bedrock-coverage.md | 2 +- docs/native-player-inventory.md | 6 +- ...ain-bedrock-creative-catalog-entries.patch | 85 ++++++++++++++++--- ...ain-bedrock-creative-catalog-entries.pr.md | 5 ++ 4 files changed, 85 insertions(+), 13 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 2e180caf..85826f3f 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, Creative catalog actions, Survival bulk and manual crafting actions, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, search retention across openings, broader Survival recipes, 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, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, 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 3275f5f3..c1556987 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -176,6 +176,9 @@ Core publishes the retained player/workbench rows and full item previews in comp Commands bind to the opening, container generation, snapshot revision, and request sequence. The add-on buffers search edits while a command awaits its reply. Returning to search after a recipe click restores text input without closing the inventory. +Search text survives player and workbench reopenings within a connection, including an explicit clear. +Each opening starts with a fresh model and no selected output. +Installed direct and ViaProxy controls cover retained text and matching rows, replacement queries, explicit clearing, and a server-accepted player craft. The default player and workbench graphs resolve real slots, recipe rows, and local Help views. Survival Help opens Your First Craft. Creative Help opens the Encyclopedia at Creative Mode. @@ -213,7 +216,8 @@ Unchanged Java Creative slot echoes preserve the existing native stack and netwo Installed direct and ViaProxy controls confirm these pointer, capacity, and reopening cases through successful server responses. Recipe families beyond the captured plank, cake, and honey controls, recipes with more than two outputs, and complete installed native parity remain unfinished. -Search text does not yet persist between player and workbench openings as it does in the native client. +Native query lifetime across disconnects and world changes remains unverified. +Query edits that race menu closure still need native comparison. 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 b145655c..98c09680 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 3e23298b27e16a05aceec84eebf13dae465c7982 Mon Sep 17 00:00:00 2001 +From 66fd325ee215965a74eea544905d79996832c448 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 @@ -351,6 +351,18 @@ predicted grid, inventory and cursor mutation for rejection rollback. Native capture and byte fixtures cover two-output cake and honey recipes. Broader recipe families, more than two outputs, live rejection controls and complete screen parity remain separate validation work. + +Retain the recipe query across player and workbench openings within the +connection. Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol +2225 retain honey-block text after complete workbench closure and inherit +the earlier oak-planks query on the next player-inventory opening. + +Copy the last model's raw query before retiring it, then seed the fresh +model with that query. Keep selected output, collections, acknowledgment, +container generation and action sequences scoped to their existing opening. +An explicit empty query remains empty after reopening. Retired models +cannot alter the replacement query, and another connection starts empty. +Native query lifetime across disconnects and world changes remains unverified. --- .../model/NativePlayerInventoryAction.java | 94 +++ .../model/NativePlayerInventoryMessage.java | 271 ++++++++ @@ -371,15 +383,15 @@ complete screen parity remain separate validation work. .../protocol/storage/CraftingDataTracker.java | 134 +++- .../storage/InventoryRequestTracker.java | 24 + .../protocol/storage/InventoryTracker.java | 21 + - .../storage/NativePlayerRecipeState.java | 649 ++++++++++++++++++ + .../storage/NativePlayerRecipeState.java | 654 ++++++++++++++++++ .../storage/PlayerInventoryScreenTracker.java | 40 +- .../NativePlayerInventoryMessageTest.java | 179 +++++ .../container/NativeRecipeCounterTest.java | 27 + .../container/NativeRecipeQuantityTest.java | 61 ++ .../ui/PlayerInventoryUiTest.java | 126 ++++ - .../protocol/model/CreativeCatalogTest.java | 382 +++++++++++ + .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++ .../protocol/packet/InventoryPacketsTest.java | 267 +++++-- - .../rewriter/PlayerRecipeModelTest.java | 471 +++++++++++++ + .../rewriter/PlayerRecipeModelTest.java | 517 ++++++++++++++ ...egisteredPlayerRecipeAvailabilityTest.java | 5 +- .../RegisteredPlayerRecipeSearchTest.java | 3 +- .../storage/CreativeInventoryActionTest.java | 262 +++++++ @@ -387,7 +399,7 @@ complete screen parity remain separate validation work. .../NativePlayerCreativeAuxiliaryTest.java | 333 +++++++++ .../storage/NativePlayerCreativePickTest.java | 138 ++++ .../storage/NativeRecipeCraftingTest.java | 491 +++++++++++++ - 35 files changed, 5679 insertions(+), 171 deletions(-) + 35 files changed, 5730 insertions(+), 171 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 @@ -3142,10 +3154,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..da892e2c +index 00000000..cde67af9 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/NativePlayerRecipeState.java -@@ -0,0 +1,649 @@ +@@ -0,0 +1,654 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -3217,6 +3229,7 @@ index 00000000..da892e2c + private long opening; + private PlayerRecipeContext context; + private PlayerRecipeModel model; ++ private byte[] query = new byte[0]; + private CreativeCatalog catalog; + private InventoryOptions preferences; + private Inputs inputs; @@ -3257,7 +3270,7 @@ index 00000000..da892e2c + final var items = this.user().get(ItemRewriter.class); + this.catalog = items.creativeCatalog(); + // The native player profile exists before this controller reads its filtering policy. -+ this.model = items.playerRecipeModel(context, this.preferences.filtering(), true, new byte[0]); ++ this.model = items.playerRecipeModel(context, this.preferences.filtering(), true, this.query); + } + + public void closePlayer(final long opening) { @@ -3774,6 +3787,10 @@ index 00000000..da892e2c + } + + private void clear() { ++ // The text survives menu closure; collections and item authority still belong to one opening. ++ if (this.model != null) { ++ this.query = this.model.query(); ++ } + this.opening = 0; + this.lastRequest = 0; + this.context = null; @@ -5054,10 +5071,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..92b4e7cd +index 00000000..df62b14f --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/rewriter/PlayerRecipeModelTest.java -@@ -0,0 +1,471 @@ +@@ -0,0 +1,517 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -5430,7 +5447,7 @@ index 00000000..92b4e7cd + } + + @Test -+ void workbenchReopeningReplacesTheModelWhenTheContainerObjectIsReused() throws Exception { ++ void workbenchReopeningRetainsQueryButReplacesTheModelWhenTheContainerObjectIsReused() throws Exception { + final var f = installed(false, false); + final var inventory = f.context.inventory.inventory(); + f.screens.request(new PlayerInventoryScreenRequest(1, true)); @@ -5439,6 +5456,8 @@ index 00000000..92b4e7cd + inventory.setCurrentContainer(workbench); + final var retired = f.state.model(); + assertNotNull(retired); ++ retired.updateQuery("Honey Block"); ++ retired.selectOutput(new BedrockItem(IDENTIFIER)); + assertThrows(IllegalStateException.class, () -> player.tick(5)); + f.screens.request(new PlayerInventoryScreenRequest(1, false)); + assertSame(retired, f.state.model()); @@ -5447,7 +5466,51 @@ index 00000000..92b4e7cd + inventory.setCurrentContainer(workbench); + assertNotNull(f.state.model()); + assertNotSame(retired, f.state.model()); ++ assertArrayEquals(NbtUtf8.encode("Honey Block"), f.state.model().query()); ++ assertTrue(f.state.model().selectedOutput().isEmpty()); + assertThrows(IllegalStateException.class, () -> retired.tick(5)); ++ assertThrows(IllegalStateException.class, () -> retired.updateQuery("late edit")); ++ assertArrayEquals(NbtUtf8.encode("Honey Block"), f.state.model().query()); ++ } ++ ++ @Test ++ void playerAndWorkbenchOpeningsRetainTheLatestQueryAndItsExplicitClear() throws Exception { ++ final var f = installed(false, false); ++ final var inventory = f.context.inventory.inventory(); ++ f.screens.request(new PlayerInventoryScreenRequest(1, true)); ++ final var player = f.state.model(); ++ player.updateQuery("oak planks"); ++ final var workbench = new CraftingTableContainer(f.context.items.user(), (byte) 5, null, null); ++ inventory.setCurrentContainer(workbench); ++ final var table = f.state.model(); ++ assertNotSame(player, table); ++ assertArrayEquals(NbtUtf8.encode("oak planks"), table.query()); ++ table.updateQuery("honey block"); ++ f.screens.request(new PlayerInventoryScreenRequest(1, false)); ++ inventory.setCurrentContainerClosed(false); ++ f.screens.request(new PlayerInventoryScreenRequest(2, true)); ++ final var replacement = f.state.model(); ++ assertNotSame(table, replacement); ++ assertArrayEquals(NbtUtf8.encode("honey block"), replacement.query()); ++ assertThrows(IllegalStateException.class, () -> table.updateQuery("late edit")); ++ replacement.updateQuery(""); ++ f.screens.request(new PlayerInventoryScreenRequest(2, false)); ++ f.screens.request(new PlayerInventoryScreenRequest(3, true)); ++ assertNotSame(replacement, f.state.model()); ++ assertArrayEquals(new byte[0], f.state.model().query()); ++ } ++ ++ @Test ++ void anotherConnectionStartsWithAnEmptyQuery() throws Exception { ++ final var first = installed(false, false); ++ first.screens.request(new PlayerInventoryScreenRequest(1, true)); ++ first.state.model().updateQuery("honey block"); ++ first.screens.request(new PlayerInventoryScreenRequest(1, false)); ++ final var second = installed(false, false); ++ second.screens.request(new PlayerInventoryScreenRequest(1, true)); ++ assertArrayEquals(new byte[0], second.state.model().query()); ++ first.screens.request(new PlayerInventoryScreenRequest(2, true)); ++ assertArrayEquals(NbtUtf8.encode("honey block"), first.state.model().query()); + } + + private static Installed installed(final boolean instabuild, final boolean filtering) throws Exception { 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 171e2686..b4d589fa 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,6 +61,11 @@ 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. +The connection retains the raw search query when an opening retires and supplies it to the next fresh model. +Native 1.26.60.5 controls establish reopening and workbench-to-player retention. +Lifecycle regressions cover reused containers, query replacement and clearing, connection separation, and refusal of retired-model edits. +See the [inventory reference](../../../docs/native-player-inventory.md) for installed validation and remaining lifetime limits. + 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. From 7f81b0fac014920553d9875b656ed06b31de0121 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:00:02 +0100 Subject: [PATCH 10/47] docs(coverage): retain confirmed rendering reports --- docs/bedrock-coverage.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 85826f3f..3689706b 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -39,7 +39,8 @@ 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 update from 0.3.61 to 0.4.2 caused reported inventory lag spikes and overlapping menus. +- The player confirms that the oversized shield, distorted skin pose, and terrain showing through the floor under a bed occur in 0.4.3 screenshots. - 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. From 1ce284b00f6eaaab5ba4dea5e2db23755bbe48e7 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:12:53 +0100 Subject: [PATCH 11/47] fix(items): allow food use on Bedrock Peaceful difficulty Retain initial and server-confirmed difficulty in core, then admit the food transaction and local consumable cycle on Peaceful. Keep requests server-authoritative and cover remotely translated ViaProxy clients. Fold core changes into their owning patches and add the client hook as an upstreamable patch with its connection-capability prerequisite. Official 1.26.60.5/BDS 1.26.60.6 captures establish full-hunger bread refusal on Normal, consumption on Peaceful, and refusal after switching back. The old installed client never starts use on Peaceful. Both rebuilt routes now pass those transitions and a hungry Normal control, with one server completion and one consumed item in each allowed case. The full build passes 3,281 tests with 325 skips. Preserve the separate DataDriven apple discrepancy and original intermittent food-use report. --- docs/bedrock-coverage.md | 1 + ...erver-authoritative-bedrock-requests.patch | 6 +- ...edrock-recipes-and-place-ingredients.patch | 6 +- ...08-signal-sustained-bedrock-item-use.patch | 144 ++++++++++--- ...08-signal-sustained-bedrock-item-use.pr.md | 119 ++++------- ...ronize-glide-rockets-with-auth-input.patch | 18 +- ...rse-properties-and-composed-metadata.patch | 6 +- ...orward-difficulty-changes-to-bedrock.patch | 191 +++++++++++++++++- ...orward-difficulty-changes-to-bedrock.pr.md | 12 ++ ...-encode-current-bedrock-skin-packets.patch | 6 +- ...e-block-state-geometry-and-materials.patch | 12 +- .../0061-expose-native-player-emotes.patch | 14 +- ...0064-evaluate-named-actor-properties.patch | 6 +- ...-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 | 24 +-- ...bridge-native-actor-picking-hitboxes.patch | 6 +- ...tive-language-parsing-and-formatting.patch | 6 +- ...transport-resolved-native-fog-stacks.patch | 6 +- ...w-confirmed-blacklisted-server-joins.patch | 4 +- patches/viafabricplus-bedrock/series.json | 11 + ...d-use-on-bedrock-peaceful-difficulty.patch | 72 +++++++ ...d-use-on-bedrock-peaceful-difficulty.pr.md | 21 ++ 28 files changed, 539 insertions(+), 194 deletions(-) create mode 100644 patches/viabedrock/upstreamable/0044-forward-difficulty-changes-to-bedrock.pr.md create mode 100644 patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.patch create mode 100644 patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 3689706b..f1631b7e 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -27,6 +27,7 @@ No complete area has verified Bedrock parity. | 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, Creative catalog actions, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | +| Food use | Native food components, hunger limits, always-edible food, Creative use, and server-confirmed Peaceful difficulty govern admission. | Both routes match selected native bread controls across live difficulty changes. DataDriven apple admission at full hunger, broader use timing, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch index fdb45743..cb8714d1 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 dc5b3eec04252a2e57c325018f4e2785cf7b6481 Mon Sep 17 00:00:00 2001 +From d20dfd5a5d8d43e8f8307ff72df48f9dba1e8077 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 @@ -11362,10 +11362,10 @@ index d22d7616..375f5f0c 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 b7fb312e..e2b41cce 100644 +index 5af0eca2..fa556209 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -446,7 +446,7 @@ public final class JoinPackets { +@@ -447,7 +447,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 628a6a6c..18d915d9 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 7a67755200518dbbbb925e488f174fbdbc7bb45e Mon Sep 17 00:00:00 2001 +From 8c8555fd27e04260b6cfa28afeccf7dd94cd5188 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 @@ -4230,10 +4230,10 @@ index 375f5f0c..176bdfd5 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 e2b41cce..1904a56d 100644 +index fa556209..5d9f0fb3 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,7 @@ public final class JoinPackets { +@@ -569,6 +569,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/deferred/0008-signal-sustained-bedrock-item-use.patch b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch index e79b887e..fa50bea2 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch @@ -1,4 +1,4 @@ -From 7ad90993cc427d71c89a7c376744a0abadb6cbcb Mon Sep 17 00:00:00 2001 +From 63776783a37c07860529639be7955eaf0419644d Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(interaction): signal sustained Bedrock item use @@ -49,17 +49,34 @@ Keep unsupported offhand use on its existing container-resynchronization path. The new eligibility check applies to the main-hand use transaction. Both raw hand paths have targeted packet controls; neither changes the canonical selected stack or queues a rejected use flag. + +Ignore hunger for ordinary food on the server-confirmed Peaceful difficulty, +independently of the item's always-edible flag. Read current game-session +difficulty, including live server updates, before admitting the request. +Java client integration must also admit its local consumable predicate. + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 refuse +full-hunger bread on Normal, consume 16->15 after switching to Peaceful, +and refuse after switching back. The old installed add-on retains 16 and +never enters use on Peaceful. Registered packet tests now carry those +difficulty transitions through the food admission boundary without +predicting any inventory consumption. + +The same target's DataDriven apple consumes at full hunger on Normal even +with an advertised can_always_eat=false component. That separate component +admission discrepancy and the original intermittent eating report remain +open; preserve the verified Normal refusal for ordinary legacy bread. --- - .../model/ItemDefinitionProperties.java | 6 ++ - .../protocol/model/ItemFoodProperties.java | 27 +++++ - .../protocol/model/ItemRegistry.java | 6 ++ - .../protocol/packet/InteractionPackets.java | 51 +++++++++- - .../protocol/rewriter/ItemRewriter.java | 11 +++ - .../model/ItemFoodPropertiesTest.java | 65 ++++++++++++ - .../packet/CompletedItemUsePacketTest.java | 98 +++++++++++++++++++ - .../packet/FoodUseAdmissionPacketTest.java | 98 +++++++++++++++++++ - .../packet/InteractionPacketsTest.java | 31 ++++++ - 9 files changed, 390 insertions(+), 3 deletions(-) + .../model/ItemDefinitionProperties.java | 6 + + .../protocol/model/ItemFoodProperties.java | 27 +++ + .../protocol/model/ItemRegistry.java | 6 + + .../protocol/packet/InteractionPackets.java | 55 +++++- + .../protocol/rewriter/ItemRewriter.java | 11 ++ + .../model/ItemFoodPropertiesTest.java | 65 +++++++ + .../packet/CompletedItemUsePacketTest.java | 98 +++++++++++ + .../packet/FoodUseAdmissionPacketTest.java | 165 ++++++++++++++++++ + .../packet/InteractionPacketsTest.java | 31 ++++ + 9 files changed, 461 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/ItemFoodProperties.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/ItemFoodPropertiesTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/CompletedItemUsePacketTest.java @@ -99,7 +116,7 @@ index 65becabc..56ab4662 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemFoodProperties.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemFoodProperties.java new file mode 100644 -index 00000000..c4e63834 +index 00000000..f365440d --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemFoodProperties.java @@ -0,0 +1,27 @@ @@ -125,8 +142,8 @@ index 00000000..c4e63834 + return new ItemFoodProperties(true, component.get("can_always_eat") instanceof ByteTag value && value.asByte() != 0); + } + -+ public boolean allowsUse(final float hunger, final float maximumHunger, final boolean creative) { -+ return !this.food || this.canAlwaysEat || creative || hunger < maximumHunger; ++ public boolean allowsUse(final float hunger, final float maximumHunger, final boolean ignoresHunger) { ++ return !this.food || this.canAlwaysEat || ignoresHunger || hunger < maximumHunger; + } + +} @@ -148,10 +165,10 @@ index a906b932..03ebaf51 100644 return this.definitions.definitions().stream().filter(definition -> definition.receiver().component()) .map(NativeItemDefinitions.Definition::identifier).collect(Collectors.toUnmodifiableSet()); 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 081338cb..1a911c7b 100644 +index 081338cb..19465cd8 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java -@@ -46,10 +46,12 @@ import net.raphimc.viabedrock.protocol.ServerboundBedrockPackets; +@@ -46,10 +46,14 @@ import net.raphimc.viabedrock.protocol.ServerboundBedrockPackets; import net.raphimc.viabedrock.protocol.data.enums.Direction; import net.raphimc.viabedrock.protocol.data.enums.bedrock.ComplexInventoryTransaction_Type; import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.*; @@ -161,10 +178,12 @@ index 081338cb..1a911c7b 100644 import net.raphimc.viabedrock.protocol.data.enums.java.generated.PlayerActionAction; import net.raphimc.viabedrock.protocol.model.BedrockItem; +import net.raphimc.viabedrock.protocol.model.ItemFoodProperties; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.Difficulty; ++import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; import net.raphimc.viabedrock.protocol.model.Position3f; import net.raphimc.viabedrock.protocol.storage.BlockUseStorage; import net.raphimc.viabedrock.protocol.storage.BlockInteractionSoundTracker; -@@ -59,10 +61,17 @@ import net.raphimc.viabedrock.protocol.storage.InventoryTracker; +@@ -59,10 +63,17 @@ import net.raphimc.viabedrock.protocol.storage.InventoryTracker; import net.raphimc.viabedrock.protocol.types.BedrockTypes; import java.util.List; @@ -182,7 +201,7 @@ index 081338cb..1a911c7b 100644 private InteractionPackets() { } -@@ -257,7 +266,6 @@ public final class InteractionPackets { +@@ -257,7 +268,6 @@ public final class InteractionPackets { wrapper.read(Types.FLOAT); // yaw wrapper.read(Types.FLOAT); // pitch @@ -190,11 +209,11 @@ index 081338cb..1a911c7b 100644 if (javaHand < 0 || javaHand >= InteractionHand.values().length) { wrapper.cancel(); return; -@@ -278,8 +286,15 @@ public final class InteractionPackets { +@@ -278,8 +288,15 @@ public final class InteractionPackets { openBook.send(BedrockProtocol.class); return; } -+ if (hand == InteractionHand.MAIN_HAND && !canStartFoodUse(clientPlayer, itemRewriter.foodProperties(usedItem))) { ++ if (hand == InteractionHand.MAIN_HAND && !canStartFoodUse(clientPlayer, itemRewriter.foodProperties(usedItem), wrapper.user().get(GameSessionStorage.class))) { + // Java sends USE_ITEM before its local food check. Native clients admit eating before sending. + wrapper.cancel(); + return; @@ -207,7 +226,7 @@ index 081338cb..1a911c7b 100644 if (transaction == null) { wrapper.cancel(); if (hand == InteractionHand.OFF_HAND && !"minecraft:shield".equals(itemIdentifier)) { -@@ -371,6 +386,24 @@ public final class InteractionPackets { +@@ -371,6 +388,24 @@ public final class InteractionPackets { } } }); @@ -232,13 +251,15 @@ index 081338cb..1a911c7b 100644 protocol.registerClientbound(ClientboundBedrockPackets.SET_ENTITY_LINK, null, wrapper -> { wrapper.cancel(); final EntityTracker entityTracker = wrapper.user().get(EntityTracker.class); -@@ -380,6 +413,15 @@ public final class InteractionPackets { +@@ -380,6 +415,17 @@ public final class InteractionPackets { } -+ static boolean canStartFoodUse(final ClientPlayerEntity player, final ItemFoodProperties food) { ++ static boolean canStartFoodUse(final ClientPlayerEntity player, final ItemFoodProperties food, final GameSessionStorage session) { + final var hunger = player.attributes().get("minecraft:player.hunger"); -+ return hunger == null || food.allowsUse(hunger.computeClampedValue(), hunger.computeMaxValue(), player.javaGameMode() == GameMode.CREATIVE); ++ final boolean ignoresHunger = player.javaGameMode() == GameMode.CREATIVE ++ || session != null && session.getDifficulty() == Difficulty.Peaceful; ++ return hunger == null || food.allowsUse(hunger.computeClampedValue(), hunger.computeMaxValue(), ignoresHunger); + } + + static boolean isContinuousUseItem(final String identifier, final Set tags) { @@ -248,7 +269,7 @@ index 081338cb..1a911c7b 100644 static void sendPassengerUpdates(final PacketWrapper source, final EntityTracker entityTracker) { for (Entity vehicle : entityTracker.drainPassengerUpdates()) { final PacketWrapper passengersPacket = PacketWrapper.create(ClientboundPackets26_3.SET_PASSENGERS, -@@ -401,7 +443,7 @@ public final class InteractionPackets { +@@ -401,7 +447,7 @@ public final class InteractionPackets { static BedrockInventoryTransaction beginItemUse(final ClientPlayerEntity player, final InteractionHand hand, final int hotbarSlot, final BedrockItem item, @@ -257,7 +278,7 @@ index 081338cb..1a911c7b 100644 if (item.isEmpty()) { return null; } -@@ -413,6 +455,9 @@ public final class InteractionPackets { +@@ -413,6 +459,9 @@ public final class InteractionPackets { return null; } player.setShieldBlockingHand(null); @@ -487,10 +508,10 @@ index 00000000..45f4a6a3 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/FoodUseAdmissionPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/FoodUseAdmissionPacketTest.java new file mode 100644 -index 00000000..2a897a27 +index 00000000..20117e06 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/FoodUseAdmissionPacketTest.java -@@ -0,0 +1,98 @@ +@@ -0,0 +1,165 @@ +/* + * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -507,18 +528,25 @@ index 00000000..2a897a27 +import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ServerboundPackets26_3; +import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity; +import net.raphimc.viabedrock.protocol.BedrockProtocol; ++import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.Difficulty; +import net.raphimc.viabedrock.api.util.PacketFactory; +import net.raphimc.viabedrock.protocol.data.enums.java.generated.InteractionHand; +import net.raphimc.viabedrock.protocol.model.BedrockItem; +import net.raphimc.viabedrock.protocol.model.ItemFoodProperties; +import net.raphimc.viabedrock.protocol.model.PlayerAbilities; +import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import net.raphimc.viabedrock.protocol.rewriter.InventoryTransactionRewriter; ++import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; ++import net.raphimc.viabedrock.protocol.types.BedrockTypes; ++import net.raphimc.viabedrock.protocol.model.inventory.InventoryTransactionData; +import net.raphimc.viabedrock.protocol.storage.EntityTracker; +import net.raphimc.viabedrock.protocol.storage.InventoryRequestTracker; +import net.raphimc.viabedrock.protocol.storage.InventoryTracker; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; ++import org.junit.jupiter.api.Test; + +import java.lang.reflect.Proxy; +import java.util.HashMap; @@ -576,9 +604,69 @@ index 00000000..2a897a27 + } + } + ++ @Test ++ void confirmedDifficultyChangesAdmitPeacefulFoodAndRestoreFullHungerRefusal() throws Exception { ++ final Map, Object> stores = new HashMap<>(); ++ final UserConnection user = (UserConnection) Proxy.newProxyInstance(UserConnection.class.getClassLoader(), ++ new Class[]{UserConnection.class}, (proxy, method, args) -> method.getName().equals("get") ? stores.get(args[0]) : null); ++ final var session = mock(GameSessionStorage.class, CALLS_REAL_METHODS); ++ stores.put(GameSessionStorage.class, session); ++ final var entities = new EntityTracker(user); ++ stores.put(EntityTracker.class, entities); ++ entities.addEntity(new ClientPlayerEntity(user, 1, UUID.randomUUID(), new PlayerAbilities(1, (byte) 0, (byte) 0)), false); ++ final var requests = new InventoryRequestTracker(user); ++ requests.setPredictingClick(true); ++ stores.put(InventoryRequestTracker.class, requests); ++ final var inventory = new InventoryTracker(user); ++ stores.put(InventoryTracker.class, inventory); ++ final var held = new BedrockItem(1); ++ held.setAmount(8); ++ inventory.getInventoryContainer().setItem(0, held); ++ final var items = mock(ItemRewriter.class); ++ when(items.foodProperties(held)).thenReturn(new ItemFoodProperties(true, false)); ++ stores.put(ItemRewriter.class, items); ++ final var transactions = new InventoryTransactionRewriter(user); ++ stores.put(InventoryTransactionRewriter.class, transactions); ++ final var protocol = new TestProtocol(); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ for (final Difficulty difficulty : new Difficulty[] {Difficulty.Normal, Difficulty.Peaceful, Difficulty.Normal}) { ++ final var update = Unpooled.buffer(); ++ final var input = Unpooled.buffer(); ++ try { ++ BedrockTypes.UNSIGNED_VAR_INT.writePrimitive(update, difficulty.getValue()); ++ protocol.clientbound(new PacketWrapperImpl(ClientboundBedrockPackets.SET_DIFFICULTY, update, user)); ++ Types.VAR_INT.writePrimitive(input, InteractionHand.MAIN_HAND.ordinal()); ++ Types.VAR_INT.writePrimitive(input, 1); ++ input.writeFloat(0F).writeFloat(0F); ++ final var packet = new PacketWrapperImpl(ServerboundPackets26_3.USE_ITEM, input, user); ++ protocol.translate(packet); ++ assertEquals(difficulty != Difficulty.Peaceful, packet.isCancelled()); ++ if (!packet.isCancelled()) { ++ final var transaction = packet.get(transactions.getInventoryTransactionType(), 0); ++ final var use = assertInstanceOf(InventoryTransactionData.UseItemTransactionData.class, transaction.transactionData()); ++ assertEquals(8, use.itemInHand().amount()); ++ } ++ assertEquals(8, held.amount()); ++ assertFalse(input.isReadable()); ++ } finally { ++ update.release(); ++ input.release(); ++ } ++ } ++ packets.verifyNoInteractions(); ++ } ++ } ++ + private static final class TestProtocol extends BedrockProtocol { + private TestProtocol() { + InteractionPackets.register(this); ++ PlayPackets.register(this); ++ } ++ ++ private void clientbound(final PacketWrapper packet) throws Exception { ++ final var mapping = this.clientboundMappings.mappedPacket(State.PLAY, packet.getId()); ++ mapping.applyType(packet); ++ mapping.handler().handle(packet); + } + + private void translate(final PacketWrapper packet) throws Exception { diff --git a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md index 9b52ca0c..613cf5d4 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md @@ -1,87 +1,48 @@ # Sustained item use and food admission -Java sends an air-use packet before its local food eligibility check. -Bedrock Dedicated Server accepts that transaction even at full hunger. -Holding ordinary food therefore consumes an extra item after the Java client stops its eating animation. - -This patch checks advertised native food properties before sending the main-hand transaction or setting `StartUsingItem`. -It uses the current native hunger value and maximum. -Food with `can_always_eat` and creative use retain their existing admission. +This patch admits main-hand food use before it sends the transaction or sets `StartUsingItem`. +Java sends its air-use packet before its local food predicate, and BDS can accept an otherwise ineligible request. +Admission uses the advertised native food component and current hunger limits. +Always-edible food, Creative use, and server-confirmed Peaceful difficulty ignore hunger. Items without advertised food data retain their existing route. Unsupported offhand use retains its container resynchronization. -## Target evidence - -The target is Bedrock 1.26.51.1, protocol 2193, with Java 26.3. -Its recorded `ITEM_REGISTRY` supplies `minecraft:food.can_always_eat` for component and legacy items. -The inspected definitions include apple, rotten flesh, honey, chorus fruit, and both golden apple variants. -The implementation retains ordered component updates and resolves them through the current registry identity. -It does not infer native food behavior from a Java item or a fixed whitelist. - -Microsoft documents the property in the [food component reference](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/itemreference/examples/itemcomponents/minecraft_food?view=minecraft-bedrock-stable). -The target capture establishes that this property exists in the pinned version. -This admission change uses captured metadata and server behavior, without claiming an original-instruction oracle for the complete eligibility predicate. - -The existing completion handling decodes the signed item ID and use method from `COMPLETED_USING_ITEM`. -It ignores replaced held items and methods outside the native range of zero through sixteen. -The owning patch records the original handler trace and packet controls. - -## Regression controls - -Model tests cover omitted and replaced component definitions, registry identity replacement, native hunger maxima, creative use, and always-edible food. -Actual packet mappings cover both raw hand paths. -A rejected main-hand request changes neither the canonical inventory nor the queued use flag. -Offhand requests retain the existing container resynchronization. -Existing completion controls still cover signed IDs, method boundaries, and replaced items. - -A private BDS fixture starts with hunger four and sixteen apples. -The original candidate consumes five apples in both grounded and delayed-correction phases. -Each phase includes a server use start at hunger twenty. -The fixed candidate consumes four apples and stops at hunger twenty, leaving twelve apples. -Both ordinary-food phases record four starts, four completions, and no start at full hunger. - -A separate full-hunger golden-apple phase starts with eight items and completes four uses. -All five starts occur at full hunger; releasing the key cancels the final incomplete use. -The server and client settle at hunger twenty with four golden apples. - -The direct run executes five historical replays with fifty-four physical steps. -Three replays occur during active eating and preserve use timers of twelve, twenty-eight, and eighteen ticks. -Every replay boundary preserves tick count, hunger, held count, and active-use state. -The fixture samples a removed player after disconnect; those later errors are excluded from active-session evidence. -The read-only client observers report no errors. - -The same three-phase fixture also passes through ViaProxy. -Its ordinary-food phases each stop after four completions, with no start at full hunger. -The full-hunger golden-apple phase completes four uses and cancels the fifth on release. -The proxy and client artifacts match their full-build checksums. - -The proxy run executes six historical replays with fifty-five physical steps. -Three active-use replays preserve timers of twenty-three, seventeen, and ten ticks. -Every replay boundary preserves tick count, hunger, held count, and active-use state. -Its private inbound control delivers 101,773 events in order, with no pending events at shutdown. -Observed delays range from 300.003 to 304.419 milliseconds. -The first, shorter proxy capture stops during the final hold and remains excluded from these results. - -The tested food candidate passes 2,520 tests across all four projects, with 215 optional skips and no failures or errors. -All 1,411 core classes match both packaged copies. -Offline tooling, static, and patch metadata checks pass. - -The combined build also preserves the newer explicit-binding renderer update. -It passes 2,523 full-build tests, with 216 optional skips and no failures or errors. -All 1,411 core classes match both packaged copies. -A focused run passes 8 native binding, native slot, and Iris GPU controls without skips. -The core source tree and proxy artifact match the live-tested candidate. -All 119 compared movement/input entries and mixin configurations also match. -Only two indexed production client classes change, both in the renderer. -This comparison does not establish fresh whole-package runtime or visible rendering parity. - -## Boundaries - -These controls establish the observed ordinary-food overconsumption fix and the stated replay counter preservation. -They do not establish all native custom-food constructors, ability exceptions, use cycles, or trajectories. -Late completion for a new use of the same item still needs verification. -Prediction, rejection, and reconciliation across every negotiated inventory authority mode remain unfinished. -The full five-area production goal remains active. +Food definitions follow ordered component updates and connection registry identity. +The implementation does not infer food behavior from the Java item or a fixed whitelist. +The [food component reference](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/itemreference/examples/itemcomponents/minecraft_food?view=minecraft-bedrock-stable) describes `can_always_eat`. +Native captures establish the behavior of the tested builds. + +`COMPLETED_USING_ITEM` supplies a signed item ID and a use method. +Matching held-item completions produce Java's `USE_ITEM_COMPLETE` event without changing canonical inventory counts. +Replaced items and methods outside zero through sixteen cannot complete the current use. +The patch body retains the original Bedrock 1.26.51.1 handler evidence and admission rationale. + +## Current native boundary + +Official Bedrock 1.26.60.5 with BDS 1.26.60.6 uses protocol 2225. +At full hunger, bread remains at 16 on Normal and decreases to 15 after a switch to Peaceful. +A switch back to Normal restores refusal and retains 16 bread. +The previous installed add-on retains all 16 bread and never enters use on Peaceful. +Core retains initial difficulty and later server confirmations. +The client integration also admits its local consumable cycle on Peaceful, including connections through ViaProxy. +Inventory consumption remains server-owned. + +Registered packet tests cover main-hand rejection, offhand resynchronization, and Normal → Peaceful → Normal admission without inventory prediction. +Model tests cover registry replacement, hunger maxima, Creative use, and always-edible food. +Completion tests cover signed IDs, method boundaries, and replaced items. + +Installed direct and ViaProxy controls each pass those three full-hunger cases and a hungry Normal control. +Allowed cases each receive one server completion, consume one bread, and settle at the server count of 15. +Refused cases never enter local use and retain 16 bread. +The full four-project build passes 3,281 tests, with 325 skipped and no failures or errors. +All 1,645 core classes match both packaged copies, and installed artifacts match the build checksums. + +## Remaining limits + +The same native build consumes a DataDriven apple at full hunger on Normal despite its advertised `can_always_eat=false` component. +That discrepancy remains open, alongside the original intermittent animation-without-feeding report. +The bread controls do not establish all custom-food constructors, ability exceptions, cycle timing, or inventory authority modes. +Late completion for a new use of the same item also needs verification. This patch remains deferred behind [ViaBedrock #276](https://github.com/ViaVersionAddons/ViaBedrock/pull/276). It does not belong in an independent upstream PR branch. 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 fec637fd..af6e01bb 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,4 +1,4 @@ -From 80c9a15b9a85b4359317ad87a210c062a18ef530 Mon Sep 17 00:00:00 2001 +From e150c7abb5366ed574c1c12bf526c9df2af4192a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(interaction): preserve native air-use prediction and @@ -151,7 +151,7 @@ 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..a0b0a0ab 100644 +index 19465cd8..fa1f583e 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -21,6 +21,7 @@ import com.viaversion.viaversion.api.minecraft.BlockFace; @@ -162,7 +162,7 @@ index 1a911c7b..a0b0a0ab 100644 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; +@@ -68,6 +69,9 @@ import java.util.logging.Level; public final class InteractionPackets { @@ -172,7 +172,7 @@ index 1a911c7b..a0b0a0ab 100644 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 { +@@ -293,7 +297,8 @@ public final class InteractionPackets { wrapper.cancel(); return; } @@ -182,7 +182,7 @@ index 1a911c7b..a0b0a0ab 100644 inventoryContainer.getSelectedHotbarSlot(), usedItem, itemIdentifier, itemIdentifier == null ? Set.of() : BedrockProtocol.MAPPINGS.getBedrockItemTags().getOrDefault(itemIdentifier, Set.of())); -@@ -302,7 +307,7 @@ public final class InteractionPackets { +@@ -304,7 +309,7 @@ public final class InteractionPackets { } return; } @@ -191,7 +191,7 @@ index 1a911c7b..a0b0a0ab 100644 }); protocol.registerServerbound(ServerboundPackets26_3.USE_ITEM_ON, null, wrapper -> { -@@ -326,22 +331,20 @@ public final class InteractionPackets { +@@ -328,22 +333,20 @@ public final class InteractionPackets { PacketFactory.sendJavaBlockChangedAck(wrapper.user(), sequence); return; } @@ -217,8 +217,8 @@ index 1a911c7b..a0b0a0ab 100644 final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); final BedrockItem usedItem = inventoryTracker.getInventoryContainer().getSelectedHotbarItem(); if (usedItem.hasBlockState()) { -@@ -418,31 +421,23 @@ public final class InteractionPackets { - return hunger == null || food.allowsUse(hunger.computeClampedValue(), hunger.computeMaxValue(), player.javaGameMode() == GameMode.CREATIVE); +@@ -422,31 +425,23 @@ public final class InteractionPackets { + return hunger == null || food.allowsUse(hunger.computeClampedValue(), hunger.computeMaxValue(), ignoresHunger); } - static boolean isContinuousUseItem(final String identifier, final Set tags) { @@ -262,7 +262,7 @@ index 1a911c7b..a0b0a0ab 100644 final String identifier, final Set tags) { if (item.isEmpty()) { return null; -@@ -458,13 +453,67 @@ public final class InteractionPackets { +@@ -462,13 +457,67 @@ public final class InteractionPackets { if (isContinuousUseItem(identifier, tags)) { player.addAuthInputData(PlayerAuthInputData.StartUsingItem); } 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 0ef5f619..96f69641 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 b81cbf7868415686067e2c14657f8ef269de84b2 Mon Sep 17 00:00:00 2001 +From af61a3324c2b4756edd6c0739826ce2dccf395fe 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,10 +701,10 @@ index 9c4b8cec..1c23b759 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 0fa95fb6..25355ba0 100644 +index cb61ffc5..9eedc5c5 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 { +@@ -294,7 +294,7 @@ public final class JoinPackets { wrapper.read(BedrockTypes.STRING); // multiplayer correlation id final boolean inventoryServerAuthoritative = wrapper.read(Types.BOOLEAN); // server authoritative inventories final String serverEngine = wrapper.read(BedrockTypes.STRING); // server engine diff --git a/patches/viabedrock/upstreamable/0044-forward-difficulty-changes-to-bedrock.patch b/patches/viabedrock/upstreamable/0044-forward-difficulty-changes-to-bedrock.patch index b799de4b..e9b123d7 100644 --- a/patches/viabedrock/upstreamable/0044-forward-difficulty-changes-to-bedrock.patch +++ b/patches/viabedrock/upstreamable/0044-forward-difficulty-changes-to-bedrock.patch @@ -1,18 +1,67 @@ -From 05f4f85c21d3f482a384d270d3a99471163e5b42 Mon Sep 17 00:00:00 2001 +From c6c107e77a038d85ba56f289da466a9ad5fa8fe4 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): forward difficulty changes to Bedrock -Java 26.3 sends menu difficulty changes as a VarInt. Translate valid difficulty IDs to Bedrock protocol 2193 SetDifficulty so the world options control reaches the server. +Translate valid Java 26.3 menu difficulty IDs to Bedrock SetDifficulty. +Retain initial StartGame difficulty and later server-confirmed updates in +the connection's game session. Forwarding a menu request does not change +that retained value before the server confirms it. + +Native Bedrock 1.26.60.5/BDS 1.26.60.6 protocol-2225 bread controls refuse +full-hunger use on Normal, consume after a live switch to Peaceful, and +refuse after switching back. Retaining current difficulty lets food +admission observe those transitions instead of a join-only snapshot. + +The packet regression verifies Normal/Peaceful/Normal state changes, +the forwarded Java fields, invalid menu input, and the unchanged state +while a valid menu request awaits confirmation. --- - .../raphimc/viabedrock/protocol/packet/PlayPackets.java | 9 +++++++++ - 1 file changed, 9 insertions(+) + .../protocol/packet/JoinPackets.java | 1 + + .../protocol/packet/PlayPackets.java | 13 +++ + .../protocol/storage/GameSessionStorage.java | 10 +++ + .../packet/DifficultyPacketsTest.java | 89 +++++++++++++++++++ + 4 files changed, 113 insertions(+) + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/DifficultyPacketsTest.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 15123622..1bfd753b 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +@@ -225,6 +225,7 @@ public final class JoinPackets { + final GameType levelGameType = GameType.getByValue(wrapper.read(BedrockTypes.VAR_INT), GameType.Undefined); // level game type + final boolean hardcore = wrapper.read(Types.BOOLEAN); // hardcore + final Difficulty difficulty = Difficulty.getByValue(wrapper.read(BedrockTypes.VAR_INT), Difficulty.Unknown); // difficulty ++ wrapper.user().get(GameSessionStorage.class).setDifficulty(difficulty); + wrapper.read(BedrockTypes.BLOCK_POSITION); // default spawn position + wrapper.read(Types.BOOLEAN); // achievements disabled + final EditorWorldType editorWorldType = EditorWorldType.getByValue(wrapper.read(BedrockTypes.VAR_INT)); // world editor type diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/PlayPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/PlayPackets.java -index ad56b55e..df5536fe 100644 +index ad56b55e..74666b4f 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/PlayPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/PlayPackets.java -@@ -85,6 +85,15 @@ public final class PlayPackets { +@@ -28,9 +28,11 @@ import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; + import net.raphimc.viabedrock.protocol.ServerboundBedrockPackets; + import net.raphimc.viabedrock.protocol.data.ProtocolConstants; + import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.CommandOriginType; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.Difficulty; + import net.raphimc.viabedrock.protocol.model.CommandOriginData; + import net.raphimc.viabedrock.protocol.storage.BlobCache; + import net.raphimc.viabedrock.protocol.storage.GameRulesStorage; ++import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; + import net.raphimc.viabedrock.protocol.types.BedrockTypes; + + import java.util.Map; +@@ -44,6 +46,8 @@ public final class PlayPackets { + public void register() { + map(BedrockTypes.UNSIGNED_VAR_INT, Types.VAR_INT); // difficulty + create(Types.BOOLEAN, false); // locked ++ handler(wrapper -> wrapper.user().get(GameSessionStorage.class).setDifficulty( ++ Difficulty.getByValue(wrapper.get(Types.VAR_INT, 0), Difficulty.Unknown))); + } + }); + protocol.registerClientbound(ClientboundBedrockPackets.CLIENT_CACHE_MISS_RESPONSE, null, wrapper -> { +@@ -85,6 +89,15 @@ public final class PlayPackets { wrapper.user().get(GameRulesStorage.class).updateGameRules(wrapper.read(BedrockTypes.GAME_RULE_ARRAY)); // game rules }); @@ -28,5 +77,135 @@ index ad56b55e..df5536fe 100644 protocol.registerServerbound(ServerboundPackets26_3.SET_GAME_RULE, null, wrapper -> { wrapper.cancel(); final GameRulesStorage gameRules = wrapper.user().get(GameRulesStorage.class); +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java +index b86fa990..fa7fddc2 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java +@@ -27,6 +27,7 @@ import net.raphimc.viabedrock.protocol.model.DimensionDefinition; + import net.raphimc.viabedrock.protocol.data.JavaRegistries; + import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ChatRestrictionLevel; + import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.GameType; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.Difficulty; + + import java.util.HashMap; + import java.util.HashSet; +@@ -43,6 +44,7 @@ public class GameSessionStorage extends StoredObject { + private boolean flatGenerator; + private int movementRewindHistorySize; + private GameType levelGameType; ++ private Difficulty difficulty = Difficulty.Unknown; + private long levelTime; + private boolean hardcoreMode; + private ChatRestrictionLevel chatRestrictionLevel; +@@ -124,6 +126,14 @@ public class GameSessionStorage extends StoredObject { + this.levelGameType = levelGameType; + } + ++ public Difficulty getDifficulty() { ++ return this.difficulty; ++ } ++ ++ public void setDifficulty(final Difficulty difficulty) { ++ this.difficulty = difficulty; ++ } ++ + public long getLevelTime() { + return this.levelTime; + } +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/DifficultyPacketsTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/DifficultyPacketsTest.java +new file mode 100644 +index 00000000..bc37e712 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/DifficultyPacketsTest.java +@@ -0,0 +1,89 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.packet; ++ ++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.type.Types; ++import com.viaversion.viaversion.protocol.packet.PacketWrapperImpl; ++import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ServerboundPackets26_3; ++import io.netty.buffer.Unpooled; ++import net.raphimc.viabedrock.protocol.BedrockProtocol; ++import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.Difficulty; ++import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; ++import net.raphimc.viabedrock.protocol.types.BedrockTypes; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class DifficultyPacketsTest { ++ ++ @Test ++ void onlyServerConfirmationChangesTheRetainedDifficulty() throws Exception { ++ final var user = mock(UserConnection.class); ++ final var session = mock(GameSessionStorage.class, CALLS_REAL_METHODS); ++ session.setDifficulty(Difficulty.Normal); ++ when(user.get(GameSessionStorage.class)).thenReturn(session); ++ final var protocol = new TestProtocol(); ++ final var request = Unpooled.buffer(); ++ try { ++ Types.VAR_INT.writePrimitive(request, 0); ++ final var packet = new PacketWrapperImpl(ServerboundPackets26_3.CHANGE_DIFFICULTY, request, user); ++ protocol.serverbound(packet); ++ assertFalse(packet.isCancelled()); ++ assertEquals(0, packet.get(BedrockTypes.UNSIGNED_VAR_INT, 0)); ++ assertEquals(Difficulty.Normal, session.getDifficulty()); ++ } finally { ++ request.release(); ++ } ++ for (final Difficulty difficulty : new Difficulty[] {Difficulty.Peaceful, Difficulty.Normal}) { ++ final var update = Unpooled.buffer(); ++ try { ++ BedrockTypes.UNSIGNED_VAR_INT.writePrimitive(update, difficulty.getValue()); ++ final var packet = new PacketWrapperImpl(ClientboundBedrockPackets.SET_DIFFICULTY, update, user); ++ protocol.clientbound(packet); ++ assertEquals(difficulty.getValue(), packet.get(Types.VAR_INT, 0)); ++ assertFalse(packet.get(Types.BOOLEAN, 0)); ++ assertEquals(difficulty, session.getDifficulty()); ++ assertFalse(update.isReadable()); ++ } finally { ++ update.release(); ++ } ++ } ++ final var invalid = Unpooled.buffer(); ++ try { ++ Types.VAR_INT.writePrimitive(invalid, 4); ++ final var packet = new PacketWrapperImpl(ServerboundPackets26_3.CHANGE_DIFFICULTY, invalid, user); ++ protocol.serverbound(packet); ++ assertTrue(packet.isCancelled()); ++ assertEquals(Difficulty.Normal, session.getDifficulty()); ++ } finally { ++ invalid.release(); ++ } ++ } ++ ++ private static final class TestProtocol extends BedrockProtocol { ++ private TestProtocol() { ++ PlayPackets.register(this); ++ } ++ ++ private void clientbound(final PacketWrapper packet) throws Exception { ++ final var mapping = this.clientboundMappings.mappedPacket(State.PLAY, packet.getId()); ++ mapping.applyType(packet); ++ mapping.handler().handle(packet); ++ } ++ ++ private void serverbound(final PacketWrapper packet) throws Exception { ++ final var mapping = this.serverboundMappings.mappedPacket(State.PLAY, packet.getId()); ++ mapping.applyType(packet); ++ mapping.handler().handle(packet); ++ } ++ } ++ ++} -- 2.56.0 diff --git a/patches/viabedrock/upstreamable/0044-forward-difficulty-changes-to-bedrock.pr.md b/patches/viabedrock/upstreamable/0044-forward-difficulty-changes-to-bedrock.pr.md new file mode 100644 index 00000000..2f51c5dc --- /dev/null +++ b/patches/viabedrock/upstreamable/0044-forward-difficulty-changes-to-bedrock.pr.md @@ -0,0 +1,12 @@ +# Retain server-confirmed difficulty + +Forward valid Java difficulty requests to Bedrock and retain the server's current difficulty in connection state. +Initial `StartGame` data and later `SET_DIFFICULTY` packets update that state. +A Java menu request leaves the retained value unchanged until the server confirms it. + +Food admission needs current difficulty because native Bedrock permits ordinary food at full hunger on Peaceful. +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 establish this boundary with bread. +Normal refuses use, a live switch to Peaceful permits one consumption, and a switch back to Normal restores refusal. + +The packet regression covers server confirmations, forwarded Java fields, valid requests awaiting confirmation, and invalid Java input. +The [food-use context](../deferred/0008-signal-sustained-bedrock-item-use.pr.md) describes the consumer and its remaining limits. 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 62ec0051..0b33a01f 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 2ccc531996fc35a8c063631104afb7cc17139579 Mon Sep 17 00:00:00 2001 +From 4ac81bfa002a49109c14ffa9aa4cc2c1bfd8fac9 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 15123622..e1a58abd 100644 +index 1bfd753b..1120676d 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -538,8 +538,10 @@ public final class JoinPackets { +@@ -539,8 +539,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 18d17446..1f239e30 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 7e64a6cab211c9cd6d1704815ea8aca4f8af0402 Mon Sep 17 00:00:00 2001 +From f8171bd71728c31689f4c04fabf72e32d56d3126 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 @@ -2638,10 +2638,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 d97ef829..7dd7f708 100644 +index 6b6a97d8..208ad42b 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -355,7 +355,8 @@ public final class JoinPackets { +@@ -356,7 +356,8 @@ public final class JoinPackets { wrapper.user().put(new JoinGameStorage(levelName, difficulty, rainLevel, lightningLevel, dayCycleStopTime, chunkTickRange)); wrapper.user().put(new GameRulesStorage(wrapper.user(), gameRules)); @@ -2651,7 +2651,7 @@ index d97ef829..7dd7f708 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()); -@@ -374,13 +375,17 @@ public final class JoinPackets { +@@ -375,13 +376,17 @@ public final class JoinPackets { updateEnabledFeatures.send(BedrockProtocol.class); } @@ -2676,7 +2676,7 @@ index d97ef829..7dd7f708 100644 }, State.PLAY, (PacketHandler) PacketWrapper::cancel // Bedrock client ignores multiple start game packets ); protocol.registerClientboundTransition(ClientboundBedrockPackets.BIOME_DEFINITION_LIST, -@@ -472,6 +477,15 @@ public final class JoinPackets { +@@ -473,6 +478,15 @@ public final class JoinPackets { } } @@ -2692,7 +2692,7 @@ index d97ef829..7dd7f708 100644 private static void handleJavaClientGameJoin(final UserConnection user) { final JoinGameStorage joinGameStorage = user.get(JoinGameStorage.class); final GameSessionStorage gameSession = user.get(GameSessionStorage.class); -@@ -607,6 +621,7 @@ public final class JoinPackets { +@@ -608,6 +622,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 93c114ec..89ff49e3 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 139058352c987d9b068a97d624d1e437f939644d Mon Sep 17 00:00:00 2001 +From 5444704220c8087544703b8bf128cb1818023bc8 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,10 +312,10 @@ 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 7dd7f708..330abbf5 100644 +index 208ad42b..7bceaa56 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 { +@@ -261,7 +261,7 @@ public final class JoinPackets { wrapper.read(Types.BOOLEAN); // only spawn v1 villagers wrapper.read(Types.BOOLEAN); // disable personas wrapper.read(Types.BOOLEAN); // disable custom skins @@ -324,7 +324,7 @@ index 7dd7f708..330abbf5 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 -@@ -341,6 +341,7 @@ public final class JoinPackets { +@@ -342,6 +342,7 @@ public final class JoinPackets { gameSession.setLevelTime(levelTime); gameSession.setHardcoreMode(hardcore); gameSession.setChatRestrictionLevel(chatRestrictionLevel); @@ -687,10 +687,10 @@ index 00000000..51f27bb5 + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java -index b86fa990..6b46f956 100644 +index fa7fddc2..fb054492 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java -@@ -46,6 +46,7 @@ public class GameSessionStorage extends StoredObject { +@@ -48,6 +48,7 @@ public class GameSessionStorage extends StoredObject { private long levelTime; private boolean hardcoreMode; private ChatRestrictionLevel chatRestrictionLevel; @@ -698,7 +698,7 @@ index b86fa990..6b46f956 100644 private boolean commandsEnabled; private boolean inventoryServerAuthoritative; private boolean blockBreakingServerAuthoritative; -@@ -59,6 +60,14 @@ public class GameSessionStorage extends StoredObject { +@@ -61,6 +62,14 @@ public class GameSessionStorage extends StoredObject { this.bedrockDimensionDefinitions.put("minecraft:the_nether", new DimensionDefinition(0, 128, 0, false)); } diff --git a/patches/viabedrock/upstreamable/0064-evaluate-named-actor-properties.patch b/patches/viabedrock/upstreamable/0064-evaluate-named-actor-properties.patch index 82f2375b..cf157b94 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 b7efde7bfa272de8ab093a4b2e780a427cc2c85a Mon Sep 17 00:00:00 2001 +From 65270a670c7eb36fbbe343fe3ce5b8a3c265fda6 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 e1a58abd..d97ef829 100644 +index 1120676d..6b6a97d8 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -539,6 +539,8 @@ public final class JoinPackets { +@@ -540,6 +540,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/0080-retain-and-transport-native-player-permissions.patch b/patches/viabedrock/upstreamable/0080-retain-and-transport-native-player-permissions.patch index ad74ee90..aa7044be 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 d94d92e4a22440a5ca29729939a95b4c2a93d746 Mon Sep 17 00:00:00 2001 +From 1ab98dc291202392dc959ca7847173ca55f4c628 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 330abbf5..b545a18f 100644 +index 7bceaa56..8fd82208 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -556,6 +556,8 @@ public final class JoinPackets { +@@ -557,6 +557,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 9e41cf23..27584674 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 6738965c6572404ec13dbe605faa7d3468e05f64 Mon Sep 17 00:00:00 2001 +From d05244624eae505c36391eee4a4481f7051641a7 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 b545a18f..dc1d68b8 100644 +index 8fd82208..baed8d07 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -558,6 +558,8 @@ public final class JoinPackets { +@@ -559,6 +559,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 8ed488d6..83979f91 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 bc8ba37809625adfe902dc002c64110a3e1e6ce8 Mon Sep 17 00:00:00 2001 +From f665b24c69bad4fbe35f435daf40f979de7fbe1a 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 dc1d68b8..f6c8e18f 100644 +index baed8d07..1ffd5be2 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -567,6 +567,7 @@ public final class JoinPackets { +@@ -568,6 +568,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 ab0e2d59..181308a1 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 1054956c42a146a26e00d27bc9d8e3ca27647313 Mon Sep 17 00:00:00 2001 +From f8d601b5ca4d149a59c65d91048dc52cbbbf81e5 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 f6c8e18f..e93b7a41 100644 +index 1ffd5be2..05664a14 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -560,6 +560,8 @@ public final class JoinPackets { +@@ -561,6 +561,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 161c9d3a..63489192 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 b3eaaf0f634f729ffc3b1d5f82523c47ef0de934 Mon Sep 17 00:00:00 2001 +From 40d278e02e62bfc6db946e03cf5d52c9eacccfa8 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 e93b7a41..64d13b71 100644 +index 05664a14..85a0d136 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 e93b7a41..64d13b71 100644 PacketFactory.sendBedrockLoadingScreen(wrapper.user(), ServerboundLoadingScreenPacketType.EndLoadingScreen, null); final PacketWrapper setLocalPlayerAsInitialized = PacketWrapper.create(ServerboundBedrockPackets.SET_LOCAL_PLAYER_AS_INITIALIZED, wrapper.user()); -@@ -480,10 +480,7 @@ public final class JoinPackets { +@@ -481,10 +481,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 19deb2fe..31659659 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 7f4cb1fa29482819ef434e70e8aefc5a67a76d87 Mon Sep 17 00:00:00 2001 +From f3b1c9fe63e42d776cad0f47987c23c87e8637ce 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 64d13b71..847efa7c 100644 +index 85a0d136..2210c336 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -337,6 +337,7 @@ public final class JoinPackets { +@@ -338,6 +338,7 @@ public final class JoinPackets { gameSession.setBedrockVanillaVersion(version); gameSession.setFlatGenerator(generatorType == GeneratorType.Flat); gameSession.setMovementRewindHistorySize(rewindHistorySize); @@ -4813,7 +4813,7 @@ index 64d13b71..847efa7c 100644 gameSession.setLevelGameType(levelGameType); gameSession.setLevelTime(levelTime); gameSession.setHardcoreMode(hardcore); -@@ -565,6 +566,7 @@ public final class JoinPackets { +@@ -566,6 +567,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 c72291e4..600c6618 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 d4890416a98517c77c4144398c6c0fd9bd12021e Mon Sep 17 00:00:00 2001 +From d6f3247318b5962dab4f649864e1c76f2a07cc22 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 847efa7c..8f8b7567 100644 +index 2210c336..29722156 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 bebb6496..0919c3c6 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 94677f7f00157de6110d2ef6b0f7f038b57dc04b Mon Sep 17 00:00:00 2001 +From cc0940d826c6524c35f956e00f26d521e6da0004 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 @@ -4842,10 +4842,10 @@ 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 8f8b7567..0fa95fb6 100644 +index 29722156..cb61ffc5 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 { +@@ -268,6 +268,7 @@ public final class JoinPackets { wrapper.read(Types.BOOLEAN); // disable custom skins final boolean emoteChatMuted = wrapper.read(Types.BOOLEAN); // mute emote chat final String vanillaVersion = wrapper.read(BedrockTypes.STRING); // vanilla version @@ -4853,7 +4853,7 @@ index 8f8b7567..0fa95fb6 100644 wrapper.read(BedrockTypes.INT_LE); // limited world width wrapper.read(BedrockTypes.INT_LE); // limited world height wrapper.read(Types.BOOLEAN); // nether type -@@ -336,7 +337,7 @@ public final class JoinPackets { +@@ -337,7 +338,7 @@ public final class JoinPackets { } if (!inventoryServerAuthoritative) { @@ -4862,7 +4862,7 @@ index 8f8b7567..0fa95fb6 100644 } gameSession.setBedrockVanillaVersion(version); -@@ -426,7 +427,8 @@ public final class JoinPackets { +@@ -427,7 +428,8 @@ public final class JoinPackets { ); protocol.registerClientbound(ClientboundBedrockPackets.ITEM_REGISTRY, null, wrapper -> { wrapper.cancel(); @@ -4872,7 +4872,7 @@ index 8f8b7567..0fa95fb6 100644 ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Received ITEM_REGISTRY after item rewriter was already initialized"); return; } -@@ -434,11 +436,14 @@ public final class JoinPackets { +@@ -435,11 +437,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); @@ -4889,7 +4889,7 @@ index 8f8b7567..0fa95fb6 100644 } for (ItemEntry itemEntry : itemEntries) { if (itemEntry.componentData() != null && itemEntry.version() == ItemVersion.DataDriven && itemRewriter.getComponentItems().contains(itemEntry.identifier())) { -@@ -561,6 +566,7 @@ public final class JoinPackets { +@@ -562,6 +567,7 @@ public final class JoinPackets { user.get(EntityTracker.class).sendNativeCustomEntitySnapshot(); user.get(PlayerInputStorage.class).markJoined(); user.get(PlayerInputStorage.class).sendSnapshot(); @@ -5848,7 +5848,7 @@ index 3d7f067e..81763394 100644 this.runtimeIdToUniqueId.remove(entity.runtimeId()); this.javaIdToUniqueId.remove(entity.javaId()); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java -index 6b46f956..fcae47ae 100644 +index fb054492..13672f9b 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/GameSessionStorage.java @@ -24,6 +24,7 @@ import com.viaversion.viaversion.api.connection.UserConnection; @@ -5859,7 +5859,7 @@ index 6b46f956..fcae47ae 100644 import net.raphimc.viabedrock.protocol.data.JavaRegistries; import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ChatRestrictionLevel; import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.GameType; -@@ -40,6 +41,7 @@ public class GameSessionStorage extends StoredObject { +@@ -41,6 +42,7 @@ public class GameSessionStorage extends StoredObject { private final Map bedrockDimensionDefinitions = new HashMap<>(); private final Set availableEntityIdentifiers = new HashSet<>(BedrockProtocol.MAPPINGS.getBedrockEntities().keySet()); private Semver bedrockVanillaVersion; @@ -5867,7 +5867,7 @@ index 6b46f956..fcae47ae 100644 private boolean flatGenerator; private int movementRewindHistorySize; private GameType levelGameType; -@@ -49,6 +51,7 @@ public class GameSessionStorage extends StoredObject { +@@ -51,6 +53,7 @@ public class GameSessionStorage extends StoredObject { private boolean emoteChatMuted; private boolean commandsEnabled; private boolean inventoryServerAuthoritative; @@ -5875,7 +5875,7 @@ index 6b46f956..fcae47ae 100644 private boolean blockBreakingServerAuthoritative; private boolean immutableWorld; -@@ -101,6 +104,14 @@ public class GameSessionStorage extends StoredObject { +@@ -103,6 +106,14 @@ public class GameSessionStorage extends StoredObject { this.availableEntityIdentifiers.add(entityIdentifier); } @@ -5890,7 +5890,7 @@ index 6b46f956..fcae47ae 100644 public Semver getBedrockVanillaVersion() { return this.bedrockVanillaVersion; } -@@ -165,6 +176,14 @@ public class GameSessionStorage extends StoredObject { +@@ -175,6 +186,14 @@ public class GameSessionStorage extends StoredObject { this.commandsEnabled = commandsEnabled; } 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 9f864d69..d79fea9f 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 f369213f195d38650eeea176dc885c20d06e3069 Mon Sep 17 00:00:00 2001 +From a3c5f42b98fa6cb643d817d93c6967966e102d55 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 25355ba0..e816a984 100644 +index 9eedc5c5..1b6ac79c 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -578,6 +578,7 @@ public final class JoinPackets { +@@ -579,6 +579,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 a36077b9..ac02990f 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 993542a82281e41669f0ea85aa688ebbc9ccf3fe Mon Sep 17 00:00:00 2001 +From ed8f66752756636cd24fb6ee5f1966946268bf2c 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 e816a984..0b9f70f8 100644 +index 1b6ac79c..4f775c01 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 e816a984..0b9f70f8 100644 import net.raphimc.viabedrock.api.util.BitSets; import net.raphimc.viabedrock.api.util.PacketFactory; import net.raphimc.viabedrock.api.util.StringUtil; -@@ -474,7 +475,8 @@ public final class JoinPackets { +@@ -475,7 +476,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 cb2574ca..ff478c96 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 c37b0cb2b03906c92236e3cb3312c85762893e65 Mon Sep 17 00:00:00 2001 +From 72e6d2fe257b07dc70b3eba4d62b981f9b21d9d5 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 0b9f70f8..b7fb312e 100644 +index 4f775c01..5af0eca2 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java -@@ -569,6 +569,8 @@ public final class JoinPackets { +@@ -570,6 +570,8 @@ public final class JoinPackets { user.get(PlayerInputStorage.class).markJoined(); user.get(PlayerInputStorage.class).sendSnapshot(); HudPackets.sendNativeRoot(user); diff --git a/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch b/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch index e4a36155..75595578 100644 --- a/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch +++ b/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch @@ -1,4 +1,4 @@ -From c7a3824fe290f9f382329811a744c1d42bdc7cde Mon Sep 17 00:00:00 2001 +From 9420af1368228b6d58cc34a6e6de25428ec29c0a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:06:07 +0100 Subject: [PATCH] feat(connection): allow confirmed blacklisted server joins @@ -247,7 +247,7 @@ index f345d7c..40e7ae9 100644 "command_block.viafabricplus.first_tick": "Execute on first tick", "base.viafabricplus.accept": "Accept", diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index ff5a82c..86671a8 100644 +index 0696216..029a4f3 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json @@ -48,6 +48,8 @@ diff --git a/patches/viafabricplus-bedrock/series.json b/patches/viafabricplus-bedrock/series.json index 93feb9e8..913bef9c 100644 --- a/patches/viafabricplus-bedrock/series.json +++ b/patches/viafabricplus-bedrock/series.json @@ -339,6 +339,17 @@ "reason": "Load the licensed grayscale End sky texture for the world's selected base-game version." } ] + }, + { + "file": "0039-allow-food-use-on-bedrock-peaceful-difficulty.patch", + "title": "fix(items): allow food use on Bedrock Peaceful difficulty", + "prerequisites": [ + { + "project": "viafabricplus-bedrock", + "patch": "0020-apply-server-movement-and-camera-permissions.patch", + "reason": "Publish the connection capability state used to recognize both direct and remotely translated Bedrock clients before food admission." + } + ] } ], "deferred": [ diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.patch b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.patch new file mode 100644 index 00000000..faf74dc7 --- /dev/null +++ b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.patch @@ -0,0 +1,72 @@ +From 854e4fa0a3c1a6a95e990e7cf62a0b116153a3a9 Mon Sep 17 00:00:00 2001 +From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> +Date: Sun, 1 Nov 2026 19:54:04 +0100 +Subject: [PATCH] fix(items): allow food use on Bedrock Peaceful difficulty + +Allow the local consumable predicate to ignore hunger on a Bedrock client +in Peaceful difficulty. Retain the ordinary Java predicate and each food's +always-edible flag. Read current level difficulty on every use, including +connections translated remotely through ViaProxy. + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 protocol-2225 bread controls refuse +full-hunger use on Normal, consume one item after switching to Peaceful, +and refuse after switching back. The previous installed client never +enters use on Peaceful and retains all 16 items. Keep inventory consumption +and completion owned by the server; this hook only admits the local cycle. + +DataDriven apple admission at full hunger differs independently from its +advertised component and remains separate native investigation. These +controls do not close the original intermittent food-use report. +--- + .../mixin/features/item/MixinConsumable.java | 27 +++++++++++++++++++ + .../viafabricplus-bedrock.mixins.json | 1 + + 2 files changed, 28 insertions(+) + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/item/MixinConsumable.java + +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/item/MixinConsumable.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/item/MixinConsumable.java +new file mode 100644 +index 0000000..e8a7d8c +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/item/MixinConsumable.java +@@ -0,0 +1,27 @@ ++/* ++ * 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.item; ++ ++import com.llamalad7.mixinextras.injector.wrapoperation.Operation; ++import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; ++import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; ++import net.minecraft.world.Difficulty; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.item.component.Consumable; ++import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.injection.At; ++ ++@Mixin(Consumable.class) ++public abstract class MixinConsumable { ++ ++ @WrapOperation(method = "canConsume", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;canEat(Z)Z")) ++ private boolean allowPeacefulBedrockFood(final Player player, final boolean canAlwaysEat, final Operation original) { ++ final boolean peaceful = player.level().isClientSide() && BedrockInputPermissions.isBedrockConnection() ++ && player.level().getDifficulty() == Difficulty.PEACEFUL; ++ return original.call(player, canAlwaysEat || peaceful); ++ } ++ ++} +diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json +index ff5a82c..0696216 100644 +--- a/src/main/resources/viafabricplus-bedrock.mixins.json ++++ b/src/main/resources/viafabricplus-bedrock.mixins.json +@@ -64,6 +64,7 @@ + "features.block.MixinAbstractCauldronBlock", + "features.block.MixinBambooStalkBlock", + "features.block.MixinBedBlock", ++ "features.item.MixinConsumable", + "features.block.MixinBlockBehaviour_Properties", + "features.block.MixinBlockColors", + "features.block.MixinCactusBlock", +-- +2.56.0 diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md new file mode 100644 index 00000000..2fb72343 --- /dev/null +++ b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md @@ -0,0 +1,21 @@ +# Allow food use on Peaceful + +Java's local consumable predicate refuses ordinary food at full hunger. +Bedrock permits that use on Peaceful difficulty. +Keep the original predicate and always-edible flag, but ignore hunger for a client in a Bedrock connection on Peaceful. +Read current level difficulty for each use. +The connection capability also covers translation through ViaProxy. + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 establish the boundary. +Full-hunger bread remains at 16 on Normal, falls to 15 after switching to Peaceful, and remains at 16 after switching back. +The previous installed add-on retains 16 bread and never enters use on Peaceful. +Inventory consumption and completion remain server-owned. + +Installed direct and ViaProxy controls each pass Normal → Peaceful → Normal at full hunger and a hungry Normal control. +Allowed cases enter local use, receive one server completion, and settle at 15 bread. +Full-hunger Normal cases never enter use and retain 16 bread. +The installed add-on and proxy match the rebuilt artifacts. +The full four-project build passes 3,281 tests, with 325 skipped and no failures or errors. +Core packet regressions cover server-confirmed difficulty changes and food admission without inventory prediction. + +The separate DataDriven apple admission discrepancy and the original intermittent food-use report remain open. From f2f1de2fc2d58b6edf3c3deb76cd89ca5af5510e Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:36:50 +0100 Subject: [PATCH 12/47] docs(items): record native component-food admission controls Compare full-hunger Normal bread and apple with paired DataDriven foods on official 1.26.60.5 and BDS 1.26.60.6. Both start_using modes and the omitted-field fixture consume despite can_always_eat=false, disproving an always-only explanation. The actual core registry rejects those controls. Keep component dispatch and the original food report unresolved. --- docs/bedrock-coverage.md | 2 +- .../0008-signal-sustained-bedrock-item-use.pr.md | 11 ++++++++++- ...llow-food-use-on-bedrock-peaceful-difficulty.pr.md | 4 +++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index f1631b7e..8435b05c 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -27,7 +27,7 @@ No complete area has verified Bedrock parity. | 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, Creative catalog actions, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | -| Food use | Native food components, hunger limits, always-edible food, Creative use, and server-confirmed Peaceful difficulty govern admission. | Both routes match selected native bread controls across live difficulty changes. DataDriven apple admission at full hunger, broader use timing, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md). | +| Food use | Native food components, hunger limits, always-edible food, Creative use, and server-confirmed Peaceful difficulty govern admission. | Both routes match selected native bread controls across live difficulty changes. DataDriven food admission at full hunger, broader use timing, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md index 613cf5d4..27f00ce7 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md @@ -40,7 +40,16 @@ All 1,645 core classes match both packaged copies, and installed artifacts match ## Remaining limits The same native build consumes a DataDriven apple at full hunger on Normal despite its advertised `can_always_eat=false` component. -That discrepancy remains open, alongside the original intermittent animation-without-feeding report. +Controlled DataDriven foods also consume one item with that flag false and `minecraft:use_modifiers` present. +Both explicit `start_using` modes, `always` and `if_first`, produce that result. +The new-format fixture that omits the field receives `if_first` from BDS and also consumes one item. +Bread remains at 16 under the same full-hunger Normal control. + +The [26.30 update reference](https://github.com/MicrosoftDocs/minecraft-creator/blob/main/creator/Documents/Update1.26.30.md) describes use-handler ordering for these modes. +The current native controls disprove an `always`-only explanation, but do not establish the complete dispatch predicate. +They do not cover food without use modifiers or competing non-food use handlers. +The current core registry rejects the false-flag DataDriven controls, so this admission discrepancy remains open. +The original intermittent animation-without-feeding report also remains open. The bread controls do not establish all custom-food constructors, ability exceptions, cycle timing, or inventory authority modes. Late completion for a new use of the same item also needs verification. diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md index 2fb72343..f40373cb 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md @@ -18,4 +18,6 @@ The installed add-on and proxy match the rebuilt artifacts. The full four-project build passes 3,281 tests, with 325 skipped and no failures or errors. Core packet regressions cover server-confirmed difficulty changes and food admission without inventory prediction. -The separate DataDriven apple admission discrepancy and the original intermittent food-use report remain open. +The separate DataDriven food admission discrepancy and the original intermittent food-use report remain open. +Native full-hunger Normal controls consume both `always` and `if_first` foods with `can_always_eat=false`. +The [food-use evidence](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) describes the controls and remaining dispatch limits. From c5708ab3c720e45d53aa3aeaf2432b071e2567ac Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 21:33:33 +0100 Subject: [PATCH 13/47] fix(food): compose native component-food use Retain installed use modifiers when admitting component foods, and project native nutrition, saturation, animation and duration into the Java item. This admits the selected native full-hunger foods and lets custom paper carriers enter local timed use. Signal admitted foods in auth input even without optional tags, while keeping inventory consumption server-owned. Add registry, wire-property and packet admission regressions. Verify the full four-project build and installed direct/proxy controls against version-matched native fixtures. Keep the observed instant-food repeat timing difference and original intermittent food report documented. --- docs/bedrock-coverage.md | 2 +- ...08-signal-sustained-bedrock-item-use.patch | 245 ++++++++++++++---- ...08-signal-sustained-bedrock-item-use.pr.md | 67 ++--- ...ain-bedrock-creative-catalog-entries.patch | 8 +- ...ronize-glide-rockets-with-auth-input.patch | 18 +- ...d-use-on-bedrock-peaceful-difficulty.pr.md | 9 +- 6 files changed, 251 insertions(+), 98 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 8435b05c..599329d0 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -27,7 +27,7 @@ No complete area has verified Bedrock parity. | 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, Creative catalog actions, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | -| Food use | Native food components, hunger limits, always-edible food, Creative use, and server-confirmed Peaceful difficulty govern admission. | Both routes match selected native bread controls across live difficulty changes. DataDriven food admission at full hunger, broader use timing, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md). | +| Food use | Installed food and use-modifier components govern admission and Java food/consumable properties, with Creative and server-confirmed Peaceful exceptions. | Both routes match selected native bread and custom-food controls. Held instant-food repeat timing, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch index fa50bea2..7547d759 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch @@ -1,4 +1,4 @@ -From 63776783a37c07860529639be7955eaf0419644d Mon Sep 17 00:00:00 2001 +From 1f0dd8cc6bc1782cd6a3fcee3a597eff7242f6d8 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(interaction): signal sustained Bedrock item use @@ -62,21 +62,40 @@ never enters use on Peaceful. Registered packet tests now carry those difficulty transitions through the food admission boundary without predicting any inventory consumption. -The same target's DataDriven apple consumes at full hunger on Normal even -with an advertised can_always_eat=false component. That separate component -admission discrepancy and the original intermittent eating report remain -open; preserve the verified Normal refusal for ordinary legacy bread. +Compose food admission with installed use modifiers on component Item +receivers. Native 1.26.60.5/BDS 1.26.60.6 controls refuse food-only use at +full hunger, but consume while hungry and stop at twenty. Adding use +modifiers permits full-hunger use with either start_using mode, including +zero duration. The native food callback leaves the result unchanged when +hunger refuses it, so another installed handler can still start use. + +Retain the advertised can_always_eat flag independently of that composed +admission. Transport effective full-hunger eligibility through Java FOOD, +including native nutrition and final saturation, so Java's local predicate +and the outgoing transaction gate agree. Project component-food use +animation and network duration into CONSUMABLE, including zero duration, +so custom paper carriers also enter the local use cycle. Consume effects +remain server-owned. Preserve initial and current Peaceful exceptions and +server ownership of inventory consumption. Mark admitted food use in auth +input even when the definition has no optional minecraft:is_food tag. +Rejected and unsupported offhand requests do not gain that flag. + +Registry and packet regressions cover installed component retention, +constructor-specific admission, full/hungry transitions and FOOD wire +properties without predicting stack consumption. Competing non-food use +handlers, complete custom consumable behavior, exact cycle timing, and the +original intermittent eating report remain separate requirements. --- .../model/ItemDefinitionProperties.java | 6 + - .../protocol/model/ItemFoodProperties.java | 27 +++ + .../protocol/model/ItemFoodProperties.java | 57 ++++++ .../protocol/model/ItemRegistry.java | 6 + - .../protocol/packet/InteractionPackets.java | 55 +++++- - .../protocol/rewriter/ItemRewriter.java | 11 ++ - .../model/ItemFoodPropertiesTest.java | 65 +++++++ - .../packet/CompletedItemUsePacketTest.java | 98 +++++++++++ - .../packet/FoodUseAdmissionPacketTest.java | 165 ++++++++++++++++++ + .../protocol/packet/InteractionPackets.java | 59 +++++- + .../protocol/rewriter/ItemRewriter.java | 20 +- + .../model/ItemFoodPropertiesTest.java | 122 +++++++++++++ + .../packet/CompletedItemUsePacketTest.java | 98 ++++++++++ + .../packet/FoodUseAdmissionPacketTest.java | 172 ++++++++++++++++++ .../packet/InteractionPacketsTest.java | 31 ++++ - 9 files changed, 461 insertions(+), 3 deletions(-) + 9 files changed, 567 insertions(+), 4 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/ItemFoodProperties.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/ItemFoodPropertiesTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/CompletedItemUsePacketTest.java @@ -84,7 +103,7 @@ open; preserve the verified Normal refusal for ordinary legacy bread. create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/InteractionPacketsTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemDefinitionProperties.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemDefinitionProperties.java -index 65becabc..56ab4662 100644 +index 65becabc..5b4769aa 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemDefinitionProperties.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemDefinitionProperties.java @@ -35,6 +35,7 @@ final class ItemDefinitionProperties { @@ -99,7 +118,7 @@ index 65becabc..56ab4662 100644 void read(final CompoundTag components, final Function resolveBlock) { this.updates.add(components.copy()); -+ this.food = this.food.withComponentData(components); ++ this.food = this.food.withComponentData(components, this.component()); this.stacking = this.stacking.withComponentData(components); this.storage = this.storage.withComponentData(components); this.cooking = this.component() ? this.cooking.withComponentItemData(components, this.identifier, resolveBlock) @@ -116,10 +135,10 @@ index 65becabc..56ab4662 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/model/ItemFoodProperties.java b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemFoodProperties.java new file mode 100644 -index 00000000..f365440d +index 00000000..23415b47 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/model/ItemFoodProperties.java -@@ -0,0 +1,27 @@ +@@ -0,0 +1,57 @@ +/* + * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -129,21 +148,51 @@ index 00000000..f365440d + +import com.viaversion.nbt.tag.ByteTag; +import com.viaversion.nbt.tag.CompoundTag; -+ -+/** Advertised native food eligibility, independent of the mapped Java item. */ -+public record ItemFoodProperties(boolean food, boolean canAlwaysEat) { -+ -+ public static final ItemFoodProperties NONE = new ItemFoodProperties(false, false); -+ -+ public ItemFoodProperties withComponentData(final CompoundTag components) { ++import com.viaversion.nbt.tag.NumberTag; ++import com.viaversion.nbt.tag.StringTag; ++import com.viaversion.viaversion.api.minecraft.Holder; ++import com.viaversion.viaversion.api.minecraft.SoundEvent; ++import com.viaversion.viaversion.api.minecraft.item.data.FoodProperties1_21_2; ++import com.viaversion.viaversion.api.minecraft.item.data.consumable.Consumable1_21_2; ++import com.viaversion.viaversion.api.minecraft.item.data.consumable.ConsumeEffect; ++ ++/** Native food admission includes other installed components that can start use. */ ++public record ItemFoodProperties(boolean food, boolean canAlwaysEat, boolean useModifiers, int nutrition, float saturationModifier, String useAnimation) { ++ ++ public static final ItemFoodProperties NONE = new ItemFoodProperties(false, false, false, 0, 0F, "none"); ++ ++ public ItemFoodProperties withComponentData(final CompoundTag components, final boolean componentItem) { ++ final boolean useModifiers = this.useModifiers || componentItem && components.get("minecraft:use_modifiers") instanceof CompoundTag; ++ final String useAnimation = componentItem && components.get("minecraft:use_animation") instanceof CompoundTag animation ++ && animation.get("value") instanceof StringTag value ? value.getValue() : this.useAnimation; + if (!(components.get("minecraft:food") instanceof CompoundTag component)) { -+ return this; ++ return useModifiers == this.useModifiers && useAnimation.equals(this.useAnimation) ? this ++ : new ItemFoodProperties(this.food, this.canAlwaysEat, useModifiers, this.nutrition, this.saturationModifier, useAnimation); + } -+ return new ItemFoodProperties(true, component.get("can_always_eat") instanceof ByteTag value && value.asByte() != 0); ++ return new ItemFoodProperties(true, component.get("can_always_eat") instanceof ByteTag value && value.asByte() != 0, ++ useModifiers, component.get("nutrition") instanceof NumberTag value ? value.asInt() : 0, ++ component.get("saturation_modifier") instanceof NumberTag value ? value.asFloat() : 0F, useAnimation); + } + + public boolean allowsUse(final float hunger, final float maximumHunger, final boolean ignoresHunger) { -+ return !this.food || this.canAlwaysEat || ignoresHunger || hunger < maximumHunger; ++ // Food refusal does not veto an installed use-modifier component's independent start. ++ return !this.food || this.canAlwaysEat || this.useModifiers || ignoresHunger || hunger < maximumHunger; ++ } ++ ++ public FoodProperties1_21_2 javaComponent() { ++ return new FoodProperties1_21_2(this.nutrition, (float) this.nutrition * 2F * this.saturationModifier, ++ this.canAlwaysEat || this.useModifiers); ++ } ++ ++ public Consumable1_21_2 javaConsumable(final Integer useDurationTicks) { ++ final int animation = switch (this.useAnimation) { ++ case "eat" -> 1; ++ case "drink" -> 2; ++ default -> 0; ++ }; ++ final String sound = animation == 2 ? "minecraft:entity.generic.drink" : "minecraft:entity.generic.eat"; ++ return new Consumable1_21_2(useDurationTicks == null ? 0F : useDurationTicks / 20F, ++ animation, Holder.of(new SoundEvent(sound, null)), animation == 1, new ConsumeEffect[0]); + } + +} @@ -165,7 +214,7 @@ index a906b932..03ebaf51 100644 return this.definitions.definitions().stream().filter(definition -> definition.receiver().component()) .map(NativeItemDefinitions.Definition::identifier).collect(Collectors.toUnmodifiableSet()); 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 081338cb..19465cd8 100644 +index 081338cb..5cc1d6f5 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -46,10 +46,14 @@ import net.raphimc.viabedrock.protocol.ServerboundBedrockPackets; @@ -209,11 +258,12 @@ index 081338cb..19465cd8 100644 if (javaHand < 0 || javaHand >= InteractionHand.values().length) { wrapper.cancel(); return; -@@ -278,8 +288,15 @@ public final class InteractionPackets { +@@ -278,8 +288,16 @@ public final class InteractionPackets { openBook.send(BedrockProtocol.class); return; } -+ if (hand == InteractionHand.MAIN_HAND && !canStartFoodUse(clientPlayer, itemRewriter.foodProperties(usedItem), wrapper.user().get(GameSessionStorage.class))) { ++ final ItemFoodProperties food = itemRewriter.foodProperties(usedItem); ++ if (hand == InteractionHand.MAIN_HAND && !canStartFoodUse(clientPlayer, food, wrapper.user().get(GameSessionStorage.class))) { + // Java sends USE_ITEM before its local food check. Native clients admit eating before sending. + wrapper.cancel(); + return; @@ -226,7 +276,17 @@ index 081338cb..19465cd8 100644 if (transaction == null) { wrapper.cancel(); if (hand == InteractionHand.OFF_HAND && !"minecraft:shield".equals(itemIdentifier)) { -@@ -371,6 +388,24 @@ public final class InteractionPackets { +@@ -287,6 +305,9 @@ public final class InteractionPackets { + } + return; + } ++ if (food.food()) { ++ clientPlayer.addAuthInputData(PlayerAuthInputData.StartUsingItem); ++ } + wrapper.write(inventoryTransactionRewriter.getInventoryTransactionType(), transaction); + }); + +@@ -371,6 +392,24 @@ public final class InteractionPackets { } } }); @@ -251,7 +311,7 @@ index 081338cb..19465cd8 100644 protocol.registerClientbound(ClientboundBedrockPackets.SET_ENTITY_LINK, null, wrapper -> { wrapper.cancel(); final EntityTracker entityTracker = wrapper.user().get(EntityTracker.class); -@@ -380,6 +415,17 @@ public final class InteractionPackets { +@@ -380,6 +419,17 @@ public final class InteractionPackets { } @@ -269,7 +329,7 @@ index 081338cb..19465cd8 100644 static void sendPassengerUpdates(final PacketWrapper source, final EntityTracker entityTracker) { for (Entity vehicle : entityTracker.drainPassengerUpdates()) { final PacketWrapper passengersPacket = PacketWrapper.create(ClientboundPackets26_3.SET_PASSENGERS, -@@ -401,7 +447,7 @@ public final class InteractionPackets { +@@ -401,7 +451,7 @@ public final class InteractionPackets { static BedrockInventoryTransaction beginItemUse(final ClientPlayerEntity player, final InteractionHand hand, final int hotbarSlot, final BedrockItem item, @@ -278,7 +338,7 @@ index 081338cb..19465cd8 100644 if (item.isEmpty()) { return null; } -@@ -413,6 +459,9 @@ public final class InteractionPackets { +@@ -413,6 +463,9 @@ public final class InteractionPackets { return null; } player.setShieldBlockingHand(null); @@ -289,7 +349,7 @@ index 081338cb..19465cd8 100644 0, null, null, ComplexInventoryTransaction_Type.ItemUseTransaction, new InventoryTransactionData.UseItemTransactionData( diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index ea5a6a11..f0ff52af 100644 +index ea5a6a11..1165eb37 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java @@ -75,6 +75,7 @@ import net.raphimc.viabedrock.protocol.model.recipe.RecipeOutputKey; @@ -331,12 +391,31 @@ index ea5a6a11..f0ff52af 100644 this.items = this.registry.ids(); this.componentItems = this.registry.componentItems(); for (ItemEntry itemEntry : this.registry.registrations()) { +@@ -385,10 +396,17 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex + + javaItem.dataContainer().set(StructuredDataKey.MAX_STACK_SIZE, Math.max(1, Math.min(99, this.maxStackSize(bedrockItem)))); + ++ final ItemAnimationData animation = this.animationData.getOrDefault(identifier, ItemAnimationData.EMPTY); + javaItem.dataContainer().set(StructuredDataKey.USE_EFFECTS, this.useModifiers(bedrockItem).javaComponent()); ++ final ItemFoodProperties food = this.foodProperties(bedrockItem); ++ if (food.food()) { ++ javaItem.dataContainer().set(StructuredDataKey.FOOD1_21_2, food.javaComponent()); ++ if (this.componentItems.contains(identifier)) { ++ javaItem.dataContainer().set(StructuredDataKey.CONSUMABLE26_3, food.javaConsumable(animation.useDurationTicks())); ++ } ++ } + + CustomAttachableResourceRewriter.applyItemModel(this.user().get(ResourcePackStorage.class), identifier, javaItem); +- final ItemAnimationData animation = this.animationData.getOrDefault(identifier, ItemAnimationData.EMPTY); + javaItem.dataContainer().set(StructuredDataKey.CUSTOM_DATA, + ItemDataRewriter.withBedrockItemData(javaItem.dataContainer().get(StructuredDataKey.CUSTOM_DATA), + identifier, new ItemAnimationData(animation.useDurationTicks(), this.registry.tags(bedrockItem), diff --git a/src/test/java/net/raphimc/viabedrock/protocol/model/ItemFoodPropertiesTest.java b/src/test/java/net/raphimc/viabedrock/protocol/model/ItemFoodPropertiesTest.java new file mode 100644 -index 00000000..91325405 +index 00000000..04631f44 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/model/ItemFoodPropertiesTest.java -@@ -0,0 +1,65 @@ +@@ -0,0 +1,122 @@ +/* + * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -345,6 +424,9 @@ index 00000000..91325405 +package net.raphimc.viabedrock.protocol.model; + +import com.viaversion.nbt.tag.CompoundTag; ++import com.viaversion.viaversion.api.minecraft.item.data.FoodProperties1_21_2; ++import com.viaversion.viaversion.api.minecraft.item.data.consumable.Consumable1_21_2; ++import io.netty.buffer.Unpooled; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ItemVersion; +import org.junit.jupiter.api.Test; + @@ -357,25 +439,25 @@ index 00000000..91325405 + final CompoundTag components = new CompoundTag(); + final CompoundTag food = new CompoundTag(); + components.put("minecraft:food", food); -+ final var ordinary = ItemFoodProperties.NONE.withComponentData(components); ++ final var ordinary = ItemFoodProperties.NONE.withComponentData(components, true); + assertTrue(ordinary.food()); + assertFalse(ordinary.canAlwaysEat()); -+ assertSame(ordinary, ordinary.withComponentData(new CompoundTag())); ++ assertSame(ordinary, ordinary.withComponentData(new CompoundTag(), true)); + food.putBoolean("can_always_eat", true); -+ final var always = ordinary.withComponentData(components); ++ final var always = ordinary.withComponentData(components, true); + assertTrue(always.canAlwaysEat()); + food.remove("can_always_eat"); -+ assertFalse(always.withComponentData(components).canAlwaysEat()); ++ assertFalse(always.withComponentData(components, true).canAlwaysEat()); + } + + @Test + void comparesNativeHungerLimitsWithoutRestrictingCreativeOrAlwaysEdibleFood() { -+ final var food = new ItemFoodProperties(true, false); ++ final var food = new ItemFoodProperties(true, false, false, 0, 0F, "none"); + assertFalse(food.allowsUse(20F, 20F, false)); + assertTrue(food.allowsUse(Math.nextDown(20F), 20F, false)); + assertFalse(food.allowsUse(12F, 12F, false)); + assertTrue(food.allowsUse(20F, 20F, true)); -+ assertTrue(new ItemFoodProperties(true, true).allowsUse(20F, 20F, false)); ++ assertTrue(new ItemFoodProperties(true, true, false, 0, 0F, "none").allowsUse(20F, 20F, false)); + assertTrue(ItemFoodProperties.NONE.allowsUse(20F, 20F, false)); + } + @@ -392,15 +474,69 @@ index 00000000..91325405 + new ItemEntry("test:first", 2, true, ItemVersion.None, new CompoundTag()), + new ItemEntry("test:second", 1, true, ItemVersion.DataDriven, null) + }); -+ assertEquals(new ItemFoodProperties(true, true), registry.foodProperties().get("test:first")); ++ assertEquals(new ItemFoodProperties(true, true, false, 0, 0F, "none"), registry.foodProperties().get("test:first")); + final var replaced = new ItemRegistry(new ItemEntry[]{ + new ItemEntry("test:first", 1, true, ItemVersion.DataDriven, data), + new ItemEntry("test:second", 1, true, ItemVersion.DataDriven, null) + }); -+ assertEquals(new ItemFoodProperties(true, true), replaced.foodProperties().get("test:first")); ++ assertEquals(new ItemFoodProperties(true, true, false, 0, 0F, "none"), replaced.foodProperties().get("test:first")); + assertEquals(ItemFoodProperties.NONE, replaced.foodProperties().get("test:second")); + } + ++ @Test ++ void componentUseModifiersAdmitFullHungerAndSurviveOmittedReads() { ++ final var food = new CompoundTag(); ++ food.putBoolean("can_always_eat", false); ++ food.putInt("nutrition", 4); ++ food.putFloat("saturation_modifier", 0.3F); ++ final var bare = new CompoundTag(); ++ bare.put("minecraft:food", food); ++ final var animation = new CompoundTag(); ++ animation.putString("value", "eat"); ++ bare.put("minecraft:use_animation", animation); ++ final var timed = bare.copy(); ++ final var modifiers = new CompoundTag(); ++ modifiers.putString("start_using", "if_first"); ++ modifiers.putFloat("use_duration", 0F); ++ timed.put("minecraft:use_modifiers", modifiers); ++ final var bareData = new CompoundTag(); ++ bareData.put("components", bare); ++ final var timedData = new CompoundTag(); ++ timedData.put("components", timed); ++ final var omittedFood = new CompoundTag(); ++ omittedFood.put("components", new CompoundTag()); ++ final var registry = new ItemRegistry(new ItemEntry[]{ ++ new ItemEntry("test:timed", 1, true, ItemVersion.DataDriven, timedData), ++ new ItemEntry("test:timed", 1, true, ItemVersion.None, omittedFood), ++ new ItemEntry("test:bare", 2, true, ItemVersion.DataDriven, bareData), ++ new ItemEntry("test:legacy", 3, false, ItemVersion.Legacy, timedData) ++ }); ++ final var component = registry.foodProperties().get("test:timed"); ++ final var ordinary = registry.foodProperties().get("test:bare"); ++ assertFalse(component.canAlwaysEat()); ++ assertTrue(component.allowsUse(20F, 20F, false)); ++ assertFalse(ordinary.allowsUse(20F, 20F, false)); ++ assertTrue(ordinary.allowsUse(10F, 20F, false)); ++ assertFalse(registry.foodProperties().get("test:legacy").allowsUse(20F, 20F, false)); ++ final var wire = Unpooled.buffer(); ++ try { ++ FoodProperties1_21_2.TYPE.write(wire, component.javaComponent()); ++ assertEquals(new FoodProperties1_21_2(4, 2.4F, true), FoodProperties1_21_2.TYPE.read(wire)); ++ assertFalse(wire.isReadable()); ++ assertEquals(new FoodProperties1_21_2(4, 2.4F, false), ordinary.javaComponent()); ++ Consumable1_21_2.TYPE26_3.write(wire, component.javaConsumable(32)); ++ final var consumable = Consumable1_21_2.TYPE26_3.read(wire); ++ assertEquals(1.6F, consumable.consumeSeconds()); ++ assertEquals(1, consumable.animationType()); ++ assertTrue(consumable.hasConsumeParticles()); ++ assertEquals(0, consumable.consumeEffects().length); ++ assertFalse(wire.isReadable()); ++ assertEquals(0F, ordinary.javaConsumable(0).consumeSeconds()); ++ } finally { ++ wire.release(); ++ } ++ } ++ +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/CompletedItemUsePacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/CompletedItemUsePacketTest.java new file mode 100644 @@ -508,10 +644,10 @@ index 00000000..45f4a6a3 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/FoodUseAdmissionPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/FoodUseAdmissionPacketTest.java new file mode 100644 -index 00000000..20117e06 +index 00000000..50acebde --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/FoodUseAdmissionPacketTest.java -@@ -0,0 +1,165 @@ +@@ -0,0 +1,172 @@ +/* + * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -530,6 +666,7 @@ index 00000000..20117e06 +import net.raphimc.viabedrock.protocol.BedrockProtocol; +import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.Difficulty; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.PlayerAuthInputData; +import net.raphimc.viabedrock.api.util.PacketFactory; +import net.raphimc.viabedrock.protocol.data.enums.java.generated.InteractionHand; +import net.raphimc.viabedrock.protocol.model.BedrockItem; @@ -581,7 +718,7 @@ index 00000000..20117e06 + inventory.getOffhandContainer().setItem(0, held); + final var items = mock(ItemRewriter.class); + when(items.getItems()).thenReturn(hand == InteractionHand.MAIN_HAND ? ImmutableBiMap.of("test:food", 1) : ImmutableBiMap.of()); -+ when(items.foodProperties(held)).thenReturn(new ItemFoodProperties(true, false)); ++ when(items.foodProperties(held)).thenReturn(new ItemFoodProperties(true, false, false, 0, 0F, "none")); + stores.put(ItemRewriter.class, items); + final var input = Unpooled.buffer(); + try (var packets = mockStatic(PacketFactory.class)) { @@ -605,7 +742,7 @@ index 00000000..20117e06 + } + + @Test -+ void confirmedDifficultyChangesAdmitPeacefulFoodAndRestoreFullHungerRefusal() throws Exception { ++ void confirmedDifficultyAndComponentUseAdmitFoodWithoutConsumingInventory() throws Exception { + final Map, Object> stores = new HashMap<>(); + final UserConnection user = (UserConnection) Proxy.newProxyInstance(UserConnection.class.getClassLoader(), + new Class[]{UserConnection.class}, (proxy, method, args) -> method.getName().equals("get") ? stores.get(args[0]) : null); @@ -623,13 +760,18 @@ index 00000000..20117e06 + held.setAmount(8); + inventory.getInventoryContainer().setItem(0, held); + final var items = mock(ItemRewriter.class); -+ when(items.foodProperties(held)).thenReturn(new ItemFoodProperties(true, false)); ++ when(items.foodProperties(held)).thenReturn(new ItemFoodProperties(true, false, false, 0, 0F, "none")); + stores.put(ItemRewriter.class, items); + final var transactions = new InventoryTransactionRewriter(user); + stores.put(InventoryTransactionRewriter.class, transactions); + final var protocol = new TestProtocol(); + try (var packets = mockStatic(PacketFactory.class)) { -+ for (final Difficulty difficulty : new Difficulty[] {Difficulty.Normal, Difficulty.Peaceful, Difficulty.Normal}) { ++ final Difficulty[] difficulties = {Difficulty.Normal, Difficulty.Peaceful, Difficulty.Normal, Difficulty.Normal}; ++ for (int index = 0; index < difficulties.length; index++) { ++ final Difficulty difficulty = difficulties[index]; ++ final boolean useModifiers = index == 3; ++ entities.getClientPlayer().authInputData().clear(); ++ when(items.foodProperties(held)).thenReturn(new ItemFoodProperties(true, false, useModifiers, 0, 0F, "none")); + final var update = Unpooled.buffer(); + final var input = Unpooled.buffer(); + try { @@ -640,13 +782,14 @@ index 00000000..20117e06 + input.writeFloat(0F).writeFloat(0F); + final var packet = new PacketWrapperImpl(ServerboundPackets26_3.USE_ITEM, input, user); + protocol.translate(packet); -+ assertEquals(difficulty != Difficulty.Peaceful, packet.isCancelled()); ++ assertEquals(difficulty != Difficulty.Peaceful && !useModifiers, packet.isCancelled()); + if (!packet.isCancelled()) { + final var transaction = packet.get(transactions.getInventoryTransactionType(), 0); + final var use = assertInstanceOf(InventoryTransactionData.UseItemTransactionData.class, transaction.transactionData()); + assertEquals(8, use.itemInHand().amount()); + } + assertEquals(8, held.amount()); ++ assertEquals(!packet.isCancelled(), entities.getClientPlayer().authInputData().contains(PlayerAuthInputData.StartUsingItem)); + assertFalse(input.isReadable()); + } finally { + update.release(); diff --git a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md index 27f00ce7..23ef1e5f 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md @@ -2,56 +2,65 @@ This patch admits main-hand food use before it sends the transaction or sets `StartUsingItem`. Java sends its air-use packet before its local food predicate, and BDS can accept an otherwise ineligible request. -Admission uses the advertised native food component and current hunger limits. +Admission uses the installed native food and use-modifier components, plus current hunger limits. Always-edible food, Creative use, and server-confirmed Peaceful difficulty ignore hunger. +An installed use-modifier handler on a component item can also start use when its food handler refuses full hunger. Items without advertised food data retain their existing route. Unsupported offhand use retains its container resynchronization. Food definitions follow ordered component updates and connection registry identity. -The implementation does not infer food behavior from the Java item or a fixed whitelist. -The [food component reference](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/itemreference/examples/itemcomponents/minecraft_food?view=minecraft-bedrock-stable) describes `can_always_eat`. -Native captures establish the behavior of the tested builds. +The implementation uses the constructed native item receiver, rather than a Java carrier or an item-name list. +It projects nutrition, final saturation, and effective full-hunger eligibility into Java's food component. +Component foods also receive the advertised use animation and network duration through Java's consumable component. +This lets custom foods carried as paper enter local use, including zero-duration use. +Consume effects and canonical inventory counts remain server-owned. +Admitted food sets `StartUsingItem` even without an optional food tag. `COMPLETED_USING_ITEM` supplies a signed item ID and a use method. Matching held-item completions produce Java's `USE_ITEM_COMPLETE` event without changing canonical inventory counts. Replaced items and methods outside zero through sixteen cannot complete the current use. The patch body retains the original Bedrock 1.26.51.1 handler evidence and admission rationale. -## Current native boundary +## Native evidence Official Bedrock 1.26.60.5 with BDS 1.26.60.6 uses protocol 2225. -At full hunger, bread remains at 16 on Normal and decreases to 15 after a switch to Peaceful. -A switch back to Normal restores refusal and retains 16 bread. -The previous installed add-on retains all 16 bread and never enters use on Peaceful. +Full-hunger bread remains at 16 on Normal, falls to 15 on Peaceful, and remains at 16 after returning to Normal. Core retains initial difficulty and later server confirmations. The client integration also admits its local consumable cycle on Peaceful, including connections through ViaProxy. -Inventory consumption remains server-owned. -Registered packet tests cover main-hand rejection, offhand resynchronization, and Normal → Peaceful → Normal admission without inventory prediction. -Model tests cover registry replacement, hunger maxima, Creative use, and always-edible food. -Completion tests cover signed IDs, method boundaries, and replaced items. +Controlled component foods distinguish food admission from another installed handler's result: -Installed direct and ViaProxy controls each pass those three full-hunger cases and a hungry Normal control. -Allowed cases each receive one server completion, consume one bread, and settle at the server count of 15. -Refused cases never enter local use and retain 16 bread. -The full four-project build passes 3,281 tests, with 325 skipped and no failures or errors. -All 1,645 core classes match both packaged copies, and installed artifacts match the build checksums. +- Food without use modifiers refuses full hunger, but consumes while hungry and stops at twenty. +- Adding use modifiers permits full-hunger use with both `always` and `if_first`, including zero duration. +- A timed food consumes one item and raises hunger from ten to fourteen and saturation from zero to 2.4. +- Admitted custom foods send `StartUsingItem` even without the optional food tag. + +The matching native food callback leaves its result unchanged when hunger refuses use. +That permits another installed handler to start the tested component items. +The [food component reference](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/itemreference/examples/itemcomponents/minecraft_food?view=minecraft-bedrock-stable) describes the advertised `can_always_eat` flag. +The [26.30 update reference](https://github.com/MicrosoftDocs/minecraft-creator/blob/main/creator/Documents/Update1.26.30.md) describes use-handler ordering. +Native controls establish the combined behavior for these fixtures; they do not establish every competing handler combination. + +## Validation + +Seventeen focused tests cover installed component retention, constructor-specific admission, Java component wire properties, difficulty changes, food-use flags, offhand resynchronization, and completion boundaries. +Admission tests retain the held quantity and verify that rejected uses leave `StartUsingItem` clear. +The full four-project build passes 3,282 tests, with 325 skipped and no failures or errors. + +Installed direct and ViaProxy controls cover eleven cases each. +Both routes match the selected native admission, hunger, timed consumption, and zero-duration outcomes. +Custom timed food enters local use, and settled client counts agree with the server. +Installed artifacts match the build checksums; core classes match both packaged copies. ## Remaining limits -The same native build consumes a DataDriven apple at full hunger on Normal despite its advertised `can_always_eat=false` component. -Controlled DataDriven foods also consume one item with that flag false and `minecraft:use_modifiers` present. -Both explicit `start_using` modes, `always` and `if_first`, produce that result. -The new-format fixture that omits the field receives `if_first` from BDS and also consumes one item. -Bread remains at 16 under the same full-hunger Normal control. - -The [26.30 update reference](https://github.com/MicrosoftDocs/minecraft-creator/blob/main/creator/Documents/Update1.26.30.md) describes use-handler ordering for these modes. -The current native controls disprove an `always`-only explanation, but do not establish the complete dispatch predicate. -They do not cover food without use modifiers or competing non-food use handlers. -The current core registry rejects the false-flag DataDriven controls, so this admission discrepancy remains open. -The original intermittent animation-without-feeding report also remains open. -The bread controls do not establish all custom-food constructors, ability exceptions, cycle timing, or inventory authority modes. +Held instant-food repeat timing still differs: the 2.1-second always-edible control consumes eleven items through Java and nine through native Bedrock. +Native input-tick intervals vary, so this evidence does not justify a fixed replacement delay. +Presence of `StartUsingItem` is verified; exact packet phase counts and complete cycle timing remain open. + +Competing non-food use handlers, other consumable animations and effects, ability exceptions, and all inventory authority modes need further comparison. Late completion for a new use of the same item also needs verification. +The original intermittent animation-without-feeding report remains open. This patch remains deferred behind [ViaBedrock #276](https://github.com/ViaVersionAddons/ViaBedrock/pull/276). It does not belong in an independent upstream PR branch. 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 98c09680..b15fa7a8 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 66fd325ee215965a74eea544905d79996832c448 Mon Sep 17 00:00:00 2001 +From 4c5cc7e5e60ec3deeb6d8d66c24d0d49a7284061 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 @@ -2709,7 +2709,7 @@ index f5cbbaf0..a3e297da 100644 if (NativeCookingUiState.actionsEnabled(wrapper.user())) { try { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java -index f0ff52af..2a48c2ad 100644 +index 1165eb37..e1489130 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/rewriter/ItemRewriter.java @@ -48,7 +48,6 @@ import net.raphimc.viabedrock.api.model.container.NativeRecipeCounter; @@ -2760,7 +2760,7 @@ index f0ff52af..2a48c2ad 100644 static { // TODO: Add missing item nbt rewriters -@@ -542,29 +547,31 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -549,29 +554,31 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex "Translating Java items to Bedrock is not yet supported"); } @@ -2811,7 +2811,7 @@ index f0ff52af..2a48c2ad 100644 } public BedrockItem[] bedrockItems(final Item[] javaItems) { -@@ -665,11 +672,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex +@@ -672,11 +679,18 @@ public class ItemRewriter extends StoredObject implements NetworkItemStackContex public int playerRecipeAvailability(final Supplier> candidates, final BedrockItem output, final BedrockItem selectedOutput, final Container owner, final boolean filtering, final boolean includeCursor) { diff --git a/patches/viabedrock/deferred/0016-synchronize-glide-rockets-with-auth-input.patch b/patches/viabedrock/deferred/0016-synchronize-glide-rockets-with-auth-input.patch index af6e01bb..f98c8d54 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,4 +1,4 @@ -From e150c7abb5366ed574c1c12bf526c9df2af4192a Mon Sep 17 00:00:00 2001 +From ee71f82f4281210f17805bf6d4e29bf57885637b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(interaction): preserve native air-use prediction and @@ -151,7 +151,7 @@ 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 19465cd8..fa1f583e 100644 +index 5cc1d6f5..aca780c1 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -21,6 +21,7 @@ import com.viaversion.viaversion.api.minecraft.BlockFace; @@ -172,7 +172,7 @@ index 19465cd8..fa1f583e 100644 private static final Set CONTINUOUS_USE_ITEMS = Set.of( "minecraft:bow", "minecraft:crossbow", "minecraft:trident", "minecraft:potion", "minecraft:milk_bucket", "minecraft:ominous_bottle", -@@ -293,7 +297,8 @@ public final class InteractionPackets { +@@ -294,7 +298,8 @@ public final class InteractionPackets { wrapper.cancel(); return; } @@ -182,16 +182,16 @@ index 19465cd8..fa1f583e 100644 inventoryContainer.getSelectedHotbarSlot(), usedItem, itemIdentifier, itemIdentifier == null ? Set.of() : BedrockProtocol.MAPPINGS.getBedrockItemTags().getOrDefault(itemIdentifier, Set.of())); -@@ -304,7 +309,7 @@ public final class InteractionPackets { - } - return; +@@ -308,7 +313,7 @@ public final class InteractionPackets { + if (food.food()) { + clientPlayer.addAuthInputData(PlayerAuthInputData.StartUsingItem); } - wrapper.write(inventoryTransactionRewriter.getInventoryTransactionType(), transaction); + writeItemUse(wrapper, inventoryTransactionRewriter.getInventoryTransactionType(), transaction, inventoryContainer); }); protocol.registerServerbound(ServerboundPackets26_3.USE_ITEM_ON, null, wrapper -> { -@@ -328,22 +333,20 @@ public final class InteractionPackets { +@@ -332,22 +337,20 @@ public final class InteractionPackets { PacketFactory.sendJavaBlockChangedAck(wrapper.user(), sequence); return; } @@ -217,7 +217,7 @@ index 19465cd8..fa1f583e 100644 final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); final BedrockItem usedItem = inventoryTracker.getInventoryContainer().getSelectedHotbarItem(); if (usedItem.hasBlockState()) { -@@ -422,31 +425,23 @@ public final class InteractionPackets { +@@ -426,31 +429,23 @@ public final class InteractionPackets { return hunger == null || food.allowsUse(hunger.computeClampedValue(), hunger.computeMaxValue(), ignoresHunger); } @@ -262,7 +262,7 @@ index 19465cd8..fa1f583e 100644 final String identifier, final Set tags) { if (item.isEmpty()) { return null; -@@ -462,13 +457,67 @@ public final class InteractionPackets { +@@ -466,13 +461,67 @@ public final class InteractionPackets { if (isContinuousUseItem(identifier, tags)) { player.addAuthInputData(PlayerAuthInputData.StartUsingItem); } diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md index f40373cb..066bb090 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md @@ -15,9 +15,10 @@ Installed direct and ViaProxy controls each pass Normal → Peaceful → Normal Allowed cases enter local use, receive one server completion, and settle at 15 bread. Full-hunger Normal cases never enter use and retain 16 bread. The installed add-on and proxy match the rebuilt artifacts. -The full four-project build passes 3,281 tests, with 325 skipped and no failures or errors. +The full four-project build passes 3,282 tests, with 325 skipped and no failures or errors. Core packet regressions cover server-confirmed difficulty changes and food admission without inventory prediction. -The separate DataDriven food admission discrepancy and the original intermittent food-use report remain open. -Native full-hunger Normal controls consume both `always` and `if_first` foods with `can_always_eat=false`. -The [food-use evidence](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) describes the controls and remaining dispatch limits. +Core also projects installed component-food admission and consumable properties into the Java item. +That covers the selected full-hunger DataDriven controls and local use of custom paper carriers. +The [food-use evidence](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) records those controls and remaining limits. +Held instant-food repeat timing and the original intermittent food-use report remain open. From 9ecaab227f2875584e257a8105411a3e4eda1872 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 22:12:05 +0100 Subject: [PATCH 14/47] docs(food): record native input deadline evidence Trace the matching native air-use path and retain its 200 ms monotonic deadline, input-frame eligibility and persistence across button releases. The held and rapid-click controls require a fresh-click gate as well as repeat scheduling, so a five-tick constant or release-reset timer would not reproduce the observed behavior. Qualify the inspected food callback evidence: the traced DataDriven item does not call it. Keep complete dispatch and the Java timing correction open. Record controlled native outcomes without claiming full parity. --- .../0008-signal-sustained-bedrock-item-use.pr.md | 15 ++++++++++++--- ...-food-use-on-bedrock-peaceful-difficulty.pr.md | 4 +++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md index 23ef1e5f..e5697b49 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md @@ -35,8 +35,9 @@ Controlled component foods distinguish food admission from another installed han - A timed food consumes one item and raises hunger from ten to fourteen and saturation from zero to 2.4. - Admitted custom foods send `StartUsingItem` even without the optional food tag. -The matching native food callback leaves its result unchanged when hunger refuses use. -That permits another installed handler to start the tested component items. +Executable inspection found a food callback that leaves its result unchanged when hunger refuses use. +The traced DataDriven fixture does not call that inspected callback, so its complete dispatch path remains unverified. +The combined controls establish the admission outcomes that this patch transports. The [food component reference](https://learn.microsoft.com/en-us/minecraft/creator/reference/content/itemreference/examples/itemcomponents/minecraft_food?view=minecraft-bedrock-stable) describes the advertised `can_always_eat` flag. The [26.30 update reference](https://github.com/MicrosoftDocs/minecraft-creator/blob/main/creator/Documents/Update1.26.30.md) describes use-handler ordering. Native controls establish the combined behavior for these fixtures; they do not establish every competing handler combination. @@ -55,7 +56,15 @@ Installed artifacts match the build checksums; core classes match both packaged ## Remaining limits Held instant-food repeat timing still differs: the 2.1-second always-edible control consumes eleven items through Java and nine through native Bedrock. -Native input-tick intervals vary, so this evidence does not justify a fixed replacement delay. +Runtime tracing establishes a 200 ms monotonic deadline before air use, with retries checked from the input loop after that deadline. +The deadline persists across button releases and gates fresh clicks. +A fresh native hold consumes nine items both with and without the debugger. +Three 80 ms clicks consume two items when the second starts about 131 ms after the first and the third starts about 616 ms after it. + +Java still uses its four-tick repeat path. +The correction needs the native deadline, fresh-click gate, and input-frame scheduling; native tick intervals do not establish a fixed five-tick delay. +Counts depend on frame cadence, so equal final counts alone would not establish timing parity. +Timer ownership across item families, screens, and connection replacement also needs verification. Presence of `StartUsingItem` is verified; exact packet phase counts and complete cycle timing remain open. Competing non-food use handlers, other consumable animations and effects, ability exceptions, and all inventory authority modes need further comparison. diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md index 066bb090..f60d2482 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md @@ -21,4 +21,6 @@ Core packet regressions cover server-confirmed difficulty changes and food admis Core also projects installed component-food admission and consumable properties into the Java item. That covers the selected full-hunger DataDriven controls and local use of custom paper carriers. The [food-use evidence](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) records those controls and remaining limits. -Held instant-food repeat timing and the original intermittent food-use report remain open. +Native tracing establishes a 200 ms air-use deadline that persists across button releases and gates fresh clicks. +Java input scheduling still needs that correction, including coordinated timer ownership across item families and screens. +The original intermittent food-use report remains open. From 038a9fcb950edba60f6323f43513208eca79aa37 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 22:55:46 +0100 Subject: [PATCH 15/47] chore(stack): refresh upstream bases and native food timing Rebase ViaBedrock, the Bedrock add-on and ViaProxy onto current upstream heads. Keep the generalized passenger tracker and cover unlinking an already removed passenger. Remove the client setup workaround now supplied by upstream and refresh the verified ViaFabricPlus Jenkins pin to 2295. Preserve the native food deadline in the existing client input patch. Gate fresh calls and schedule held retries from input frames, retaining food timing across release while placement keeps its separate lifetime. Keep consumption server-owned and record the remaining parity limits. Verify full patch replay, all four builds (3285 passed, 325 skipped), the modpack bundle and installed direct/proxy food controls, including rapid clicks and pause cancellation. --- docs/bedrock-coverage.md | 2 +- ...08-signal-sustained-bedrock-item-use.pr.md | 20 +- .../0002-restore-map-rendering.patch | 6 +- .../0008-discard-stale-passenger-links.patch | 42 +- .../0008-discard-stale-passenger-links.pr.md | 3 + ...n-completed-client-prediction-frames.patch | 6 +- ...w-confirmed-blacklisted-server-joins.patch | 6 +- patches/viafabricplus-bedrock/series.json | 2 +- .../setup/0001-stackanvil-branding.patch | 52 +-- .../0001-social-party-realms.patch | 44 ++- .../0002-native-entities-and-skins.patch | 4 +- .../0003-classic-dressing-room.patch | 10 +- .../0007-join-offline-lan-worlds.patch | 26 +- ...re-native-actor-and-resource-runtime.patch | 8 +- ...acter-creator-accounts-and-wardrobes.patch | 6 +- ...11-request-live-classic-skin-changes.patch | 6 +- ...solve-licensed-native-vanilla-images.patch | 4 +- ...-actors-with-shared-animation-graphs.patch | 4 +- .../0015-add-featured-server-browser.patch | 6 +- ...ritative-bedrock-cartography-results.patch | 4 +- ...rver-movement-and-camera-permissions.patch | 16 +- ...rver-element-visibility-restrictions.patch | 4 +- .../0022-render-native-locator-groups.patch | 4 +- ...render-core-resolved-camera-movement.patch | 6 +- ...ompleted-local-physics-to-viabedrock.patch | 16 +- ...thoritative-projectile-owner-changes.patch | 4 +- ...8-apply-native-fishing-bite-impulses.patch | 8 +- ...9-present-authored-native-containers.patch | 10 +- ...ender-authored-forms-and-npc-dialogs.patch | 4 +- ...0029-share-native-ui-scene-rendering.patch | 6 +- ...orted-native-world-actor-definitions.patch | 4 +- ...1-edit-native-command-block-settings.patch | 4 +- ...33-align-native-block-random-offsets.patch | 6 +- ...0038-predict-held-trapdoor-placement.patch | 359 ++++++++++++------ ...0038-predict-held-trapdoor-placement.pr.md | 96 +++-- ...d-use-on-bedrock-peaceful-difficulty.patch | 4 +- ...d-use-on-bedrock-peaceful-difficulty.pr.md | 4 +- ...ply-native-command-stacks-to-dry-air.patch | 6 +- ...d-http-signaling-for-bds-connections.patch | 4 +- targets.json | 6 +- viafabricplus.json | 16 +- 41 files changed, 488 insertions(+), 360 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 599329d0..e8f87fed 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -27,7 +27,7 @@ No complete area has verified Bedrock parity. | 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, Creative catalog actions, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | -| Food use | Installed food and use-modifier components govern admission and Java food/consumable properties, with Creative and server-confirmed Peaceful exceptions. | Both routes match selected native bread and custom-food controls. Held instant-food repeat timing, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md). | +| Food use | Installed food and use-modifier components govern admission and Java food/consumable properties, with Creative and server-confirmed Peaceful exceptions. | Both routes pass selected native admission controls and the strict input deadline, release, and pause checks. Broader use cycles, item-family handoffs, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md index e5697b49..841fde77 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md @@ -46,7 +46,7 @@ Native controls establish the combined behavior for these fixtures; they do not Seventeen focused tests cover installed component retention, constructor-specific admission, Java component wire properties, difficulty changes, food-use flags, offhand resynchronization, and completion boundaries. Admission tests retain the held quantity and verify that rejected uses leave `StartUsingItem` clear. -The full four-project build passes 3,282 tests, with 325 skipped and no failures or errors. +The full four-project build passes 3,285 tests, with 325 skipped and no failures or errors. Installed direct and ViaProxy controls cover eleven cases each. Both routes match the selected native admission, hunger, timed consumption, and zero-duration outcomes. @@ -55,16 +55,16 @@ Installed artifacts match the build checksums; core classes match both packaged ## Remaining limits -Held instant-food repeat timing still differs: the 2.1-second always-edible control consumes eleven items through Java and nine through native Bedrock. -Runtime tracing establishes a 200 ms monotonic deadline before air use, with retries checked from the input loop after that deadline. -The deadline persists across button releases and gates fresh clicks. -A fresh native hold consumes nine items both with and without the debugger. -Three 80 ms clicks consume two items when the second starts about 131 ms after the first and the third starts about 616 ms after it. +The [client input patch](../../viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md) now replaces the food four-tick repeat path with the native strict 200 ms deadline. +Fresh calls share that gate, release retains the deadline, and held retries run from input frames. +Direct and ViaProxy controls verify rapid clicks and pause cancellation alongside the eleven food cases. +A private timing observer checks each original admission decision and confirms that eligible input frames lead to use. +The installed JAR checksums match the build; observation adds logging to the running timing class. -Java still uses its four-tick repeat path. -The correction needs the native deadline, fresh-click gate, and input-frame scheduling; native tick intervals do not establish a fixed five-tick delay. -Counts depend on frame cadence, so equal final counts alone would not establish timing parity. -Timer ownership across item families, screens, and connection replacement also needs verification. +Native tracing establishes the deadline before air use and its check in the shared input function. +Native's 2.1-second instant-food control consumes nine items; the direct Java control consumes eleven at its different frame cadence. +Counts alone do not establish or refute the scheduling rule. +Complete handoffs between food and other item families still need native comparisons. Presence of `StartUsingItem` is verified; exact packet phase counts and complete cycle timing remain open. Competing non-food use handlers, other consumable animations and effects, ability exceptions, and all inventory authority modes need further comparison. diff --git a/patches/viabedrock/upstreamable/0002-restore-map-rendering.patch b/patches/viabedrock/upstreamable/0002-restore-map-rendering.patch index 8cda81fb..ad41a759 100644 --- a/patches/viabedrock/upstreamable/0002-restore-map-rendering.patch +++ b/patches/viabedrock/upstreamable/0002-restore-map-rendering.patch @@ -1,4 +1,4 @@ -From ddcf78aa788e355784eab031eba9e5a605b38e06 Mon Sep 17 00:00:00 2001 +From fafaa16bd8c0c58152e2927a18d776ed8d93d7cf Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:33 +0200 Subject: [PATCH] fix(maps): restore held and framed map rendering @@ -86,7 +86,7 @@ index d7c69522..6197d5f0 100644 if (entity == null) { 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 310b49a0..e4147ec3 100644 +index 20a180fb..d7260f7c 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -19,6 +19,7 @@ package net.raphimc.viabedrock.protocol.packet; @@ -427,7 +427,7 @@ index 310b49a0..e4147ec3 100644 }); protocol.registerClientbound(ClientboundBedrockPackets.INVENTORY_TRANSACTION, null, wrapper -> { final InventoryTransactionRewriter inventoryTransactionRewriter = wrapper.user().get(InventoryTransactionRewriter.class); -@@ -351,7 +360,4 @@ public final class InteractionPackets { +@@ -356,7 +365,4 @@ public final class InteractionPackets { } diff --git a/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.patch b/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.patch index 6670fe88..06b59f4f 100644 --- a/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.patch +++ b/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.patch @@ -1,4 +1,4 @@ -From ce3b14e64985fdeb13f588b9b81db95d715db09f Mon Sep 17 00:00:00 2001 +From c01ffe7080e8397ec84906c1f63a1f7492efb4b2 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:33 +0200 Subject: [PATCH] fix(entities): resolve and order Bedrock passenger links @@ -44,16 +44,16 @@ fixture inputs; unrelated position, notification and event effects remain boundaries. Queue timing, derived vehicle overrides, full cycle handling, removal lifetimes and mounted-camera rendering remain separate work. --- - .../api/model/entity/ClientPlayerEntity.java | 17 ++ + .../api/model/entity/ClientPlayerEntity.java | 17 + .../viabedrock/api/model/entity/Entity.java | 15 + .../protocol/PlayerActionPacketFactory.java | 9 +- .../protocol/packet/EntityPackets.java | 12 +- - .../protocol/packet/InteractionPackets.java | 65 ++-- + .../protocol/packet/InteractionPackets.java | 70 ++--- .../protocol/packet/OtherPlayerPackets.java | 7 +- .../protocol/storage/EntityTracker.java | 126 +++++++- .../api/model/entity/EntityRiderSeatTest.java | 36 +++ - .../protocol/storage/EntityTrackerTest.java | 278 ++++++++++++++++++ - 9 files changed, 516 insertions(+), 49 deletions(-) + .../protocol/storage/EntityTrackerTest.java | 293 ++++++++++++++++++ + 9 files changed, 531 insertions(+), 54 deletions(-) create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/entity/EntityRiderSeatTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/EntityTrackerTest.java @@ -188,7 +188,7 @@ index 39c8bf5f..c45e07d7 100644 protocol.registerClientbound(ClientboundBedrockPackets.ADD_PAINTING, ClientboundPackets26_3.ADD_ENTITY, wrapper -> { final EntityTracker entityTracker = wrapper.user().get(EntityTracker.class); 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 e4147ec3..919182e9 100644 +index d7260f7c..919182e9 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -46,7 +46,6 @@ import net.raphimc.viabedrock.protocol.data.enums.java.generated.GameMode; @@ -199,7 +199,7 @@ index e4147ec3..919182e9 100644 import net.raphimc.viabedrock.protocol.model.Position3f; import net.raphimc.viabedrock.protocol.storage.ChunkTracker; import net.raphimc.viabedrock.protocol.storage.EntityTracker; -@@ -312,52 +311,32 @@ public final class InteractionPackets { +@@ -312,57 +311,32 @@ public final class InteractionPackets { } } }); @@ -223,6 +223,10 @@ index e4147ec3..919182e9 100644 - // TODO: Handle Passenger type if needed - switch (linkType.type()) { - case Riding, Passenger -> { // TODO: This needs to be ordered properly based on the link types (rider first, then passengers) +- if (passenger == null) { +- wrapper.cancel(); +- return; +- } - vehicle.addPassenger(passenger.uniqueId()); - - wrapper.write(Types.VAR_INT, entityTracker.getEntityByUid(linkType.fromEntityUniqueId()).javaId()); // vehicle @@ -237,7 +241,8 @@ index e4147ec3..919182e9 100644 - } - } - case None -> { // Remove -- vehicle.removePassenger(passenger.uniqueId()); +- // Passenger may already be despawned, but its uid can still be in the vehicle's passenger list +- vehicle.removePassenger(linkType.toEntityUniqueId()); - - wrapper.write(Types.VAR_INT, vehicle.javaId()); // vehicle - wrapper.write(Types.VAR_INT, vehicle.passengers().size()); // number of passengers @@ -246,7 +251,7 @@ index e4147ec3..919182e9 100644 - } + } -- if (passenger.uniqueId() == entityTracker.getClientPlayer().uniqueId()) { // TODO: This could be applied to all passengers not just players +- if (linkType.toEntityUniqueId() == entityTracker.getClientPlayer().uniqueId()) { // TODO: This could be applied to all passengers not just players - // The player is no longer riding an entity, update the state - entityTracker.getClientPlayer().setMountEntityRuntimeId(-1); - entityTracker.getClientPlayer().setRequestedDismount(false); @@ -537,10 +542,10 @@ index 00000000..25f75218 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/EntityTrackerTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/EntityTrackerTest.java new file mode 100644 -index 00000000..f618cb94 +index 00000000..77b9559d --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/EntityTrackerTest.java -@@ -0,0 +1,278 @@ +@@ -0,0 +1,293 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -594,6 +599,21 @@ index 00000000..f618cb94 + } + + @Test ++ void unlinkRemovesStaleUniqueIdWithoutLivePassenger() { ++ final EntityTracker tracker = new EntityTracker(user()); ++ final Entity vehicle = tracker.addEntity(10, 100, EntityTypes26_3.PIG.identifier(), EntityTypes26_3.PIG); ++ final Entity remaining = tracker.addEntity(12, 102, EntityTypes26_3.PIG.identifier(), EntityTypes26_3.PIG); ++ vehicle.addPassenger(11); ++ vehicle.addPassenger(remaining.uniqueId()); ++ ++ tracker.applyEntityLink(link(vehicle.uniqueId(), 11, ActorLinkType.None)); ++ ++ assertEquals(List.of(remaining.uniqueId()), vehicle.passengers()); ++ assertEquals(List.of(remaining), tracker.getTrackedPassengers(vehicle)); ++ assertEquals(List.of(vehicle), tracker.drainPassengerUpdates()); ++ } ++ ++ @Test + void prunesUnknownPassengersBeforeEncodingPassengers() { + final EntityTracker tracker = new EntityTracker(user()); + final Entity vehicle = tracker.addEntity(10, 100, EntityTypes26_3.PIG.identifier(), EntityTypes26_3.PIG); diff --git a/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.pr.md b/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.pr.md index a8ac1b12..a091f84a 100644 --- a/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.pr.md +++ b/patches/viabedrock/upstreamable/0008-discard-stale-passenger-links.pr.md @@ -1,5 +1,8 @@ Resolve links received before their actors, preserve native player seat priority, and clear passenger membership and mount state when links or actors disappear. Remounting removes the passenger from its previous vehicle; a stale unlink cannot clear a newer remote mount. +Unlinking also removes a stale unique ID when its passenger actor is already gone. +A regression retains the other live passenger and verifies the resulting vehicle update. + Expose the optional finite native rider vector at metadata 56 for client adapters. Bedrock 1.26.51.1 protocol 2193 packets provide this vector on mounted actors. Two saved player/chair pairs independently place the player eye at vehicle position plus the vector rotated by negative vehicle yaw, within 1e-7 world units. The vector uses native actor-origin units; a Java adapter subtracts the passenger eye offset once. The earlier core checkpoint passed 293 tests with no failures, errors or skips. Targeted tests cover finite and absent vectors, pending links, stale unlinks, remounts and vehicle removal. Ordinary Java clients behind ViaProxy retain Java attachment placement unless a native client adapter receives the metadata. 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 31659659..cc266b96 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 f3b1c9fe63e42d776cad0f47987c23c87e8637ce Mon Sep 17 00:00:00 2001 +From 3bb6e1edc1a46357bd3e7e02243b81528f1ad1b2 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 @@ -11558,10 +11558,10 @@ index 00000000..caaa6306 + +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/EntityTrackerTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/EntityTrackerTest.java -index f618cb94..2b4db601 100644 +index 77b9559d..1eb2c103 100644 --- a/src/test/java/net/raphimc/viabedrock/protocol/storage/EntityTrackerTest.java +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/EntityTrackerTest.java -@@ -70,14 +70,14 @@ class EntityTrackerTest { +@@ -85,14 +85,14 @@ class EntityTrackerTest { tracker.addEntity(player, false); final Entity vehicle = tracker.addEntity(10, 100, EntityTypes26_3.PIG.identifier(), EntityTypes26_3.PIG); vehicle.setPosition(new Position3f(12F, 64F, 8F)); diff --git a/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch b/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch index 75595578..184c02c9 100644 --- a/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch +++ b/patches/viafabricplus-bedrock/deferred/0001-allow-confirmed-blacklisted-server-joins.patch @@ -1,4 +1,4 @@ -From 9420af1368228b6d58cc34a6e6de25428ec29c0a Mon Sep 17 00:00:00 2001 +From 24a18417c6941d1921b5eb6c6a67bc7d9d182c89 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:06:07 +0100 Subject: [PATCH] feat(connection): allow confirmed blacklisted server joins @@ -123,7 +123,7 @@ index 0000000..f7578dc + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java -index 6561291..d6967f2 100644 +index 99ca7a2..664d5db 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java @@ -27,18 +27,23 @@ import com.llamalad7.mixinextras.sugar.Local; @@ -247,7 +247,7 @@ index f345d7c..40e7ae9 100644 "command_block.viafabricplus.first_tick": "Execute on first tick", "base.viafabricplus.accept": "Accept", diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 0696216..029a4f3 100644 +index ac083ae..8777544 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json @@ -48,6 +48,8 @@ diff --git a/patches/viafabricplus-bedrock/series.json b/patches/viafabricplus-bedrock/series.json index 913bef9c..1ca9f308 100644 --- a/patches/viafabricplus-bedrock/series.json +++ b/patches/viafabricplus-bedrock/series.json @@ -300,7 +300,7 @@ }, { "file": "0038-predict-held-trapdoor-placement.patch", - "title": "feat(interaction): predict held trapdoor placement", + "title": "feat(interaction): preserve native held item use", "prerequisites": [ { "project": "viabedrock", diff --git a/patches/viafabricplus-bedrock/setup/0001-stackanvil-branding.patch b/patches/viafabricplus-bedrock/setup/0001-stackanvil-branding.patch index e32804bd..4eb479c8 100644 --- a/patches/viafabricplus-bedrock/setup/0001-stackanvil-branding.patch +++ b/patches/viafabricplus-bedrock/setup/0001-stackanvil-branding.patch @@ -1,4 +1,4 @@ -From 6c453cf96ffc4c53c8095d5dd12f8f3c650c1c8e Mon Sep 17 00:00:00 2001 +From cc6281f481b81d4a67f7a57e37e779fe5b1dd681 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] chore(build): configure StackAnvil identity and compatibility @@ -10,9 +10,8 @@ the removed debug HUD integration. Exclude setup from upstream PRs. build.gradle.kts | 20 +++++++++++++++++-- gradle.properties | 2 +- settings.gradle.kts | 13 ++++++++++++ - .../integration/MixinServerListsScreen.java | 16 ++++----------- .../settings/impl/BedrockSettings.java | 6 ------ - 5 files changed, 36 insertions(+), 21 deletions(-) + 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 200aa63..a38c2db 100644 @@ -48,15 +47,15 @@ index 200aa63..a38c2db 100644 exclude(group = "at.yawk.lz4", module = "lz4-java") exclude(group = "io.netty") diff --git a/gradle.properties b/gradle.properties -index b1e89c3..5d9a6a6 100644 +index 786121b..13f251a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ jvm_version=25 project_group=com.viaversion project_name=viafabricplus-bedrock --project_version=1.1.1-SNAPSHOT -+project_version=1.1.1-StackAnvil +-project_version=1.2.1-SNAPSHOT ++project_version=1.2.1-StackAnvil project_description=Minecraft: Bedrock Edition support for the ViaFabricPlus mod. publish_owner_id=florianreuth @@ -84,47 +83,8 @@ index 9b57f98..c4d1e0e 100644 maven("https://repo.viaversion.com") maven("https://maven.lenni0451.net/everything") maven("https://repo.opencollab.dev/maven-snapshots") { -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 859e0b9..f5f2953 100644 ---- 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 -@@ -41,14 +41,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - @Mixin(value = ServerListsScreen.class, remap = false) - public abstract class MixinServerListsScreen extends VFPPopup { - -- @Shadow -- @Final -- private static int BUTTON_WIDTH; -- -- @Shadow -- @Final -- private static int BUTTON_HEIGHT; -- - @Shadow - @Final - private static int BUTTON_MARGIN; -@@ -59,15 +51,15 @@ public abstract class MixinServerListsScreen extends VFPPopup { - - @Inject(method = "initBody", at = @At("TAIL")) - private void addBedrockServerLists(final ScreenRectangle body, final CallbackInfo ci) { -- final int x = body.left() + (body.width() - BUTTON_WIDTH) / 2; -- final int y = body.top() + BUTTON_MARGIN + 2 * (BUTTON_HEIGHT + BUTTON_MARGIN); // Below ViaFabricPlus' own server lists -+ final int x = body.left() + (body.width() - Button.BIG_WIDTH) / 2; -+ final int y = body.top() + BUTTON_MARGIN + 2 * (Button.DEFAULT_HEIGHT + BUTTON_MARGIN); // Below ViaFabricPlus' own server lists - this.viaFabricPlusBedrock$addServerList(BedrockRealmsScreen.TITLE, () -> new BedrockRealmsScreen().open(this.prevScreen), x, y); -- this.viaFabricPlusBedrock$addServerList(BedrockFriendsScreen.TITLE, () -> new BedrockFriendsScreen().open(this.prevScreen), x, y + BUTTON_HEIGHT + BUTTON_MARGIN); -+ this.viaFabricPlusBedrock$addServerList(BedrockFriendsScreen.TITLE, () -> new BedrockFriendsScreen().open(this.prevScreen), x, y + Button.DEFAULT_HEIGHT + BUTTON_MARGIN); - } - - @Unique - private void viaFabricPlusBedrock$addServerList(final Component title, final Runnable open, final int x, final int y) { -- final Button.Builder builder = Button.builder(title, _ -> open.run()).pos(x, y).size(BUTTON_WIDTH, BUTTON_HEIGHT); -+ final Button.Builder builder = Button.builder(title, _ -> open.run()).pos(x, y).size(Button.BIG_WIDTH, Button.DEFAULT_HEIGHT); - final boolean missingAccount = ViaFabricPlusBedrock.impl().account().get() == null; // Only check for presence, later validate - if (missingAccount) { - builder.tooltip(Tooltip.create(Component.translatable("bedrock_realms.viafabricplus.warning"))); 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 39fb9d4..d17d0fe 100644 +index 346801e..1001738 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 @@ -35,13 +35,11 @@ import net.raphimc.viabedrock.protocol.data.ProtocolConstants; diff --git a/patches/viafabricplus-bedrock/upstreamable/0001-social-party-realms.patch b/patches/viafabricplus-bedrock/upstreamable/0001-social-party-realms.patch index 82e12c6c..4745316b 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0001-social-party-realms.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0001-social-party-realms.patch @@ -1,4 +1,4 @@ -From a9266dca7db1628cc89e64f4910d2d930fd3cf0e Mon Sep 17 00:00:00 2001 +From 9b3efac0b6dd0dc450b29876779ed0f50864dd70 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(bedrock): add social, party, and Realm Hub support @@ -58,7 +58,7 @@ Use the current BedrockProtocolVersion API in the independent social patch, which must compile before the downstream entity rendering patch applies. --- build.gradle.kts | 14 + - gradle/libs.versions.toml | 3 +- + gradle/libs.versions.toml | 5 +- .../friends/BedrockFriendsService.java | 401 ++-- .../friends/BedrockSocialService.java | 181 +- .../features/friends/BedrockXboxError.java | 27 + @@ -161,7 +161,7 @@ which must compile before the downstream entity rendering patch applies. .../realms/BedrockRealmScreenshotTest.java | 64 + .../realms/BedrockRealmStoriesTest.java | 154 ++ .../party/BedrockPartyDestinationTest.java | 105 + - 104 files changed, 13550 insertions(+), 456 deletions(-) + 104 files changed, 13551 insertions(+), 457 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/features/realms/BedrockRealmData.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/features/realms/BedrockRealmHttp.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/features/realms/BedrockRealmHubService.java @@ -275,20 +275,22 @@ index a38c2db..6936fa8 100644 + useJUnitPlatform() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml -index 7ead823..eb5261d 100644 +index f3ca449..1fa6908 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml -@@ -11,6 +11,7 @@ fabric-loom-plugin = { module = "net.fabricmc.fabric-loom:net.fabricmc.fabric-lo +@@ -10,7 +10,8 @@ fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-l + fabric-loom-plugin = { module = "net.fabricmc.fabric-loom:net.fabricmc.fabric-loom.gradle.plugin", version.ref = "fabric-loom" } foojay-resolver-convention-plugin = { module = "org.gradle.toolchains.foojay-resolver-convention:org.gradle.toolchains.foojay-resolver-convention.gradle.plugin", version = "1.0.0" } - viafabricplus = { module = "com.viaversion:viafabricplus", version = "5.1.3-SNAPSHOT" } - viabedrock = { module = "net.raphimc:ViaBedrock", version = "0.0.32-SNAPSHOT" } + viafabricplus = { module = "com.viaversion:viafabricplus", version = "5.1.3" } +-viabedrock = { module = "net.raphimc:ViaBedrock", version = "0.0.32-20261030.114258-1" } -minecraftauth = { module = "net.raphimc:MinecraftAuth", version = "5.0.2" } ++viabedrock = { module = "net.raphimc:ViaBedrock", version = "0.0.32-SNAPSHOT" } +minecraftauth = { module = "net.raphimc:MinecraftAuth", version = "5.0.3-SNAPSHOT" } netty-transport-nethernet = { module = "org.cloudburstmc.netty:netty-transport-nethernet", version = "2.0.0.CR4-20261001.235918-13" } +netty-transport-raknet = { module = "dev.kastle.netty:netty-transport-raknet", version = "1.7.0" } libdatachannel-java-arch-detect = { module = "dev.opencollab:libdatachannel-java-arch-detect", version = "0.24.5.0-20260921.140330-12" } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockFriendsService.java b/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockFriendsService.java -index c1a81f5..ab390f7 100644 +index 1ddd9d4..24560dc 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockFriendsService.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockFriendsService.java @@ -26,6 +26,13 @@ import com.google.gson.JsonElement; @@ -823,7 +825,7 @@ index c1a81f5..ab390f7 100644 - } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockSocialService.java b/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockSocialService.java -index ab6d69a..4643673 100644 +index af809fd..677cb3f 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockSocialService.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockSocialService.java @@ -21,7 +21,9 @@ @@ -1085,7 +1087,7 @@ index ab6d69a..4643673 100644 public String name() { return !this.displayName.isBlank() ? this.displayName : !this.gamertag.isBlank() ? this.gamertag : this.xuid; diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockXboxError.java b/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockXboxError.java -index cf16e52..a8bfd6f 100644 +index 6ccab46..7cf7c90 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockXboxError.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockXboxError.java @@ -27,7 +27,9 @@ import java.io.IOException; @@ -1138,7 +1140,7 @@ index cf16e52..a8bfd6f 100644 for (final String key : keys) { if (object.has(key) && object.get(key).isJsonPrimitive()) { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/features/global/BedrockAccount.java b/src/main/java/com/viaversion/viafabricplus/bedrock/features/global/BedrockAccount.java -index e0933ad..3ebab06 100644 +index 3152b87..bd42e63 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/features/global/BedrockAccount.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/features/global/BedrockAccount.java @@ -25,6 +25,7 @@ import com.mojang.blaze3d.Blaze3D; @@ -2845,12 +2847,12 @@ index 0000000..86e9349 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/features/realms/BedrockRealmTimelineService.java b/src/main/java/com/viaversion/viafabricplus/bedrock/features/realms/BedrockRealmTimelineService.java deleted file mode 100644 -index 52402df..0000000 +index 8495164..0000000 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/features/realms/BedrockRealmTimelineService.java +++ /dev/null @@ -1,108 +0,0 @@ -/* -- * This file is part of ViaFabricPlus Bedrock - https://github.com/florianreuth/viafabricplus-bedrock +- * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock - * Copyright (C) 2021-2026 the original authors - * - Florian Reuth - * - RK_01/RaphiMC @@ -3123,7 +3125,7 @@ index 0000000..8ca9a3c + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/features/realms/BedrockRealmsError.java b/src/main/java/com/viaversion/viafabricplus/bedrock/features/realms/BedrockRealmsError.java -index 6a96ce3..d8b675f 100644 +index 289bdb6..95a1c90 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/features/realms/BedrockRealmsError.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/features/realms/BedrockRealmsError.java @@ -30,7 +30,7 @@ import org.jetbrains.annotations.Nullable; @@ -3263,7 +3265,7 @@ index 0000000..b38d696 + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/misc/MixinMinecraft.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/misc/MixinMinecraft.java -index 624c45e..37fcdb5 100644 +index ad4de65..88c63a8 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/misc/MixinMinecraft.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/misc/MixinMinecraft.java @@ -23,6 +23,8 @@ package com.viaversion.viafabricplus.bedrock.injection.mixin.features.misc; @@ -5296,7 +5298,7 @@ index 0000000..44acf79 + +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java -index b601050..939283a 100644 +index e8e3adb..cc61945 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java @@ -31,6 +31,7 @@ public final class ViaFabricPlusSkinProvider extends SkinProvider { @@ -12633,7 +12635,7 @@ index 0000000..27bbc77 + +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/friends/BedrockFriendProfileScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/friends/BedrockFriendProfileScreen.java -index 898a206..79cca19 100644 +index 017ef96..0c5e360 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/friends/BedrockFriendProfileScreen.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/friends/BedrockFriendProfileScreen.java @@ -21,47 +21,204 @@ @@ -12862,7 +12864,7 @@ index 898a206..79cca19 100644 } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/friends/BedrockFriendsScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/friends/BedrockFriendsScreen.java -index 042e605..708511c 100644 +index e24eae4..a6d796e 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/friends/BedrockFriendsScreen.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/friends/BedrockFriendsScreen.java @@ -21,20 +21,29 @@ @@ -13299,7 +13301,7 @@ index 042e605..708511c 100644 SLOT_MARGIN + font.lineHeight + 4, SECONDARY_COLOR); } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/AcceptInvitationCodeScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/AcceptInvitationCodeScreen.java -index d889d46..a9fa277 100644 +index a27e831..f7fddc5 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/AcceptInvitationCodeScreen.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/AcceptInvitationCodeScreen.java @@ -21,6 +21,9 @@ @@ -13344,7 +13346,7 @@ index d889d46..a9fa277 100644 @Override diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/BedrockRealmTimelineScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/BedrockRealmTimelineScreen.java -index 3a52a59..d308e07 100644 +index 498a30d..e19b8b8 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/BedrockRealmTimelineScreen.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/BedrockRealmTimelineScreen.java @@ -22,128 +22,114 @@ @@ -13538,7 +13540,7 @@ index 3a52a59..d308e07 100644 this.join.run(); } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/BedrockRealmsScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/BedrockRealmsScreen.java -index 455feda..60f2386 100644 +index 2189591..6227723 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/BedrockRealmsScreen.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/impl/realms/BedrockRealmsScreen.java @@ -21,12 +21,23 @@ diff --git a/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.patch b/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.patch index aaddf000..af24dec5 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0002-native-entities-and-skins.patch @@ -1,4 +1,4 @@ -From 55556165f87f286adcf3d068689b407bdf93a305 Mon Sep 17 00:00:00 2001 +From c1ce6257302505e0c708407f4337806b2c432cbe 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(bedrock): render custom entities and player skins @@ -1027,7 +1027,7 @@ index c4d1e0e..e1bd73b 100644 if (stackAnvilMavenRepo.isPresent) { exclusiveContent { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 180d312..03c7b23 100644 +index 312ef8d..073a05f 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -21,6 +21,14 @@ diff --git a/patches/viafabricplus-bedrock/upstreamable/0003-classic-dressing-room.patch b/patches/viafabricplus-bedrock/upstreamable/0003-classic-dressing-room.patch index d32c5a23..a268bb0f 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0003-classic-dressing-room.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0003-classic-dressing-room.patch @@ -1,4 +1,4 @@ -From e98e9b7abe8c95a6987611b31b82c88bf0d78e37 Mon Sep 17 00:00:00 2001 +From 23e1b8560c8dee5fd6a92553004035b3bcef6f59 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(bedrock): add classic Dressing Room @@ -131,7 +131,7 @@ reproduces a null-pointer failure with unloaded mappings. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockVanillaSkinsTest.java diff --git a/README.md b/README.md -index 1c48d82..5f060d4 100644 +index 96f45ce..1e71dc0 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,28 @@ Addon for [ViaFabricPlus](https://github.com/ViaVersion/ViaFabricPlus) that adds @@ -173,7 +173,7 @@ index 1c48d82..5f060d4 100644 ```kotlin diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 03c7b23..18232f0 100644 +index 073a05f..779891c 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -36,6 +36,7 @@ import com.viaversion.viafabricplus.bedrock.protocoltranslator.impl.platform.Via @@ -1532,7 +1532,7 @@ index 0000000..d6afe12 + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java -index 939283a..c2521c7 100644 +index cc61945..0bb017c 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java @@ -21,17 +21,54 @@ @@ -2109,7 +2109,7 @@ index 0000000..8acdbce + +} 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 d17d0fe..b0b13f9 100644 +index 1001738..f2def64 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 @@ -26,8 +26,10 @@ import com.viaversion.viafabricplus.api.settings.base.BooleanSetting; diff --git a/patches/viafabricplus-bedrock/upstreamable/0007-join-offline-lan-worlds.patch b/patches/viafabricplus-bedrock/upstreamable/0007-join-offline-lan-worlds.patch index 89e707b9..e043d5f6 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0007-join-offline-lan-worlds.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0007-join-offline-lan-worlds.patch @@ -1,4 +1,4 @@ -From 94d2718a8174a4b63af99a359f9bbb765aef325b Mon Sep 17 00:00:00 2001 +From c3d04e883b1191b3bba190257eb6f84f24ec315c 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(bedrock): join offline LAN worlds @@ -73,7 +73,7 @@ and registers its browser in the current Server Lists popup. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/lan/NetherNetConnectResponseTest.java diff --git a/README.md b/README.md -index 5f060d4..52496bd 100644 +index 1e71dc0..0e04f81 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ To join a NetherNet server, select the Bedrock version and enter an address from @@ -196,7 +196,7 @@ index 0000000..a4c0787 + +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java -index 065dc9e..6561291 100644 +index a609289..99ca7a2 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnectScreen_1.java @@ -26,12 +26,14 @@ import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; @@ -252,7 +252,7 @@ index 065dc9e..6561291 100644 if (account == null) { ViaFabricPlusBedrock.impl().logger().warn("Could not get Bedrock account. Joining online mode servers will not work!"); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnection.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnection.java -index 0a84116..5e5d5a6 100644 +index 5c52ed3..ab65451 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnection.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnection.java @@ -28,11 +28,28 @@ import com.llamalad7.mixinextras.sugar.Local; @@ -522,7 +522,7 @@ index 0a84116..5e5d5a6 100644 return original.call(instance, inetHost, inetPort); } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnection_1.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnection_1.java -index 104c27a..c653b09 100644 +index a61bfc3..2ced78a 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnection_1.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinConnection_1.java @@ -23,19 +23,24 @@ package com.viaversion.viafabricplus.bedrock.injection.mixin.core.connection; @@ -637,7 +637,7 @@ index 0000000..3a9fbe1 + +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinServerNameResolver.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinServerNameResolver.java -index 5444e77..574e73c 100644 +index a55b2ab..e22c645 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinServerNameResolver.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/connection/MixinServerNameResolver.java @@ -23,6 +23,9 @@ package com.viaversion.viafabricplus.bedrock.injection.mixin.core.connection; @@ -662,7 +662,7 @@ index 5444e77..574e73c 100644 } })); 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 f5f2953..caf35d9 100644 +index 426c332..19b3a21 100644 --- 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 @@ -22,6 +22,8 @@ @@ -685,7 +685,7 @@ index f5f2953..caf35d9 100644 @Unique diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinServerStatusPinger.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinServerStatusPinger.java -index 062470f..9e0d2ad 100644 +index 8cf1b93..a65f184 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinServerStatusPinger.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinServerStatusPinger.java @@ -21,14 +21,17 @@ @@ -761,7 +761,7 @@ index 062470f..9e0d2ad 100644 + } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinVFPPopup.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinVFPPopup.java -index ef4d06f..1bdfbe1 100644 +index aebb2b4..9bec483 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinVFPPopup.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinVFPPopup.java @@ -33,7 +33,7 @@ public abstract class MixinVFPPopup { @@ -1250,7 +1250,7 @@ index 0000000..e90206d + +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusNettyPipelineProvider.java b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusNettyPipelineProvider.java -index 85d460b..c5b19cb 100644 +index 3b03fca..35cf96d 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusNettyPipelineProvider.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusNettyPipelineProvider.java @@ -26,6 +26,8 @@ import io.netty.channel.Channel; @@ -1283,7 +1283,7 @@ index 85d460b..c5b19cb 100644 } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java -index c2521c7..bb38975 100644 +index 0bb017c..620300f 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java @@ -26,6 +26,7 @@ import com.viaversion.viafabricplus.bedrock.party.BedrockPartyService; @@ -1383,7 +1383,7 @@ index 0000000..026a8b8 + +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/BedrockConnectionUtil.java b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/BedrockConnectionUtil.java -index fc089d9..babcaed 100644 +index 3661891..e946d43 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/BedrockConnectionUtil.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/BedrockConnectionUtil.java @@ -22,6 +22,7 @@ @@ -1570,7 +1570,7 @@ index 0000000..a31665e + +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/NetherNetAddressParser.java b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/NetherNetAddressParser.java -index e424912..00fb25a 100644 +index 7f10afb..f58013a 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/NetherNetAddressParser.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/network/NetherNetAddressParser.java @@ -47,6 +47,7 @@ public final class NetherNetAddressParser { 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 8d22810d..21e472d4 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 cda49860833316900d23783748ee0d3afb25d64f Mon Sep 17 00:00:00 2001 +From 357c77aad1b996db375932d8e50869f1748e9522 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 @@ -896,7 +896,7 @@ index 580bdf9..909ac3c 100644 + sourceSets.main { resources.srcDir(bundledBedrockAssets) } } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 18232f0..4b20512 100644 +index 779891c..7179ef5 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -25,8 +25,24 @@ import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerSkins; @@ -12667,7 +12667,7 @@ index 0000000..9433e74 + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java -index bb38975..435d396 100644 +index 620300f..39a1e1f 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java @@ -22,6 +22,7 @@ @@ -14445,7 +14445,7 @@ index 0000000..a86d037 + } +} 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 b0b13f9..2982b14 100644 +index f2def64..3ee1544 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 @@ -21,8 +21,13 @@ diff --git a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch index b2a08287..dd9dcb8d 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch @@ -1,4 +1,4 @@ -From 0c1ca832878240637badd8ff07494ef84169bc7a Mon Sep 17 00:00:00 2001 +From b0e1fc1eef7900a8c633db102001af2e8d2572f8 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(appearance): manage Character Creator accounts and @@ -265,7 +265,7 @@ an unmounted predicate while proving cached closures do not hold avatars. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockStandardPlayerGeometryTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 4b20512..46921d9 100644 +index 7179ef5..97d342e 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -21,6 +21,9 @@ @@ -6788,7 +6788,7 @@ index 0000000..8242a24 + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java -index 435d396..779a24e 100644 +index 39a1e1f..59657b0 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java @@ -49,24 +49,32 @@ public final class ViaFabricPlusSkinProvider extends SkinProvider { diff --git a/patches/viafabricplus-bedrock/upstreamable/0011-request-live-classic-skin-changes.patch b/patches/viafabricplus-bedrock/upstreamable/0011-request-live-classic-skin-changes.patch index b1c55369..7dad569a 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0011-request-live-classic-skin-changes.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0011-request-live-classic-skin-changes.patch @@ -1,4 +1,4 @@ -From 9d89bdbf0e32932208145ec446c517ca71418167 Mon Sep 17 00:00:00 2001 +From f88ec7a0af3eba00f4c5acebb17c276a5d967b1b 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): request live skin changes @@ -41,7 +41,7 @@ archive cache. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/SkinChangeSessionTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 46921d9..fad171d 100644 +index 97d342e..cab8ba7 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -21,6 +21,9 @@ @@ -239,7 +239,7 @@ index 0000000..6112972 + private record Pending(O owner, UUID request, CompletableFuture confirmation) {} +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java -index 779a24e..a09c2aa 100644 +index 59657b0..193d971 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/protocoltranslator/impl/provider/viabedrock/ViaFabricPlusSkinProvider.java @@ -87,4 +87,5 @@ public final class ViaFabricPlusSkinProvider extends SkinProvider { diff --git a/patches/viafabricplus-bedrock/upstreamable/0013-resolve-licensed-native-vanilla-images.patch b/patches/viafabricplus-bedrock/upstreamable/0013-resolve-licensed-native-vanilla-images.patch index 4418caa4..8c7242f4 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0013-resolve-licensed-native-vanilla-images.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0013-resolve-licensed-native-vanilla-images.patch @@ -1,4 +1,4 @@ -From 98fecaf1c01eb52df1bcd66ee871cbd8dc50e623 Mon Sep 17 00:00:00 2001 +From 92c16589929727ffc6c0f51b11bdf94c1fd67357 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(resources): select native libraries by world version @@ -79,7 +79,7 @@ and audible world-transition comparisons remain pending. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/appearance/ViaFabricPlusBuiltinResourcePackProviderTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 3284096..44cdd68 100644 +index b0be31d..e84e7d8 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -60,9 +60,11 @@ import com.viaversion.viafabricplus.bedrock.protocoltranslator.impl.provider.via diff --git a/patches/viafabricplus-bedrock/upstreamable/0014-evaluate-custom-actors-with-shared-animation-graphs.patch b/patches/viafabricplus-bedrock/upstreamable/0014-evaluate-custom-actors-with-shared-animation-graphs.patch index cb1d895b..d0277a96 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0014-evaluate-custom-actors-with-shared-animation-graphs.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0014-evaluate-custom-actors-with-shared-animation-graphs.patch @@ -1,4 +1,4 @@ -From d3acf41bfe3cac1a0ad8daf269d864cb9bc53f2c Mon Sep 17 00:00:00 2001 +From 128b805279485f660b521103dce94babdb3951d4 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 custom actors with shared animation @@ -149,7 +149,7 @@ native equipment transport. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockModelLocatorIndexTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index fad171d..3284096 100644 +index cab8ba7..b0be31d 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -40,6 +40,7 @@ import com.viaversion.viafabricplus.bedrock.render.NativeActorColorPayload; 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 9a6d80cc..2627017e 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 2c433b2d3bf45713a802c3aba3e9694505db5bbe Mon Sep 17 00:00:00 2001 +From 817f695838b50b26351f5fd401ac929bc869f88f 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 @@ -55,7 +55,7 @@ selection evidence, not a claim of complete server gameplay parity. 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 +index 19b3a21..4d1f114 100644 --- 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 @@ -22,10 +22,9 @@ @@ -94,7 +94,7 @@ index caf35d9..f76e762 100644 @Unique diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinVFPPopup.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinVFPPopup.java -index 1bdfbe1..4c4d80c 100644 +index 9bec483..842efcb 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinVFPPopup.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinVFPPopup.java @@ -33,7 +33,7 @@ public abstract class MixinVFPPopup { diff --git a/patches/viafabricplus-bedrock/upstreamable/0018-preserve-authoritative-bedrock-cartography-results.patch b/patches/viafabricplus-bedrock/upstreamable/0018-preserve-authoritative-bedrock-cartography-results.patch index be5b7f8f..fa65e4c7 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0018-preserve-authoritative-bedrock-cartography-results.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0018-preserve-authoritative-bedrock-cartography-results.patch @@ -1,4 +1,4 @@ -From 75a8706f5670ee6defa2a99535983f4cc8357796 Mon Sep 17 00:00:00 2001 +From 9559adc1a76b3b6af9e6fa0ed3d990d98af95e55 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(inventory): render authoritative native Bedrock @@ -51,7 +51,7 @@ cartography graphics on older templates still require visual comparison. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/inventory/CartographyNameSynchronizationTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 44cdd68..f25184e 100644 +index e84e7d8..da55770 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -59,6 +59,8 @@ import com.viaversion.viafabricplus.bedrock.protocoltranslator.impl.platform.Via diff --git a/patches/viafabricplus-bedrock/upstreamable/0020-apply-server-movement-and-camera-permissions.patch b/patches/viafabricplus-bedrock/upstreamable/0020-apply-server-movement-and-camera-permissions.patch index 2c37a78a..4db6d25a 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0020-apply-server-movement-and-camera-permissions.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0020-apply-server-movement-and-camera-permissions.patch @@ -1,4 +1,4 @@ -From 64872fb2362a437a8c0637941b3ad6e4b4d65a8b Mon Sep 17 00:00:00 2001 +From a32ea638f811b65d7eb3869a40cfcdfeb024a4d8 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(input): apply server movement and camera permissions @@ -59,7 +59,7 @@ introduced by a later owner; no dependency or transport format moves. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/BedrockInputPermissionsTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index f25184e..0c0682e 100644 +index da55770..b7ef94f 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -61,6 +61,9 @@ import com.viaversion.viafabricplus.bedrock.protocoltranslator.impl.provider.via @@ -275,7 +275,7 @@ index 0000000..93fce20 + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntity.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntity.java -index e29965b..236c05d 100644 +index c5b8e48..8c9bbaa 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntity.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntity.java @@ -24,6 +24,7 @@ package com.viaversion.viafabricplus.bedrock.injection.mixin.features.movement; @@ -303,7 +303,7 @@ index e29965b..236c05d 100644 } else { return box; diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction.java -index 2bface7..6cd82ab 100644 +index 212dd7f..c644853 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction.java @@ -25,10 +25,9 @@ import com.llamalad7.mixinextras.expression.Definition; @@ -346,7 +346,7 @@ index 2bface7..6cd82ab 100644 } else { return constant; diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction_CurrentAccumulator.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction_CurrentAccumulator.java -index b135083..122cd78 100644 +index 73ab89e..b3be043 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction_CurrentAccumulator.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction_CurrentAccumulator.java @@ -25,11 +25,10 @@ import com.llamalad7.mixinextras.expression.Definition; @@ -379,7 +379,7 @@ index b135083..122cd78 100644 } else { return constant; diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java -index 90050a0..1c9125e 100644 +index d3baf2e..cb89991 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java @@ -21,14 +21,13 @@ @@ -415,7 +415,7 @@ index 90050a0..1c9125e 100644 } } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLocalPlayer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLocalPlayer.java -index 614dc41..e5a8167 100644 +index 9c690a0..0e466e5 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLocalPlayer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLocalPlayer.java @@ -21,9 +21,8 @@ @@ -439,7 +439,7 @@ index 614dc41..e5a8167 100644 } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinPlayer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinPlayer.java -index 47c2725..49a37ac 100644 +index 181d2aa..8d10241 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinPlayer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinPlayer.java @@ -24,6 +24,7 @@ package com.viaversion.viafabricplus.bedrock.injection.mixin.features.movement; diff --git a/patches/viafabricplus-bedrock/upstreamable/0021-apply-server-element-visibility-restrictions.patch b/patches/viafabricplus-bedrock/upstreamable/0021-apply-server-element-visibility-restrictions.patch index ec4a4900..90a8d131 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0021-apply-server-element-visibility-restrictions.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0021-apply-server-element-visibility-restrictions.patch @@ -1,4 +1,4 @@ -From 9485592765b09d9d0c6dc1176f3a22cc8368a219 Mon Sep 17 00:00:00 2001 +From 838587b7e8859dc9df1391265d29eab6abdc7760 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(hud): apply server element visibility restrictions @@ -22,7 +22,7 @@ Paper doll, touch-control, and control-hint restrictions remain retained by core create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinMinecraftHudVisibility.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 0c0682e..efda9b9 100644 +index b7ef94f..734c4bb 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -63,6 +63,8 @@ import com.viaversion.viafabricplus.bedrock.inventory.BedrockCartographyContext; diff --git a/patches/viafabricplus-bedrock/upstreamable/0022-render-native-locator-groups.patch b/patches/viafabricplus-bedrock/upstreamable/0022-render-native-locator-groups.patch index 2583b9eb..2246eedd 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0022-render-native-locator-groups.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0022-render-native-locator-groups.patch @@ -1,4 +1,4 @@ -From 9380fd2a45b06fc45a57cdfbacf728578537ec8c Mon Sep 17 00:00:00 2001 +From a5c679e122ba3f2308ec41c9ff9988a2751efb0a 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(hud): render native locator groups @@ -45,7 +45,7 @@ remaining native comparison matrix are still incomplete. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/hud/LocatorBarSessionTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index efda9b9..6bc2c4c 100644 +index 734c4bb..0b69037 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -65,6 +65,8 @@ import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; diff --git a/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.patch b/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.patch index e6a7b918..c8e66dd8 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.patch @@ -1,4 +1,4 @@ -From 6b1d8feb65d8c05c39aff4c34178278d03c0ee68 Mon Sep 17 00:00:00 2001 +From d35cf5dcdee30561fb4616d2e217dd66dc2cf449 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(camera): render core-resolved server camera movement @@ -175,7 +175,7 @@ behavior until its physical center is available. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/sound/MixinBedrockCameraListener.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 6bc2c4c..53e6c84 100644 +index 0b69037..6d7f012 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -64,6 +64,19 @@ import com.viaversion.viafabricplus.bedrock.inventory.CartographyPayload; @@ -1284,7 +1284,7 @@ index 0000000..9a5f289 + +} 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 2982b14..48d994f 100644 +index 3ee1544..872df4b 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 @@ -44,6 +44,7 @@ public final class BedrockSettings { diff --git a/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.patch b/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.patch index bfdea9c9..ec880a2c 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.patch @@ -1,4 +1,4 @@ -From 4d7fddd6eb669318d9cc58008dbb5b18a6503ac8 Mon Sep 17 00:00:00 2001 +From 66c257b2fc992acf8ab7cc0b6abd49db0b6bed94 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(movement): send completed local physics to ViaBedrock @@ -882,7 +882,7 @@ index 909ac3c..2e80f04 100644 jarInJar("org.redlance.mochafloats:runtime:6.1.0") { exclude(group = "io.netty") diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 53e6c84..7542382 100644 +index 6d7f012..5765f62 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -27,6 +27,7 @@ import com.viaversion.viafabricplus.bedrock.appearance.BedrockSkinChange; @@ -1293,7 +1293,7 @@ index 0000000..71edbae + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntity.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntity.java -index 236c05d..dda48a4 100644 +index 8c9bbaa..fa13133 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntity.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntity.java @@ -25,21 +25,17 @@ import com.llamalad7.mixinextras.injector.ModifyExpressionValue; @@ -1416,7 +1416,7 @@ index 0000000..082ce4d + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction.java -index 6cd82ab..6497fd0 100644 +index c644853..62bc850 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction.java @@ -26,8 +26,12 @@ import com.llamalad7.mixinextras.expression.Expression; @@ -1452,7 +1452,7 @@ index 6cd82ab..6497fd0 100644 @Definition(id = "box", local = @Local(type = AABB.class, name = "box")) @Definition(id = "minY", field = "Lnet/minecraft/world/phys/AABB;minY:D") diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction_CurrentAccumulator.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction_CurrentAccumulator.java -index 122cd78..6936521 100644 +index b3be043..b81adbe 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction_CurrentAccumulator.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinEntityFluidInteraction_CurrentAccumulator.java @@ -21,38 +21,67 @@ @@ -2019,7 +2019,7 @@ index 0000000..b6ee81c + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java -index 1c9125e..16fe173 100644 +index cb89991..81b514b 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java @@ -21,27 +21,169 @@ @@ -2344,7 +2344,7 @@ index 0000000..3cad3f3 + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLocalPlayer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLocalPlayer.java -index e5a8167..5a53c06 100644 +index 0e466e5..4ead330 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLocalPlayer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLocalPlayer.java @@ -21,22 +21,396 @@ @@ -2846,7 +2846,7 @@ index 0000000..3dda60f + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinPlayer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinPlayer.java -index 49a37ac..fba1edb 100644 +index 8d10241..59d6646 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinPlayer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinPlayer.java @@ -21,37 +21,114 @@ diff --git a/patches/viafabricplus-bedrock/upstreamable/0027-apply-authoritative-projectile-owner-changes.patch b/patches/viafabricplus-bedrock/upstreamable/0027-apply-authoritative-projectile-owner-changes.patch index 387e8c3b..8db696f1 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0027-apply-authoritative-projectile-owner-changes.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0027-apply-authoritative-projectile-owner-changes.patch @@ -1,4 +1,4 @@ -From 45586829416bab4c742af7bb4bfa2d27bf2cd980 Mon Sep 17 00:00:00 2001 +From 19324513a2086412843215c919b3e01beef1859e 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): apply authoritative projectile owner changes @@ -37,7 +37,7 @@ comparisons; this change does not claim full projectile action parity. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/NativeProjectileOwnerPayload.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 7542382..6a7b234 100644 +index 5765f62..1267a0a 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -41,6 +41,8 @@ import com.viaversion.viafabricplus.bedrock.render.NativeActorColorPayload; diff --git a/patches/viafabricplus-bedrock/upstreamable/0028-apply-native-fishing-bite-impulses.patch b/patches/viafabricplus-bedrock/upstreamable/0028-apply-native-fishing-bite-impulses.patch index a7f1d95c..466bbc5c 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0028-apply-native-fishing-bite-impulses.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0028-apply-native-fishing-bite-impulses.patch @@ -1,4 +1,4 @@ -From 9f1e46d3eb83492f5ab97f7e53a3e9468b96d660 Mon Sep 17 00:00:00 2001 +From 9b024cec4804082ced28ab1cf06033a8f7249a3b 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): apply native fishing feedback @@ -71,7 +71,7 @@ simulation in the parity ledger until separately verified. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/settings/base/ParticleViewDistanceSettingTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java -index 6a7b234..7fa3316 100644 +index 1267a0a..e0cc7f9 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -42,6 +42,8 @@ import com.viaversion.viafabricplus.bedrock.render.NativeActorPickingPayload; @@ -234,7 +234,7 @@ index d1dcc23..1d24f7d 100644 static BedrockResourceLibrary library() throws IOException { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinSettingsScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinSettingsScreen.java -index 3ce3ab2..af236b7 100644 +index d83f8b4..bb5c227 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinSettingsScreen.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/core/integration/MixinSettingsScreen.java @@ -24,6 +24,8 @@ package com.viaversion.viafabricplus.bedrock.injection.mixin.core.integration; @@ -488,7 +488,7 @@ index 0000000..56ca177 + } +} 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 48d994f..999857f 100644 +index 872df4b..a0f9d2f 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 @@ -23,6 +23,7 @@ package com.viaversion.viafabricplus.bedrock.settings.impl; 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 cfafb445..a72e3acd 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 a47637bf85996556ce609de662cded209c5cc817 Mon Sep 17 00:00:00 2001 +From 2bd6f7803d6a4335a151f97c670d73891449cda0 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 @@ -850,7 +850,7 @@ inventory action semantics. 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 fe230de..6071d4a 100644 +index 9cb7400..2abeb28 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -104,6 +104,8 @@ import com.viaversion.viafabricplus.bedrock.screen.ui.BedrockHudOverlays; @@ -979,12 +979,12 @@ index 0000000..37cf62b +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/misc/MixinInventoryScreen.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/misc/MixinInventoryScreen.java deleted file mode 100644 -index 1349cd6..0000000 +index cf6ed35..0000000 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/misc/MixinInventoryScreen.java +++ /dev/null @@ -1,61 +0,0 @@ -/* -- * This file is part of ViaFabricPlus Bedrock - https://github.com/florianreuth/viafabricplus-bedrock +- * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock - * Copyright (C) 2021-2026 the original authors - * - Florian Reuth - * - RK_01/RaphiMC @@ -6505,7 +6505,7 @@ index 0000000..a1d1f14 + 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 +index a2c1af8..a0b21b8 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 { 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 c789c4b8..f2efb618 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 17c1979986c39d38d6c1ad977a4a7d982f94b11f Mon Sep 17 00:00:00 2001 +From 36a5b02df60ad15a78fb9daa3073d1763d49e98b 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,7 +195,7 @@ 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 b468961..fe230de 100644 +index f64ad7c..9cb7400 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -102,6 +102,8 @@ import org.apache.logging.log4j.LogManager; 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 843b7a05..fa7dd61c 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 a40914495edf5de66e2a919bb1ccf3dd4152e224 Mon Sep 17 00:00:00 2001 +From ccfa20c4b552c7adf3338412b1e2198e4fa2ec24 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 @@ -992,7 +992,7 @@ image tint and native frame timing parity remain separate requirements. 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..b468961 100644 +index e0cc7f9..f64ad7c 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; @@ -8992,7 +8992,7 @@ index 0000000..b3f8333 + } +} 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 999857f..de0e5b6 100644 +index a0f9d2f..a2c1af8 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 @@ -21,6 +21,9 @@ 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 46ece2fc..49179262 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 b312a8bb53be58a1c9e428cc41d1cdfd126e685f Mon Sep 17 00:00:00 2001 +From f57560f5c106eee11fa3c33938f51c725330903c 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,7 +239,7 @@ 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 6071d4a..d908685 100644 +index 2abeb28..b345de2 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java @@ -123,6 +123,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { 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 7ff21b62..668881f8 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 e7f07347b5a9ffe63223e669762996d1a37133a0 Mon Sep 17 00:00:00 2001 +From b6821c9d03c321f5ab9b3039ef967d9fd474bc98 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 d908685..f7bc20a 100644 +index b345de2..ff8ddd0 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; diff --git a/patches/viafabricplus-bedrock/upstreamable/0033-align-native-block-random-offsets.patch b/patches/viafabricplus-bedrock/upstreamable/0033-align-native-block-random-offsets.patch index 28188b57..d50014f5 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0033-align-native-block-random-offsets.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0033-align-native-block-random-offsets.patch @@ -1,4 +1,4 @@ -From 6b54964a78df845ea2227db31c3227f4055ff7a6 Mon Sep 17 00:00:00 2001 +From e9dae29c4bb1caeafbd24a05e845b1cdc4412f0b 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(blocks): align native block random offsets @@ -52,7 +52,7 @@ index 0000000..cd9dfa0 + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinBambooStalkBlock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinBambooStalkBlock.java -index b86bf54..c7870f9 100644 +index 54cf119..48a4411 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinBambooStalkBlock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinBambooStalkBlock.java @@ -51,10 +51,10 @@ public abstract class MixinBambooStalkBlock { @@ -69,7 +69,7 @@ index b86bf54..c7870f9 100644 @Inject(method = {"getShape", "getCollisionShape"}, at = @At("HEAD"), cancellable = true) private void fixBambooShape(final BlockState state, final BlockGetter level, final BlockPos pos, final CollisionContext context, final CallbackInfoReturnable cir) { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinBlockBehaviour_Properties.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinBlockBehaviour_Properties.java -index 7111032..ce3b5f9 100644 +index b1eb7dd..5525847 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinBlockBehaviour_Properties.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinBlockBehaviour_Properties.java @@ -22,109 +22,33 @@ 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 dfbb9351..57094e8b 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch @@ -1,7 +1,7 @@ -From 1eee620b0197015e5e1858fbabc8b9a323233994 Mon Sep 17 00:00:00 2001 +From 7910a3442e6fce14787e72f3f3ea15714569e997 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 +Subject: [PATCH] feat(interaction): preserve native held item use Native Bedrock 1.26.60.5 keeps the first predicted trapdoor facing, half and world hit during held placement. Preserve that state across trapdoor @@ -22,7 +22,7 @@ after camera movement, before the next game tick publishes rotation. Suppress Java's parallel four-tick repeat path for that active hold. This preserves native placement/camera ordering without forcing server orientation. Release, screens and connection replacement retire the -input timer and placement state. +placement input timer and placement state. Keep the existing native outline. Fresh native bottom-face hits use 0.8175; the earlier 0.8125 claim came from the Java fixture. Clock-boundary, @@ -34,41 +34,70 @@ Select the existing native outline and occlusion behavior from the active Bedrock connection, including remote ViaProxy sessions. Their local Java target version does not identify the backend. Without this check fresh proxy hits retained Java's 0.8125 bottom instead of native 0.8175. + +Coordinate native food use with the existing placement input owner. Retain +the connection's 200 ms monotonic food deadline separately from placement's +300 ms first-attempt/success delay. Gate fresh calls before packet/local +use, and retry held foods from input frames after the strict deadline. +Keep the food deadline across physical release and item changes; clear it +on connection replacement. Release still clears the held placement timer +and retained trapdoor state. Missing optional block-use channels do not +retire the independent food deadline. + +Select food from the transported FOOD component, including custom paper +carriers. Set the deadline before main-hand air use even if hunger later +refuses the request. Canonical consumption and completion stay server-owned. +Other air-use families retain their existing route; a pending food deadline +still gates a subsequent attempt, including a change to a block item. + +Native 1.26.60.5/BDS 1.26.60.6 protocol-2225 runtime tracing establishes +the monotonic clock and strict deadline check before main-hand air use. Both +fresh-press and held-frame callers reach that boundary. A 2.1-second +instant-food hold consumes nine items with or without the debugger; the +old Java four-tick path consumes eleven. Three 80 ms clicks consume two +when their starts are about 0,131,616 ms. Input-frame cadence determines +quantization, so do not replace this deadline with a fixed five-tick delay. + +Behavioral timing fixtures cover variable input frames, released food, +connection reset, both clock boundaries, rollover and separate food/block +lifetimes during held item changes. Complete component dispatch, other +use families, exact phase counts and the original intermittent food report +remain open; these controls do not establish full native use parity. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + - .../features/block/MixinTrapDoorBlock.java | 18 ++- - .../features/input/MixinBlockUseControl.java | 53 ++++++ - .../inventory/MixinHeldBlockPlacement.java | 28 ++++ - .../features/inventory/MixinHeldBlockUse.java | 61 +++++++ - .../bedrock/input/BedrockBlockUse.java | 152 ++++++++++++++++++ + .../features/block/MixinTrapDoorBlock.java | 18 +- + .../features/input/MixinItemUseControl.java | 69 +++++++ + .../inventory/MixinHeldBlockPlacement.java | 28 +++ + .../features/inventory/MixinHeldBlockUse.java | 61 ++++++ + .../bedrock/input/BedrockItemUse.java | 182 ++++++++++++++++++ .../bedrock/input/BlockUsePayload.java | 24 +++ .../input/BlockUsePredictionPayload.java | 24 +++ - .../bedrock/input/HeldBlockUseTiming.java | 32 ++++ - .../bedrock/inventory/TrapdoorPlacement.java | 55 +++++++ + .../bedrock/input/ItemUseTiming.java | 53 +++++ + .../bedrock/inventory/TrapdoorPlacement.java | 55 ++++++ .../viafabricplus-bedrock.mixins.json | 3 + - .../bedrock/input/HeldBlockUseTimingTest.java | 37 +++++ - .../inventory/TrapdoorPlacementTest.java | 100 ++++++++++++ - 13 files changed, 585 insertions(+), 4 deletions(-) - create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinBlockUseControl.java + .../bedrock/input/ItemUseTimingTest.java | 72 +++++++ + .../inventory/TrapdoorPlacementTest.java | 100 ++++++++++ + 13 files changed, 687 insertions(+), 4 deletions(-) + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockUse.java - create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockBlockUse.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePredictionPayload.java - create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/HeldBlockUseTiming.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTiming.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacement.java - create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/HeldBlockUseTimingTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTimingTest.java 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 f7bc20a..ef06561 100644 +index ff8ddd0..384a74d 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; import com.viaversion.viafabricplus.bedrock.hud.BedrockLocatorBar; import com.viaversion.viafabricplus.bedrock.hud.LocatorBarPayload; import com.viaversion.viafabricplus.bedrock.input.RawInputPayload; -+import com.viaversion.viafabricplus.bedrock.input.BedrockBlockUse; ++import com.viaversion.viafabricplus.bedrock.input.BedrockItemUse; import com.viaversion.viafabricplus.bedrock.input.PlayerPredictionPayload; import com.viaversion.viafabricplus.bedrock.input.BedrockPlayerCorrections; import com.viaversion.viafabricplus.bedrock.input.BedrockMovementEffects; @@ -76,12 +105,12 @@ index f7bc20a..ef06561 100644 ClientPlayNetworking.registerGlobalReceiver(NativeParticlePlaybackPayload.TYPE, (payload, context) -> BedrockServerParticles.accept(payload.message())); PayloadTypeRegistry.serverboundPlay().register(RawInputPayload.TYPE, RawInputPayload.CODEC); -+ BedrockBlockUse.register(); ++ BedrockItemUse.register(); PayloadTypeRegistry.serverboundPlay().register(CommandBlockEditPayload.TYPE, CommandBlockEditPayload.CODEC); BedrockCommandBlockScreen.register(); PayloadTypeRegistry.serverboundPlay().register(PlayerPredictionPayload.TYPE, PlayerPredictionPayload.CODEC); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinTrapDoorBlock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinTrapDoorBlock.java -index c9866b0..720727a 100644 +index 82db187..eda72b3 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinTrapDoorBlock.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/block/MixinTrapDoorBlock.java @@ -21,7 +21,11 @@ @@ -90,7 +119,7 @@ index c9866b0..720727a 100644 -import com.viaversion.viafabricplus.ViaFabricPlus; +import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; -+import com.viaversion.viafabricplus.bedrock.input.BedrockBlockUse; ++import com.viaversion.viafabricplus.bedrock.input.BedrockItemUse; +import net.minecraft.world.item.context.BlockPlaceContext; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @@ -119,7 +148,7 @@ index c9866b0..720727a 100644 + @Inject(method = "getStateForPlacement", at = @At("RETURN"), cancellable = true) + private void retainHeldPlacement(final BlockPlaceContext context, final CallbackInfoReturnable cir) { + if (cir.getReturnValue() != null) { -+ cir.setReturnValue(BedrockBlockUse.placement(cir.getReturnValue())); ++ cir.setReturnValue(BedrockItemUse.placement(cir.getReturnValue())); + } + } + @@ -130,12 +159,12 @@ index c9866b0..720727a 100644 return SHAPES.get(state.getValue(OPEN) ? state.getValue(FACING) : (state.getValue(HALF) == Half.TOP ? Direction.DOWN : Direction.UP)); } else { return super.getOcclusionShape(state); -diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinBlockUseControl.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinBlockUseControl.java +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java new file mode 100644 -index 0000000..86e7b5e +index 0000000..c5701b4 --- /dev/null -+++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinBlockUseControl.java -@@ -0,0 +1,53 @@ ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java +@@ -0,0 +1,69 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -144,8 +173,14 @@ index 0000000..86e7b5e +package com.viaversion.viafabricplus.bedrock.injection.mixin.features.input; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -+import com.viaversion.viafabricplus.bedrock.input.BedrockBlockUse; ++import com.llamalad7.mixinextras.injector.wrapoperation.Operation; ++import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; ++import com.viaversion.viafabricplus.bedrock.input.BedrockItemUse; +import net.minecraft.client.Minecraft; ++import net.minecraft.client.multiplayer.MultiPlayerGameMode; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; +import net.minecraft.client.gui.screens.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; @@ -154,31 +189,41 @@ index 0000000..86e7b5e +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Minecraft.class) -+public abstract class MixinBlockUseControl { ++public abstract class MixinItemUseControl { + @Shadow protected abstract void pick(float partialTick); + @Shadow protected abstract void startUseItem(); -+ @Inject(method = "startUseItem", at = @At("HEAD")) -+ private void beginBlockUse(final CallbackInfo ci) { -+ BedrockBlockUse.begin(); ++ @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) ++ private void beginItemUse(final CallbackInfo ci) { ++ if (!BedrockItemUse.begin()) { ++ ci.cancel(); ++ } ++ } ++ ++ @WrapOperation(method = "startUseItem", at = @At(value = "INVOKE", ++ target = "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;useItem(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/InteractionResult;")) ++ private InteractionResult admitFoodUse(final MultiPlayerGameMode gameMode, final Player player, ++ final InteractionHand hand, final Operation original) { ++ return BedrockItemUse.beginFoodUse(hand, player.getItemInHand(hand)) ++ ? original.call(gameMode, player, hand) : InteractionResult.PASS; + } + + @Inject(method = "startUseItem", at = @At("RETURN")) + private void retainNativeRepeatDelay(final CallbackInfo ci) { -+ BedrockBlockUse.attempted(); ++ BedrockItemUse.attempted(); + } + + @ModifyExpressionValue(method = "handleKeybinds", at = @At(value = "FIELD", + target = "Lnet/minecraft/client/Minecraft;rightClickDelay:I")) -+ private int repeatHeldPlacementFromFrames(final int original) { -+ return BedrockBlockUse.repeatsFromFrames() ? 1 : original; ++ private int repeatHeldUseFromFrames(final int original) { ++ return BedrockItemUse.repeatsFromFrames() ? 1 : original; + } + + @Inject(method = "runTick", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/MouseHandler;handleAccumulatedMovement()V", shift = At.Shift.AFTER)) + private void repeatAfterCameraInput(final boolean renderLevel, final CallbackInfo ci) { -+ BedrockBlockUse.refresh(); ++ BedrockItemUse.refresh(); + final var player = ((Minecraft) (Object) this).player; -+ if (player != null && !player.isUsingItem() && BedrockBlockUse.frameReady()) { ++ if (player != null && !player.isUsingItem() && BedrockItemUse.frameReady()) { + this.pick(1F); + this.startUseItem(); + } @@ -186,12 +231,12 @@ index 0000000..86e7b5e + + @Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;ZZ)V", at = @At("HEAD")) + private void releaseBeforeDisconnect(final Screen screen, final boolean transferring, final boolean skipSaving, final CallbackInfo ci) { -+ BedrockBlockUse.release(); ++ BedrockItemUse.release(); + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java new file mode 100644 -index 0000000..10e5793 +index 0000000..4be16c1 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java @@ -0,0 +1,28 @@ @@ -202,7 +247,7 @@ index 0000000..10e5793 + */ +package com.viaversion.viafabricplus.bedrock.injection.mixin.features.inventory; + -+import com.viaversion.viafabricplus.bedrock.input.BedrockBlockUse; ++import com.viaversion.viafabricplus.bedrock.input.BedrockItemUse; +import net.minecraft.client.Minecraft; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.BlockItem; @@ -219,13 +264,13 @@ index 0000000..10e5793 + if (cir.getReturnValue().consumesAction() && context.getPlayer() == Minecraft.getInstance().player) { + final var support = context.replacingClickedOnBlock() ? context.getClickedPos() + : context.getClickedPos().relative(context.getClickedFace().getOpposite()); -+ BedrockBlockUse.placed(context.getLevel().getBlockState(context.getClickedPos()), support, context.getClickLocation()); ++ BedrockItemUse.placed(context.getLevel().getBlockState(context.getClickedPos()), support, context.getClickLocation()); + } + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockUse.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockUse.java new file mode 100644 -index 0000000..e44f1d9 +index 0000000..69387b7 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockUse.java @@ -0,0 +1,61 @@ @@ -238,7 +283,7 @@ index 0000000..e44f1d9 + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -+import com.viaversion.viafabricplus.bedrock.input.BedrockBlockUse; ++import com.viaversion.viafabricplus.bedrock.input.BedrockItemUse; +import net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.world.InteractionHand; @@ -257,13 +302,13 @@ index 0000000..e44f1d9 + @WrapOperation(method = "performUseItemOn", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/player/LocalPlayer;isSecondaryUseActive()Z")) + private boolean suppressBlockInteraction(final LocalPlayer player, final Operation original) { -+ return BedrockBlockUse.suppressInteraction() || original.call(player); ++ return BedrockItemUse.suppressInteraction() || original.call(player); + } + + @Inject(method = "useItemOn", at = @At("HEAD"), cancellable = true) + private void restrictSnappedTargets(final LocalPlayer player, final InteractionHand hand, final BlockHitResult hit, + final CallbackInfoReturnable cir) { -+ if (!BedrockBlockUse.allows(hit.getBlockPos(), player.level().getBlockState(hit.getBlockPos()))) { ++ if (!BedrockItemUse.allows(hit.getBlockPos(), player.level().getBlockState(hit.getBlockPos()))) { + cir.setReturnValue(InteractionResult.FAIL); + } + } @@ -271,31 +316,31 @@ index 0000000..e44f1d9 + @Inject(method = "performUseItemOn", at = @At("HEAD")) + private void beginLocalPrediction(final LocalPlayer player, final InteractionHand hand, final BlockHitResult hit, + final CallbackInfoReturnable cir) { -+ BedrockBlockUse.beginPrediction(hit); ++ BedrockItemUse.beginPrediction(hit); + } + + @Inject(method = "performUseItemOn", at = @At("RETURN")) + private void retainLocalResult(final LocalPlayer player, final InteractionHand hand, final BlockHitResult hit, + final CallbackInfoReturnable cir) { -+ BedrockBlockUse.predicted(cir.getReturnValue().consumesAction()); ++ BedrockItemUse.predicted(cir.getReturnValue().consumesAction()); + } + + @WrapOperation(method = "startPrediction", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;send(Lnet/minecraft/network/protocol/Packet;)V")) + private void sendBlockUsePrediction(final ClientPacketListener connection, final Packet packet, final Operation original) { + if (packet instanceof ServerboundUseItemOnPacket use) { -+ BedrockBlockUse.sendPrediction(use.sequence()); ++ BedrockItemUse.sendPrediction(use.sequence()); + } + original.call(connection, packet); + } + +} -diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockBlockUse.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockBlockUse.java +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java new file mode 100644 -index 0000000..1a75cce +index 0000000..d1e8f89 --- /dev/null -+++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockBlockUse.java -@@ -0,0 +1,152 @@ ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java +@@ -0,0 +1,182 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -310,6 +355,9 @@ index 0000000..1a75cce +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; ++import net.minecraft.core.component.DataComponents; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.level.block.TrapDoorBlock; +import net.raphimc.viabedrock.api.model.BlockUsePrediction; @@ -319,16 +367,16 @@ index 0000000..1a75cce +import net.raphimc.viabedrock.protocol.model.Position3f; +import net.raphimc.viabedrock.api.model.PlayerBlockUse; + -+public final class BedrockBlockUse { ++public final class BedrockItemUse { + private static final TrapdoorPlacement PLACEMENT = new TrapdoorPlacement(); + private static boolean held; + private static boolean placedBlock; + private static boolean successfulAttempt; -+ private static final HeldBlockUseTiming REPEATS = new HeldBlockUseTiming(); ++ private static final ItemUseTiming TIMING = new ItemUseTiming(); + private static Position3f cursor; + private static BlockUsePrediction.Result predictedResult = BlockUsePrediction.Result.FAILED; + -+ private BedrockBlockUse() {} ++ private BedrockItemUse() {} + + public static void register() { + PayloadTypeRegistry.serverboundPlay().register(BlockUsePayload.TYPE, BlockUsePayload.CODEC); @@ -338,9 +386,29 @@ index 0000000..1a75cce + ClientPlayConnectionEvents.DISCONNECT.register((listener, client) -> reset()); + } + -+ public static void begin() { ++ public static boolean begin() { ++ if (bedrockConnection() && !TIMING.canAttempt(System.nanoTime())) { ++ return false; ++ } + update(true); + successfulAttempt = false; ++ return true; ++ } ++ ++ public static boolean beginFoodUse(final InteractionHand hand, final ItemStack item) { ++ return !bedrockConnection() || hand != InteractionHand.MAIN_HAND || !item.has(DataComponents.FOOD) ++ || TIMING.tryFoodUse(System.nanoTime()); ++ } ++ ++ private static boolean bedrockConnection() { ++ return Minecraft.getInstance().getConnection() != null && BedrockInputPermissions.isBedrockConnection(); ++ } ++ ++ private static boolean holdingFood() { ++ final var client = Minecraft.getInstance(); ++ return bedrockConnection() && client.player != null && !client.player.isSpectator() ++ && client.gui.screen() == null && client.gui.overlay() == null && client.options.keyUse.isDown() ++ && !client.player.getMainHandItem().isEmpty() && client.player.getMainHandItem().has(DataComponents.FOOD); + } + + public static void release() { @@ -348,18 +416,20 @@ index 0000000..1a75cce + } + + private static void update(final boolean value) { -+ if (!BedrockInputPermissions.isBedrockConnection() || Minecraft.getInstance().getConnection() == null -+ || !ClientPlayNetworking.canSend(BlockUsePayload.TYPE) -+ || !ClientPlayNetworking.canSend(BlockUsePredictionPayload.TYPE)) { ++ if (!bedrockConnection()) { + reset(); + return; + } ++ if (!ClientPlayNetworking.canSend(BlockUsePayload.TYPE) || !ClientPlayNetworking.canSend(BlockUsePredictionPayload.TYPE)) { ++ resetBlockUse(); ++ return; ++ } + if (held != value) { + held = value; + ClientPlayNetworking.send(new BlockUsePayload(new PlayerBlockUse(value))); + } + if (!value) { -+ REPEATS.clear(); ++ TIMING.release(); + PLACEMENT.clear(); + } + } @@ -375,17 +445,17 @@ index 0000000..1a75cce + } + + public static void attempted() { -+ if (held && (REPEATS.active() || usesNativeRepeatDelay())) { -+ REPEATS.attempted(System.nanoTime(), successfulAttempt); ++ if (held && (TIMING.blockActive() || usesNativeRepeatDelay())) { ++ TIMING.attemptedBlock(System.nanoTime(), successfulAttempt); + } + } + + public static boolean repeatsFromFrames() { -+ return held && REPEATS.active(); ++ return holdingFood() || held && TIMING.blockActive(); + } + + public static boolean frameReady() { -+ return repeatsFromFrames() && REPEATS.ready(System.nanoTime()); ++ return repeatsFromFrames() && TIMING.frameReady(System.nanoTime(), holdingFood()); + } + + public static void refresh() { @@ -396,7 +466,7 @@ index 0000000..1a75cce + + public static boolean suppressInteraction() { + final var player = Minecraft.getInstance().player; -+ return held && REPEATS.active() && player != null && player.getMainHandItem().getItem() instanceof BlockItem; ++ return held && TIMING.blockActive() && player != null && player.getMainHandItem().getItem() instanceof BlockItem; + } + + public static boolean allows(final BlockPos target, final BlockState state) { @@ -441,12 +511,17 @@ index 0000000..1a75cce + return held ? PLACEMENT.apply(state) : state; + } + -+ private static void reset() { ++ private static void resetBlockUse() { + held = false; -+ REPEATS.clear(); ++ TIMING.release(); + clearPrediction(); + PLACEMENT.clear(); + } ++ ++ private static void reset() { ++ resetBlockUse(); ++ TIMING.clear(); ++ } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePayload.java new file mode 100644 @@ -508,12 +583,12 @@ index 0000000..a98d448 + return TYPE; + } +} -diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/HeldBlockUseTiming.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/HeldBlockUseTiming.java +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTiming.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTiming.java new file mode 100644 -index 0000000..58b9e5d +index 0000000..d102001 --- /dev/null -+++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/HeldBlockUseTiming.java -@@ -0,0 +1,32 @@ ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTiming.java +@@ -0,0 +1,53 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -521,29 +596,50 @@ index 0000000..58b9e5d + */ +package com.viaversion.viafabricplus.bedrock.input; + -+/** Native use waits after the first attempt or a success, then retries on each input frame. */ -+public final class HeldBlockUseTiming { -+ private static final long USE_DELAY_NANOS = 300_000_000L; -+ private boolean active; -+ private long delayStartedAt; ++/** Food's connection deadline and held placement's success delay have different lifetimes. */ ++public final class ItemUseTiming { ++ private static final long FOOD_DELAY_NANOS = 200_000_000L; ++ private static final long BLOCK_DELAY_NANOS = 300_000_000L; ++ private boolean foodAttempted; ++ private long foodStartedAt; ++ private boolean blockActive; ++ private long blockStartedAt; + -+ public void attempted(final long now, final boolean success) { -+ if (!this.active || success) { -+ this.delayStartedAt = now; -+ this.active = true; ++ public boolean canAttempt(final long now) { ++ return !this.foodAttempted || now - this.foodStartedAt > FOOD_DELAY_NANOS; ++ } ++ ++ public boolean tryFoodUse(final long now) { ++ if (!this.canAttempt(now)) { ++ return false; + } ++ this.foodAttempted = true; ++ this.foodStartedAt = now; ++ return true; + } + -+ public boolean active() { -+ return this.active; ++ public void attemptedBlock(final long now, final boolean success) { ++ if (!this.blockActive || success) { ++ this.blockStartedAt = now; ++ this.blockActive = true; ++ } ++ } ++ ++ public boolean blockActive() { ++ return this.blockActive; + } + -+ public boolean ready(final long now) { -+ return this.active && now - this.delayStartedAt >= USE_DELAY_NANOS; ++ public boolean frameReady(final long now, final boolean food) { ++ return this.canAttempt(now) && (food || this.blockActive && now - this.blockStartedAt >= BLOCK_DELAY_NANOS); ++ } ++ ++ public void release() { ++ this.blockActive = false; + } + + public void clear() { -+ this.active = false; ++ this.release(); ++ this.foodAttempted = false; + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacement.java b/src/main/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacement.java @@ -608,60 +704,95 @@ index 0000000..7a2e793 + } +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index f7c3bf4..4bdcb1f 100644 +index f7c3bf4..416a16f 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json @@ -92,6 +92,9 @@ "features.block.MixinRedstoneWireBlock", "features.block.MixinSeaPickleBlock", "features.block.MixinTrapDoorBlock", -+ "features.input.MixinBlockUseControl", ++ "features.input.MixinItemUseControl", + "features.inventory.MixinHeldBlockUse", + "features.inventory.MixinHeldBlockPlacement", "features.creative.MixinCreativeModeInventoryScreen", "features.creative.MixinCreativeModeTab_ItemDisplayBuilder", "features.creative.MixinCreativeModeTabs", -diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/HeldBlockUseTimingTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/HeldBlockUseTimingTest.java +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTimingTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTimingTest.java new file mode 100644 -index 0000000..ce41850 +index 0000000..db49305 --- /dev/null -+++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/HeldBlockUseTimingTest.java -@@ -0,0 +1,37 @@ ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTimingTest.java +@@ -0,0 +1,72 @@ +package com.viaversion.viafabricplus.bedrock.input; + +import org.junit.jupiter.api.Test; + ++import java.util.ArrayList; ++import java.util.List; ++ +import static org.junit.jupiter.api.Assertions.*; + -+final class HeldBlockUseTimingTest { ++final class ItemUseTimingTest { + @Test -+ void successfulUsesRestartTheDelayWhileFailedRepeatsAndItemChangesDoNot() { -+ final var timing = new HeldBlockUseTiming(); -+ assertFalse(timing.ready(1_000_000_000L)); -+ timing.attempted(1_000_000_000L, false); -+ assertFalse(timing.ready(1_299_999_999L)); -+ assertTrue(timing.ready(1_300_000_000L)); -+ timing.attempted(1_350_000_000L, false); -+ assertTrue(timing.ready(1_350_000_000L)); -+ assertTrue(timing.ready(1_351_000_000L)); -+ timing.attempted(1_400_000_000L, true); -+ assertFalse(timing.ready(1_699_999_999L)); -+ assertTrue(timing.ready(1_700_000_000L)); ++ void releasedFoodKeepsItsDeadlineForFreshClicksUntilConnectionReplacement() { ++ final var timing = new ItemUseTiming(); ++ assertTrue(timing.tryFoodUse(1_000_000_000L)); ++ timing.release(); ++ assertFalse(timing.tryFoodUse(1_131_000_000L)); ++ assertFalse(timing.tryFoodUse(1_200_000_000L)); ++ assertTrue(timing.tryFoodUse(1_616_000_000L)); ++ assertFalse(timing.canAttempt(1_700_000_000L)); + timing.clear(); -+ timing.clear(); -+ assertFalse(timing.active()); -+ timing.attempted(2_000_000_000L, false); -+ assertFalse(timing.ready(2_299_999_999L)); -+ assertTrue(timing.ready(2_300_000_000L)); ++ assertTrue(timing.tryFoodUse(1_700_000_000L)); ++ } ++ ++ @Test ++ void heldFoodUsesTheFirstEligibleInputFrameInsteadOfARepeatedTickCount() { ++ final var timing = new ItemUseTiming(); ++ final List usedAt = new ArrayList<>(); ++ for (final int milliseconds : new int[] {0, 47, 94, 141, 188, 236, 284, 332, 380, 428, 475, 542, 609, 676}) { ++ final long now = 1_000_000_000L + milliseconds * 1_000_000L; ++ if (timing.frameReady(now, true) && timing.tryFoodUse(now)) { ++ usedAt.add(milliseconds); ++ } ++ } ++ assertEquals(List.of(0, 236, 475, 676), usedAt); ++ } ++ ++ @Test ++ void foodAndPlacementKeepIndependentDelaysAcrossHeldItemChanges() { ++ final var timing = new ItemUseTiming(); ++ assertFalse(timing.frameReady(1_000_000_000L, false)); ++ timing.attemptedBlock(1_000_000_000L, false); ++ assertTrue(timing.frameReady(1_100_000_000L, true)); ++ assertTrue(timing.tryFoodUse(1_100_000_000L)); ++ assertFalse(timing.frameReady(1_300_000_000L, false)); ++ assertTrue(timing.frameReady(1_301_000_000L, false)); ++ timing.attemptedBlock(1_310_000_000L, false); ++ assertTrue(timing.frameReady(1_311_000_000L, false)); ++ timing.attemptedBlock(1_320_000_000L, true); ++ assertTrue(timing.tryFoodUse(1_400_000_000L)); ++ assertFalse(timing.frameReady(1_600_000_000L, true)); ++ assertTrue(timing.frameReady(1_600_000_001L, true)); ++ assertFalse(timing.frameReady(1_619_999_999L, false)); ++ assertTrue(timing.frameReady(1_620_000_000L, false)); ++ assertTrue(timing.tryFoodUse(1_630_000_000L)); ++ timing.release(); ++ assertFalse(timing.frameReady(1_700_000_000L, true)); ++ assertTrue(timing.frameReady(1_830_000_001L, true)); ++ assertFalse(timing.frameReady(1_900_000_000L, false)); + } + + @Test -+ void monotonicClockRolloverKeepsTheSameDelay() { -+ final var timing = new HeldBlockUseTiming(); ++ void monotonicClockRolloverKeepsBothDeadlines() { ++ final var timing = new ItemUseTiming(); + final long start = Long.MAX_VALUE - 150_000_000L; -+ timing.attempted(start, false); -+ assertFalse(timing.ready(start + 299_999_999L)); -+ assertTrue(timing.ready(start + 300_000_000L)); ++ timing.attemptedBlock(start, true); ++ assertTrue(timing.tryFoodUse(start)); ++ assertFalse(timing.frameReady(start + 200_000_000L, true)); ++ assertTrue(timing.frameReady(start + 200_000_001L, true)); ++ assertFalse(timing.frameReady(start + 299_999_999L, false)); ++ assertTrue(timing.frameReady(start + 300_000_000L, false)); + } +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacementTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacementTest.java diff --git a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md index cea7f5e6..04a77217 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md @@ -1,48 +1,60 @@ -# Held trapdoor placement - -The Java baseline opened a trapdoor during held placement where Bedrock added a closed neighbor. -Send the physical use lifetime and each local prediction to the core before its concluding Java packet. -Prediction format 2 carries the result and native cursor. Core and add-on consumers require the matching contract. -Enable the feature only when the server advertises both channels. - -Keep the first predicted facing, half, and world hit until physical release or cancellation. -Retain this state across trapdoor materials, temporary item-family changes, server correction, and stack exhaustion. -Apply the snap only while the player holds a trapdoor. -Other block items use fresh hits and suppress repeated activation. Empty hands restore ordinary interaction. -Returning to a trapdoor restores the original snap. - -Express the retained world hit relative to each later target, including coordinates outside its unit cube. -Keep the ordinary Java hit packet unchanged for other consumers. -Retain each new block's water, power, open state, and material properties. -Separate trapdoors and the original support remain valid targets. +# Native held item use + +Java's four-tick repeat path does not reproduce Bedrock's input timing. +Coordinate food retries and held block placement through one client input owner. +Inventory consumption and completion remain server-owned. -Match the native 300 ms delay after the first attempt and each successful local use. +Food uses a strict 200 ms monotonic deadline, checked after camera input on each frame. +Gate fresh calls before local use or packet publication. +Set the deadline before main-hand food air use, including a request that hunger later refuses. +Select food from its transported component, including custom items carried as paper. +Physical release and item changes retain this connection deadline. +Connection replacement clears it. +Missing optional block-use channels do not clear the food deadline. + +Held placement keeps its separate 300 ms delay after the first attempt or a successful local use. Failed repeats do not restart that delay. -Poll subsequent attempts after input movement, before the next network tick publishes rotation. -Suppress Java's parallel four-tick repeat path for the active hold. -Release, screens, and connection replacement clear the timer and placement state. +Release, screens, and connection replacement clear the placement timer and retained trapdoor state. +Suppress Java's parallel repeat path while the frame route owns the attempt. + +## Placement constraints + +Retain the first predicted trapdoor facing, half, and world hit through material changes, temporary item-family changes, server correction, and stack exhaustion. +Apply that snap only while holding a trapdoor. +Other block items use fresh hits and suppress repeated activation; empty hands restore ordinary interaction. +Returning to a trapdoor restores the snap. + +Send the physical lifetime and sequence-owned prediction before the concluding Java packet. +Prediction format 2 carries the result and native cursor; both consumers require the matching contract. +Express the retained hit relative to each later target, including coordinates outside its unit cube. +Keep the ordinary Java raycast packet unchanged. +Preserve each new block's water, power, open state, and material properties. +Separate trapdoors and the original support remain valid targets. + +Select native outlines and occlusion from the active Bedrock connection, including ViaProxy sessions with a local Java protocol target. +Native bottom-face hits use approximately 0.8175; the earlier 0.8125 attribution came from the Java fixture. ## Evidence and validation -Native 1.26.60.5 fixtures against BDS 1.26.60.6 establish the held behavior. -Physical camera changes retain facing, upper-half state, and closed state through direct Java and ViaProxy. -Both routes consume two items, leaving 62 from 64. -Material changes and fresh activation after release match their native fixtures. -The original native outline remains correct: native bottom-face hits use approximately 0.8175. -The earlier 0.8125 attribution came from the Java fixture. -The outline and occlusion checks now use the active Bedrock connection, including ViaProxy sessions with a local Java protocol target. -A fresh proxy fixture confirms the 0.8175 hit, placement-only air phase, fresh Stone coordinates, and restored trapdoor snap. - -Native and direct Java retry one rejected first placement within the same held session. -Native item-family fixtures retain the snap through Stone selection and restore it on return to a trapdoor. -The empty-hand fixture uses fresh coordinates and activates existing trapdoors after stack exhaustion. -Direct Java and ViaProxy pause cancellation stop the active session before physical release. -Fresh native and ViaProxy sessions match repeated rejection followed by rotation without block consumption. -After a preceding successful hold, both native and ViaProxy instead place a neighbor and consume one item. -Matching session history explains the earlier apparent correction mismatch. -The translator preserves both authoritative outcomes. - -All four project builds and the modpack bundle pass. -The add-on suite passes 1,281 tests with 142 optional skips and no failures/errors. +Official Bedrock 1.26.60.5 against BDS 1.26.60.6 establishes these controls on protocol 2225. +Native runtime tracing observes the 200 ms deadline before air use and the strict check in the shared fresh-press and held-input function. +A 2.1-second instant-food hold consumes nine items with or without the debugger. +Frame cadence determines retry quantization, so equal counts alone do not establish timing parity. + +Installed direct and ViaProxy food controls check the deadline, rapid clicks after release, pause cancellation, admission, hunger, and settled server counts. +The [food-use validation](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) records the current build and remaining use-cycle limits. +A private observer logs the timing class's original decisions without changing them; this is an instrumented runtime check. +Four timing tests cover variable input frames, released food, connection reset, independent delays, strict boundaries, and rollover. Five block-state tests cover retained orientation, materials, independent properties, targets, frozen hits, release, and ordinary blocks. -Two timer tests cover successful repeats, failed repeats, cancellation, and clock rollover. + +Earlier installed placement fixtures match native camera changes, material changes, and fresh activation after release. +Both routes consume two items, leaving 62 from 64. +Native and direct Java retry a rejected first placement within the same hold. +Both Java routes cancel the session on Pause before physical release. +Fresh native and proxy sessions match repeated rejection followed by rotation without consumption. +After a preceding successful hold, both instead place a neighbor and consume one item. + +## Remaining limits + +Complete food and block handoffs, competing non-food use handlers, exact input-flag phase counts, and full use-cycle timing need more native comparisons. +The original intermittent animation-without-feeding report remains open. diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.patch b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.patch index faf74dc7..035d76a9 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.patch @@ -1,4 +1,4 @@ -From 854e4fa0a3c1a6a95e990e7cf62a0b116153a3a9 Mon Sep 17 00:00:00 2001 +From 9baa15cd26954ae94911aa32f624628a8e252d69 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:54:04 +0100 Subject: [PATCH] fix(items): allow food use on Bedrock Peaceful difficulty @@ -57,7 +57,7 @@ index 0000000..e8a7d8c + +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index ff5a82c..0696216 100644 +index ce75e15..ac083ae 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json @@ -64,6 +64,7 @@ diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md index f60d2482..5fb9bc3d 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md @@ -15,12 +15,12 @@ Installed direct and ViaProxy controls each pass Normal → Peaceful → Normal Allowed cases enter local use, receive one server completion, and settle at 15 bread. Full-hunger Normal cases never enter use and retain 16 bread. The installed add-on and proxy match the rebuilt artifacts. -The full four-project build passes 3,282 tests, with 325 skipped and no failures or errors. Core packet regressions cover server-confirmed difficulty changes and food admission without inventory prediction. Core also projects installed component-food admission and consumable properties into the Java item. That covers the selected full-hunger DataDriven controls and local use of custom paper carriers. The [food-use evidence](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) records those controls and remaining limits. Native tracing establishes a 200 ms air-use deadline that persists across button releases and gates fresh clicks. -Java input scheduling still needs that correction, including coordinated timer ownership across item families and screens. +The [client input patch](0038-predict-held-trapdoor-placement.pr.md) implements that deadline and input-frame retries, with rapid-click and pause controls on both routes. +Complete item-family handoffs still need native comparisons. The original intermittent food-use report remains open. 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 47d3a8f9..10a81fd0 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 f8e383b01d46aab1afd45ce4075930d13ad387c1 Mon Sep 17 00:00:00 2001 +From 1e05dca61a150524bee04ffc696afa99f433f747 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 ef06561..c6b6f3d 100644 +index 384a74d..8ed6c7a 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; @@ -591,7 +591,7 @@ index 0000000..afd7b44 + texCoord0 = UV0; +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 4bdcb1f..ff5a82c 100644 +index 416a16f..ce75e15 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json @@ -140,6 +140,9 @@ diff --git a/patches/viaproxy/upstreamable/0001-use-shared-http-signaling-for-bds-connections.patch b/patches/viaproxy/upstreamable/0001-use-shared-http-signaling-for-bds-connections.patch index 66b25d90..7c2470b3 100644 --- a/patches/viaproxy/upstreamable/0001-use-shared-http-signaling-for-bds-connections.patch +++ b/patches/viaproxy/upstreamable/0001-use-shared-http-signaling-for-bds-connections.patch @@ -1,4 +1,4 @@ -From a0cbc376e900ef982585539e4f74f2858d60380c Mon Sep 17 00:00:00 2001 +From bf9951aa321644c9cf8d4dff8f1975df8b92da50 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:02:37 +0200 Subject: [PATCH] fix(bedrock): use shared HTTP signaling for BDS connections @@ -57,7 +57,7 @@ index ce13ccd..b3a1780 100644 manifest { attributes( diff --git a/src/main/java/net/raphimc/viaproxy/proxy/client2proxy/Client2ProxyHandler.java b/src/main/java/net/raphimc/viaproxy/proxy/client2proxy/Client2ProxyHandler.java -index 1e5b0a3..ba79abd 100644 +index 6546639..342426c 100644 --- a/src/main/java/net/raphimc/viaproxy/proxy/client2proxy/Client2ProxyHandler.java +++ b/src/main/java/net/raphimc/viaproxy/proxy/client2proxy/Client2ProxyHandler.java @@ -34,6 +34,9 @@ import net.raphimc.netminecraft.constants.ConnectionState; diff --git a/targets.json b/targets.json index 9a630dce..7688b97b 100644 --- a/targets.json +++ b/targets.json @@ -16,7 +16,7 @@ "fork": "StackAnvil/ViaBedrock", "prAssignees": ["RaphiMC", "Exterminate5573"], "baseBranch": "main", - "baseSha": "6891e00652485f9c2dc4f5519f071fbea2707638", + "baseSha": "e54158276fcc9d4fff34258379259069ac4848fd", "java": "25", "publication": "MavenJava", "buildTask": "build" @@ -27,7 +27,7 @@ "fork": "StackAnvil/viafabricplus-bedrock", "prAssignees": ["RaphiMC", "Exterminate5573", "florianreuth"], "baseBranch": "main", - "baseSha": "c57ccfad6da6c54bc68019fa2a2a1e31b4502219", + "baseSha": "6c11a7edc29d766cd783e8ac7c9b14133155b4c9", "java": "25", "publication": "Maven", "buildTask": "build" @@ -38,7 +38,7 @@ "fork": "StackAnvil/ViaProxy", "prAssignees": ["RaphiMC"], "baseBranch": "main", - "baseSha": "d823e9d609281fabcd669e5395cabe76d494e91d", + "baseSha": "2d578e951a65d7278134e350c734f76f2c985fa2", "java": "25", "publication": "MavenJava", "buildTask": "build" diff --git a/viafabricplus.json b/viafabricplus.json index eb76f0a3..08dd0029 100644 --- a/viafabricplus.json +++ b/viafabricplus.json @@ -1,10 +1,10 @@ { - "build": 2292, - "commit": "faa87560b0a8dfd5dfa3aa9c559ac178d715f0c4", - "version": "5.1.3-SNAPSHOT", - "mavenVersion": "5.1.3-20261030.230713-6", - "sha256": "ee905dbd99b816a9a3f300b3067df7cc3245e4613ba28b08287a98523b5b889b", - "pomSha256": "5ac2a58a303ffce558c414610dce2723b66facd5e335296d0a9b86adb355505d", - "apiSha256": "5b1dc0c0b36c1167f024b26b69734129a3af009f8cf00136926d1d65ff5234c2", - "apiPomSha256": "c36fad64ec6473d14aef2c07aa01390ab9af1846535a6ed3a2c0bf2567a44999" + "build": 2295, + "commit": "546c4fbf79df1036574a3f0bfc20bb17c5df9d92", + "version": "5.1.4-SNAPSHOT", + "mavenVersion": "5.1.4-20261101.155810-1", + "sha256": "fb28fd936403ba6314f87f48573a79933bafd31ef35e4ebe26fda4a2dc037265", + "pomSha256": "87720823df2792894748d3082992009d93186c45b283734880ead4c8b9763918", + "apiSha256": "a479914e0364f24b9bf1f40437eabb4017e8492ea89fed236758671d1c3439a9", + "apiPomSha256": "506feba24cec28c82ac1c16565c40fdc3b7dc3b819cbfcc0f50fbdc8ffd8985b" } From 62865017ce14d52fd425cf493ff4c5f0d7ad6720 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 1 Nov 2026 23:32:20 +0100 Subject: [PATCH 16/47] fix(input): retain native use mode across item changes Keep a food-started air hold on its air route when the player selects a block. Keep a trapdoor-started placement hold from falling through to eating. The previous client placed blocks and consumed food in those transitions, while native Bedrock retained the original interaction mode. Generalize the existing input state, retire offhand fallback while a mode owns the hold, and clear the mode on release or cancellation. Preserve the connection deadline and separate placement delay. Record version-matched native tracing, installed comparisons and remaining parity limits. Verify 3286 tests, 325 optional skips, full stack replay and bundle, plus native/direct/proxy transitions, release/Pause controls and the eleven food-admission cases on both installed routes. --- docs/bedrock-coverage.md | 2 +- ...08-signal-sustained-bedrock-item-use.pr.md | 10 +- ...0038-predict-held-trapdoor-placement.patch | 321 ++++++++++-------- ...0038-predict-held-trapdoor-placement.pr.md | 20 +- ...d-use-on-bedrock-peaceful-difficulty.pr.md | 3 +- 5 files changed, 210 insertions(+), 146 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index e8f87fed..adca0269 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -27,7 +27,7 @@ No complete area has verified Bedrock parity. | 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, Creative catalog actions, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | -| Food use | Installed food and use-modifier components govern admission and Java food/consumable properties, with Creative and server-confirmed Peaceful exceptions. | Both routes pass selected native admission controls and the strict input deadline, release, and pause checks. Broader use cycles, item-family handoffs, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md). | +| Food use | Installed food and use-modifier components govern admission and Java food/consumable properties, with Creative and server-confirmed Peaceful exceptions. | Both routes pass selected admission, input-deadline, held-mode, release, and pause controls. Broader use cycles, other item-family handoffs, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md index 841fde77..3978a480 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md @@ -46,7 +46,7 @@ Native controls establish the combined behavior for these fixtures; they do not Seventeen focused tests cover installed component retention, constructor-specific admission, Java component wire properties, difficulty changes, food-use flags, offhand resynchronization, and completion boundaries. Admission tests retain the held quantity and verify that rejected uses leave `StartUsingItem` clear. -The full four-project build passes 3,285 tests, with 325 skipped and no failures or errors. +The full four-project build passes 3,286 tests, with 325 skipped and no failures or errors. Installed direct and ViaProxy controls cover eleven cases each. Both routes match the selected native admission, hunger, timed consumption, and zero-duration outcomes. @@ -57,14 +57,14 @@ Installed artifacts match the build checksums; core classes match both packaged The [client input patch](../../viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md) now replaces the food four-tick repeat path with the native strict 200 ms deadline. Fresh calls share that gate, release retains the deadline, and held retries run from input frames. -Direct and ViaProxy controls verify rapid clicks and pause cancellation alongside the eleven food cases. -A private timing observer checks each original admission decision and confirms that eligible input frames lead to use. -The installed JAR checksums match the build; observation adds logging to the running timing class. +Direct and ViaProxy controls cover the eleven food cases, retained interaction modes, release, and pause cancellation. +Food-started air holds do not place newly selected blocks; trapdoor-started placement holds do not eat newly selected food. +The installed JAR checksums match the build. Native tracing establishes the deadline before air use and its check in the shared input function. Native's 2.1-second instant-food control consumes nine items; the direct Java control consumes eleven at its different frame cadence. Counts alone do not establish or refute the scheduling rule. -Complete handoffs between food and other item families still need native comparisons. +Other initial use families, timed and refused-food handoffs, and live entity transitions still need native comparisons. Presence of `StartUsingItem` is verified; exact packet phase counts and complete cycle timing remain open. Competing non-food use handlers, other consumable animations and effects, ability exceptions, and all inventory authority modes need further comparison. 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 57094e8b..28ba5106 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.patch @@ -1,92 +1,79 @@ -From 7910a3442e6fce14787e72f3f3ea15714569e997 Mon Sep 17 00:00:00 2001 +From b3ae5a296f0f813c6f54f860adacc12d07d78c73 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): preserve native held item use Native Bedrock 1.26.60.5 keeps the first predicted trapdoor facing, half -and world hit during held placement. Preserve that state across trapdoor -materials, temporary item-family changes and server correction. Apply the -snap only while holding a trapdoor. Other block items use fresh hits while -suppressing repeated activation; an empty hand restores native interaction. -Returning to a trapdoor reuses the original snap until physical release. +and world hit during held placement. Retain that state across materials, +item changes, correction and stack exhaustion. Apply the snap only to +trapdoors; other block items use fresh hits, and empty hands restore +interaction. Returning to a trapdoor restores its snap until release. +Preserve independent water, power and material properties. Separate +trapdoors and the original support remain eligible targets. -Carry the use lifetime and sequence-owned prediction format 2 through -direct and ViaProxy sessions. Keep the native cursor separate from the -ordinary Java raycast packet, including coordinates outside later target -cubes. Preserve independent water, power and material properties. Separate -trapdoors and the original support remain eligible while snapped. +Carry the physical use lifetime and sequence-owned prediction format 2 +through direct and ViaProxy sessions before the concluding Java packet. +Keep the native cursor separate from the ordinary Java raycast, including +coordinates outside a later target cube. Select native outline/occlusion +from the active Bedrock connection rather than the local Java protocol. +Native bottom-face hits are about 0.8175, including translated proxy joins. -Match the 300 ms delay after the first attempt or a successful use. Failed -repeats do not restart it. Poll subsequent attempts from the input frame -after camera movement, before the next game tick publishes rotation. -Suppress Java's parallel four-tick repeat path for that active hold. -This preserves native placement/camera ordering without forcing server -orientation. Release, screens and connection replacement retire the -placement input timer and placement state. +Keep two clock lifetimes: air use has a strict 200 ms connection deadline; +held placement waits 300 ms after its first attempt or each local success. +Failed placement repeats do not restart their delay. Retry from input +frames after camera movement, before the next tick publishes rotation, +and suppress Java's parallel four-tick path. Native frame quantization +rules out a guessed five-tick delay. Release/screens retire the held mode +and placement state while retaining the air deadline. Connection +replacement resets both. Missing optional block channels clear only +placement state, preserving an active air hold and its deadline. -Keep the existing native outline. Fresh native bottom-face hits use -0.8175; the earlier 0.8125 claim came from the Java fixture. Clock-boundary, -item-state and block-state tests cover the retained input state. Private -native, direct and proxy captures establish gameplay behavior separately -from these model tests. +Retain the interaction mode across item changes. Current native tracing +starts with fresh flags 176 then keeps held air-only flag 16 after selecting +a trapdoor. Only the initial call visits block use; held frames call air +use after the deadline. Unattached stone and late trapdoor controls agree. +The reverse trapdoor-to-food control keeps simulation block uses and +consumes no food. A new press after release can place the selected block. -Select the existing native outline and occlusion behavior from the active -Bedrock connection, including remote ViaProxy sessions. Their local Java -target version does not identify the backend. Without this check fresh -proxy hits retained Java's 0.8125 bottom instead of native 0.8175. +Start the food air route from transported FOOD components, including +custom paper carriers. Gate local/packet use and set the deadline before +main-hand air use even when hunger later refuses it. An owned air hold +skips block/entity dispatch and offhand fallback; newly selected items +share its air deadline. A placement hold cannot fall through to eating. +Canonical consumption/completion remain server-owned. Other initial air-use +families retain their existing route pending matching controls. -Coordinate native food use with the existing placement input owner. Retain -the connection's 200 ms monotonic food deadline separately from placement's -300 ms first-attempt/success delay. Gate fresh calls before packet/local -use, and retry held foods from input frames after the strict deadline. -Keep the food deadline across physical release and item changes; clear it -on connection replacement. Release still clears the held placement timer -and retained trapdoor state. Missing optional block-use channels do not -retire the independent food deadline. - -Select food from the transported FOOD component, including custom paper -carriers. Set the deadline before main-hand air use even if hunger later -refuses the request. Canonical consumption and completion stay server-owned. -Other air-use families retain their existing route; a pending food deadline -still gates a subsequent attempt, including a change to a block item. - -Native 1.26.60.5/BDS 1.26.60.6 protocol-2225 runtime tracing establishes -the monotonic clock and strict deadline check before main-hand air use. Both -fresh-press and held-frame callers reach that boundary. A 2.1-second -instant-food hold consumes nine items with or without the debugger; the -old Java four-tick path consumes eleven. Three 80 ms clicks consume two -when their starts are about 0,131,616 ms. Input-frame cadence determines -quantization, so do not replace this deadline with a fixed five-tick delay. - -Behavioral timing fixtures cover variable input frames, released food, -connection reset, both clock boundaries, rollover and separate food/block -lifetimes during held item changes. Complete component dispatch, other -use families, exact phase counts and the original intermittent food report -remain open; these controls do not establish full native use parity. +The installed baseline places four Stone blocks after a food hold and +consumes six foods after a trapdoor hold, unlike native. Input-state tests +cover both directions, hand/target routing, variable frames, release, +connection reset, clock boundaries and rollover. Native and installed +captures establish gameplay behavior separately from model tests. Other +use families, timed/refused-food transitions, exact packet phases and the +original intermittent food report remain open. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../features/block/MixinTrapDoorBlock.java | 18 +- - .../features/input/MixinItemUseControl.java | 69 +++++++ + .../features/input/MixinItemUseControl.java | 77 +++++++ .../inventory/MixinHeldBlockPlacement.java | 28 +++ .../features/inventory/MixinHeldBlockUse.java | 61 ++++++ - .../bedrock/input/BedrockItemUse.java | 182 ++++++++++++++++++ + .../bedrock/input/BedrockItemUse.java | 188 ++++++++++++++++++ .../bedrock/input/BlockUsePayload.java | 24 +++ .../input/BlockUsePredictionPayload.java | 24 +++ - .../bedrock/input/ItemUseTiming.java | 53 +++++ - .../bedrock/inventory/TrapdoorPlacement.java | 55 ++++++ + .../bedrock/input/ItemUseState.java | 81 ++++++++ + .../bedrock/inventory/TrapdoorPlacement.java | 55 +++++ .../viafabricplus-bedrock.mixins.json | 3 + - .../bedrock/input/ItemUseTimingTest.java | 72 +++++++ + .../bedrock/input/ItemUseStateTest.java | 94 +++++++++ .../inventory/TrapdoorPlacementTest.java | 100 ++++++++++ - 13 files changed, 687 insertions(+), 4 deletions(-) + 13 files changed, 751 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockUse.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePredictionPayload.java - create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTiming.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacement.java - create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTimingTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java 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 @@ -161,10 +148,10 @@ index 82db187..eda72b3 100644 return super.getOcclusionShape(state); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java new file mode 100644 -index 0000000..c5701b4 +index 0000000..e5d5ff1 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java -@@ -0,0 +1,69 @@ +@@ -0,0 +1,77 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -173,12 +160,14 @@ index 0000000..c5701b4 +package com.viaversion.viafabricplus.bedrock.injection.mixin.features.input; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; ++import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.viaversion.viafabricplus.bedrock.input.BedrockItemUse; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.phys.HitResult; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.client.gui.screens.Screen; @@ -199,11 +188,17 @@ index 0000000..c5701b4 + } + } + ++ @ModifyExpressionValue(method = "startUseItem", at = @At(value = "INVOKE", ++ target = "Lnet/minecraft/world/phys/HitResult;getType()Lnet/minecraft/world/phys/HitResult$Type;")) ++ private HitResult.Type retainHeldUseRoute(final HitResult.Type original, @Local final InteractionHand hand) { ++ return BedrockItemUse.hitType(original, hand); ++ } ++ + @WrapOperation(method = "startUseItem", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;useItem(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/InteractionResult;")) -+ private InteractionResult admitFoodUse(final MultiPlayerGameMode gameMode, final Player player, ++ private InteractionResult admitAirUse(final MultiPlayerGameMode gameMode, final Player player, + final InteractionHand hand, final Operation original) { -+ return BedrockItemUse.beginFoodUse(hand, player.getItemInHand(hand)) ++ return BedrockItemUse.beginAirUse(hand, player.getItemInHand(hand)) + ? original.call(gameMode, player, hand) : InteractionResult.PASS; + } + @@ -337,10 +332,10 @@ index 0000000..69387b7 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java new file mode 100644 -index 0000000..d1e8f89 +index 0000000..76ebff8 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java -@@ -0,0 +1,182 @@ +@@ -0,0 +1,188 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -363,6 +358,7 @@ index 0000000..d1e8f89 +import net.raphimc.viabedrock.api.model.BlockUsePrediction; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.raphimc.viabedrock.protocol.model.Position3f; +import net.raphimc.viabedrock.api.model.PlayerBlockUse; @@ -372,7 +368,7 @@ index 0000000..d1e8f89 + private static boolean held; + private static boolean placedBlock; + private static boolean successfulAttempt; -+ private static final ItemUseTiming TIMING = new ItemUseTiming(); ++ private static final ItemUseState INPUT = new ItemUseState(); + private static Position3f cursor; + private static BlockUsePrediction.Result predictedResult = BlockUsePrediction.Result.FAILED; + @@ -387,7 +383,7 @@ index 0000000..d1e8f89 + } + + public static boolean begin() { -+ if (bedrockConnection() && !TIMING.canAttempt(System.nanoTime())) { ++ if (bedrockConnection() && !INPUT.canAttempt(System.nanoTime())) { + return false; + } + update(true); @@ -395,9 +391,12 @@ index 0000000..d1e8f89 + return true; + } + -+ public static boolean beginFoodUse(final InteractionHand hand, final ItemStack item) { -+ return !bedrockConnection() || hand != InteractionHand.MAIN_HAND || !item.has(DataComponents.FOOD) -+ || TIMING.tryFoodUse(System.nanoTime()); ++ public static boolean beginAirUse(final InteractionHand hand, final ItemStack item) { ++ return !bedrockConnection() || INPUT.admitAirUse(System.nanoTime(), hand, item.has(DataComponents.FOOD)); ++ } ++ ++ public static HitResult.Type hitType(final HitResult.Type target, final InteractionHand hand) { ++ return INPUT.hitType(target, hand); + } + + private static boolean bedrockConnection() { @@ -420,6 +419,9 @@ index 0000000..d1e8f89 + reset(); + return; + } ++ if (!value) { ++ INPUT.release(); ++ } + if (!ClientPlayNetworking.canSend(BlockUsePayload.TYPE) || !ClientPlayNetworking.canSend(BlockUsePredictionPayload.TYPE)) { + resetBlockUse(); + return; @@ -429,7 +431,6 @@ index 0000000..d1e8f89 + ClientPlayNetworking.send(new BlockUsePayload(new PlayerBlockUse(value))); + } + if (!value) { -+ TIMING.release(); + PLACEMENT.clear(); + } + } @@ -445,17 +446,17 @@ index 0000000..d1e8f89 + } + + public static void attempted() { -+ if (held && (TIMING.blockActive() || usesNativeRepeatDelay())) { -+ TIMING.attemptedBlock(System.nanoTime(), successfulAttempt); ++ if (held && (INPUT.blockActive() || usesNativeRepeatDelay())) { ++ INPUT.attemptedBlock(System.nanoTime(), successfulAttempt); + } + } + + public static boolean repeatsFromFrames() { -+ return holdingFood() || held && TIMING.blockActive(); ++ return INPUT.repeatsFromFrames(holdingFood()); + } + + public static boolean frameReady() { -+ return repeatsFromFrames() && TIMING.frameReady(System.nanoTime(), holdingFood()); ++ return repeatsFromFrames() && INPUT.frameReady(System.nanoTime(), holdingFood()); + } + + public static void refresh() { @@ -466,7 +467,7 @@ index 0000000..d1e8f89 + + public static boolean suppressInteraction() { + final var player = Minecraft.getInstance().player; -+ return held && TIMING.blockActive() && player != null && player.getMainHandItem().getItem() instanceof BlockItem; ++ return held && INPUT.blockActive() && player != null && player.getMainHandItem().getItem() instanceof BlockItem; + } + + public static boolean allows(final BlockPos target, final BlockState state) { @@ -513,14 +514,14 @@ index 0000000..d1e8f89 + + private static void resetBlockUse() { + held = false; -+ TIMING.release(); ++ INPUT.cancelPlacement(); + clearPrediction(); + PLACEMENT.clear(); + } + + private static void reset() { + resetBlockUse(); -+ TIMING.clear(); ++ INPUT.clear(); + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePayload.java @@ -583,12 +584,12 @@ index 0000000..a98d448 + return TYPE; + } +} -diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTiming.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTiming.java +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java new file mode 100644 -index 0000000..d102001 +index 0000000..f2343fd --- /dev/null -+++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTiming.java -@@ -0,0 +1,53 @@ ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java +@@ -0,0 +1,81 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -596,30 +597,48 @@ index 0000000..d102001 + */ +package com.viaversion.viafabricplus.bedrock.input; + -+/** Food's connection deadline and held placement's success delay have different lifetimes. */ -+public final class ItemUseTiming { -+ private static final long FOOD_DELAY_NANOS = 200_000_000L; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.phys.HitResult; ++ ++/** A physical hold retains its air or placement route until release. */ ++public final class ItemUseState { ++ private static final long AIR_DELAY_NANOS = 200_000_000L; + private static final long BLOCK_DELAY_NANOS = 300_000_000L; -+ private boolean foodAttempted; -+ private long foodStartedAt; ++ private boolean airActive; ++ private boolean airAttempted; ++ private long airStartedAt; + private boolean blockActive; + private long blockStartedAt; + + public boolean canAttempt(final long now) { -+ return !this.foodAttempted || now - this.foodStartedAt > FOOD_DELAY_NANOS; ++ return !this.airAttempted || now - this.airStartedAt > AIR_DELAY_NANOS; + } + -+ public boolean tryFoodUse(final long now) { ++ public boolean admitAirUse(final long now, final InteractionHand hand, final boolean food) { ++ if (hand != InteractionHand.MAIN_HAND) { ++ return !this.airActive && !this.blockActive; ++ } ++ if (this.blockActive) { ++ return false; ++ } ++ if (!this.airActive && !food) { ++ return true; ++ } + if (!this.canAttempt(now)) { + return false; + } -+ this.foodAttempted = true; -+ this.foodStartedAt = now; ++ this.airActive = true; ++ this.airAttempted = true; ++ this.airStartedAt = now; + return true; + } + ++ public HitResult.Type hitType(final HitResult.Type target, final InteractionHand hand) { ++ return this.airActive || this.blockActive && hand != InteractionHand.MAIN_HAND ? HitResult.Type.MISS : target; ++ } ++ + public void attemptedBlock(final long now, final boolean success) { -+ if (!this.blockActive || success) { ++ if (!this.airActive && (!this.blockActive || success)) { + this.blockStartedAt = now; + this.blockActive = true; + } @@ -629,17 +648,27 @@ index 0000000..d102001 + return this.blockActive; + } + ++ public boolean repeatsFromFrames(final boolean food) { ++ return this.airActive || this.blockActive || food; ++ } ++ + public boolean frameReady(final long now, final boolean food) { -+ return this.canAttempt(now) && (food || this.blockActive && now - this.blockStartedAt >= BLOCK_DELAY_NANOS); ++ return this.canAttempt(now) && (this.airActive || !this.blockActive && food ++ || this.blockActive && now - this.blockStartedAt >= BLOCK_DELAY_NANOS); + } + -+ public void release() { ++ public void cancelPlacement() { + this.blockActive = false; + } + ++ public void release() { ++ this.airActive = false; ++ this.cancelPlacement(); ++ } ++ + public void clear() { + this.release(); -+ this.foodAttempted = false; ++ this.airAttempted = false; + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacement.java b/src/main/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacement.java @@ -717,42 +746,44 @@ index f7c3bf4..416a16f 100644 "features.creative.MixinCreativeModeInventoryScreen", "features.creative.MixinCreativeModeTab_ItemDisplayBuilder", "features.creative.MixinCreativeModeTabs", -diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTimingTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTimingTest.java +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java new file mode 100644 -index 0000000..db49305 +index 0000000..01e1133 --- /dev/null -+++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseTimingTest.java -@@ -0,0 +1,72 @@ ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java +@@ -0,0 +1,94 @@ +package com.viaversion.viafabricplus.bedrock.input; + +import org.junit.jupiter.api.Test; ++import net.minecraft.world.InteractionHand; ++import net.minecraft.world.phys.HitResult; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + -+final class ItemUseTimingTest { ++final class ItemUseStateTest { + @Test + void releasedFoodKeepsItsDeadlineForFreshClicksUntilConnectionReplacement() { -+ final var timing = new ItemUseTiming(); -+ assertTrue(timing.tryFoodUse(1_000_000_000L)); ++ final var timing = new ItemUseState(); ++ assertTrue(timing.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true)); + timing.release(); -+ assertFalse(timing.tryFoodUse(1_131_000_000L)); -+ assertFalse(timing.tryFoodUse(1_200_000_000L)); -+ assertTrue(timing.tryFoodUse(1_616_000_000L)); ++ assertFalse(timing.admitAirUse(1_131_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertFalse(timing.admitAirUse(1_200_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertTrue(timing.admitAirUse(1_616_000_000L, InteractionHand.MAIN_HAND, true)); + assertFalse(timing.canAttempt(1_700_000_000L)); + timing.clear(); -+ assertTrue(timing.tryFoodUse(1_700_000_000L)); ++ assertTrue(timing.admitAirUse(1_700_000_000L, InteractionHand.MAIN_HAND, true)); + } + + @Test + void heldFoodUsesTheFirstEligibleInputFrameInsteadOfARepeatedTickCount() { -+ final var timing = new ItemUseTiming(); ++ final var timing = new ItemUseState(); + final List usedAt = new ArrayList<>(); + for (final int milliseconds : new int[] {0, 47, 94, 141, 188, 236, 284, 332, 380, 428, 475, 542, 609, 676}) { + final long now = 1_000_000_000L + milliseconds * 1_000_000L; -+ if (timing.frameReady(now, true) && timing.tryFoodUse(now)) { ++ if (timing.frameReady(now, true) && timing.admitAirUse(now, InteractionHand.MAIN_HAND, true)) { + usedAt.add(milliseconds); + } + } @@ -760,37 +791,57 @@ index 0000000..db49305 + } + + @Test -+ void foodAndPlacementKeepIndependentDelaysAcrossHeldItemChanges() { -+ final var timing = new ItemUseTiming(); -+ assertFalse(timing.frameReady(1_000_000_000L, false)); -+ timing.attemptedBlock(1_000_000_000L, false); -+ assertTrue(timing.frameReady(1_100_000_000L, true)); -+ assertTrue(timing.tryFoodUse(1_100_000_000L)); -+ assertFalse(timing.frameReady(1_300_000_000L, false)); -+ assertTrue(timing.frameReady(1_301_000_000L, false)); -+ timing.attemptedBlock(1_310_000_000L, false); -+ assertTrue(timing.frameReady(1_311_000_000L, false)); -+ timing.attemptedBlock(1_320_000_000L, true); -+ assertTrue(timing.tryFoodUse(1_400_000_000L)); -+ assertFalse(timing.frameReady(1_600_000_000L, true)); -+ assertTrue(timing.frameReady(1_600_000_001L, true)); -+ assertFalse(timing.frameReady(1_619_999_999L, false)); -+ assertTrue(timing.frameReady(1_620_000_000L, false)); -+ assertTrue(timing.tryFoodUse(1_630_000_000L)); -+ timing.release(); -+ assertFalse(timing.frameReady(1_700_000_000L, true)); -+ assertTrue(timing.frameReady(1_830_000_001L, true)); -+ assertFalse(timing.frameReady(1_900_000_000L, false)); ++ void foodHoldKeepsAirUseAcrossBlockAndEntityTargetsUntilRelease() { ++ final var input = new ItemUseState(); ++ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); ++ input.attemptedBlock(1_100_000_000L, true); ++ assertFalse(input.frameReady(1_200_000_000L, false)); ++ assertTrue(input.frameReady(1_200_000_001L, false)); ++ assertTrue(input.admitAirUse(1_200_000_001L, InteractionHand.MAIN_HAND, false)); ++ assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.OFF_HAND, false)); ++ input.cancelPlacement(); ++ assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ input.release(); ++ assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertFalse(input.canAttempt(1_400_000_001L)); ++ assertTrue(input.canAttempt(1_400_000_002L)); ++ } ++ ++ @Test ++ void placementHoldDoesNotStartEatingWhenTheSelectedItemChanges() { ++ final var input = new ItemUseState(); ++ input.attemptedBlock(1_000_000_000L, true); ++ assertFalse(input.frameReady(1_200_000_001L, true)); ++ assertTrue(input.frameReady(1_300_000_000L, true)); ++ assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.OFF_HAND)); ++ assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.MAIN_HAND, true)); ++ input.attemptedBlock(1_310_000_000L, false); ++ assertTrue(input.frameReady(1_311_000_000L, true)); ++ input.attemptedBlock(1_320_000_000L, true); ++ assertFalse(input.frameReady(1_619_999_999L, false)); ++ assertTrue(input.frameReady(1_620_000_000L, false)); ++ input.release(); ++ assertTrue(input.admitAirUse(1_630_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertFalse(input.canAttempt(1_800_000_000L)); ++ input.clear(); ++ assertEquals(HitResult.Type.ENTITY, input.hitType(HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); ++ assertTrue(input.admitAirUse(1_800_000_000L, InteractionHand.MAIN_HAND, true)); + } + + @Test + void monotonicClockRolloverKeepsBothDeadlines() { -+ final var timing = new ItemUseTiming(); ++ final var timing = new ItemUseState(); + final long start = Long.MAX_VALUE - 150_000_000L; + timing.attemptedBlock(start, true); -+ assertTrue(timing.tryFoodUse(start)); ++ timing.release(); ++ assertTrue(timing.admitAirUse(start, InteractionHand.MAIN_HAND, true)); + assertFalse(timing.frameReady(start + 200_000_000L, true)); + assertTrue(timing.frameReady(start + 200_000_001L, true)); ++ timing.release(); ++ timing.attemptedBlock(start, true); + assertFalse(timing.frameReady(start + 299_999_999L, false)); + assertTrue(timing.frameReady(start + 300_000_000L, false)); + } diff --git a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md index 04a77217..82203716 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md @@ -10,7 +10,14 @@ Set the deadline before main-hand food air use, including a request that hunger Select food from its transported component, including custom items carried as paper. Physical release and item changes retain this connection deadline. Connection replacement clears it. -Missing optional block-use channels do not clear the food deadline. +Missing optional block-use channels do not clear the air-use hold or its deadline. + +Keep the interaction mode until physical release or cancellation. +A hold started by food air use continues main-hand air use after selecting a block item. +Skip block and entity dispatch inside the use method without changing the visible raycast. +A placement hold keeps its block route after selecting food and cannot fall through to eating. +Retire offhand fallback while either route owns the hold. +Release permits a fresh press to choose another interaction mode. Held placement keeps its separate 300 ms delay after the first attempt or a successful local use. Failed repeats do not restart that delay. @@ -43,8 +50,13 @@ Frame cadence determines retry quantization, so equal counts alone do not establ Installed direct and ViaProxy food controls check the deadline, rapid clicks after release, pause cancellation, admission, hunger, and settled server counts. The [food-use validation](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) records the current build and remaining use-cycle limits. -A private observer logs the timing class's original decisions without changing them; this is an instrumented runtime check. -Four timing tests cover variable input frames, released food, connection reset, independent delays, strict boundaries, and rollover. +Native tracing observes fresh flags 176 followed by held air-only flag 16 across a food-to-trapdoor change. +Only the initial call visits block use; later frames retry air use after the deadline. +Unattached stone and late trapdoor controls agree. +The reverse trapdoor-to-food control retains simulation block uses and consumes no food. +Installed direct and proxy controls check both directions and fresh placement after release or Pause. +The previous installed client placed blocks after food use and ate food during the placement hold. +Five input-state tests cover retained modes, hand and target routing, variable frames, release, connection reset, independent delays, strict boundaries, and rollover. Five block-state tests cover retained orientation, materials, independent properties, targets, frozen hits, release, and ordinary blocks. Earlier installed placement fixtures match native camera changes, material changes, and fresh activation after release. @@ -56,5 +68,5 @@ After a preceding successful hold, both instead place a neighbor and consume one ## Remaining limits -Complete food and block handoffs, competing non-food use handlers, exact input-flag phase counts, and full use-cycle timing need more native comparisons. +Other initial air-use families, timed and refused-food handoffs, live entity transitions, competing use handlers, exact input-flag phase counts, and full use-cycle timing need more native comparisons. The original intermittent animation-without-feeding report remains open. diff --git a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md index 5fb9bc3d..2fb9ecab 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0039-allow-food-use-on-bedrock-peaceful-difficulty.pr.md @@ -22,5 +22,6 @@ That covers the selected full-hunger DataDriven controls and local use of custom The [food-use evidence](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) records those controls and remaining limits. Native tracing establishes a 200 ms air-use deadline that persists across button releases and gates fresh clicks. The [client input patch](0038-predict-held-trapdoor-placement.pr.md) implements that deadline and input-frame retries, with rapid-click and pause controls on both routes. -Complete item-family handoffs still need native comparisons. +Selected instant-food and block transitions retain the native held interaction mode. +Other initial use families and complete timed-item handoffs still need native comparisons. The original intermittent food-use report remains open. From 3d84df09ad4899ef282901fe120093eced6a14f2 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 00:35:54 +0100 Subject: [PATCH 17/47] fix(input): preserve native food holds and active item identity Keep refused food air transactions while gating only StartUsingItem. Choose the initial hold route from native food admission, retain that route across item changes, and separate block continuation from the air input deadline. Apply the successful block-use delay to ordinary blocks. Cancel an active custom-item cycle when the native definition changes, even when both definitions share Java's paper carrier. This prevents an old timed-food cycle from adopting a zero-duration item and producing non-finite first-person normals. Fold the fixes into their owning patches and retain native-version rationale and regression coverage. Keep the observed ordinary Stone placement mismatch explicit; these changes do not establish complete held-placement or use-cycle parity. --- docs/bedrock-coverage.md | 2 +- ...08-signal-sustained-bedrock-item-use.patch | 100 ++++++------ ...08-signal-sustained-bedrock-item-use.pr.md | 19 ++- ...ronize-glide-rockets-with-auth-input.patch | 56 +++---- patches/viafabricplus-bedrock/series.json | 5 + ...ompleted-local-physics-to-viabedrock.patch | 136 +++++++++++++++- ...ompleted-local-physics-to-viabedrock.pr.md | 7 + ...0038-predict-held-trapdoor-placement.patch | 151 +++++++++++++----- ...0038-predict-held-trapdoor-placement.pr.md | 20 ++- 9 files changed, 361 insertions(+), 135 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index adca0269..3b3cef63 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -27,7 +27,7 @@ No complete area has verified Bedrock parity. | 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, Creative catalog actions, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | -| Food use | Installed food and use-modifier components govern admission and Java food/consumable properties, with Creative and server-confirmed Peaceful exceptions. | Both routes pass selected admission, input-deadline, held-mode, release, and pause controls. Broader use cycles, other item-family handoffs, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md). | +| Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Ordinary held placement still differs after target changes. Broader use cycles and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch index 7547d759..54806f5c 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch @@ -1,4 +1,4 @@ -From 1f0dd8cc6bc1782cd6a3fcee3a597eff7242f6d8 Mon Sep 17 00:00:00 2001 +From 173f2b1402f8b56343dce93e6856de849613851b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(interaction): signal sustained Bedrock item use @@ -25,11 +25,11 @@ acknowledgments cannot complete a valid active use. Boundary packet tests cover negative values, 16, 17, and signed integer extremes. This does not resolve local prediction or late acknowledgments for a new same-item use. -Admit food use with the advertised native food component and current -hunger limits before emitting a transaction or StartUsingItem input. Java -sends USE_ITEM before its local food predicate, while BDS accepts those -transactions even after ordinary food reaches full hunger. A target-build -fixture consumes an extra apple at full hunger without this client gate. +Use the advertised native food component and current hunger limits to +admit StartUsingItem input. Preserve the air-use transaction when hunger +refuses food. Native 1.26.60.5/protocol 2225 sends that transaction without +the start flag, including bread and component food at full hunger. +The earlier interpretation incorrectly suppressed the transaction too. Retain ordered food component updates by connection registry identity. Preserve can_always_eat and creative use; do not infer food behavior from @@ -38,8 +38,8 @@ food data on their existing route. The captured 1.26.51.1 registry contains this property for component and legacy foods, including apple, honey, chorus fruit and both golden apple variants. -Targeted model and real packet controls verify full-hunger rejection before -input flags and inventory writes, changed native hunger maxima, always +Targeted model and real packet controls verify refused-use transactions +without input flags or inventory writes, changed hunger maxima, always edible food, creative use and omitted/replaced registry definitions. The source format is also documented by Microsoft's minecraft:food item component reference. Full custom item use-cycle and rejection/prediction @@ -72,7 +72,7 @@ hunger refuses it, so another installed handler can still start use. Retain the advertised can_always_eat flag independently of that composed admission. Transport effective full-hunger eligibility through Java FOOD, including native nutrition and final saturation, so Java's local predicate -and the outgoing transaction gate agree. Project component-food use +and outgoing use admission agree. Project component-food use animation and network duration into CONSUMABLE, including zero duration, so custom paper carriers also enter the local use cycle. Consume effects remain server-owned. Preserve initial and current Peaceful exceptions and @@ -85,17 +85,23 @@ constructor-specific admission, full/hungry transitions and FOOD wire properties without predicting stack consumption. Competing non-food use handlers, complete custom consumable behavior, exact cycle timing, and the original intermittent eating report remain separate requirements. + +Official 1.26.60.5/BDS 1.26.60.6 held-use controls preserve the refused +air payload and continue block mode. Already admitted air mode still +sends retries after hunger becomes full. Registered packet regressions +cover both payload presence and absent StartUsingItem without changing +the canonical held count or offhand resynchronization. --- .../model/ItemDefinitionProperties.java | 6 + .../protocol/model/ItemFoodProperties.java | 57 ++++++ .../protocol/model/ItemRegistry.java | 6 + - .../protocol/packet/InteractionPackets.java | 59 +++++- + .../protocol/packet/InteractionPackets.java | 55 +++++- .../protocol/rewriter/ItemRewriter.java | 20 +- - .../model/ItemFoodPropertiesTest.java | 122 +++++++++++++ + .../model/ItemFoodPropertiesTest.java | 122 ++++++++++++ .../packet/CompletedItemUsePacketTest.java | 98 ++++++++++ - .../packet/FoodUseAdmissionPacketTest.java | 172 ++++++++++++++++++ - .../packet/InteractionPacketsTest.java | 31 ++++ - 9 files changed, 567 insertions(+), 4 deletions(-) + .../packet/FoodUseAdmissionPacketTest.java | 183 ++++++++++++++++++ + .../packet/InteractionPacketsTest.java | 31 +++ + 9 files changed, 574 insertions(+), 4 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/model/ItemFoodProperties.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/model/ItemFoodPropertiesTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/CompletedItemUsePacketTest.java @@ -214,7 +220,7 @@ index a906b932..03ebaf51 100644 return this.definitions.definitions().stream().filter(definition -> definition.receiver().component()) .map(NativeItemDefinitions.Definition::identifier).collect(Collectors.toUnmodifiableSet()); 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 081338cb..5cc1d6f5 100644 +index 081338cb..203cd594 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -46,10 +46,14 @@ import net.raphimc.viabedrock.protocol.ServerboundBedrockPackets; @@ -258,35 +264,24 @@ index 081338cb..5cc1d6f5 100644 if (javaHand < 0 || javaHand >= InteractionHand.values().length) { wrapper.cancel(); return; -@@ -278,8 +288,16 @@ public final class InteractionPackets { +@@ -278,8 +288,15 @@ public final class InteractionPackets { openBook.send(BedrockProtocol.class); return; } + final ItemFoodProperties food = itemRewriter.foodProperties(usedItem); -+ if (hand == InteractionHand.MAIN_HAND && !canStartFoodUse(clientPlayer, food, wrapper.user().get(GameSessionStorage.class))) { -+ // Java sends USE_ITEM before its local food check. Native clients admit eating before sending. -+ wrapper.cancel(); -+ return; -+ } ++ // Native still sends refused food use; only admission controls StartUsingItem. ++ final boolean startUsingItem = food.food() ++ ? canStartFoodUse(clientPlayer, food, wrapper.user().get(GameSessionStorage.class)) ++ : isContinuousUseItem(itemIdentifier, itemIdentifier == null ? Set.of() ++ : BedrockProtocol.MAPPINGS.getBedrockItemTags().getOrDefault(itemIdentifier, Set.of())); final BedrockInventoryTransaction transaction = beginItemUse(clientPlayer, hand, - inventoryContainer.getSelectedHotbarSlot(), usedItem, itemIdentifier); + inventoryContainer.getSelectedHotbarSlot(), usedItem, itemIdentifier, -+ itemIdentifier == null ? Set.of() -+ : BedrockProtocol.MAPPINGS.getBedrockItemTags().getOrDefault(itemIdentifier, Set.of())); ++ startUsingItem); if (transaction == null) { wrapper.cancel(); if (hand == InteractionHand.OFF_HAND && !"minecraft:shield".equals(itemIdentifier)) { -@@ -287,6 +305,9 @@ public final class InteractionPackets { - } - return; - } -+ if (food.food()) { -+ clientPlayer.addAuthInputData(PlayerAuthInputData.StartUsingItem); -+ } - wrapper.write(inventoryTransactionRewriter.getInventoryTransactionType(), transaction); - }); - -@@ -371,6 +392,24 @@ public final class InteractionPackets { +@@ -371,6 +388,24 @@ public final class InteractionPackets { } } }); @@ -311,7 +306,7 @@ index 081338cb..5cc1d6f5 100644 protocol.registerClientbound(ClientboundBedrockPackets.SET_ENTITY_LINK, null, wrapper -> { wrapper.cancel(); final EntityTracker entityTracker = wrapper.user().get(EntityTracker.class); -@@ -380,6 +419,17 @@ public final class InteractionPackets { +@@ -380,6 +415,17 @@ public final class InteractionPackets { } @@ -329,20 +324,20 @@ index 081338cb..5cc1d6f5 100644 static void sendPassengerUpdates(final PacketWrapper source, final EntityTracker entityTracker) { for (Entity vehicle : entityTracker.drainPassengerUpdates()) { final PacketWrapper passengersPacket = PacketWrapper.create(ClientboundPackets26_3.SET_PASSENGERS, -@@ -401,7 +451,7 @@ public final class InteractionPackets { +@@ -401,7 +447,7 @@ public final class InteractionPackets { static BedrockInventoryTransaction beginItemUse(final ClientPlayerEntity player, final InteractionHand hand, final int hotbarSlot, final BedrockItem item, - final String identifier) { -+ final String identifier, final Set tags) { ++ final String identifier, final boolean startUsingItem) { if (item.isEmpty()) { return null; } -@@ -413,6 +463,9 @@ public final class InteractionPackets { +@@ -413,6 +459,9 @@ public final class InteractionPackets { return null; } player.setShieldBlockingHand(null); -+ if (isContinuousUseItem(identifier, tags)) { ++ if (startUsingItem) { + player.addAuthInputData(PlayerAuthInputData.StartUsingItem); + } return new BedrockInventoryTransaction( @@ -644,10 +639,10 @@ index 00000000..45f4a6a3 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/FoodUseAdmissionPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/FoodUseAdmissionPacketTest.java new file mode 100644 -index 00000000..50acebde +index 00000000..8ff80429 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/FoodUseAdmissionPacketTest.java -@@ -0,0 +1,172 @@ +@@ -0,0 +1,183 @@ +/* + * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -697,7 +692,7 @@ index 00000000..50acebde + + @ParameterizedTest + @EnumSource(InteractionHand.class) -+ void rejectsFullHungerUseWithoutLosingOffhandResynchronization(final InteractionHand hand) throws Exception { ++ void sendsRefusedFoodWithoutStartingUseAndKeepsOffhandResynchronization(final InteractionHand hand) throws Exception { + final Map, Object> stores = new HashMap<>(); + final UserConnection user = (UserConnection) Proxy.newProxyInstance(UserConnection.class.getClassLoader(), + new Class[]{UserConnection.class}, (proxy, method, args) -> method.getName().equals("get") ? stores.get(args[0]) : null); @@ -712,14 +707,16 @@ index 00000000..50acebde + }); + final var inventory = new InventoryTracker(user); + stores.put(InventoryTracker.class, inventory); -+ final var held = new BedrockItem(1); ++ final var held = new BedrockItem(261); + held.setAmount(8); + inventory.getInventoryContainer().setItem(0, held); + inventory.getOffhandContainer().setItem(0, held); + final var items = mock(ItemRewriter.class); -+ when(items.getItems()).thenReturn(hand == InteractionHand.MAIN_HAND ? ImmutableBiMap.of("test:food", 1) : ImmutableBiMap.of()); ++ when(items.getItems()).thenReturn(hand == InteractionHand.MAIN_HAND ? ImmutableBiMap.of("minecraft:bread", 261) : ImmutableBiMap.of()); + when(items.foodProperties(held)).thenReturn(new ItemFoodProperties(true, false, false, 0, 0F, "none")); + stores.put(ItemRewriter.class, items); ++ final var transactions = new InventoryTransactionRewriter(user); ++ stores.put(InventoryTransactionRewriter.class, transactions); + final var input = Unpooled.buffer(); + try (var packets = mockStatic(PacketFactory.class)) { + Types.VAR_INT.writePrimitive(input, hand.ordinal()); @@ -727,7 +724,16 @@ index 00000000..50acebde + input.writeFloat(0F).writeFloat(0F); + final var packet = new PacketWrapperImpl(ServerboundPackets26_3.USE_ITEM, input, user); + new TestProtocol().translate(packet); -+ assertTrue(packet.isCancelled()); ++ assertEquals(hand == InteractionHand.OFF_HAND, packet.isCancelled()); ++ if (hand == InteractionHand.MAIN_HAND) { ++ final var transaction = packet.get(transactions.getInventoryTransactionType(), 0); ++ final var use = assertInstanceOf(InventoryTransactionData.UseItemTransactionData.class, transaction.transactionData()); ++ assertEquals(261, use.itemInHand().identifier()); ++ assertEquals(8, use.itemInHand().amount()); ++ assertEquals(0, use.hotbarSlot()); ++ assertEquals(0, transaction.legacyRequestId()); ++ assertTrue(transaction.actions() == null || transaction.actions().isEmpty()); ++ } + assertFalse(input.isReadable()); + assertTrue(player.authInputData().isEmpty()); + assertEquals(8, held.amount()); @@ -782,14 +788,14 @@ index 00000000..50acebde + input.writeFloat(0F).writeFloat(0F); + final var packet = new PacketWrapperImpl(ServerboundPackets26_3.USE_ITEM, input, user); + protocol.translate(packet); -+ assertEquals(difficulty != Difficulty.Peaceful && !useModifiers, packet.isCancelled()); ++ assertFalse(packet.isCancelled()); + if (!packet.isCancelled()) { + final var transaction = packet.get(transactions.getInventoryTransactionType(), 0); + final var use = assertInstanceOf(InventoryTransactionData.UseItemTransactionData.class, transaction.transactionData()); + assertEquals(8, use.itemInHand().amount()); + } + assertEquals(8, held.amount()); -+ assertEquals(!packet.isCancelled(), entities.getClientPlayer().authInputData().contains(PlayerAuthInputData.StartUsingItem)); ++ assertEquals(difficulty == Difficulty.Peaceful || useModifiers, entities.getClientPlayer().authInputData().contains(PlayerAuthInputData.StartUsingItem)); + assertFalse(input.isReadable()); + } finally { + update.release(); diff --git a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md index 3978a480..791493e6 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md @@ -1,7 +1,7 @@ # Sustained item use and food admission -This patch admits main-hand food use before it sends the transaction or sets `StartUsingItem`. -Java sends its air-use packet before its local food predicate, and BDS can accept an otherwise ineligible request. +This patch uses native food admission to set `StartUsingItem`. +A refused main-hand food request still sends its air-use transaction, matching native packet behavior. Admission uses the installed native food and use-modifier components, plus current hunger limits. Always-edible food, Creative use, and server-confirmed Peaceful difficulty ignore hunger. An installed use-modifier handler on a component item can also start use when its food handler refuses full hunger. @@ -19,7 +19,7 @@ Admitted food sets `StartUsingItem` even without an optional food tag. `COMPLETED_USING_ITEM` supplies a signed item ID and a use method. Matching held-item completions produce Java's `USE_ITEM_COMPLETE` event without changing canonical inventory counts. Replaced items and methods outside zero through sixteen cannot complete the current use. -The patch body retains the original Bedrock 1.26.51.1 handler evidence and admission rationale. +The patch body retains the versioned completion-handler evidence and current admission rationale. ## Native evidence @@ -45,8 +45,8 @@ Native controls establish the combined behavior for these fixtures; they do not ## Validation Seventeen focused tests cover installed component retention, constructor-specific admission, Java component wire properties, difficulty changes, food-use flags, offhand resynchronization, and completion boundaries. -Admission tests retain the held quantity and verify that rejected uses leave `StartUsingItem` clear. -The full four-project build passes 3,286 tests, with 325 skipped and no failures or errors. +Admission tests retain the held quantity and verify refused-use payloads with `StartUsingItem` clear. +The full four-project build passes 3,291 tests, with 325 skipped and no failures or errors. Installed direct and ViaProxy controls cover eleven cases each. Both routes match the selected native admission, hunger, timed consumption, and zero-duration outcomes. @@ -62,9 +62,11 @@ Food-started air holds do not place newly selected blocks; trapdoor-started plac The installed JAR checksums match the build. Native tracing establishes the deadline before air use and its check in the shared input function. +Initial food refusal retains block mode, whose frame continuation bypasses this air deadline. +Already accepted air mode remains active when later food attempts reach full hunger. Native's 2.1-second instant-food control consumes nine items; the direct Java control consumes eleven at its different frame cadence. Counts alone do not establish or refute the scheduling rule. -Other initial use families, timed and refused-food handoffs, and live entity transitions still need native comparisons. +Other initial use families and live entity transitions still need native comparisons. Presence of `StartUsingItem` is verified; exact packet phase counts and complete cycle timing remain open. Competing non-food use handlers, other consumable animations and effects, ability exceptions, and all inventory authority modes need further comparison. @@ -73,3 +75,8 @@ The original intermittent animation-without-feeding report remains open. This patch remains deferred behind [ViaBedrock #276](https://github.com/ViaVersionAddons/ViaBedrock/pull/276). It does not belong in an independent upstream PR branch. + +The current timed-to-instant custom-food switch cancels the old definition instead of retaining its Java paper use cycle. +Early switches consume none of the old food; late switches retain one completion. +The long-hold control retains three completions and reaches full hunger. +The client input sidecar records the separate ordinary-block placement mismatch. 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 f98c8d54..420bb330 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,4 +1,4 @@ -From ee71f82f4281210f17805bf6d4e29bf57885637b Mon Sep 17 00:00:00 2001 +From 5d016bec6e5e0f4593a4a0cb0cc6b70de79e77a7 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(interaction): preserve native air-use prediction and @@ -151,7 +151,7 @@ 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 5cc1d6f5..aca780c1 100644 +index 203cd594..58b3ee44 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -21,6 +21,7 @@ import com.viaversion.viaversion.api.minecraft.BlockFace; @@ -173,25 +173,25 @@ index 5cc1d6f5..aca780c1 100644 "minecraft:bow", "minecraft:crossbow", "minecraft:trident", "minecraft:potion", "minecraft:milk_bucket", "minecraft:ominous_bottle", @@ -294,7 +298,8 @@ public final class InteractionPackets { - wrapper.cancel(); - return; - } + ? canStartFoodUse(clientPlayer, food, wrapper.user().get(GameSessionStorage.class)) + : isContinuousUseItem(itemIdentifier, itemIdentifier == null ? Set.of() + : BedrockProtocol.MAPPINGS.getBedrockItemTags().getOrDefault(itemIdentifier, Set.of())); - final BedrockInventoryTransaction transaction = beginItemUse(clientPlayer, hand, + final BedrockInventoryTransaction transaction = beginItemUse(clientPlayer, + wrapper.user().get(InventoryTracker.class), hand, inventoryContainer.getSelectedHotbarSlot(), usedItem, itemIdentifier, - itemIdentifier == null ? Set.of() - : BedrockProtocol.MAPPINGS.getBedrockItemTags().getOrDefault(itemIdentifier, Set.of())); -@@ -308,7 +313,7 @@ public final class InteractionPackets { - if (food.food()) { - clientPlayer.addAuthInputData(PlayerAuthInputData.StartUsingItem); + startUsingItem); + if (transaction == null) { +@@ -304,7 +309,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 -> { -@@ -332,22 +337,20 @@ public final class InteractionPackets { +@@ -328,22 +333,20 @@ public final class InteractionPackets { PacketFactory.sendJavaBlockChangedAck(wrapper.user(), sequence); return; } @@ -217,7 +217,7 @@ index 5cc1d6f5..aca780c1 100644 final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); final BedrockItem usedItem = inventoryTracker.getInventoryContainer().getSelectedHotbarItem(); if (usedItem.hasBlockState()) { -@@ -426,31 +429,23 @@ public final class InteractionPackets { +@@ -422,31 +425,23 @@ public final class InteractionPackets { return hunger == null || food.allowsUse(hunger.computeClampedValue(), hunger.computeMaxValue(), ignoresHunger); } @@ -259,11 +259,11 @@ index 5cc1d6f5..aca780c1 100644 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) { + final String identifier, final boolean startUsingItem) { if (item.isEmpty()) { return null; -@@ -466,13 +461,67 @@ public final class InteractionPackets { - if (isContinuousUseItem(identifier, tags)) { +@@ -462,13 +457,67 @@ public final class InteractionPackets { + if (startUsingItem) { player.addAuthInputData(PlayerAuthInputData.StartUsingItem); } - return new BedrockInventoryTransaction( @@ -648,7 +648,7 @@ 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..4a770d23 100644 +index 0bf1b705..1cdc17f1 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,39 @@ @@ -708,12 +708,12 @@ index 0bf1b705..4a770d23 100644 + player.setGliding(true); + final BedrockItem item = new BedrockItem(1); + assertNull(InteractionPackets.beginItemUse(player, new InventoryTracker(user()), InteractionHand.OFF_HAND, -+ 2, item, "minecraft:firework_rocket", Set.of())); ++ 2, item, "minecraft:firework_rocket", false)); + assertTrue(player.pendingFlightItemUses().isEmpty()); + assertTrue(player.authInputData().isEmpty()); + + assertNull(InteractionPackets.beginItemUse(player, new InventoryTracker(user()), InteractionHand.OFF_HAND, -+ 2, item, "minecraft:apple", Set.of("minecraft:is_food"))); ++ 2, item, "minecraft:apple", true)); + assertTrue(player.authInputData().isEmpty()); + assertFalse(player.isShieldBlocking()); + } @@ -722,7 +722,7 @@ index 0bf1b705..4a770d23 100644 + void offhandShieldUseBlocksThroughSneakingWithoutAnInventoryTransaction() { + final ClientPlayerEntity player = player(); + assertNull(InteractionPackets.beginItemUse(player, new InventoryTracker(user()), InteractionHand.OFF_HAND, -+ 2, new BedrockItem(1), "minecraft:shield", Set.of())); ++ 2, new BedrockItem(1), "minecraft:shield", false)); + assertEquals(InteractionHand.OFF_HAND, player.shieldBlockingHand()); + player.updateBedrockSneakingInput(player.isSneaking()); + assertTrue(player.authInputData().contains(PlayerAuthInputData.StartSneaking)); @@ -736,7 +736,7 @@ index 0bf1b705..4a770d23 100644 + final ClientPlayerEntity player = player(); + final BedrockItem item = new BedrockItem(1); + final var consumption = InteractionPackets.beginItemUse(player, new InventoryTracker(user()), InteractionHand.MAIN_HAND, -+ 2, item, "minecraft:apple", Set.of("minecraft:is_food")); ++ 2, item, "minecraft:apple", true); + assertNotNull(consumption); + final var use = (InventoryTransactionData.UseItemTransactionData) consumption.transactionData(); + assertEquals(HandSlot.Mainhand, use.handSlot()); @@ -748,7 +748,7 @@ index 0bf1b705..4a770d23 100644 + item.setAmount(3); + item.setNetId(27); + assertNull(InteractionPackets.beginItemUse(player, new InventoryTracker(user()), InteractionHand.MAIN_HAND, -+ 2, item, "minecraft:firework_rocket", Set.of())); ++ 2, item, "minecraft:firework_rocket", false)); + assertEquals(1, player.pendingFlightItemUses().size()); + final var rocket = (InventoryTransactionData.UseItemTransactionData) + player.pendingFlightItemUses().getFirst().transactionData(); @@ -777,7 +777,7 @@ index 0bf1b705..4a770d23 100644 + player.setGliding(true); + final BedrockItem item = new BedrockItem(1); + assertNull(InteractionPackets.beginItemUse(player, new InventoryTracker(user()), InteractionHand.MAIN_HAND, -+ 2, item, "minecraft:firework_rocket", Set.of())); ++ 2, item, "minecraft:firework_rocket", false)); + final var action = player.pendingFlightItemUses().getFirst().actions().get(0); + assertEquals(1, action.fromItem().amount()); + assertTrue(action.toItem().isEmpty()); @@ -794,7 +794,7 @@ index 0bf1b705..4a770d23 100644 + item.setAmount(3); + assertEquals(-3, requests.nextRequestId()); + assertNull(InteractionPackets.beginItemUse(player, inventory, InteractionHand.MAIN_HAND, -+ 2, item, "minecraft:firework_rocket", Set.of())); ++ 2, item, "minecraft:firework_rocket", false)); + final var first = player.pendingFlightItemUses().removeFirst(); + assertEquals(-4, first.legacyRequestId()); + assertEquals(1, first.legacySlots().size()); @@ -803,10 +803,10 @@ index 0bf1b705..4a770d23 100644 + assertEquals(-5, requests.nextRequestId()); + + assertNull(InteractionPackets.beginItemUse(player, inventory, InteractionHand.OFF_HAND, -+ 2, item, "minecraft:firework_rocket", Set.of())); ++ 2, item, "minecraft:firework_rocket", false)); + assertTrue(player.pendingFlightItemUses().isEmpty()); + assertNull(InteractionPackets.beginItemUse(player, inventory, InteractionHand.MAIN_HAND, -+ 2, item, "minecraft:firework_rocket", Set.of())); ++ 2, item, "minecraft:firework_rocket", false)); + assertEquals(-6, player.pendingFlightItemUses().removeFirst().legacyRequestId()); + assertEquals(-7, requests.nextRequestId()); + final BedrockItem block = item.copy(); @@ -847,7 +847,7 @@ index 0bf1b705..4a770d23 100644 + pearl.setNetId(27); + contents.setItem(0, pearl); + final var first = InteractionPackets.beginItemUse(player, inventory, InteractionHand.MAIN_HAND, -+ 0, pearl, "minecraft:ender_pearl", Set.of()); ++ 0, pearl, "minecraft:ender_pearl", false); + assertEquals(-4, first.legacyRequestId()); + assertEquals(ContainerEnumName.InventoryContainer, first.legacySlots().getFirst().container()); + assertArrayEquals(new byte[]{0}, first.legacySlots().getFirst().slots()); @@ -866,7 +866,7 @@ index 0bf1b705..4a770d23 100644 + windCharge.setNetId(28); + contents.setItem(1, windCharge); + final var second = InteractionPackets.beginItemUse(player, inventory, InteractionHand.MAIN_HAND, -+ 1, windCharge, "minecraft:wind_charge", Set.of()); ++ 1, windCharge, "minecraft:wind_charge", false); + assertEquals(-6, second.legacyRequestId()); + assertArrayEquals(new byte[]{1}, second.legacySlots().getFirst().slots()); + assertEquals(1, second.actions().getFirst().slot()); @@ -876,7 +876,7 @@ index 0bf1b705..4a770d23 100644 + 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()); ++ 0, contents.getItem(0), "minecraft:ender_pearl", false); + assertEquals(-8, repeated.legacyRequestId()); + assertEquals(15, repeated.actions().getFirst().fromItem().amount()); + assertEquals(14, repeated.actions().getFirst().toItem().amount()); diff --git a/patches/viafabricplus-bedrock/series.json b/patches/viafabricplus-bedrock/series.json index 1ca9f308..fe516045 100644 --- a/patches/viafabricplus-bedrock/series.json +++ b/patches/viafabricplus-bedrock/series.json @@ -204,6 +204,11 @@ "project": "viabedrock", "patch": "0103-honor-native-ability-layer-priority.patch", "reason": "Apply native game-type transitions to historical Base and Spectator layers while preserving higher ability owners." + }, + { + "project": "viafabricplus-bedrock", + "patch": "0008-share-native-actor-and-resource-runtime.patch", + "reason": "Share cached native item definition identity before retaining local use cycles." } ] }, diff --git a/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.patch b/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.patch index ec880a2c..efbe00cf 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.patch @@ -1,4 +1,4 @@ -From 66c257b2fc992acf8ab7cc0b6abd49db0b6bed94 Mon Sep 17 00:00:00 2001 +From 314153e0af766210ce5a61c7054acfcb13882e81 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(movement): send completed local physics to ViaBedrock @@ -664,6 +664,19 @@ impulse retains its first eight physical frames. An additional next-tick correction still occurs, and direct translation can emit input before client application catches up with receipt. These scheduling gaps and complete native trajectories remain open. Raw captures stay private. + +Retain native item definition identity when Java updates or releases the +local active stack. Custom definitions can share the same paper carrier. +Comparing only that carrier lets a timed food cycle adopt an instant +food while retaining the old duration, producing a non-finite hand pose. +Use the shared cached native metadata, and keep count or unrelated +component changes on the same definition within the current use cycle. + +Official Bedrock 1.26.60.5/protocol 2225 cancels the old timed cycle on +this hotbar change and consumes the new instant food. It sends no release +transaction for that change. Preserve Java's local stop path without an +extra release packet. Tests cover distinct definitions sharing a carrier, +same-definition updates and absent-metadata Java identity fallback. --- build.gradle.kts | 2 +- .../bedrock/ViaFabricPlusBedrock.java | 14 + @@ -688,7 +701,7 @@ complete native trajectories remain open. Raw captures stay private. .../movement/MixinGameModePhysicsReplay.java | 29 + .../movement/MixinHorsePhysicsState.java | 63 +++ .../movement/MixinLevelPhysicsReplay.java | 25 + - .../features/movement/MixinLivingEntity.java | 142 +++++ + .../features/movement/MixinLivingEntity.java | 150 +++++ .../movement/MixinLivingPhysicsState.java | 145 +++++ .../features/movement/MixinLocalPlayer.java | 378 ++++++++++++- .../movement/MixinMobEffectPhysicsState.java | 18 + @@ -707,6 +720,7 @@ complete native trajectories remain open. Raw captures stay private. .../bedrock/input/BedrockPhysicalReplay.java | 535 ++++++++++++++++++ .../input/BedrockPlayerCorrections.java | 64 +++ .../bedrock/input/BedrockPlayerModes.java | 176 ++++++ + .../bedrock/input/BedrockUsedItem.java | 23 + .../input/BedrockVehiclePredictions.java | 48 ++ .../bedrock/input/FlightMovementPayload.java | 24 + .../bedrock/input/MovementEffectPayload.java | 24 + @@ -745,6 +759,7 @@ complete native trajectories remain open. Raw captures stay private. .../viafabricplus-bedrock.mixins.json | 25 + .../BedrockPhysicalMotionLifecycleTest.java | 190 +++++++ .../bedrock/input/BedrockPlayerModesTest.java | 168 ++++++ + .../bedrock/input/BedrockUsedItemTest.java | 66 +++ .../input/BedrockVehiclePredictionsTest.java | 155 +++++ .../input/VehiclePredictionPayloadTest.java | 75 +++ .../input/VehiclePredictionSessionTest.java | 284 ++++++++++ @@ -768,7 +783,7 @@ complete native trajectories remain open. Raw captures stay private. .../replay/PhysicalWorldCollisionTest.java | 111 ++++ .../render/PhysicalColliderSourcesTest.java | 151 +++++ .../render/PhysicalReplayLifetimeTest.java | 151 +++++ - 103 files changed, 8276 insertions(+), 74 deletions(-) + 105 files changed, 8373 insertions(+), 74 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockFlightMotion.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockPlayerPosture.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/access/IBedrockTeleportTravel.java @@ -802,6 +817,7 @@ complete native trajectories remain open. Raw captures stay private. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockPhysicalReplay.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockPlayerCorrections.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockPlayerModes.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockUsedItem.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockVehiclePredictions.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/FlightMovementPayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/MovementEffectPayload.java @@ -837,6 +853,7 @@ complete native trajectories remain open. Raw captures stay private. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/movement/replay/PhysicalStateAccess.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/BedrockPhysicalMotionLifecycleTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/BedrockPlayerModesTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/BedrockUsedItemTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/BedrockVehiclePredictionsTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/VehiclePredictionPayloadTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/VehiclePredictionSessionTest.java @@ -2019,10 +2036,10 @@ index 0000000..b6ee81c + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java -index cb89991..81b514b 100644 +index cb89991..afc332f 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinLivingEntity.java -@@ -21,27 +21,169 @@ +@@ -21,27 +21,177 @@ package com.viaversion.viafabricplus.bedrock.injection.mixin.features.movement; @@ -2034,6 +2051,7 @@ index cb89991..81b514b 100644 +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; +import com.viaversion.viafabricplus.bedrock.input.BedrockMovementEffects; ++import com.viaversion.viafabricplus.bedrock.input.BedrockUsedItem; +import com.viaversion.viafabricplus.bedrock.movement.BedrockGeyserPhysics; +import com.viaversion.viafabricplus.bedrock.movement.BedrockClimbingPhysics; +import com.viaversion.viafabricplus.bedrock.movement.BedrockFlightPhysics; @@ -2047,6 +2065,7 @@ index cb89991..81b514b 100644 import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; ++import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +import net.minecraft.world.item.enchantment.Enchantments; import net.minecraft.world.phys.Vec3; @@ -2177,6 +2196,12 @@ index cb89991..81b514b 100644 + cir.setReturnValue(new Vec3(nativeMotion.x(), nativeMotion.y(), nativeMotion.z())); + } + ++ @WrapOperation(method = {"updatingUsingItem", "releaseUsingItem"}, at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;isSameItem(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemStack;)Z")) ++ private boolean retainNativeUseIdentity(final ItemStack held, final ItemStack active, final Operation original) { ++ return (Object) this instanceof LocalPlayer && BedrockInputPermissions.isBedrockConnection() ++ ? BedrockUsedItem.matches(held, active) : original.call(held, active); ++ } ++ + @WrapOperation(method = "updateUsingItem", at = @At(value = "FIELD", target = "Lnet/minecraft/world/entity/LivingEntity;useItemRemaining:I", opcode = Opcodes.PUTFIELD)) + private void boundNativeItemUseDuration(final LivingEntity entity, final int remaining, final Operation original) { + // Native decrements positive duration only; delayed completion never makes it negative. @@ -4392,6 +4417,35 @@ index 0000000..2f9f49a + + private enum Field { NATIVE_MODE } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockUsedItem.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockUsedItem.java +new file mode 100644 +index 0000000..8b10c88 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockUsedItem.java +@@ -0,0 +1,23 @@ ++/* ++ * 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.input; ++ ++import com.viaversion.viafabricplus.bedrock.appearance.BedrockItemMetadata; ++import java.util.Objects; ++import net.minecraft.core.component.DataComponents; ++import net.minecraft.world.item.ItemStack; ++ ++/** Keeps a native use cycle attached to its definition, rather than its Java carrier. */ ++public final class BedrockUsedItem { ++ private BedrockUsedItem() { ++ } ++ ++ public static boolean matches(final ItemStack held, final ItemStack active) { ++ return ItemStack.isSameItem(held, active) && Objects.equals( ++ BedrockItemMetadata.get(held.get(DataComponents.CUSTOM_DATA)).identifier(), ++ BedrockItemMetadata.get(active.get(DataComponents.CUSTOM_DATA)).identifier()); ++ } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockVehiclePredictions.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockVehiclePredictions.java new file mode 100644 index 0000000..61ad9d3 @@ -7208,6 +7262,78 @@ index 0000000..e925d06 + } + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/BedrockUsedItemTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/BedrockUsedItemTest.java +new file mode 100644 +index 0000000..7da63b1 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/BedrockUsedItemTest.java +@@ -0,0 +1,66 @@ ++/* ++ * 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.input; ++ ++import net.minecraft.SharedConstants; ++import net.minecraft.core.component.DataComponentMap; ++import net.minecraft.core.component.DataComponents; ++import net.minecraft.nbt.CompoundTag; ++import net.minecraft.server.Bootstrap; ++import net.minecraft.world.item.Item; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.item.Items; ++import net.minecraft.world.item.component.CustomData; ++import net.raphimc.viabedrock.protocol.rewriter.ItemDataRewriter; ++import org.junit.jupiter.api.BeforeAll; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++final class BedrockUsedItemTest { ++ @BeforeAll ++ static void initializeGame() { ++ SharedConstants.tryDetectVersion(); ++ Bootstrap.bootStrap(); ++ for (final var item : new Item[]{Items.PAPER, Items.STONE}) { ++ if (!item.builtInRegistryHolder().areComponentsBound()) { ++ item.builtInRegistryHolder().bindComponents(DataComponentMap.EMPTY); ++ } ++ } ++ } ++ ++ @Test ++ void differentNativeDefinitionsCannotContinueTheSameCarrierUseCycle() { ++ final var timed = food("fixture:timed", 32); ++ final var instant = food("fixture:instant", 0); ++ assertFalse(BedrockUsedItem.matches(instant, timed)); ++ assertFalse(BedrockUsedItem.matches(new ItemStack(Items.PAPER), timed)); ++ } ++ ++ @Test ++ void countAndComponentUpdatesKeepTheSameNativeUseCycle() { ++ final var active = food("fixture:food", 32); ++ final var updated = food("fixture:food", 32); ++ updated.setCount(12); ++ updated.set(DataComponents.CUSTOM_DATA, updated.get(DataComponents.CUSTOM_DATA).update(tag -> tag.putInt("network_id", 42))); ++ assertTrue(BedrockUsedItem.matches(updated, active)); ++ } ++ ++ @Test ++ void absentNativeMetadataRetainsJavaItemIdentity() { ++ assertTrue(BedrockUsedItem.matches(new ItemStack(Items.PAPER), new ItemStack(Items.PAPER, 2))); ++ assertFalse(BedrockUsedItem.matches(new ItemStack(Items.STONE), new ItemStack(Items.PAPER))); ++ } ++ ++ private static ItemStack food(final String identifier, final int duration) { ++ final var tag = new CompoundTag(); ++ tag.putString(ItemDataRewriter.BEDROCK_IDENTIFIER_TAG, identifier); ++ tag.putInt(ItemDataRewriter.BEDROCK_USE_DURATION_TAG, duration); ++ final var stack = new ItemStack(Items.PAPER, 64); ++ stack.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); ++ return stack; ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/BedrockVehiclePredictionsTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/BedrockVehiclePredictionsTest.java new file mode 100644 index 0000000..4f2bef0 diff --git a/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md b/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md index f59779dc..d67a0038 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md @@ -14,6 +14,8 @@ Prerequisites: - viabedrock/0103-honor-native-ability-layer-priority.patch: Apply native game-type transitions to historical Base and Spectator layers while preserving higher ability owners. +- viafabricplus-bedrock/0008-share-native-actor-and-resource-runtime.patch: Share cached native item definition identity for local use cycles. + ## Validation and remaining limits Regression coverage includes `BedrockPhysicalMotionLifecycleTest`, `BedrockPlayerModesTest`, `BedrockVehiclePredictionsTest`, `VehiclePredictionPayloadTest`. @@ -23,3 +25,8 @@ Native and live controls establish selected movement, correction, and flight beh Bedrock 1.26.60.5 and BDS 1.26.60.6 establish four stationary ten-block drops. Native, direct, and ViaProxy lose seven health on dry ground and at both partial overlaps with a solid/water boundary. A fully wet landing loses none. These controls cover fall damage with full blocks and source water. Other fluid movement and correction behavior remain separate. The [owning patch](0025-send-completed-local-physics-to-viabedrock.patch) retains implementation rationale and versioned evidence. + +Native 1.26.60.5 cancels a timed custom food cycle when the selected definition changes to an instant food. +Both definitions use the same Java paper carrier. Local use now checks the native definition before retaining or releasing the active stack. +Count and unrelated component updates preserve the use cycle for the same definition. +`BedrockUsedItemTest` covers these identity boundaries and Java fallback. 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 28ba5106..7e5588f0 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 b3ae5a296f0f813c6f54f860adacc12d07d78c73 Mon Sep 17 00:00:00 2001 +From e202968c90ce14fcf896f42f9e1241c72a22eaa4 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): preserve native held item use @@ -19,7 +19,8 @@ from the active Bedrock connection rather than the local Java protocol. Native bottom-face hits are about 0.8175, including translated proxy joins. Keep two clock lifetimes: air use has a strict 200 ms connection deadline; -held placement waits 300 ms after its first attempt or each local success. +held placement waits 300 ms after each local success. Trapdoor holds also +retain the previously established first-attempt delay. Failed placement repeats do not restart their delay. Retry from input frames after camera movement, before the next tick publishes rotation, and suppress Java's parallel four-tick path. Native frame quantization @@ -35,9 +36,10 @@ use after the deadline. Unattached stone and late trapdoor controls agree. The reverse trapdoor-to-food control keeps simulation block uses and consumes no food. A new press after release can place the selected block. -Start the food air route from transported FOOD components, including -custom paper carriers. Gate local/packet use and set the deadline before -main-hand air use even when hunger later refuses it. An owned air hold +Choose the initial food route from transported FOOD and the local +consumable predicate, including custom paper carriers. Set the air deadline +before main-hand food use even when hunger refuses it. Initial refusal +retains untimed block mode, rather than acquiring the air route. An owned air hold skips block/entity dispatch and offhand fallback; newly selected items share its air deadline. A placement hold cannot fall through to eating. Canonical consumption/completion remain server-owned. Other initial air-use @@ -48,23 +50,32 @@ consumes six foods after a trapdoor hold, unlike native. Input-state tests cover both directions, hand/target routing, variable frames, release, connection reset, clock boundaries and rollover. Native and installed captures establish gameplay behavior separately from model tests. Other -use families, timed/refused-food transitions, exact packet phases and the -original intermittent food report remain open. +use families, exact packet phases, complete use cycles and the original +intermittent food report remain open. + +Current 1.26.60.5/protocol 2225 refused-food traces retain held flags 17 +with or without an initial block target. Accepted food retains flags 16 +even after hunger fills. The refused route writes a 200 ms air deadline +but bypasses it for held block continuation. A Stone selected 110 ms +after refused bread can place immediately. Successful ordinary block +uses start the same 300 ms placement delay. Separate block mode ownership +from its active delay, and distinguish frame continuation from fresh +physical calls without allocating callbacks on the input path. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../features/block/MixinTrapDoorBlock.java | 18 +- - .../features/input/MixinItemUseControl.java | 77 +++++++ + .../features/input/MixinItemUseControl.java | 85 ++++++++ .../inventory/MixinHeldBlockPlacement.java | 28 +++ .../features/inventory/MixinHeldBlockUse.java | 61 ++++++ - .../bedrock/input/BedrockItemUse.java | 188 ++++++++++++++++++ + .../bedrock/input/BedrockItemUse.java | 193 ++++++++++++++++++ .../bedrock/input/BlockUsePayload.java | 24 +++ .../input/BlockUsePredictionPayload.java | 24 +++ - .../bedrock/input/ItemUseState.java | 81 ++++++++ + .../bedrock/input/ItemUseState.java | 93 +++++++++ .../bedrock/inventory/TrapdoorPlacement.java | 55 +++++ .../viafabricplus-bedrock.mixins.json | 3 + - .../bedrock/input/ItemUseStateTest.java | 94 +++++++++ - .../inventory/TrapdoorPlacementTest.java | 100 ++++++++++ - 13 files changed, 751 insertions(+), 4 deletions(-) + .../bedrock/input/ItemUseStateTest.java | 121 +++++++++++ + .../inventory/TrapdoorPlacementTest.java | 100 +++++++++ + 13 files changed, 803 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockUse.java @@ -148,10 +159,10 @@ index 82db187..eda72b3 100644 return super.getOcclusionShape(state); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java new file mode 100644 -index 0000000..e5d5ff1 +index 0000000..a2a315d --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java -@@ -0,0 +1,77 @@ +@@ -0,0 +1,85 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -173,17 +184,20 @@ index 0000000..e5d5ff1 +import net.minecraft.client.gui.screens.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; ++import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Minecraft.class) +public abstract class MixinItemUseControl { ++ @Unique private boolean viaFabricPlusBedrock$frameRepeat; ++ + @Shadow protected abstract void pick(float partialTick); + @Shadow protected abstract void startUseItem(); + @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) + private void beginItemUse(final CallbackInfo ci) { -+ if (!BedrockItemUse.begin()) { ++ if (!BedrockItemUse.begin(this.viaFabricPlusBedrock$frameRepeat)) { + ci.cancel(); + } + } @@ -220,7 +234,12 @@ index 0000000..e5d5ff1 + final var player = ((Minecraft) (Object) this).player; + if (player != null && !player.isUsingItem() && BedrockItemUse.frameReady()) { + this.pick(1F); -+ this.startUseItem(); ++ this.viaFabricPlusBedrock$frameRepeat = true; ++ try { ++ this.startUseItem(); ++ } finally { ++ this.viaFabricPlusBedrock$frameRepeat = false; ++ } + } + } + @@ -332,10 +351,10 @@ index 0000000..69387b7 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java new file mode 100644 -index 0000000..76ebff8 +index 0000000..ba0fa35 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java -@@ -0,0 +1,188 @@ +@@ -0,0 +1,193 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -382,8 +401,8 @@ index 0000000..76ebff8 + ClientPlayConnectionEvents.DISCONNECT.register((listener, client) -> reset()); + } + -+ public static boolean begin() { -+ if (bedrockConnection() && !INPUT.canAttempt(System.nanoTime())) { ++ public static boolean begin(final boolean frameRepeat) { ++ if (bedrockConnection() && !INPUT.canBegin(System.nanoTime(), frameRepeat)) { + return false; + } + update(true); @@ -392,7 +411,12 @@ index 0000000..76ebff8 + } + + public static boolean beginAirUse(final InteractionHand hand, final ItemStack item) { -+ return !bedrockConnection() || INPUT.admitAirUse(System.nanoTime(), hand, item.has(DataComponents.FOOD)); ++ if (!bedrockConnection()) { ++ return true; ++ } ++ final var consumable = item.get(DataComponents.CONSUMABLE); ++ final boolean canConsume = consumable != null && consumable.canConsume(Minecraft.getInstance().player, item); ++ return INPUT.admitAirUse(System.nanoTime(), hand, item.has(DataComponents.FOOD), canConsume); + } + + public static HitResult.Type hitType(final HitResult.Type target, final InteractionHand hand) { @@ -446,7 +470,7 @@ index 0000000..76ebff8 + } + + public static void attempted() { -+ if (held && (INPUT.blockActive() || usesNativeRepeatDelay())) { ++ if (held && (successfulAttempt || INPUT.blockActive() || usesNativeRepeatDelay())) { + INPUT.attemptedBlock(System.nanoTime(), successfulAttempt); + } + } @@ -586,10 +610,10 @@ index 0000000..a98d448 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java new file mode 100644 -index 0000000..f2343fd +index 0000000..3bbe039 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java -@@ -0,0 +1,81 @@ +@@ -0,0 +1,93 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -609,12 +633,17 @@ index 0000000..f2343fd + private long airStartedAt; + private boolean blockActive; + private long blockStartedAt; ++ private boolean blockDelayed; + + public boolean canAttempt(final long now) { + return !this.airAttempted || now - this.airStartedAt > AIR_DELAY_NANOS; + } + -+ public boolean admitAirUse(final long now, final InteractionHand hand, final boolean food) { ++ public boolean canBegin(final long now, final boolean frameRepeat) { ++ return frameRepeat && this.blockActive || this.canAttempt(now); ++ } ++ ++ public boolean admitAirUse(final long now, final InteractionHand hand, final boolean food, final boolean canConsume) { + if (hand != InteractionHand.MAIN_HAND) { + return !this.airActive && !this.blockActive; + } @@ -627,7 +656,11 @@ index 0000000..f2343fd + if (!this.canAttempt(now)) { + return false; + } -+ this.airActive = true; ++ if (this.airActive || canConsume) { ++ this.airActive = true; ++ } else { ++ this.blockActive = true; ++ } + this.airAttempted = true; + this.airStartedAt = now; + return true; @@ -640,6 +673,7 @@ index 0000000..f2343fd + public void attemptedBlock(final long now, final boolean success) { + if (!this.airActive && (!this.blockActive || success)) { + this.blockStartedAt = now; ++ this.blockDelayed = true; + this.blockActive = true; + } + } @@ -653,12 +687,14 @@ index 0000000..f2343fd + } + + public boolean frameReady(final long now, final boolean food) { -+ return this.canAttempt(now) && (this.airActive || !this.blockActive && food -+ || this.blockActive && now - this.blockStartedAt >= BLOCK_DELAY_NANOS); ++ return this.blockActive ++ ? !this.blockDelayed || now - this.blockStartedAt >= BLOCK_DELAY_NANOS ++ : this.canAttempt(now) && (this.airActive || food); + } + + public void cancelPlacement() { + this.blockActive = false; ++ this.blockDelayed = false; + } + + public void release() { @@ -748,10 +784,10 @@ index f7c3bf4..416a16f 100644 "features.creative.MixinCreativeModeTabs", diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java new file mode 100644 -index 0000000..01e1133 +index 0000000..c26e38b --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java -@@ -0,0 +1,94 @@ +@@ -0,0 +1,121 @@ +package com.viaversion.viafabricplus.bedrock.input; + +import org.junit.jupiter.api.Test; @@ -767,14 +803,14 @@ index 0000000..01e1133 + @Test + void releasedFoodKeepsItsDeadlineForFreshClicksUntilConnectionReplacement() { + final var timing = new ItemUseState(); -+ assertTrue(timing.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertTrue(timing.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, true)); + timing.release(); -+ assertFalse(timing.admitAirUse(1_131_000_000L, InteractionHand.MAIN_HAND, true)); -+ assertFalse(timing.admitAirUse(1_200_000_000L, InteractionHand.MAIN_HAND, true)); -+ assertTrue(timing.admitAirUse(1_616_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertFalse(timing.admitAirUse(1_131_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertFalse(timing.admitAirUse(1_200_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertTrue(timing.admitAirUse(1_616_000_000L, InteractionHand.MAIN_HAND, true, true)); + assertFalse(timing.canAttempt(1_700_000_000L)); + timing.clear(); -+ assertTrue(timing.admitAirUse(1_700_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertTrue(timing.admitAirUse(1_700_000_000L, InteractionHand.MAIN_HAND, true, true)); + } + + @Test @@ -783,7 +819,7 @@ index 0000000..01e1133 + final List usedAt = new ArrayList<>(); + for (final int milliseconds : new int[] {0, 47, 94, 141, 188, 236, 284, 332, 380, 428, 475, 542, 609, 676}) { + final long now = 1_000_000_000L + milliseconds * 1_000_000L; -+ if (timing.frameReady(now, true) && timing.admitAirUse(now, InteractionHand.MAIN_HAND, true)) { ++ if (timing.frameReady(now, true) && timing.admitAirUse(now, InteractionHand.MAIN_HAND, true, true)) { + usedAt.add(milliseconds); + } + } @@ -793,14 +829,14 @@ index 0000000..01e1133 + @Test + void foodHoldKeepsAirUseAcrossBlockAndEntityTargetsUntilRelease() { + final var input = new ItemUseState(); -+ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, true)); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); + input.attemptedBlock(1_100_000_000L, true); + assertFalse(input.frameReady(1_200_000_000L, false)); + assertTrue(input.frameReady(1_200_000_001L, false)); -+ assertTrue(input.admitAirUse(1_200_000_001L, InteractionHand.MAIN_HAND, false)); -+ assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.OFF_HAND, false)); ++ assertTrue(input.admitAirUse(1_200_000_001L, InteractionHand.MAIN_HAND, false, true)); ++ assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.OFF_HAND, false, true)); + input.cancelPlacement(); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + input.release(); @@ -817,18 +853,45 @@ index 0000000..01e1133 + assertTrue(input.frameReady(1_300_000_000L, true)); + assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.OFF_HAND)); -+ assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.MAIN_HAND, true, true)); + input.attemptedBlock(1_310_000_000L, false); + assertTrue(input.frameReady(1_311_000_000L, true)); + input.attemptedBlock(1_320_000_000L, true); + assertFalse(input.frameReady(1_619_999_999L, false)); + assertTrue(input.frameReady(1_620_000_000L, false)); + input.release(); -+ assertTrue(input.admitAirUse(1_630_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertTrue(input.admitAirUse(1_630_000_000L, InteractionHand.MAIN_HAND, true, true)); + assertFalse(input.canAttempt(1_800_000_000L)); + input.clear(); + assertEquals(HitResult.Type.ENTITY, input.hitType(HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); -+ assertTrue(input.admitAirUse(1_800_000_000L, InteractionHand.MAIN_HAND, true)); ++ assertTrue(input.admitAirUse(1_800_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ } ++ ++ @Test ++ void initiallyRefusedFoodKeepsUntimedBlockRepeatsWhileFreshCallsKeepTheAirDeadline() { ++ final var input = new ItemUseState(); ++ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false)); ++ input.attemptedBlock(1_010_000_000L, false); ++ assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertTrue(input.frameReady(1_052_000_000L, true)); ++ assertTrue(input.canBegin(1_052_000_000L, true)); ++ assertFalse(input.canBegin(1_052_000_000L, false)); ++ assertFalse(input.admitAirUse(1_052_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ input.attemptedBlock(1_105_000_000L, true); ++ assertFalse(input.frameReady(1_404_999_999L, false)); ++ assertTrue(input.frameReady(1_405_000_000L, false)); ++ input.release(); ++ assertTrue(input.admitAirUse(1_500_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ } ++ ++ @Test ++ void foodThatBecomesRefusedDuringAnAirHoldRetainsItsAirRouteAndDeadline() { ++ final var input = new ItemUseState(); ++ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertTrue(input.admitAirUse(1_201_000_000L, InteractionHand.MAIN_HAND, true, false)); ++ assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertFalse(input.frameReady(1_401_000_000L, false)); ++ assertTrue(input.frameReady(1_401_000_001L, false)); + } + + @Test @@ -837,7 +900,7 @@ index 0000000..01e1133 + final long start = Long.MAX_VALUE - 150_000_000L; + timing.attemptedBlock(start, true); + timing.release(); -+ assertTrue(timing.admitAirUse(start, InteractionHand.MAIN_HAND, true)); ++ assertTrue(timing.admitAirUse(start, InteractionHand.MAIN_HAND, true, true)); + assertFalse(timing.frameReady(start + 200_000_000L, true)); + assertTrue(timing.frameReady(start + 200_000_001L, true)); + timing.release(); diff --git a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md index 82203716..1cb4ee4d 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md @@ -13,13 +13,17 @@ Connection replacement clears it. Missing optional block-use channels do not clear the air-use hold or its deadline. Keep the interaction mode until physical release or cancellation. -A hold started by food air use continues main-hand air use after selecting a block item. +A hold started by admitted food air use continues main-hand air use after selecting a block item. +Initially refused food retains block mode, even without an initial block target. +The same air deadline applies to fresh calls, while held block continuation bypasses it. Skip block and entity dispatch inside the use method without changing the visible raycast. A placement hold keeps its block route after selecting food and cannot fall through to eating. Retire offhand fallback while either route owns the hold. Release permits a fresh press to choose another interaction mode. -Held placement keeps its separate 300 ms delay after the first attempt or a successful local use. +Held placement waits 300 ms after a successful local block use. +Trapdoor holds also retain the established first-attempt delay. +Initially refused food retains untimed block continuation until a block use succeeds. Failed repeats do not restart that delay. Release, screens, and connection replacement clear the placement timer and retained trapdoor state. Suppress Java's parallel repeat path while the frame route owns the attempt. @@ -56,7 +60,7 @@ Unattached stone and late trapdoor controls agree. The reverse trapdoor-to-food control retains simulation block uses and consumes no food. Installed direct and proxy controls check both directions and fresh placement after release or Pause. The previous installed client placed blocks after food use and ate food during the placement hold. -Five input-state tests cover retained modes, hand and target routing, variable frames, release, connection reset, independent delays, strict boundaries, and rollover. +Seven input-state tests cover retained modes, hand and target routing, variable frames, release, connection reset, independent delays, strict boundaries, and rollover. Five block-state tests cover retained orientation, materials, independent properties, targets, frozen hits, release, and ordinary blocks. Earlier installed placement fixtures match native camera changes, material changes, and fresh activation after release. @@ -68,5 +72,13 @@ After a preceding successful hold, both instead place a neighbor and consume one ## Remaining limits -Other initial air-use families, timed and refused-food handoffs, live entity transitions, competing use handlers, exact input-flag phase counts, and full use-cycle timing need more native comparisons. +Other initial air-use families, live entity transitions, competing use handlers, exact input-flag phase counts, and full use-cycle timing need more native comparisons. The original intermittent animation-without-feeding report remains open. + +Timed-food controls distinguish an early switch, a switch after completion, and a held three-completion cycle. +Initial refusal sends one air payload without `StartUsingItem`, then keeps block continuation. +An admitted air hold keeps its route after hunger fills. + +Ordinary Stone placement still differs after the ray reaches another face: both installed routes place four blocks where native places two. +The first two successes follow the 300 ms delay. The later placement selection remains unresolved. +Do not treat these mode and timing fixes as complete held-placement parity. From 70843f46a52bb31c47c56d4559cb93bab8ad4227 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 01:37:14 +0100 Subject: [PATCH 18/47] fix(interaction): retain native held placement paths Keep stationary held builds on their retained direction and next cell. Use the native float segment/box test to select the support and face without changing the visible ray or original world hit. This prevents a new top hit from placing extra blocks outside the established path. Retain rejected candidates, advance on successful placement, and reset with the existing hold owner. Apply strict 300 ms timing before the line and 200 ms afterward. Preserve float cell collapse and the native upper height exception when retaining build-attempt history. Fold the implementation, native evidence, and boundary regressions into the owning input patch. Selected stationary route controls pass; moving, air/entity dispatch, build-height trajectories and first-rejected block retry cadence remain separate compatibility work. --- docs/bedrock-coverage.md | 4 +- ...08-signal-sustained-bedrock-item-use.pr.md | 4 +- ...0038-predict-held-trapdoor-placement.patch | 398 ++++++++++++++++-- ...0038-predict-held-trapdoor-placement.pr.md | 114 +++-- 4 files changed, 407 insertions(+), 113 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 3b3cef63..0b3807f9 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -24,10 +24,10 @@ No complete area has verified Bedrock parity. | --- | --- | --- | | Protocol 2225 | Updated packet layouts, explicit enum values, stonecutter notifications, cursor drags, skin identity, and sound unions. | Block seating, signed content playback, matchmaking UI, and environment layers remain partial or unsupported. | | 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. | +| Held block placement | Retained stationary build paths, next-cell ray selection, use timing, trapdoor orientation, cancellation, and prediction. | Native, direct, and proxy controls cover selected stationary paths, re-aiming, rejection, and release. Moving placement, air/entity dispatch, build-height limits, and first-rejected ordinary-block retry timing remain open. | | 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, Creative catalog actions, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | -| Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Ordinary held placement still differs after target changes. Broader use cycles and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md). | +| Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Broader use cycles and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md index 791493e6..c2ea90e7 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md @@ -46,7 +46,7 @@ Native controls establish the combined behavior for these fixtures; they do not Seventeen focused tests cover installed component retention, constructor-specific admission, Java component wire properties, difficulty changes, food-use flags, offhand resynchronization, and completion boundaries. Admission tests retain the held quantity and verify refused-use payloads with `StartUsingItem` clear. -The full four-project build passes 3,291 tests, with 325 skipped and no failures or errors. +The full four-project build passes 3,299 tests, with 325 skipped and no failures or errors. Installed direct and ViaProxy controls cover eleven cases each. Both routes match the selected native admission, hunger, timed consumption, and zero-duration outcomes. @@ -79,4 +79,4 @@ It does not belong in an independent upstream PR branch. The current timed-to-instant custom-food switch cancels the old definition instead of retaining its Java paper use cycle. Early switches consume none of the old food; late switches retain one completion. The long-hold control retains three completions and reaches full hunger. -The client input sidecar records the separate ordinary-block placement mismatch. +The client input sidecar records the remaining placement timing and dispatch gaps. 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 7e5588f0..50890056 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 e202968c90ce14fcf896f42f9e1241c72a22eaa4 Mon Sep 17 00:00:00 2001 +From 7e5778f2d845977fa2f93a6d1a88ee88ddb22cb1 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): preserve native held item use @@ -6,21 +6,22 @@ Subject: [PATCH] feat(interaction): preserve native held item use Native Bedrock 1.26.60.5 keeps the first predicted trapdoor facing, half and world hit during held placement. Retain that state across materials, item changes, correction and stack exhaustion. Apply the snap only to -trapdoors; other block items use fresh hits, and empty hands restore -interaction. Returning to a trapdoor restores its snap until release. +trapdoors; ordinary blocks share the retained placement path below, +and empty hands restore interaction. Returning to a trapdoor restores its snap until release. Preserve independent water, power and material properties. Separate trapdoors and the original support remain eligible targets. Carry the physical use lifetime and sequence-owned prediction format 2 through direct and ViaProxy sessions before the concluding Java packet. -Keep the native cursor separate from the ordinary Java raycast, including -coordinates outside a later target cube. Select native outline/occlusion +Keep use-target selection separate from the visible Java raycast. Retain +native cursor coordinates outside a later target cube. Select native outline/occlusion from the active Bedrock connection rather than the local Java protocol. Native bottom-face hits are about 0.8175, including translated proxy joins. Keep two clock lifetimes: air use has a strict 200 ms connection deadline; -held placement waits 300 ms after each local success. Trapdoor holds also -retain the previously established first-attempt delay. +held placement waits strictly more than 300 ms before an established +line exists, then strictly more than 200 ms after each line success. +Trapdoor holds also retain the established first-attempt delay. Failed placement repeats do not restart their delay. Retry from input frames after camera movement, before the next tick publishes rotation, and suppress Java's parallel four-tick path. Native frame quantization @@ -61,29 +62,75 @@ after refused bread can place immediately. Successful ordinary block uses start the same 300 ms placement delay. Separate block mode ownership from its active delay, and distinguish frame continuation from fresh physical calls without allocating callbacks on the input path. + +Retain a build direction when adjacent placement attempts establish a +line. Test the ray segment against the next cell, then dispatch the +retained support and face without replacing the visible hit or its world +location. Advance the pending cell only on a successful local placement. +Clear it with the physical hold and the existing connection/screen owner. + +Original 1.26.60.5/protocol 2225 helper14376b8d0 clips the current ray +against the retained next-cell AABB. Build14376bb80 records adjacent +integer deltas and advances the cell only when its build succeeds. The +clip helper142d345c0 uses float segment/box SAT with half extents and a +0.0001 cross-axis tolerance. Preserve that arithmetic rather than using +Java's double ray intersection or a same-face rule. + +Fresh/refused Stone native controls place two blocks, then reject the +new top hit outside the pending cell. Re-aiming onto the floor crosses +that cell and dispatches the old north face with the original world hit, +including a negative relative Z cursor. The third placement succeeds; +further player-collision failures keep the next cell pending. Returning +to the old aim stops attempts, and release allows fresh top placement. +Regression fixtures retain those native float coordinates, pending-cell +rejection, advancement, all six axes and release behavior. Moving actors, +entity/air targets and broader mixed-use state still need comparisons. + +Read the build-delay constants from this same executable:300.0 at +14f10c440,200.0 at14efc03a8. Once line mode is active the latter wins, +and the clock comparison rejects equality. Keep this independent from +the separate air-use deadline and preserve failed-repeat behavior. + +Construct each box maximum by adding one to its already converted float +minimum. Converting an incremented integer instead adds a spurious extent +when the native cell collapses near float precision limits. A concrete +large-coordinate ray regression fails before that arithmetic correction. +The same executable's underlying build callback143771570 retains its +first-success flag across failures, matching the path owner's lifetime. + +Preserve the last successful build position on ordinary rejection. The +native wrapper records a rejected target only at its upper height limit. +Keep that exception separate from the first-success flag and pending-cell +advancement. Regressions cover a failed distant attempt before returning +to the original line, and the upper-height history branch. Live height +limits and broader mixed-use cases remain separate validation work. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../features/block/MixinTrapDoorBlock.java | 18 +- - .../features/input/MixinItemUseControl.java | 85 ++++++++ - .../inventory/MixinHeldBlockPlacement.java | 28 +++ - .../features/inventory/MixinHeldBlockUse.java | 61 ++++++ - .../bedrock/input/BedrockItemUse.java | 193 ++++++++++++++++++ - .../bedrock/input/BlockUsePayload.java | 24 +++ - .../input/BlockUsePredictionPayload.java | 24 +++ - .../bedrock/input/ItemUseState.java | 93 +++++++++ + .../features/input/MixinItemUseControl.java | 96 ++++++++ + .../inventory/MixinHeldBlockPlacement.java | 32 +++ + .../features/inventory/MixinHeldBlockUse.java | 61 +++++ + .../bedrock/input/BedrockItemUse.java | 209 ++++++++++++++++++ + .../bedrock/input/BlockPlacementPath.java | 83 +++++++ + .../bedrock/input/BlockUsePayload.java | 24 ++ + .../input/BlockUsePredictionPayload.java | 24 ++ + .../bedrock/input/ItemUseState.java | 94 ++++++++ .../bedrock/inventory/TrapdoorPlacement.java | 55 +++++ .../viafabricplus-bedrock.mixins.json | 3 + - .../bedrock/input/ItemUseStateTest.java | 121 +++++++++++ + .../bedrock/input/BlockPlacementPathTest.java | 128 +++++++++++ + .../bedrock/input/ItemUseStateTest.java | 125 +++++++++++ .../inventory/TrapdoorPlacementTest.java | 100 +++++++++ - 13 files changed, 803 insertions(+), 4 deletions(-) + 15 files changed, 1050 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockUse.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPath.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePredictionPayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacement.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPathTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacementTest.java @@ -159,10 +206,10 @@ index 82db187..eda72b3 100644 return super.getOcclusionShape(state); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java new file mode 100644 -index 0000000..a2a315d +index 0000000..21a773c --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java -@@ -0,0 +1,85 @@ +@@ -0,0 +1,96 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -179,6 +226,8 @@ index 0000000..a2a315d +import net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.HitResult; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.client.gui.screens.Screen; @@ -209,6 +258,15 @@ index 0000000..a2a315d + } + + @WrapOperation(method = "startUseItem", at = @At(value = "INVOKE", ++ target = "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;useItemOn(Lnet/minecraft/client/player/LocalPlayer;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/InteractionResult;")) ++ private InteractionResult retainBuildPath(final MultiPlayerGameMode gameMode, final LocalPlayer player, ++ final InteractionHand hand, final BlockHitResult hit, ++ final Operation original) { ++ final BlockHitResult selected = BedrockItemUse.blockHit(hit, this.viaFabricPlusBedrock$frameRepeat); ++ return selected == null ? InteractionResult.FAIL : original.call(gameMode, player, hand, selected); ++ } ++ ++ @WrapOperation(method = "startUseItem", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;useItem(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/InteractionResult;")) + private InteractionResult admitAirUse(final MultiPlayerGameMode gameMode, final Player player, + final InteractionHand hand, final Operation original) { @@ -250,10 +308,10 @@ index 0000000..a2a315d +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java new file mode 100644 -index 0000000..4be16c1 +index 0000000..a54ba3a --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java -@@ -0,0 +1,28 @@ +@@ -0,0 +1,32 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -275,6 +333,10 @@ index 0000000..4be16c1 +public abstract class MixinHeldBlockPlacement { + @Inject(method = "place", at = @At("RETURN")) + private void retainTrapdoorOrientation(final BlockPlaceContext context, final CallbackInfoReturnable cir) { ++ if (context.getPlayer() == Minecraft.getInstance().player) { ++ BedrockItemUse.buildAttempt(context.getClickedPos(), cir.getReturnValue().consumesAction(), ++ context.getClickedPos().getY() > context.getLevel().getMaxY()); ++ } + if (cir.getReturnValue().consumesAction() && context.getPlayer() == Minecraft.getInstance().player) { + final var support = context.replacingClickedOnBlock() ? context.getClickedPos() + : context.getClickedPos().relative(context.getClickedFace().getOpposite()); @@ -351,10 +413,10 @@ index 0000000..69387b7 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java new file mode 100644 -index 0000000..ba0fa35 +index 0000000..6633a2b --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java -@@ -0,0 +1,193 @@ +@@ -0,0 +1,209 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -384,6 +446,7 @@ index 0000000..ba0fa35 + +public final class BedrockItemUse { + private static final TrapdoorPlacement PLACEMENT = new TrapdoorPlacement(); ++ private static final BlockPlacementPath PATH = new BlockPlacementPath(); + private static boolean held; + private static boolean placedBlock; + private static boolean successfulAttempt; @@ -456,6 +519,7 @@ index 0000000..ba0fa35 + } + if (!value) { + PLACEMENT.clear(); ++ PATH.clear(); + } + } + @@ -480,7 +544,7 @@ index 0000000..ba0fa35 + } + + public static boolean frameReady() { -+ return repeatsFromFrames() && INPUT.frameReady(System.nanoTime(), holdingFood()); ++ return repeatsFromFrames() && INPUT.frameReady(System.nanoTime(), holdingFood(), PATH.active()); + } + + public static void refresh() { @@ -498,6 +562,19 @@ index 0000000..ba0fa35 + return !held || !holdingTrapdoor() || PLACEMENT.allows(target, state); + } + ++ public static BlockHitResult blockHit(final BlockHitResult hit, final boolean frameRepeat) { ++ final var player = Minecraft.getInstance().player; ++ return held && frameRepeat && INPUT.blockActive() && player != null ++ && player.getMainHandItem().getItem() instanceof BlockItem ++ ? PATH.select(player.getEyePosition(1F), hit) : hit; ++ } ++ ++ public static void buildAttempt(final BlockPos position, final boolean success, final boolean atUpperBuildLimit) { ++ if (held) { ++ PATH.attempted(position, success, atUpperBuildLimit); ++ } ++ } ++ + public static void placed(final BlockState state, final BlockPos support, final Vec3 hit) { + placedBlock = true; + if (held) { @@ -541,6 +618,7 @@ index 0000000..ba0fa35 + INPUT.cancelPlacement(); + clearPrediction(); + PLACEMENT.clear(); ++ PATH.clear(); + } + + private static void reset() { @@ -548,6 +626,95 @@ index 0000000..ba0fa35 + INPUT.clear(); + } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPath.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPath.java +new file mode 100644 +index 0000000..dff8bff +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPath.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.input; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.Vec3; ++ ++/** Retains the next cell after adjacent build attempts establish a placement direction. */ ++public final class BlockPlacementPath { ++ private BlockPos previous; ++ private BlockPos next; ++ private Direction direction; ++ private boolean succeeded; ++ ++ public void attempted(final BlockPos position, final boolean success, final boolean atUpperBuildLimit) { ++ if (this.next == null && this.succeeded && this.previous != null && this.previous.distManhattan(position) == 1) { ++ final BlockPos delta = position.subtract(this.previous); ++ this.direction = Direction.getNearest(delta, Direction.UP); ++ this.next = position.immutable(); ++ } ++ if (success && position.equals(this.next)) { ++ this.next = position.relative(this.direction); ++ } ++ // Ordinary failure preserves the last build position; the upper height limit records its attempt. ++ if (success || atUpperBuildLimit) { ++ this.previous = position.immutable(); ++ } ++ this.succeeded |= success; ++ } ++ ++ public boolean active() { ++ return this.next != null; ++ } ++ ++ public BlockHitResult select(final Vec3 origin, final BlockHitResult hit) { ++ if (this.next == null) { ++ return hit; ++ } ++ return intersects(this.next, origin, hit.getLocation()) ++ ? hit.withPosition(this.next.relative(this.direction.getOpposite())).withDirection(this.direction) : null; ++ } ++ ++ // Native 1.26.60.5 segment/box SAT uses float midpoints, half extents and 0.0001 cross-axis tolerance. ++ static boolean intersects(final BlockPos cell, final Vec3 origin, final Vec3 end) { ++ final float ox = (float) origin.x; ++ final float oy = (float) origin.y; ++ final float oz = (float) origin.z; ++ final float dx = ((float) end.x - ox) * .5F; ++ final float dy = ((float) end.y - oy) * .5F; ++ final float dz = ((float) end.z - oz) * .5F; ++ final float minX = cell.getX(); ++ final float minY = cell.getY(); ++ final float minZ = cell.getZ(); ++ final float maxX = minX + 1F; ++ final float maxY = minY + 1F; ++ final float maxZ = minZ + 1F; ++ final float ex = (maxX - minX) * .5F; ++ final float ey = (maxY - minY) * .5F; ++ final float ez = (maxZ - minZ) * .5F; ++ final float x = (ox + dx) - (maxX + minX) * .5F; ++ final float y = (oy + dy) - (maxY + minY) * .5F; ++ final float z = (oz + dz) - (maxZ + minZ) * .5F; ++ final float ax = Math.abs(dx); ++ final float ay = Math.abs(dy); ++ final float az = Math.abs(dz); ++ return Math.abs(x) <= ax + ex && Math.abs(y) <= ay + ey && Math.abs(z) <= az + ez ++ && Math.abs(dy * z - y * dz) <= ay * ez + ey * az + .0001F ++ && Math.abs(dz * x - dx * z) <= az * ex + ax * ez + .0001F ++ && Math.abs(dx * y - dy * x) <= ax * ey + ay * ex + .0001F; ++ } ++ ++ public void clear() { ++ this.previous = null; ++ this.next = null; ++ this.direction = null; ++ this.succeeded = false; ++ } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePayload.java new file mode 100644 index 0000000..52df66b @@ -610,10 +777,10 @@ index 0000000..a98d448 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java new file mode 100644 -index 0000000..3bbe039 +index 0000000..6e20ef0 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java -@@ -0,0 +1,93 @@ +@@ -0,0 +1,94 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -628,6 +795,7 @@ index 0000000..3bbe039 +public final class ItemUseState { + private static final long AIR_DELAY_NANOS = 200_000_000L; + private static final long BLOCK_DELAY_NANOS = 300_000_000L; ++ private static final long LINE_DELAY_NANOS = 200_000_000L; + private boolean airActive; + private boolean airAttempted; + private long airStartedAt; @@ -686,9 +854,9 @@ index 0000000..3bbe039 + return this.airActive || this.blockActive || food; + } + -+ public boolean frameReady(final long now, final boolean food) { ++ public boolean frameReady(final long now, final boolean food, final boolean placementLine) { + return this.blockActive -+ ? !this.blockDelayed || now - this.blockStartedAt >= BLOCK_DELAY_NANOS ++ ? !this.blockDelayed || now - this.blockStartedAt > (placementLine ? LINE_DELAY_NANOS : BLOCK_DELAY_NANOS) + : this.canAttempt(now) && (this.airActive || food); + } + @@ -782,12 +950,146 @@ index f7c3bf4..416a16f 100644 "features.creative.MixinCreativeModeInventoryScreen", "features.creative.MixinCreativeModeTab_ItemDisplayBuilder", "features.creative.MixinCreativeModeTabs", +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPathTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPathTest.java +new file mode 100644 +index 0000000..cc7d3e8 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPathTest.java +@@ -0,0 +1,128 @@ ++/* ++ * 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.input; ++ ++import net.minecraft.core.BlockPos; ++import net.minecraft.core.Direction; ++import net.minecraft.world.phys.BlockHitResult; ++import net.minecraft.world.phys.Vec3; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++final class BlockPlacementPathTest { ++ private static final Vec3 EYE = new Vec3(.5F, -58.37998962402344F, -3.5F); ++ ++ @Test ++ void rejectsTheNewTopFaceUntilTheRayCrossesTheRetainedNextCell() { ++ final var path = northPath(); ++ final var top = new BlockHitResult(new Vec3(.5001634359359741F, -59F, -1.7961400747299194F), ++ Direction.UP, new BlockPos(0, -60, -2), false); ++ assertNull(path.select(EYE, top)); ++ ++ // Native re-aims onto the floor, but dispatches the retained north face of the last placed block. ++ final var floor = new BlockHitResult(new Vec3(.5001088380813599F, -60F, -2.365708589553833F), ++ Direction.UP, new BlockPos(0, -61, -3), false); ++ final var selected = path.select(EYE, floor); ++ assertNotNull(selected); ++ assertEquals(new BlockPos(0, -60, -2), selected.getBlockPos()); ++ assertEquals(Direction.NORTH, selected.getDirection()); ++ assertEquals(floor.getLocation(), selected.getLocation()); ++ assertEquals(new BlockPos(0, -61, -3), floor.getBlockPos()); ++ } ++ ++ @Test ++ void successfulBuildsAdvanceTheCellWhileRejectedBuildsKeepRetryingIt() { ++ final var path = northPath(); ++ path.attempted(new BlockPos(0, -60, -3), true, false); ++ final var hit = new BlockHitResult(new Vec3(.5000479817390442F, -59.09409713745117F, -3F), ++ Direction.NORTH, new BlockPos(0, -60, -3), false); ++ assertEquals(new BlockPos(0, -60, -3), path.select(EYE, hit).getBlockPos()); ++ path.attempted(new BlockPos(0, -60, -4), false, false); ++ assertEquals(new BlockPos(0, -60, -3), path.select(EYE, hit).getBlockPos()); ++ path.clear(); ++ assertSame(hit, path.select(EYE, hit)); ++ } ++ ++ @Test ++ void aRejectedFirstBuildOrNonadjacentTargetsDoNotStartAPlacementLine() { ++ final var path = new BlockPlacementPath(); ++ final var hit = new BlockHitResult(Vec3.ZERO, Direction.UP, BlockPos.ZERO, false); ++ path.attempted(BlockPos.ZERO, false, false); ++ path.attempted(new BlockPos(1, 0, 0), false, false); ++ assertSame(hit, path.select(EYE, hit)); ++ path.attempted(new BlockPos(4, 0, 0), true, false); ++ path.attempted(new BlockPos(6, 0, 0), true, false); ++ assertSame(hit, path.select(EYE, hit)); ++ } ++ ++ @Test ++ void retainsTheAxisForUpwardDownwardAndHorizontalLines() { ++ for (final Direction direction : Direction.values()) { ++ final var path = new BlockPlacementPath(); ++ path.attempted(BlockPos.ZERO, true, false); ++ path.attempted(BlockPos.ZERO.relative(direction), true, false); ++ final BlockPos next = BlockPos.ZERO.relative(direction, 2); ++ final var point = Vec3.atCenterOf(next); ++ final var hit = new BlockHitResult(point, Direction.UP, next, false); ++ final var selected = path.select(point, hit); ++ assertNotNull(selected, direction.name()); ++ assertEquals(direction, selected.getDirection()); ++ assertEquals(BlockPos.ZERO.relative(direction), selected.getBlockPos()); ++ } ++ } ++ ++ @Test ++ void anEstablishedLineUsesTheStrictShorterDelayAfterTheSecondBuild() { ++ final var path = new BlockPlacementPath(); ++ final var input = new ItemUseState(); ++ path.attempted(new BlockPos(0, -60, -1), true, false); ++ input.attemptedBlock(1_000_000_000L, true); ++ assertFalse(input.frameReady(1_300_000_000L, false, path.active())); ++ assertTrue(input.frameReady(1_300_000_001L, false, path.active())); ++ path.attempted(new BlockPos(0, -60, -2), true, false); ++ input.attemptedBlock(1_310_000_000L, true); ++ assertFalse(input.frameReady(1_510_000_000L, false, path.active())); ++ assertTrue(input.frameReady(1_510_000_001L, false, path.active())); ++ } ++ ++ @Test ++ void largeCoordinateCellsRetainNativeFloatCollapseInsteadOfGainingAnExtraExtent() { ++ assertFalse(BlockPlacementPath.intersects(new BlockPos(16_777_217, 0, 0), ++ new Vec3(16_777_216, 2.1F, .5F), new Vec3(16_777_218, .1F, .5F))); ++ } ++ ++ @Test ++ void aFailedDistantAttemptDoesNotReplaceTheLastSuccessfulBuildPosition() { ++ final var path = new BlockPlacementPath(); ++ path.attempted(new BlockPos(0, -60, -1), true, false); ++ path.attempted(new BlockPos(2, -60, -1), false, false); ++ path.attempted(new BlockPos(0, -60, -2), true, false); ++ final var top = new BlockHitResult(new Vec3(.5001634359359741F, -59F, -1.7961400747299194F), ++ Direction.UP, new BlockPos(0, -60, -2), false); ++ assertNull(path.select(EYE, top)); ++ } ++ ++ @Test ++ void theUpperBuildLimitKeepsItsSeparateAttemptHistoryException() { ++ final var path = new BlockPlacementPath(); ++ path.attempted(BlockPos.ZERO, true, false); ++ path.attempted(new BlockPos(3, 320, 0), false, true); ++ path.attempted(new BlockPos(3, 319, 0), true, false); ++ final Vec3 point = Vec3.atCenterOf(new BlockPos(3, 318, 0)); ++ final var hit = new BlockHitResult(point, Direction.UP, new BlockPos(3, 318, 0), false); ++ final var selected = path.select(point, hit); ++ assertEquals(Direction.DOWN, selected.getDirection()); ++ assertEquals(new BlockPos(3, 319, 0), selected.getBlockPos()); ++ } ++ ++ private static BlockPlacementPath northPath() { ++ final var path = new BlockPlacementPath(); ++ path.attempted(new BlockPos(0, -60, -1), true, false); ++ path.attempted(new BlockPos(0, -60, -2), true, false); ++ return path; ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java new file mode 100644 -index 0000000..c26e38b +index 0000000..4f27af1 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java -@@ -0,0 +1,121 @@ +@@ -0,0 +1,125 @@ +package com.viaversion.viafabricplus.bedrock.input; + +import org.junit.jupiter.api.Test; @@ -819,7 +1121,7 @@ index 0000000..c26e38b + final List usedAt = new ArrayList<>(); + for (final int milliseconds : new int[] {0, 47, 94, 141, 188, 236, 284, 332, 380, 428, 475, 542, 609, 676}) { + final long now = 1_000_000_000L + milliseconds * 1_000_000L; -+ if (timing.frameReady(now, true) && timing.admitAirUse(now, InteractionHand.MAIN_HAND, true, true)) { ++ if (timing.frameReady(now, true, false) && timing.admitAirUse(now, InteractionHand.MAIN_HAND, true, true)) { + usedAt.add(milliseconds); + } + } @@ -833,8 +1135,8 @@ index 0000000..c26e38b + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); + input.attemptedBlock(1_100_000_000L, true); -+ assertFalse(input.frameReady(1_200_000_000L, false)); -+ assertTrue(input.frameReady(1_200_000_001L, false)); ++ assertFalse(input.frameReady(1_200_000_000L, false, false)); ++ assertTrue(input.frameReady(1_200_000_001L, false, false)); + assertTrue(input.admitAirUse(1_200_000_001L, InteractionHand.MAIN_HAND, false, true)); + assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.OFF_HAND, false, true)); + input.cancelPlacement(); @@ -849,16 +1151,18 @@ index 0000000..c26e38b + void placementHoldDoesNotStartEatingWhenTheSelectedItemChanges() { + final var input = new ItemUseState(); + input.attemptedBlock(1_000_000_000L, true); -+ assertFalse(input.frameReady(1_200_000_001L, true)); -+ assertTrue(input.frameReady(1_300_000_000L, true)); ++ assertFalse(input.frameReady(1_200_000_001L, true, false)); ++ assertFalse(input.frameReady(1_300_000_000L, true, false)); ++ assertTrue(input.frameReady(1_300_000_001L, true, false)); + assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.OFF_HAND)); + assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.MAIN_HAND, true, true)); + input.attemptedBlock(1_310_000_000L, false); -+ assertTrue(input.frameReady(1_311_000_000L, true)); ++ assertTrue(input.frameReady(1_311_000_000L, true, false)); + input.attemptedBlock(1_320_000_000L, true); -+ assertFalse(input.frameReady(1_619_999_999L, false)); -+ assertTrue(input.frameReady(1_620_000_000L, false)); ++ assertFalse(input.frameReady(1_619_999_999L, false, false)); ++ assertFalse(input.frameReady(1_620_000_000L, false, false)); ++ assertTrue(input.frameReady(1_620_000_001L, false, false)); + input.release(); + assertTrue(input.admitAirUse(1_630_000_000L, InteractionHand.MAIN_HAND, true, true)); + assertFalse(input.canAttempt(1_800_000_000L)); @@ -873,13 +1177,14 @@ index 0000000..c26e38b + assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false)); + input.attemptedBlock(1_010_000_000L, false); + assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); -+ assertTrue(input.frameReady(1_052_000_000L, true)); ++ assertTrue(input.frameReady(1_052_000_000L, true, false)); + assertTrue(input.canBegin(1_052_000_000L, true)); + assertFalse(input.canBegin(1_052_000_000L, false)); + assertFalse(input.admitAirUse(1_052_000_000L, InteractionHand.MAIN_HAND, true, true)); + input.attemptedBlock(1_105_000_000L, true); -+ assertFalse(input.frameReady(1_404_999_999L, false)); -+ assertTrue(input.frameReady(1_405_000_000L, false)); ++ assertFalse(input.frameReady(1_404_999_999L, false, false)); ++ assertFalse(input.frameReady(1_405_000_000L, false, false)); ++ assertTrue(input.frameReady(1_405_000_001L, false, false)); + input.release(); + assertTrue(input.admitAirUse(1_500_000_000L, InteractionHand.MAIN_HAND, true, true)); + } @@ -890,8 +1195,8 @@ index 0000000..c26e38b + assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, true)); + assertTrue(input.admitAirUse(1_201_000_000L, InteractionHand.MAIN_HAND, true, false)); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); -+ assertFalse(input.frameReady(1_401_000_000L, false)); -+ assertTrue(input.frameReady(1_401_000_001L, false)); ++ assertFalse(input.frameReady(1_401_000_000L, false, false)); ++ assertTrue(input.frameReady(1_401_000_001L, false, false)); + } + + @Test @@ -901,12 +1206,13 @@ index 0000000..c26e38b + timing.attemptedBlock(start, true); + timing.release(); + assertTrue(timing.admitAirUse(start, InteractionHand.MAIN_HAND, true, true)); -+ assertFalse(timing.frameReady(start + 200_000_000L, true)); -+ assertTrue(timing.frameReady(start + 200_000_001L, true)); ++ assertFalse(timing.frameReady(start + 200_000_000L, true, false)); ++ assertTrue(timing.frameReady(start + 200_000_001L, true, false)); + timing.release(); + timing.attemptedBlock(start, true); -+ assertFalse(timing.frameReady(start + 299_999_999L, false)); -+ assertTrue(timing.frameReady(start + 300_000_000L, false)); ++ assertFalse(timing.frameReady(start + 299_999_999L, false, false)); ++ assertFalse(timing.frameReady(start + 300_000_000L, false, false)); ++ assertTrue(timing.frameReady(start + 300_000_001L, false, false)); + } +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacementTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacementTest.java diff --git a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md index 1cb4ee4d..2f2eb780 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md @@ -1,84 +1,72 @@ # Native held item use -Java's four-tick repeat path does not reproduce Bedrock's input timing. -Coordinate food retries and held block placement through one client input owner. +Coordinate food retries and block placement through one client input owner. Inventory consumption and completion remain server-owned. -Food uses a strict 200 ms monotonic deadline, checked after camera input on each frame. -Gate fresh calls before local use or packet publication. -Set the deadline before main-hand food air use, including a request that hunger later refuses. +Food uses a strict 200 ms monotonic deadline, shared by fresh calls and frame repeats after camera input. Select food from its transported component, including custom items carried as paper. -Physical release and item changes retain this connection deadline. -Connection replacement clears it. -Missing optional block-use channels do not clear the air-use hold or its deadline. - -Keep the interaction mode until physical release or cancellation. -A hold started by admitted food air use continues main-hand air use after selecting a block item. -Initially refused food retains block mode, even without an initial block target. -The same air deadline applies to fresh calls, while held block continuation bypasses it. -Skip block and entity dispatch inside the use method without changing the visible raycast. -A placement hold keeps its block route after selecting food and cannot fall through to eating. -Retire offhand fallback while either route owns the hold. -Release permits a fresh press to choose another interaction mode. - -Held placement waits 300 ms after a successful local block use. +A refused first food request retains block continuation while still recording the air deadline. +Already admitted air use retains its route after hunger fills or the selected item changes. +Block continuation bypasses the air deadline and cannot fall through to eating. +Offhand fallback cannot take over an owned hold. +Release and screens retire the mode; the connection retains its air deadline until replacement. +Missing optional block-use channels do not clear the air-use hold or deadline. + +Placement has a separate strict delay: 300 ms before a line is established and 200 ms afterward. +Successful local use restarts it; failed repeats do not. Trapdoor holds also retain the established first-attempt delay. -Initially refused food retains untimed block continuation until a block use succeeds. -Failed repeats do not restart that delay. -Release, screens, and connection replacement clear the placement timer and retained trapdoor state. -Suppress Java's parallel repeat path while the frame route owns the attempt. +The frame owner suppresses Java's parallel four-tick path. -## Placement constraints +## Placement selection -Retain the first predicted trapdoor facing, half, and world hit through material changes, temporary item-family changes, server correction, and stack exhaustion. -Apply that snap only while holding a trapdoor. -Other block items use fresh hits and suppress repeated activation; empty hands restore ordinary interaction. -Returning to a trapdoor restores the snap. +Adjacent build attempts establish a retained direction and next cell. +For a block hit, test the visible ray segment against that cell before dispatching use. +If it intersects, use the retained support and face while keeping the original world hit location. +A failed placement keeps the pending cell; successful placement advances it. +Release, screens, and connection replacement clear this state. +The owning patch records the matching native implementation and float arithmetic. -Send the physical lifetime and sequence-owned prediction before the concluding Java packet. -Prediction format 2 carries the result and native cursor; both consumers require the matching contract. -Express the retained hit relative to each later target, including coordinates outside its unit cube. -Keep the ordinary Java raycast packet unchanged. -Preserve each new block's water, power, open state, and material properties. +Trapdoors also retain the first predicted facing, half, and world hit across material changes, temporary item-family changes, correction, and exhaustion. +Apply that orientation only to trapdoors and preserve independent water, power, open state, and material properties. +Returning to a trapdoor restores its snap. Separate trapdoors and the original support remain valid targets. -Select native outlines and occlusion from the active Bedrock connection, including ViaProxy sessions with a local Java protocol target. -Native bottom-face hits use approximately 0.8175; the earlier 0.8125 attribution came from the Java fixture. +Send physical use lifetime and sequence-owned prediction before the concluding Java packet. +Prediction format 2 carries the result and native cursor, including coordinates outside the selected support cube. +Use the active Bedrock connection for native outlines and occlusion, including ViaProxy sessions with a local Java protocol target. +The visible raycast remains unchanged. ## Evidence and validation -Official Bedrock 1.26.60.5 against BDS 1.26.60.6 establishes these controls on protocol 2225. -Native runtime tracing observes the 200 ms deadline before air use and the strict check in the shared fresh-press and held-input function. -A 2.1-second instant-food hold consumes nine items with or without the debugger. -Frame cadence determines retry quantization, so equal counts alone do not establish timing parity. +Official Bedrock 1.26.60.5 and BDS 1.26.60.6 establish the selected behavior on protocol 2225. +Runtime tracing distinguishes fresh input, admitted air holds, and initially refused food. +It also observes the retained next-cell ray test, advancement, rejection, and clock transition. + +Fresh and refused-food Stone controls place two blocks, then reject a new top hit outside the pending cell. +Re-aiming onto the floor crosses that cell and places a third block through the retained north face. +The cursor keeps its original world location, yielding a negative relative Z coordinate. +Player-collision failures retain the next cell. +Returning to the old aim stops those attempts; a fresh press after release can use the top face. -Installed direct and ViaProxy food controls check the deadline, rapid clicks after release, pause cancellation, admission, hunger, and settled server counts. -The [food-use validation](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) records the current build and remaining use-cycle limits. -Native tracing observes fresh flags 176 followed by held air-only flag 16 across a food-to-trapdoor change. -Only the initial call visits block use; later frames retry air use after the deadline. -Unattached stone and late trapdoor controls agree. -The reverse trapdoor-to-food control retains simulation block uses and consumes no food. -Installed direct and proxy controls check both directions and fresh placement after release or Pause. -The previous installed client placed blocks after food use and ate food during the placement hold. -Seven input-state tests cover retained modes, hand and target routing, variable frames, release, connection reset, independent delays, strict boundaries, and rollover. -Five block-state tests cover retained orientation, materials, independent properties, targets, frozen hits, release, and ordinary blocks. +Seven input-state regressions cover ownership, hand/target routing, clocks, release, connection reset, and rollover. +Eight path regressions cover captured native float points, rejection, advancement, all six axes, release, the shorter line delay, and large-coordinate float collapse, and failed-attempt history. +Five trapdoor regressions cover orientation, materials, independent properties, targets, frozen hits, and ordinary blocks. +Unit cases for all axes do not establish live native parity in every direction. -Earlier installed placement fixtures match native camera changes, material changes, and fresh activation after release. -Both routes consume two items, leaving 62 from 64. -Native and direct Java retry a rejected first placement within the same hold. -Both Java routes cancel the session on Pause before physical release. +Installed direct and ViaProxy controls cover the formerly failing Stone cases, re-aiming, rejected placement, and fresh use after release. +Both routes also retain the six timed/refused-food controls, three earlier hold-mode controls, release/Pause, and eleven food-admission cases. +The [food-use validation](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) records admission and use-cycle limits. +The full four-project build passes 3,299 tests, with 325 skipped and no failures or errors. +Installed JAR checksums match the build. + +Earlier trapdoor fixtures cover camera and material changes, cancellation, reconciliation, and specific rejection histories. Fresh native and proxy sessions match repeated rejection followed by rotation without consumption. -After a preceding successful hold, both instead place a neighbor and consume one item. +After a preceding successful hold, both can instead place a neighbor and consume one item. ## Remaining limits -Other initial air-use families, live entity transitions, competing use handlers, exact input-flag phase counts, and full use-cycle timing need more native comparisons. +Live build-height limits, moving placement, air/entity target dispatch, other initial use families, mixed-use state, and complete input phases need more native comparisons. +A first rejected ordinary-block use still follows Java tick retries instead of native frame continuation. +An unchanged stack does not prove repeat-timing parity. +Frame cadence changes retry and instant-food counts, so these controls do not establish equal coordinates or complete scheduling parity. The original intermittent animation-without-feeding report remains open. - -Timed-food controls distinguish an early switch, a switch after completion, and a held three-completion cycle. -Initial refusal sends one air payload without `StartUsingItem`, then keeps block continuation. -An admitted air hold keeps its route after hunger fills. - -Ordinary Stone placement still differs after the ray reaches another face: both installed routes place four blocks where native places two. -The first two successes follow the 300 ms delay. The later placement selection remains unresolved. -Do not treat these mode and timing fixes as complete held-placement parity. From 2dff1d8b710229189f4668bb80fc8123c6a73633 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 02:09:56 +0100 Subject: [PATCH 19/47] fix(input): continue rejected block use from input frames Restore the first ordinary air phase after rejected block placement, then retain untimed block continuation. Record the first successful placement air deadline only when blocks remain, without duplicating the core transaction. Fold the change into the existing held-use patch. Native tracing and completed direct/proxy sessions verify rejection, first success, empty-target use, last-stack placement and activation. Add three input-state regressions and retain the broader compatibility limits. --- docs/bedrock-coverage.md | 2 +- ...0038-predict-held-trapdoor-placement.patch | 172 +++++++++++++----- ...0038-predict-held-trapdoor-placement.pr.md | 29 ++- 3 files changed, 150 insertions(+), 53 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 0b3807f9..469b3c0d 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -24,7 +24,7 @@ No complete area has verified Bedrock parity. | --- | --- | --- | | Protocol 2225 | Updated packet layouts, explicit enum values, stonecutter notifications, cursor drags, skin identity, and sound unions. | Block seating, signed content playback, matchmaking UI, and environment layers remain partial or unsupported. | | 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 block placement | Retained stationary build paths, next-cell ray selection, use timing, trapdoor orientation, cancellation, and prediction. | Native, direct, and proxy controls cover selected stationary paths, re-aiming, rejection, and release. Moving placement, air/entity dispatch, build-height limits, and first-rejected ordinary-block retry timing remain open. | +| Held block placement | Retained stationary build paths, next-cell ray selection, initial air phases, frame retries after rejection, use timing, trapdoor orientation, cancellation, and prediction. | Native, direct, and proxy controls cover selected stationary paths, first rejection, last-stack placement, activation, re-aiming, and release. Moving placement, air/entity dispatch, build-height limits, and broader input scheduling remain open. | | 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, Creative catalog actions, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | | Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Broader use cycles and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md). | 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 50890056..222599af 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 7e5778f2d845977fa2f93a6d1a88ee88ddb22cb1 Mon Sep 17 00:00:00 2001 +From 302e0708e38e062c8b6fbe0843854a72759c606e 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): preserve native held item use @@ -21,7 +21,6 @@ Native bottom-face hits are about 0.8175, including translated proxy joins. Keep two clock lifetimes: air use has a strict 200 ms connection deadline; held placement waits strictly more than 300 ms before an established line exists, then strictly more than 200 ms after each line success. -Trapdoor holds also retain the established first-attempt delay. Failed placement repeats do not restart their delay. Retry from input frames after camera movement, before the next tick publishes rotation, and suppress Java's parallel four-tick path. Native frame quantization @@ -43,8 +42,9 @@ before main-hand food use even when hunger refuses it. Initial refusal retains untimed block mode, rather than acquiring the air route. An owned air hold skips block/entity dispatch and offhand fallback; newly selected items share its air deadline. A placement hold cannot fall through to eating. -Canonical consumption/completion remain server-owned. Other initial air-use -families retain their existing route pending matching controls. +Canonical consumption/completion remain server-owned. Initial block-item +air use retains untimed block mode. Other initial air-use families retain +their existing route pending matching controls. The installed baseline places four Stone blocks after a food hold and consumes six foods after a trapdoor hold, unlike native. Input-state tests @@ -104,23 +104,45 @@ Keep that exception separate from the first-success flag and pending-cell advancement. Regressions cover a failed distant attempt before returning to the original line, and the upper-height history branch. Live height limits and broader mixed-use cases remain separate validation work. + +Native first-rejected Stone and trapdoor controls invoke ordinary air use +once, write the strict 200 ms deadline, then keep flags 17 and untimed +block continuation. Java returns before that air phase on FAIL. Permit +its initial main-hand block fallback, acquire block mode from the air +phase, and prevent subsequent air/offhand fallback. The same native +unattached-Stone control selects this block mode without a block target. +Remove the old trapdoor-only first-rejection delay. + +Record the client air deadline for the first successful placement only +when an item remains. Core patch 0091 already emits that air transaction. Do not +send another from the add-on. Held placement success must not refresh +this deadline. Native last-block placement and trapdoor activation write +no air deadline; leave those phases separate. Current Bedrock 1.26.60.5 tracing +observes setter 1465c0050 from 14658623d before air use 14376e0c0, +plus repeat 14376b8d0 and the resulting protocol 2225 transactions. + +Input regressions cover initial rejection, immediate frame continuation, +one air phase, later success, first-placement deadline ownership, held +success, last-stack exclusion and release into food. Native capture frame +cadence differs from Java, so retry counts are not equality assertions. +Rapid input edge sampling and broader item dispatch remain open. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../features/block/MixinTrapDoorBlock.java | 18 +- - .../features/input/MixinItemUseControl.java | 96 ++++++++ + .../features/input/MixinItemUseControl.java | 97 ++++++++ .../inventory/MixinHeldBlockPlacement.java | 32 +++ .../features/inventory/MixinHeldBlockUse.java | 61 +++++ - .../bedrock/input/BedrockItemUse.java | 209 ++++++++++++++++++ + .../bedrock/input/BedrockItemUse.java | 216 ++++++++++++++++++ .../bedrock/input/BlockPlacementPath.java | 83 +++++++ .../bedrock/input/BlockUsePayload.java | 24 ++ .../input/BlockUsePredictionPayload.java | 24 ++ - .../bedrock/input/ItemUseState.java | 94 ++++++++ + .../bedrock/input/ItemUseState.java | 104 +++++++++ .../bedrock/inventory/TrapdoorPlacement.java | 55 +++++ .../viafabricplus-bedrock.mixins.json | 3 + .../bedrock/input/BlockPlacementPathTest.java | 128 +++++++++++ - .../bedrock/input/ItemUseStateTest.java | 125 +++++++++++ - .../inventory/TrapdoorPlacementTest.java | 100 +++++++++ - 15 files changed, 1050 insertions(+), 4 deletions(-) + .../bedrock/input/ItemUseStateTest.java | 167 ++++++++++++++ + .../inventory/TrapdoorPlacementTest.java | 100 ++++++++ + 15 files changed, 1110 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockUse.java @@ -206,10 +228,10 @@ index 82db187..eda72b3 100644 return super.getOcclusionShape(state); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java new file mode 100644 -index 0000000..21a773c +index 0000000..d94e0c5 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java -@@ -0,0 +1,96 @@ +@@ -0,0 +1,97 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -263,7 +285,8 @@ index 0000000..21a773c + final InteractionHand hand, final BlockHitResult hit, + final Operation original) { + final BlockHitResult selected = BedrockItemUse.blockHit(hit, this.viaFabricPlusBedrock$frameRepeat); -+ return selected == null ? InteractionResult.FAIL : original.call(gameMode, player, hand, selected); ++ return selected == null ? InteractionResult.FAIL ++ : BedrockItemUse.blockResult(original.call(gameMode, player, hand, selected), hand); + } + + @WrapOperation(method = "startUseItem", at = @At(value = "INVOKE", @@ -413,10 +436,10 @@ index 0000000..69387b7 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java new file mode 100644 -index 0000000..6633a2b +index 0000000..854a58b --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java -@@ -0,0 +1,209 @@ +@@ -0,0 +1,216 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -433,6 +456,7 @@ index 0000000..6633a2b +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.InteractionHand; ++import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.level.block.TrapDoorBlock; @@ -479,7 +503,7 @@ index 0000000..6633a2b + } + final var consumable = item.get(DataComponents.CONSUMABLE); + final boolean canConsume = consumable != null && consumable.canConsume(Minecraft.getInstance().player, item); -+ return INPUT.admitAirUse(System.nanoTime(), hand, item.has(DataComponents.FOOD), canConsume); ++ return INPUT.admitAirUse(System.nanoTime(), hand, item.has(DataComponents.FOOD), held && item.getItem() instanceof BlockItem, canConsume); + } + + public static HitResult.Type hitType(final HitResult.Type target, final InteractionHand hand) { @@ -529,12 +553,16 @@ index 0000000..6633a2b + && block.getBlock() instanceof TrapDoorBlock; + } + -+ private static boolean usesNativeRepeatDelay() { -+ return held && (PLACEMENT.active() || holdingTrapdoor()); ++ public static InteractionResult blockResult(final InteractionResult result, final InteractionHand hand) { ++ final var player = Minecraft.getInstance().player; ++ // Java stops at FAIL; native still invokes the block item's first ordinary air use. ++ return held && hand == InteractionHand.MAIN_HAND && !INPUT.blockActive() && player != null ++ && player.getMainHandItem().getItem() instanceof BlockItem && result instanceof InteractionResult.Fail ++ ? InteractionResult.PASS : result; + } + + public static void attempted() { -+ if (held && (successfulAttempt || INPUT.blockActive() || usesNativeRepeatDelay())) { ++ if (held && (successfulAttempt || INPUT.blockActive())) { + INPUT.attemptedBlock(System.nanoTime(), successfulAttempt); + } + } @@ -578,6 +606,8 @@ index 0000000..6633a2b + public static void placed(final BlockState state, final BlockPos support, final Vec3 hit) { + placedBlock = true; + if (held) { ++ // Core sends this first air phase after successful placement; retain its client deadline. ++ INPUT.placedBlock(System.nanoTime(), !Minecraft.getInstance().player.getMainHandItem().isEmpty()); + PLACEMENT.remember(state, support, hit); + } + } @@ -777,10 +807,10 @@ index 0000000..a98d448 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java new file mode 100644 -index 0000000..6e20ef0 +index 0000000..334d0bf --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java -@@ -0,0 +1,94 @@ +@@ -0,0 +1,104 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -811,27 +841,37 @@ index 0000000..6e20ef0 + return frameRepeat && this.blockActive || this.canAttempt(now); + } + -+ public boolean admitAirUse(final long now, final InteractionHand hand, final boolean food, final boolean canConsume) { ++ public boolean admitAirUse(final long now, final InteractionHand hand, final boolean food, final boolean blockItem, final boolean canConsume) { + if (hand != InteractionHand.MAIN_HAND) { + return !this.airActive && !this.blockActive; + } + if (this.blockActive) { + return false; + } -+ if (!this.airActive && !food) { ++ if (!this.airActive && !food && !blockItem) { + return true; + } + if (!this.canAttempt(now)) { + return false; + } -+ if (this.airActive || canConsume) { ++ if (this.airActive || food && canConsume) { + this.airActive = true; + } else { + this.blockActive = true; + } ++ this.recordAirUse(now); ++ return true; ++ } ++ ++ public void placedBlock(final long now, final boolean hasRemainder) { ++ if (!this.airActive && !this.blockActive && hasRemainder) { ++ this.recordAirUse(now); ++ } ++ } ++ ++ private void recordAirUse(final long now) { + this.airAttempted = true; + this.airStartedAt = now; -+ return true; + } + + public HitResult.Type hitType(final HitResult.Type target, final InteractionHand hand) { @@ -1086,10 +1126,10 @@ index 0000000..cc7d3e8 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java new file mode 100644 -index 0000000..4f27af1 +index 0000000..a97e4b1 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java -@@ -0,0 +1,125 @@ +@@ -0,0 +1,167 @@ +package com.viaversion.viafabricplus.bedrock.input; + +import org.junit.jupiter.api.Test; @@ -1105,14 +1145,14 @@ index 0000000..4f27af1 + @Test + void releasedFoodKeepsItsDeadlineForFreshClicksUntilConnectionReplacement() { + final var timing = new ItemUseState(); -+ assertTrue(timing.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertTrue(timing.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + timing.release(); -+ assertFalse(timing.admitAirUse(1_131_000_000L, InteractionHand.MAIN_HAND, true, true)); -+ assertFalse(timing.admitAirUse(1_200_000_000L, InteractionHand.MAIN_HAND, true, true)); -+ assertTrue(timing.admitAirUse(1_616_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertFalse(timing.admitAirUse(1_131_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ assertFalse(timing.admitAirUse(1_200_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ assertTrue(timing.admitAirUse(1_616_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + assertFalse(timing.canAttempt(1_700_000_000L)); + timing.clear(); -+ assertTrue(timing.admitAirUse(1_700_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertTrue(timing.admitAirUse(1_700_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + } + + @Test @@ -1121,7 +1161,7 @@ index 0000000..4f27af1 + final List usedAt = new ArrayList<>(); + for (final int milliseconds : new int[] {0, 47, 94, 141, 188, 236, 284, 332, 380, 428, 475, 542, 609, 676}) { + final long now = 1_000_000_000L + milliseconds * 1_000_000L; -+ if (timing.frameReady(now, true, false) && timing.admitAirUse(now, InteractionHand.MAIN_HAND, true, true)) { ++ if (timing.frameReady(now, true, false) && timing.admitAirUse(now, InteractionHand.MAIN_HAND, true, false, true)) { + usedAt.add(milliseconds); + } + } @@ -1131,14 +1171,14 @@ index 0000000..4f27af1 + @Test + void foodHoldKeepsAirUseAcrossBlockAndEntityTargetsUntilRelease() { + final var input = new ItemUseState(); -+ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); + input.attemptedBlock(1_100_000_000L, true); + assertFalse(input.frameReady(1_200_000_000L, false, false)); + assertTrue(input.frameReady(1_200_000_001L, false, false)); -+ assertTrue(input.admitAirUse(1_200_000_001L, InteractionHand.MAIN_HAND, false, true)); -+ assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.OFF_HAND, false, true)); ++ assertTrue(input.admitAirUse(1_200_000_001L, InteractionHand.MAIN_HAND, false, false, true)); ++ assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.OFF_HAND, false, false, true)); + input.cancelPlacement(); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + input.release(); @@ -1156,7 +1196,7 @@ index 0000000..4f27af1 + assertTrue(input.frameReady(1_300_000_001L, true, false)); + assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.OFF_HAND)); -+ assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + input.attemptedBlock(1_310_000_000L, false); + assertTrue(input.frameReady(1_311_000_000L, true, false)); + input.attemptedBlock(1_320_000_000L, true); @@ -1164,48 +1204,90 @@ index 0000000..4f27af1 + assertFalse(input.frameReady(1_620_000_000L, false, false)); + assertTrue(input.frameReady(1_620_000_001L, false, false)); + input.release(); -+ assertTrue(input.admitAirUse(1_630_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertTrue(input.admitAirUse(1_630_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + assertFalse(input.canAttempt(1_800_000_000L)); + input.clear(); + assertEquals(HitResult.Type.ENTITY, input.hitType(HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); -+ assertTrue(input.admitAirUse(1_800_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertTrue(input.admitAirUse(1_800_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + } + + @Test + void initiallyRefusedFoodKeepsUntimedBlockRepeatsWhileFreshCallsKeepTheAirDeadline() { + final var input = new ItemUseState(); -+ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false)); ++ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, false)); + input.attemptedBlock(1_010_000_000L, false); + assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + assertTrue(input.frameReady(1_052_000_000L, true, false)); + assertTrue(input.canBegin(1_052_000_000L, true)); + assertFalse(input.canBegin(1_052_000_000L, false)); -+ assertFalse(input.admitAirUse(1_052_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertFalse(input.admitAirUse(1_052_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + input.attemptedBlock(1_105_000_000L, true); + assertFalse(input.frameReady(1_404_999_999L, false, false)); + assertFalse(input.frameReady(1_405_000_000L, false, false)); + assertTrue(input.frameReady(1_405_000_001L, false, false)); + input.release(); -+ assertTrue(input.admitAirUse(1_500_000_000L, InteractionHand.MAIN_HAND, true, true)); ++ assertTrue(input.admitAirUse(1_500_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + } + + @Test + void foodThatBecomesRefusedDuringAnAirHoldRetainsItsAirRouteAndDeadline() { + final var input = new ItemUseState(); -+ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, true)); -+ assertTrue(input.admitAirUse(1_201_000_000L, InteractionHand.MAIN_HAND, true, false)); ++ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ assertTrue(input.admitAirUse(1_201_000_000L, InteractionHand.MAIN_HAND, true, false, false)); + assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + assertFalse(input.frameReady(1_401_000_000L, false, false)); + assertTrue(input.frameReady(1_401_000_001L, false, false)); + } + + @Test ++ void rejectedBlockStartsUntimedFrameContinuationAndOnlyOneAirUse() { ++ final var input = new ItemUseState(); ++ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, false, true, false)); ++ input.attemptedBlock(1_001_000_000L, false); ++ assertTrue(input.blockActive()); ++ assertTrue(input.frameReady(1_016_000_000L, false, false)); ++ assertTrue(input.canBegin(1_016_000_000L, true)); ++ assertFalse(input.canBegin(1_016_000_000L, false)); ++ assertFalse(input.admitAirUse(1_016_000_000L, InteractionHand.MAIN_HAND, false, true, false)); ++ assertFalse(input.admitAirUse(1_016_000_000L, InteractionHand.OFF_HAND, true, false, true)); ++ input.attemptedBlock(1_080_000_000L, false); ++ assertTrue(input.frameReady(1_096_000_000L, false, false)); ++ input.attemptedBlock(1_100_000_000L, true); ++ assertFalse(input.frameReady(1_400_000_000L, false, false)); ++ assertTrue(input.frameReady(1_400_000_001L, false, false)); ++ } ++ ++ @Test ++ void successfulFirstPlacementSharesItsAirDeadlineButHeldPlacementDoesNotRestartIt() { ++ final var input = new ItemUseState(); ++ input.placedBlock(1_000_000_000L, true); ++ input.attemptedBlock(1_001_000_000L, true); ++ assertFalse(input.canBegin(1_200_000_000L, false)); ++ assertTrue(input.canBegin(1_200_000_001L, false)); ++ input.placedBlock(1_350_000_000L, true); ++ input.attemptedBlock(1_351_000_000L, true); ++ input.release(); ++ assertTrue(input.canBegin(1_400_000_000L, false)); ++ assertTrue(input.admitAirUse(1_400_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ assertFalse(input.canBegin(1_600_000_000L, false)); ++ } ++ ++ @Test ++ void placingTheLastBlockDoesNotCreateAnAirDeadline() { ++ final var input = new ItemUseState(); ++ input.placedBlock(1_000_000_000L, false); ++ input.attemptedBlock(1_001_000_000L, true); ++ input.release(); ++ assertTrue(input.admitAirUse(1_050_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ } ++ ++ @Test + void monotonicClockRolloverKeepsBothDeadlines() { + final var timing = new ItemUseState(); + final long start = Long.MAX_VALUE - 150_000_000L; + timing.attemptedBlock(start, true); + timing.release(); -+ assertTrue(timing.admitAirUse(start, InteractionHand.MAIN_HAND, true, true)); ++ assertTrue(timing.admitAirUse(start, InteractionHand.MAIN_HAND, true, false, true)); + assertFalse(timing.frameReady(start + 200_000_000L, true, false)); + assertTrue(timing.frameReady(start + 200_000_001L, true, false)); + timing.release(); diff --git a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md index 2f2eb780..d7669c92 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md @@ -3,9 +3,14 @@ Coordinate food retries and block placement through one client input owner. Inventory consumption and completion remain server-owned. -Food uses a strict 200 ms monotonic deadline, shared by fresh calls and frame repeats after camera input. +Food and the first block-item air phase share a strict 200 ms monotonic deadline. +Fresh calls retain that deadline after release. +Frame repeats run after camera input. Select food from its transported component, including custom items carried as paper. A refused first food request retains block continuation while still recording the air deadline. +The first rejected block placement also performs one air use and acquires untimed block continuation. +Java normally stops before that air phase on `FAIL`. +The client permits that first fallback and prevents later air or offhand fallback. Already admitted air use retains its route after hunger fills or the selected item changes. Block continuation bypasses the air deadline and cannot fall through to eating. Offhand fallback cannot take over an owned hold. @@ -14,8 +19,11 @@ Missing optional block-use channels do not clear the air-use hold or deadline. Placement has a separate strict delay: 300 ms before a line is established and 200 ms afterward. Successful local use restarts it; failed repeats do not. -Trapdoor holds also retain the established first-attempt delay. The frame owner suppresses Java's parallel four-tick path. +A first successful placement records the air deadline when blocks remain. +Core already sends that air phase, so the client does not send another transaction. +Held placement success does not refresh the air deadline. +Last-block placement and block activation do not create an air deadline. ## Placement selection @@ -41,6 +49,9 @@ The visible raycast remains unchanged. Official Bedrock 1.26.60.5 and BDS 1.26.60.6 establish the selected behavior on protocol 2225. Runtime tracing distinguishes fresh input, admitted air holds, and initially refused food. It also observes the retained next-cell ray test, advancement, rejection, and clock transition. +First-rejected Stone and trapdoor controls send failed block use, one air use, then immediate frame retries. +Unattached Stone selects the same block continuation. +Successful placement, last-block placement, and trapdoor activation establish the distinct air phases above. Fresh and refused-food Stone controls place two blocks, then reject a new top hit outside the pending cell. Re-aiming onto the floor crosses that cell and places a third block through the retained north face. @@ -48,15 +59,18 @@ The cursor keeps its original world location, yielding a negative relative Z coo Player-collision failures retain the next cell. Returning to the old aim stops those attempts; a fresh press after release can use the top face. -Seven input-state regressions cover ownership, hand/target routing, clocks, release, connection reset, and rollover. -Eight path regressions cover captured native float points, rejection, advancement, all six axes, release, the shorter line delay, and large-coordinate float collapse, and failed-attempt history. +Ten input-state regressions cover ownership, hand/target routing, initial rejection, clocks, last-stack placement, release, connection reset, and rollover. +Eight path regressions cover native float points, rejection, advancement, all six axes, release, line delays, precision limits, and failed-attempt history. Five trapdoor regressions cover orientation, materials, independent properties, targets, frozen hits, and ordinary blocks. Unit cases for all axes do not establish live native parity in every direction. Installed direct and ViaProxy controls cover the formerly failing Stone cases, re-aiming, rejected placement, and fresh use after release. +First-rejected Stone and trapdoor controls send one native air payload, then immediate simulation retries without consuming items. +Their air fields and fresh/simulation triggers match native, excluding player-coordinate equality. +Both routes also verify unattached block use, first-success air use, last-stack exclusion, and activation without air use. Both routes also retain the six timed/refused-food controls, three earlier hold-mode controls, release/Pause, and eleven food-admission cases. The [food-use validation](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) records admission and use-cycle limits. -The full four-project build passes 3,299 tests, with 325 skipped and no failures or errors. +The full four-project build passes 3,302 tests, with 325 skipped and no failures or errors. Installed JAR checksums match the build. Earlier trapdoor fixtures cover camera and material changes, cancellation, reconciliation, and specific rejection histories. @@ -65,8 +79,9 @@ After a preceding successful hold, both can instead place a neighbor and consume ## Remaining limits -Live build-height limits, moving placement, air/entity target dispatch, other initial use families, mixed-use state, and complete input phases need more native comparisons. -A first rejected ordinary-block use still follows Java tick retries instead of native frame continuation. +Live build-height limits, moving placement, and air/entity target dispatch need more native comparisons. +Other initial use families, mixed-use state, and complete input phases also remain open. An unchanged stack does not prove repeat-timing parity. Frame cadence changes retry and instant-food counts, so these controls do not establish equal coordinates or complete scheduling parity. +Short native rapid-click trials did not sample every physical edge and do not establish edge parity. The original intermittent animation-without-feeding report remains open. From 6d0c5b58b285632e2ed66817165bf033b4f51ac0 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 03:17:40 +0100 Subject: [PATCH 20/47] fix(input): separate block dispatch from air admission Allow fresh block uses before the previous air deadline while retaining that deadline for air and fresh entity admission. Negotiate sequence-owned air admission so core can omit only the extra air phase after a successful early placement, with the legacy channel preserved. Keep consumed clicks separate from tick and frame continuation. Require initial ownership before frame retries and retain empty-hand block continuation. Native tracing and final direct/proxy controls verify the selected boundaries; six new regressions cover admission, legacy decoding, ownership and targets. --- docs/bedrock-coverage.md | 4 +- ...erver-authoritative-bedrock-requests.patch | 10 +- ...edrock-recipes-and-place-ingredients.patch | 10 +- ...08-signal-sustained-bedrock-item-use.patch | 4 +- ...ain-bedrock-creative-catalog-entries.patch | 4 +- ...ronize-glide-rockets-with-auth-input.patch | 4 +- ...1-confirm-survival-offhand-transfers.patch | 4 +- ...etain-native-held-block-use-sessions.patch | 165 +++++--- ...etain-native-held-block-use-sessions.pr.md | 24 +- ...transport-resolved-native-fog-stacks.patch | 6 +- ...0038-predict-held-trapdoor-placement.patch | 353 ++++++++++++------ ...0038-predict-held-trapdoor-placement.pr.md | 29 +- 12 files changed, 425 insertions(+), 192 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 469b3c0d..fce9075d 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -24,10 +24,10 @@ No complete area has verified Bedrock parity. | --- | --- | --- | | Protocol 2225 | Updated packet layouts, explicit enum values, stonecutter notifications, cursor drags, skin identity, and sound unions. | Block seating, signed content playback, matchmaking UI, and environment layers remain partial or unsupported. | | 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 block placement | Retained stationary build paths, next-cell ray selection, initial air phases, frame retries after rejection, use timing, trapdoor orientation, cancellation, and prediction. | Native, direct, and proxy controls cover selected stationary paths, first rejection, last-stack placement, activation, re-aiming, and release. Moving placement, air/entity dispatch, build-height limits, and broader input scheduling remain open. | +| Held block placement | Retained stationary build paths, next-cell ray selection, independent fresh block/air admission, frame retries after rejection, use timing, trapdoor orientation, cancellation, and prediction. | Native, direct, and proxy controls cover selected stationary paths, first rejection, last-stack placement, activation, re-aiming, and release. Moving placement, air/entity dispatch, build-height limits, and broader input scheduling remain open. | | 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, Creative catalog actions, Survival bulk and manual crafting actions, retained recipe queries, compressed snapshots, local Help, inactive item lighting, native storage weights, and native stack identities. | Complete E-key/workbench presentation, bundle actions/tooltips, query lifetime across connections, broader Survival recipes, and original live bug reports remain unresolved. See the [inventory reference](native-player-inventory.md). | -| Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Broader use cycles and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md). | +| Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Short input edges during active Java use, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch index cb8714d1..21df092e 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 d20dfd5a5d8d43e8f8307ff72df48f9dba1e8077 Mon Sep 17 00:00:00 2001 +From 220b535527f2c106a7e0e5df6dc7f5f16d7dee67 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 @@ -9546,7 +9546,7 @@ index 36f0e61c..16803a78 100644 } 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 01389150..081338cb 100644 +index 223db3d9..d8e307df 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -104,42 +104,39 @@ public final class InteractionPackets { @@ -11375,7 +11375,7 @@ index 5af0eca2..fa556209 100644 itemDefinitions.addFromNetworkTag(itemEntry.identifier(), itemEntry.componentData()); } 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 d41a2a00..9c74bc69 100644 +index 67129d78..57b24cc8 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java @@ -17,17 +17,14 @@ @@ -11420,7 +11420,7 @@ index d41a2a00..9c74bc69 100644 if (channel.equals(CameraAimAssistActivation.Control.CHANNEL)) { if (wrapper.user().getProtocolInfo().getClientState() == State.PLAY) { final var input = io.netty.buffer.Unpooled.wrappedBuffer(wrapper.read(Types.REMAINING_BYTES)); -@@ -264,6 +272,10 @@ public final class MultiStatePackets { +@@ -265,6 +273,10 @@ public final class MultiStatePackets { if (channels.contains(EmoteProvider.CHANNEL) || channels.contains(EmoteAction.Policy.CHANNEL)) { EmoteProvider.refreshActionCapability(wrapper.user()); } @@ -11431,7 +11431,7 @@ index d41a2a00..9c74bc69 100644 if (channels.contains(PlayerInputPermissions.CHANNEL) && wrapper.user().has(PlayerInputStorage.class)) { wrapper.user().get(PlayerInputStorage.class).withdrawRawInput(); } -@@ -351,6 +363,10 @@ public final class MultiStatePackets { +@@ -353,6 +365,10 @@ public final class MultiStatePackets { if (channels.contains(EmoteProvider.CHANNEL) || channels.contains(EmoteAction.Policy.CHANNEL)) { EmoteProvider.refreshActionCapability(wrapper.user()); } 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 18d915d9..5045d1ee 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 8c8555fd27e04260b6cfa28afeccf7dd94cd5188 Mon Sep 17 00:00:00 2001 +From e5a2c73169b4716d835b00d94b971701542d529e 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 @@ -4242,7 +4242,7 @@ index fa556209..5d9f0fb3 100644 user.get(FogStorage.class).markJoined(); user.get(FogStorage.class).sendSnapshot(); 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 9c74bc69..f5e0d204 100644 +index 57b24cc8..8a380c35 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java @@ -20,7 +20,14 @@ package net.raphimc.viabedrock.protocol.packet; @@ -4305,7 +4305,7 @@ index 9c74bc69..f5e0d204 100644 if (channel.equals(NativeContainerMessage.CURRENT_CHANNEL)) { if (wrapper.user().getProtocolInfo().getClientState() == State.PLAY && wrapper.user().get(ChannelStorage.class).hasChannel(channel) -@@ -276,6 +314,12 @@ public final class MultiStatePackets { +@@ -277,6 +315,12 @@ public final class MultiStatePackets { if (screens != null) { screens.refreshCapability(); } @@ -4318,7 +4318,7 @@ index 9c74bc69..f5e0d204 100644 if (channels.contains(PlayerInputPermissions.CHANNEL) && wrapper.user().has(PlayerInputStorage.class)) { wrapper.user().get(PlayerInputStorage.class).withdrawRawInput(); } -@@ -288,6 +332,11 @@ public final class MultiStatePackets { +@@ -289,6 +333,11 @@ public final class MultiStatePackets { && wrapper.user().has(NativeContainerUiState.class)) { wrapper.user().get(NativeContainerUiState.class).clearActions(); } @@ -4330,7 +4330,7 @@ index 9c74bc69..f5e0d204 100644 if (wrapper.user().has(VehiclePredictionStorage.class)) { wrapper.user().get(VehiclePredictionStorage.class).refresh(); } -@@ -367,6 +416,12 @@ public final class MultiStatePackets { +@@ -369,6 +418,12 @@ public final class MultiStatePackets { if (screens != null) { screens.refreshCapability(); } diff --git a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch index 54806f5c..e818ab88 100644 --- a/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch +++ b/patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.patch @@ -1,4 +1,4 @@ -From 173f2b1402f8b56343dce93e6856de849613851b Mon Sep 17 00:00:00 2001 +From a993a09cae274a0aa62143078ddb5ccb85017c4c Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(interaction): signal sustained Bedrock item use @@ -220,7 +220,7 @@ index a906b932..03ebaf51 100644 return this.definitions.definitions().stream().filter(definition -> definition.receiver().component()) .map(NativeItemDefinitions.Definition::identifier).collect(Collectors.toUnmodifiableSet()); 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 081338cb..203cd594 100644 +index d8e307df..b1ffa95c 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -46,10 +46,14 @@ import net.raphimc.viabedrock.protocol.ServerboundBedrockPackets; 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 b15fa7a8..0610d046 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 4c5cc7e5e60ec3deeb6d8d66c24d0d49a7284061 Mon Sep 17 00:00:00 2001 +From 455e1690491b995eb190e70e5efd5175034b9b7b 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 @@ -2669,7 +2669,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..a3e297da 100644 +index c47f9dde..cf71a5b3 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 @@ 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 420bb330..830db6a8 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,4 +1,4 @@ -From 5d016bec6e5e0f4593a4a0cb0cc6b70de79e77a7 Mon Sep 17 00:00:00 2001 +From b4d843ac3da79844de4cdf07cdc0ebd092c85b09 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:37 +0200 Subject: [PATCH] fix(interaction): preserve native air-use prediction and @@ -151,7 +151,7 @@ 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 203cd594..58b3ee44 100644 +index b1ffa95c..567b27df 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -21,6 +21,7 @@ import com.viaversion.viaversion.api.minecraft.BlockFace; diff --git a/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch b/patches/viabedrock/deferred/0021-confirm-survival-offhand-transfers.patch index ed1f4c6d..74700771 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 cc017567c3740e16afd9b84f20730e887ec64ec6 Mon Sep 17 00:00:00 2001 +From a87f46e12536e5d3afdef00987a1aaeec15f89b5 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 @@ -277,7 +277,7 @@ index 176bdfd5..c7461506 100644 throws InformativeException { 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 +index 8a380c35..c47f9dde 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java @@ -19,6 +19,10 @@ package net.raphimc.viabedrock.protocol.packet; diff --git a/patches/viabedrock/upstreamable/0091-retain-native-held-block-use-sessions.patch b/patches/viabedrock/upstreamable/0091-retain-native-held-block-use-sessions.patch index df4af5b8..b71fa9d0 100644 --- a/patches/viabedrock/upstreamable/0091-retain-native-held-block-use-sessions.patch +++ b/patches/viabedrock/upstreamable/0091-retain-native-held-block-use-sessions.patch @@ -1,4 +1,4 @@ -From 9c8c517925758438a76f556d0237f6810da64ccc Mon Sep 17 00:00:00 2001 +From eb7c9aec7d7bef63fa4a757a3e94c5ce23de41f9 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:34:25 +0100 Subject: [PATCH] feat(interaction): retain native held block-use sessions @@ -14,8 +14,8 @@ world hit even outside later target cubes. Reject non-finite coordinates and stale sequence ownership without changing the Java raycast packet. Start the session only after predicted success. Native Survival predicts -one consumed block and sends its remaining stack in the first use-in-air -phase. Use a legacy request while the predicted stack remains nonempty; +one consumed block. Send its remaining stack in the first use-in-air +phase only when the client admits that phase. Use a legacy request while the predicted stack remains nonempty; consuming the last block uses ID zero and skips the empty use-in-air phase. Creative placements, failed attempts and ordinary interactions carry no placement delta. Retain predicted counts until authoritative correction, @@ -40,21 +40,36 @@ The extra first use-in-air phase follows placement, not block activation. A fresh trapdoor activation consumes the interaction without calling the block item's air use. Failed use-on attempts retain Java's ordinary air fallback rather than synthesizing another phase. + +Fresh native block dispatch is independent of the strict 200 ms air +clock. A verified current-build fresh callback dispatches rejected block +use 27 ms before that clock expires, without another air transaction. +The same source gates the air phase after successful placement. Do not +synthesize an air packet solely because a fresh placement succeeded. + +Negotiate prediction format 3 on a distinct optional channel. Carry air +admission with the sequence-owned result and cursor. A matched false +admission suppresses only the extra air transaction, preserving build +prediction, inventory consumption and session start. Keep format 2 on +its existing channel for older clients, with its prior air default. +Withdraw either prediction capability through the existing held owner. +Tests cover the legacy wire fixture, the suppressed-air codec, sequence +ownership and build success independently from the air decision. --- - .../api/model/BlockUsePrediction.java | 74 +++++++++++ + .../api/model/BlockUsePrediction.java | 90 +++++++++++++ .../viabedrock/api/model/PlayerBlockUse.java | 35 +++++ .../container/player/InventoryContainer.java | 2 +- .../viabedrock/protocol/BedrockProtocol.java | 1 + .../protocol/packet/ClientPlayerPackets.java | 2 +- .../protocol/packet/InteractionPackets.java | 120 +++++++++++------- - .../protocol/packet/MultiStatePackets.java | 33 +++++ - .../protocol/storage/BlockUseStorage.java | 82 ++++++++++++ + .../protocol/packet/MultiStatePackets.java | 35 +++++ + .../protocol/storage/BlockUseStorage.java | 83 ++++++++++++ .../protocol/storage/InventoryTracker.java | 6 + .../protocol/storage/PlayerInputStorage.java | 4 +- - .../api/model/BlockUsePredictionTest.java | 50 ++++++++ + .../api/model/BlockUsePredictionTest.java | 70 ++++++++++ .../packet/BlockItemUseTransactionTest.java | 96 ++++++++++++++ - .../protocol/storage/BlockUseStorageTest.java | 101 +++++++++++++++ - 13 files changed, 560 insertions(+), 46 deletions(-) + .../protocol/storage/BlockUseStorageTest.java | 120 ++++++++++++++++++ + 13 files changed, 618 insertions(+), 46 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/BlockUsePrediction.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/PlayerBlockUse.java create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/storage/BlockUseStorage.java @@ -64,10 +79,10 @@ fallback rather than synthesizing another phase. diff --git a/src/main/java/net/raphimc/viabedrock/api/model/BlockUsePrediction.java b/src/main/java/net/raphimc/viabedrock/api/model/BlockUsePrediction.java new file mode 100644 -index 00000000..580ded14 +index 00000000..8183b1e0 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/BlockUsePrediction.java -@@ -0,0 +1,74 @@ +@@ -0,0 +1,90 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -85,28 +100,44 @@ index 00000000..580ded14 +import java.util.Objects; + +/** Local result matched to the concluding Java prediction sequence. */ -+public record BlockUsePrediction(int sequence, Result result, Position3f cursor) { ++public record BlockUsePrediction(int sequence, Result result, Position3f cursor, boolean airAllowed) { + + public static final String CHANNEL = "viabedrock:block_use_prediction"; -+ public static final Type TYPE = new Type<>(BlockUsePrediction.class) { -+ @Override -+ public BlockUsePrediction read(final ByteBuf buffer) { -+ if (Types.VAR_INT.readPrimitive(buffer) != 2 -+ || Types.VAR_INT.readPrimitive(buffer) != ProtocolConstants.BEDROCK_PROTOCOL_VERSION) { -+ throw new IllegalArgumentException("Unsupported player block use"); ++ public static final String AIR_CHANNEL = "viabedrock:block_use_prediction_v3"; ++ public static final Type TYPE = codec(false); ++ public static final Type AIR_TYPE = codec(true); ++ ++ private static Type codec(final boolean withAirAdmission) { ++ return new Type<>(BlockUsePrediction.class) { ++ @Override ++ public BlockUsePrediction read(final ByteBuf buffer) { ++ if (Types.VAR_INT.readPrimitive(buffer) != (withAirAdmission ? 3 : 2) ++ || Types.VAR_INT.readPrimitive(buffer) != ProtocolConstants.BEDROCK_PROTOCOL_VERSION) { ++ throw new IllegalArgumentException("Unsupported player block use"); ++ } ++ final int sequence = Types.VAR_INT.readPrimitive(buffer); ++ final Result result = Result.read(buffer.readUnsignedByte()); ++ final Position3f cursor = BedrockTypes.POSITION_3F.read(buffer); ++ return new BlockUsePrediction(sequence, result, cursor, !withAirAdmission || buffer.readBoolean()); + } -+ return new BlockUsePrediction(Types.VAR_INT.readPrimitive(buffer), Result.read(buffer.readUnsignedByte()), BedrockTypes.POSITION_3F.read(buffer)); -+ } + -+ @Override -+ public void write(final ByteBuf buffer, final BlockUsePrediction value) { -+ Types.VAR_INT.writePrimitive(buffer, 2); -+ Types.VAR_INT.writePrimitive(buffer, ProtocolConstants.BEDROCK_PROTOCOL_VERSION); -+ Types.VAR_INT.writePrimitive(buffer, value.sequence()); -+ buffer.writeByte(value.result().id); -+ BedrockTypes.POSITION_3F.write(buffer, value.cursor()); -+ } -+ }; ++ @Override ++ public void write(final ByteBuf buffer, final BlockUsePrediction value) { ++ Types.VAR_INT.writePrimitive(buffer, withAirAdmission ? 3 : 2); ++ Types.VAR_INT.writePrimitive(buffer, ProtocolConstants.BEDROCK_PROTOCOL_VERSION); ++ Types.VAR_INT.writePrimitive(buffer, value.sequence()); ++ buffer.writeByte(value.result().id); ++ BedrockTypes.POSITION_3F.write(buffer, value.cursor()); ++ if (withAirAdmission) { ++ buffer.writeBoolean(value.airAllowed()); ++ } ++ } ++ }; ++ } ++ ++ public BlockUsePrediction(final int sequence, final Result result, final Position3f cursor) { ++ this(sequence, result, cursor, true); ++ } + + public BlockUsePrediction { + Objects.requireNonNull(result); @@ -222,7 +253,7 @@ index 98d7d643..36f0e61c 100644 } else { PacketFactory.sendJavaContainerSetContent(wrapper.user(), inventoryContainer); 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 0acee785..01389150 100644 +index 0acee785..223db3d9 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InteractionPackets.java @@ -25,6 +25,7 @@ import com.viaversion.viaversion.api.type.Types; @@ -347,7 +378,7 @@ index 0acee785..01389150 100644 - new BlockPosition(0, 0, 0), - 0 - ); -+ if (blockUse.held() && use.trigger() == ItemUseTriggerType.Player_Input && use.result() == BlockUsePrediction.Result.PLACED ++ if (blockUse.held() && use.airAllowed() && use.trigger() == ItemUseTriggerType.Player_Input && use.result() == BlockUsePrediction.Result.PLACED + && usedItem.hasBlockState() && !prediction.remainder().isEmpty()) { + // Native input invokes the block item's ordinary use after its first successful use-on. + final PacketWrapper itemUse = PacketWrapper.create(ServerboundBedrockPackets.INVENTORY_TRANSACTION, user); @@ -418,7 +449,7 @@ index 0acee785..01389150 100644 + } 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 29541609..9cbf7341 100644 +index 29541609..8210c4cc 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java @@ -53,6 +53,9 @@ import net.raphimc.viabedrock.api.model.NativeActorPickingMessage; @@ -431,25 +462,27 @@ index 29541609..9cbf7341 100644 import net.raphimc.viabedrock.api.model.PlayerPrediction; import net.raphimc.viabedrock.api.model.VehiclePrediction; import net.raphimc.viabedrock.api.model.PlayerInputPermissions; -@@ -250,6 +253,10 @@ public final class MultiStatePackets { +@@ -250,6 +253,11 @@ public final class MultiStatePackets { if (channel.equals("minecraft:unregister")) { final List channels = Arrays.asList(new String(wrapper.read(Types.SERVERBOUND_CUSTOM_PAYLOAD_DATA), StandardCharsets.UTF_8).split("\0")); wrapper.user().get(ChannelStorage.class).removeChannels(channels); + if (channels.contains(PlayerBlockUse.CHANNEL) || channels.contains(BlockUsePrediction.CHANNEL) ++ || channels.contains(BlockUsePrediction.AIR_CHANNEL) + || channels.contains(PlayerInputPermissions.CHANNEL)) { + InteractionPackets.stopBlockUse(wrapper.user()); + } if (channels.contains(NativeSkinFragment.CHANNEL) || channels.contains(NativeSkinChange.Result.CHANNEL)) { SkinChangeStorage.refreshCapability(wrapper.user()); } -@@ -300,6 +307,32 @@ public final class MultiStatePackets { +@@ -300,6 +308,33 @@ public final class MultiStatePackets { } return; } -+ if (channel.equals(BlockUsePrediction.CHANNEL)) { ++ if (channel.equals(BlockUsePrediction.CHANNEL) || channel.equals(BlockUsePrediction.AIR_CHANNEL)) { + if (wrapper.user().getProtocolInfo().getClientState() == State.PLAY + && wrapper.user().get(ChannelStorage.class).hasChannel(PlayerInputPermissions.CHANNEL)) { -+ final BlockUsePrediction prediction = wrapper.read(BlockUsePrediction.TYPE); ++ final BlockUsePrediction prediction = wrapper.read(channel.equals(BlockUsePrediction.AIR_CHANNEL) ++ ? BlockUsePrediction.AIR_TYPE : BlockUsePrediction.TYPE); + if (wrapper.read(Types.REMAINING_BYTES).length != 0) { + throw new IllegalArgumentException("Unexpected block-use prediction fields"); + } @@ -477,10 +510,10 @@ index 29541609..9cbf7341 100644 && wrapper.user().get(ChannelStorage.class).hasChannel(PlayerInputPermissions.CHANNEL)) { diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/BlockUseStorage.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/BlockUseStorage.java new file mode 100644 -index 00000000..11fde035 +index 00000000..c2f5f75a --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/BlockUseStorage.java -@@ -0,0 +1,82 @@ +@@ -0,0 +1,83 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -521,6 +554,7 @@ index 00000000..11fde035 + final BlockUsePrediction.Result result = matched ? this.prediction.result() : BlockUsePrediction.Result.INTERACTED; + final Position3f nativeCursor = matched ? this.prediction.cursor() : cursor; + final boolean success = result != BlockUsePrediction.Result.FAILED; ++ final boolean airAllowed = !matched || this.prediction.airAllowed(); + this.prediction = null; + final ItemUseTriggerType trigger = this.used ? ItemUseTriggerType.Simulation_Tick : ItemUseTriggerType.Player_Input; + this.used = true; @@ -529,7 +563,7 @@ index 00000000..11fde035 + if (this.active) { + this.resultPosition = resultPosition; + } -+ return new Use(trigger, start, result, nativeCursor); ++ return new Use(trigger, start, result, nativeCursor, airAllowed); + } + + public BlockPosition complete() { @@ -546,7 +580,7 @@ index 00000000..11fde035 + return result; + } + -+ public record Use(ItemUseTriggerType trigger, boolean start, BlockUsePrediction.Result result, Position3f cursor) { ++ public record Use(ItemUseTriggerType trigger, boolean start, BlockUsePrediction.Result result, Position3f cursor, boolean airAllowed) { + + public boolean successful() { + return this.result != BlockUsePrediction.Result.FAILED; @@ -588,7 +622,7 @@ index 84f976c9..7472880e 100644 if (containerId == this.inventoryContainer.containerId()) { return this.inventoryContainer; diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInputStorage.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInputStorage.java -index f8bf20f8..e4484383 100644 +index f8bf20f8..91374668 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInputStorage.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInputStorage.java @@ -11,6 +11,8 @@ import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; @@ -605,16 +639,16 @@ index f8bf20f8..e4484383 100644 final String vehicleChannel = this.user().get(ChannelStorage.class).hasChannel(VehiclePredictionContext.CHANNEL) ? "\0" + VehiclePrediction.CHANNEL : ""; - register.write(Types.REMAINING_BYTES, (PlayerInput.CHANNEL + "\0" + PlayerPrediction.CHANNEL + vehicleChannel).getBytes(StandardCharsets.UTF_8)); -+ register.write(Types.REMAINING_BYTES, (PlayerInput.CHANNEL + "\0" + PlayerPrediction.CHANNEL + "\0" + PlayerBlockUse.CHANNEL + "\0" + BlockUsePrediction.CHANNEL + vehicleChannel).getBytes(StandardCharsets.UTF_8)); ++ register.write(Types.REMAINING_BYTES, (PlayerInput.CHANNEL + "\0" + PlayerPrediction.CHANNEL + "\0" + PlayerBlockUse.CHANNEL + "\0" + BlockUsePrediction.CHANNEL + "\0" + BlockUsePrediction.AIR_CHANNEL + vehicleChannel).getBytes(StandardCharsets.UTF_8)); register.send(BedrockProtocol.class); final PacketWrapper packet = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, this.user()); packet.write(Types.STRING, PlayerInputPermissions.CHANNEL); diff --git a/src/test/java/net/raphimc/viabedrock/api/model/BlockUsePredictionTest.java b/src/test/java/net/raphimc/viabedrock/api/model/BlockUsePredictionTest.java new file mode 100644 -index 00000000..1b628fba +index 00000000..819ca122 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/model/BlockUsePredictionTest.java -@@ -0,0 +1,50 @@ +@@ -0,0 +1,70 @@ +package net.raphimc.viabedrock.api.model; + +import com.viaversion.viaversion.api.type.Types; @@ -648,6 +682,26 @@ index 00000000..1b628fba + } + + @Test ++ void admissionChannelPreservesSuppressedAirAndLegacyPayloadsKeepTheirPriorDefault() { ++ final var buffer = Unpooled.buffer(); ++ try { ++ final var value = new BlockUsePrediction(7, BlockUsePrediction.Result.PLACED, new Position3f(0.5F, 0.625F, 1F), false); ++ BlockUsePrediction.AIR_TYPE.write(buffer, value); ++ assertEquals(value, BlockUsePrediction.AIR_TYPE.read(buffer)); ++ assertFalse(buffer.isReadable()); ++ buffer.clear(); ++ buffer.writeBytes(new byte[] {2, (byte) 0xB1, 17, 7, 2, 0, 0, 0, 63, 0, 0, 32, 63, 0, 0, (byte) 128, 63}); ++ final var legacy = BlockUsePrediction.TYPE.read(buffer); ++ assertEquals(7, legacy.sequence()); ++ assertEquals(BlockUsePrediction.Result.PLACED, legacy.result()); ++ assertTrue(legacy.airAllowed()); ++ assertFalse(buffer.isReadable()); ++ } finally { ++ buffer.release(); ++ } ++ } ++ ++ @Test + void rejectsUnknownActionsAndNegativeSequences() { + final var buffer = Unpooled.buffer(); + try { @@ -769,10 +823,10 @@ index 00000000..9b018d43 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/BlockUseStorageTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/BlockUseStorageTest.java new file mode 100644 -index 00000000..d2dd9cf6 +index 00000000..25cfe501 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/BlockUseStorageTest.java -@@ -0,0 +1,101 @@ +@@ -0,0 +1,120 @@ +package net.raphimc.viabedrock.protocol.storage; + +import com.viaversion.viaversion.api.minecraft.BlockPosition; @@ -873,6 +927,25 @@ index 00000000..d2dd9cf6 + assertEquals(actual, use.begin(3, position, actual).cursor()); + } + ++ @Test ++ void airAdmissionBelongsToItsPredictionSequenceAndDoesNotChangeBuildSuccess() { ++ final var use = new BlockUseStorage(); ++ final var position = new BlockPosition(0, 64, 0); ++ final var cursor = new Position3f(0.5F, 0.625F, 1F); ++ use.hold(); ++ use.predict(new BlockUsePrediction(1, BlockUsePrediction.Result.PLACED, cursor, false)); ++ final var suppressed = use.begin(1, position, cursor); ++ assertTrue(suppressed.successful()); ++ assertTrue(suppressed.start()); ++ assertFalse(suppressed.airAllowed()); ++ use.release(); ++ use.hold(); ++ use.predict(new BlockUsePrediction(2, BlockUsePrediction.Result.PLACED, cursor, true)); ++ assertTrue(use.begin(2, position, cursor).airAllowed()); ++ use.predict(new BlockUsePrediction(3, BlockUsePrediction.Result.PLACED, cursor, false)); ++ assertTrue(use.begin(4, position, cursor).airAllowed()); ++ } ++ +} -- 2.56.0 diff --git a/patches/viabedrock/upstreamable/0091-retain-native-held-block-use-sessions.pr.md b/patches/viabedrock/upstreamable/0091-retain-native-held-block-use-sessions.pr.md index 93709c6f..04b6d974 100644 --- a/patches/viabedrock/upstreamable/0091-retain-native-held-block-use-sessions.pr.md +++ b/patches/viabedrock/upstreamable/0091-retain-native-held-block-use-sessions.pr.md @@ -5,7 +5,9 @@ The previous Java translation sent a start and stop for every use-on-block packe It opened a trapdoor where native Bedrock placed a closed neighbor. Transport the control lifetime and match each local prediction to its concluding Java sequence number. -Prediction format 2 distinguishes placement, interaction, and failure and carries the native cursor relative to the target. +Prediction format 3 adds sequence-owned air admission to placement, interaction, failure, and the native cursor. +A distinct optional channel negotiates this format. +The existing format-2 channel retains its prior default for older clients. Native held placement reuses its first world hit, including coordinates outside later target cubes. The core preserves those coordinates and rejects non-finite values or stale sequence ownership. @@ -15,7 +17,9 @@ A failed local prediction does not start a session or submit a placement delta. Clients without the channels keep independent block uses. Native Survival predicts one consumed block for successful local placement. -The first placement also sends a use-in-air phase with the remaining stack. +The first placement sends a use-in-air phase with the remaining stack only when the client admits that phase. +A fresh block call can run before the strict 200 ms air deadline expires. +Suppressing its air phase does not suppress placement, inventory prediction, or session start. The last block uses request ID zero and omits the empty use-in-air phase. Creative placements and ordinary interactions carry no placement delta. The inventory owns one even legacy ID sequence for placements and other legacy transactions, independently of odd stack requests. @@ -27,6 +31,9 @@ Held-use swings share the target protocol factory with ordinary punches and expl Native 1.26.60.5 and BDS 1.26.60.6 fixtures use protocol 2225. Fresh Survival fixtures force the native game mode and verify its HUD and inventory predictions. Earlier captures combined a Creative client default with server Survival consumption and did not establish the Survival inventory format. +A fresh native flags-176 callback dispatches a rejected block use about 27 ms before the previous air deadline. +It emits no second air transaction. +The matching source applies that deadline to the air phase after block dispatch. Raw journals, screenshots, and proprietary resources remain private. Physical mouse rotation now retains the initial facing, upper-half state, and closed state in native, direct Java, and ViaProxy fixtures. @@ -48,7 +55,14 @@ Matching session history explains the earlier apparent correction mismatch. The translator preserves both authoritative outcomes. All four project builds and the modpack bundle pass. -Core: 2,052 tests, zero failures/errors, 176 optional skips. -Add-on: 1,281 tests, zero failures/errors, 142 optional skips. +Core: 2,038 passed, 176 skipped, zero failures or errors. +Add-on: 1,245 passed, 149 skipped, zero failures or errors. CubeConverter: 21 tests. ViaProxy: four tests. Neither reports failures or errors. -Focused tests cover held state, sequence ownership, cursor validation, use phases, inventory prediction, and independent request IDs. +Focused tests cover held state, sequence ownership, cursor validation, air admission, use phases, inventory prediction, and independent request IDs. +A literal legacy wire fixture retains the format-2 default. +Format 3 preserves a suppressed air decision without changing build success. +Current direct and ViaProxy rapid controls verify that a successful early placement keeps its consumption prediction but omits the air packet. + +Older cores cannot honor the new admission field. +The add-on retains their legacy channel instead of sending an unsupported payload. +Complete physical-edge sampling, dynamic capability changes, and broader use families still need comparisons. 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 ff478c96..216b82ba 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 72e6d2fe257b07dc70b3eba4d62b981f9b21d9d5 Mon Sep 17 00:00:00 2001 +From d548c45c70243e92242daee198be6f000f39ed3e 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 @@ -386,7 +386,7 @@ index 4f775c01..5af0eca2 100644 user.get(HudVisibilityStorage.class).sendSnapshot(); user.get(CameraEffectsStorage.class).markJoined(); 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 9cbf7341..d41a2a00 100644 +index 8210c4cc..67129d78 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java @@ -88,6 +88,7 @@ import net.raphimc.viabedrock.protocol.storage.PlayerInputStorage; @@ -397,7 +397,7 @@ index 9cbf7341..d41a2a00 100644 import net.raphimc.viabedrock.protocol.storage.CameraEffectsStorage; import net.raphimc.viabedrock.protocol.types.BedrockTypes; -@@ -390,6 +391,9 @@ public final class MultiStatePackets { +@@ -392,6 +393,9 @@ public final class MultiStatePackets { HudPackets.sendNativeRoot(wrapper.user()); wrapper.user().get(PlayerInputStorage.class).sendSnapshot(); wrapper.user().get(HudVisibilityStorage.class).sendSnapshot(); 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 222599af..7aaaee79 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 302e0708e38e062c8b6fbe0843854a72759c606e Mon Sep 17 00:00:00 2001 +From 1b98feab407cb99a67127b576bb27e9401389af0 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): preserve native held item use @@ -11,8 +11,9 @@ and empty hands restore interaction. Returning to a trapdoor restores its snap u Preserve independent water, power and material properties. Separate trapdoors and the original support remain eligible targets. -Carry the physical use lifetime and sequence-owned prediction format 2 -through direct and ViaProxy sessions before the concluding Java packet. +Carry physical use lifetime and sequence-owned prediction through direct +and ViaProxy sessions before the concluding Java packet. Negotiate +format 3 for air admission, with format 2 retained for older cores. Keep use-target selection separate from the visible Java raycast. Retain native cursor coordinates outside a later target cube. Select native outline/occlusion from the active Bedrock connection rather than the local Java protocol. @@ -57,7 +58,7 @@ intermittent food report remain open. Current 1.26.60.5/protocol 2225 refused-food traces retain held flags 17 with or without an initial block target. Accepted food retains flags 16 even after hunger fills. The refused route writes a 200 ms air deadline -but bypasses it for held block continuation. A Stone selected 110 ms +but bypasses it for block dispatch. A Stone selected 110 ms after refused bread can place immediately. Successful ordinary block uses start the same 300 ms placement delay. Separate block mode ownership from its active delay, and distinguish frame continuation from fresh @@ -114,7 +115,7 @@ unattached-Stone control selects this block mode without a block target. Remove the old trapdoor-only first-rejection delay. Record the client air deadline for the first successful placement only -when an item remains. Core patch 0091 already emits that air transaction. Do not +when an item remains and its air phase passes the previous deadline. Core patch 0091 already emits that air transaction. Do not send another from the add-on. Held placement success must not refresh this deadline. Native last-block placement and trapdoor activation write no air deadline; leave those phases separate. Current Bedrock 1.26.60.5 tracing @@ -126,23 +127,65 @@ one air phase, later success, first-placement deadline ownership, held success, last-stack exclusion and release into food. Native capture frame cadence differs from Java, so retry counts are not equality assertions. Rapid input edge sampling and broader item dispatch remain open. + +Fresh block dispatch does not wait for the air clock. Native tracing of +1.26.60.5 observes a fresh flags 176 callback and failed block use about +27 ms before the preceding air deadline expires. It emits no new air +transaction. Remove the whole-call gate; admit air separately. If the +first known food/block air phase is suppressed, retain untimed block +continuation instead of starting air use after the deadline on that hold. + +A fresh physical call clears the previous mode, placement path and core +session before starting its new dispatch. This also handles a release +and re-press between sampled frames. Retain the connection air deadline. +A first successful placement records air admission with its prediction +before the concluding Java use-on packet. Core can then suppress only +its extra air phase. Keep the legacy format/channel fallback for older +cores, whose unconditional first-success air behavior remains limited. +Regression cases cover suppressed fresh air, independent block success, +unchanged air deadlines and release into a new admitted food hold. + +Mark fresh calls at the consumed-click call site in handleKeybinds, +separately from Java tick fallback and the frame owner. A legacy tick +retry must not reset the native physical lifetime or acquire another +Player_Input trigger. Retain untimed block continuation for empty hands, +including a fresh press after the last placed block. Preserve an already +owned air route. Installed rapid last-stack controls exposed the extra +fresh trigger before this distinction. + +Only an owned air or block mode can retry from frames. The earlier +holding-food shortcut could preempt Java's queued fresh click, then +reset its new air hold when the click was consumed on the next tick. +The old whole-call clock gate hid that duplicate dispatch. Remove the +shortcut now that initial air admission selects its continuation mode. +A boundary regression rejects frame use before physical admission, then +checks the first eligible frames of the admitted hold. Installed food-to- +block controls expose the duplicate before this ownership correction. + +Preserve fresh entity admission behind the air deadline. The same native +common-input source gates its entity branch with the clock while fresh +block dispatch uses flags 0x21 independently. Suppress only the entity +target before admission, allowing the ordinary air predicate to select +the continuation. A target-boundary regression keeps block hits eligible +without opening entity or air admission early. Held entity dispatch and +broader actor handlers remain separate comparison work. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../features/block/MixinTrapDoorBlock.java | 18 +- - .../features/input/MixinItemUseControl.java | 97 ++++++++ + .../features/input/MixinItemUseControl.java | 107 +++++++++ .../inventory/MixinHeldBlockPlacement.java | 32 +++ .../features/inventory/MixinHeldBlockUse.java | 61 +++++ - .../bedrock/input/BedrockItemUse.java | 216 ++++++++++++++++++ + .../bedrock/input/BedrockItemUse.java | 221 ++++++++++++++++++ .../bedrock/input/BlockPlacementPath.java | 83 +++++++ .../bedrock/input/BlockUsePayload.java | 24 ++ - .../input/BlockUsePredictionPayload.java | 24 ++ - .../bedrock/input/ItemUseState.java | 104 +++++++++ + .../input/BlockUsePredictionPayload.java | 33 +++ + .../bedrock/input/ItemUseState.java | 114 +++++++++ .../bedrock/inventory/TrapdoorPlacement.java | 55 +++++ .../viafabricplus-bedrock.mixins.json | 3 + - .../bedrock/input/BlockPlacementPathTest.java | 128 +++++++++++ - .../bedrock/input/ItemUseStateTest.java | 167 ++++++++++++++ + .../bedrock/input/BlockPlacementPathTest.java | 128 ++++++++++ + .../bedrock/input/ItemUseStateTest.java | 221 ++++++++++++++++++ .../inventory/TrapdoorPlacementTest.java | 100 ++++++++ - 15 files changed, 1110 insertions(+), 4 deletions(-) + 15 files changed, 1198 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockPlacement.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinHeldBlockUse.java @@ -228,10 +271,10 @@ index 82db187..eda72b3 100644 return super.getOcclusionShape(state); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java new file mode 100644 -index 0000000..d94e0c5 +index 0000000..b6ef2d9 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/input/MixinItemUseControl.java -@@ -0,0 +1,97 @@ +@@ -0,0 +1,107 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -263,13 +306,23 @@ index 0000000..d94e0c5 +@Mixin(Minecraft.class) +public abstract class MixinItemUseControl { + @Unique private boolean viaFabricPlusBedrock$frameRepeat; ++ @Unique private boolean viaFabricPlusBedrock$freshPress; + + @Shadow protected abstract void pick(float partialTick); + @Shadow protected abstract void startUseItem(); -+ @Inject(method = "startUseItem", at = @At("HEAD"), cancellable = true) ++ @Inject(method = "startUseItem", at = @At("HEAD")) + private void beginItemUse(final CallbackInfo ci) { -+ if (!BedrockItemUse.begin(this.viaFabricPlusBedrock$frameRepeat)) { -+ ci.cancel(); ++ BedrockItemUse.begin(this.viaFabricPlusBedrock$freshPress); ++ } ++ ++ @WrapOperation(method = "handleKeybinds", at = @At(value = "INVOKE", ++ target = "Lnet/minecraft/client/Minecraft;startUseItem()V", ordinal = 0)) ++ private void beginFreshPress(final Minecraft client, final Operation original) { ++ this.viaFabricPlusBedrock$freshPress = true; ++ try { ++ original.call(client); ++ } finally { ++ this.viaFabricPlusBedrock$freshPress = false; + } + } + @@ -436,10 +489,10 @@ index 0000000..69387b7 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java new file mode 100644 -index 0000000..854a58b +index 0000000..45409b3 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockItemUse.java -@@ -0,0 +1,216 @@ +@@ -0,0 +1,221 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -474,6 +527,7 @@ index 0000000..854a58b + private static boolean held; + private static boolean placedBlock; + private static boolean successfulAttempt; ++ private static boolean airAllowed; + private static final ItemUseState INPUT = new ItemUseState(); + private static Position3f cursor; + private static BlockUsePrediction.Result predictedResult = BlockUsePrediction.Result.FAILED; @@ -483,18 +537,18 @@ index 0000000..854a58b + public static void register() { + PayloadTypeRegistry.serverboundPlay().register(BlockUsePayload.TYPE, BlockUsePayload.CODEC); + PayloadTypeRegistry.serverboundPlay().register(BlockUsePredictionPayload.TYPE, BlockUsePredictionPayload.CODEC); ++ PayloadTypeRegistry.serverboundPlay().register(BlockUsePredictionPayload.AIR_TYPE, BlockUsePredictionPayload.AIR_CODEC); + ClientTickEvents.START_CLIENT_TICK.register(client -> refresh()); + ClientPlayConnectionEvents.INIT.register((listener, client) -> reset()); + ClientPlayConnectionEvents.DISCONNECT.register((listener, client) -> reset()); + } + -+ public static boolean begin(final boolean frameRepeat) { -+ if (bedrockConnection() && !INPUT.canBegin(System.nanoTime(), frameRepeat)) { -+ return false; ++ public static void begin(final boolean freshPress) { ++ if (freshPress) { ++ update(false); + } + update(true); + successfulAttempt = false; -+ return true; + } + + public static boolean beginAirUse(final InteractionHand hand, final ItemStack item) { @@ -507,24 +561,22 @@ index 0000000..854a58b + } + + public static HitResult.Type hitType(final HitResult.Type target, final InteractionHand hand) { -+ return INPUT.hitType(target, hand); ++ return INPUT.hitType(System.nanoTime(), target, hand); + } + + private static boolean bedrockConnection() { + return Minecraft.getInstance().getConnection() != null && BedrockInputPermissions.isBedrockConnection(); + } + -+ private static boolean holdingFood() { -+ final var client = Minecraft.getInstance(); -+ return bedrockConnection() && client.player != null && !client.player.isSpectator() -+ && client.gui.screen() == null && client.gui.overlay() == null && client.options.keyUse.isDown() -+ && !client.player.getMainHandItem().isEmpty() && client.player.getMainHandItem().has(DataComponents.FOOD); -+ } -+ + public static void release() { + update(false); + } + ++ private static boolean canPredict() { ++ return ClientPlayNetworking.canSend(BlockUsePredictionPayload.TYPE) ++ || ClientPlayNetworking.canSend(BlockUsePredictionPayload.AIR_TYPE); ++ } ++ + private static void update(final boolean value) { + if (!bedrockConnection()) { + reset(); @@ -533,7 +585,7 @@ index 0000000..854a58b + if (!value) { + INPUT.release(); + } -+ if (!ClientPlayNetworking.canSend(BlockUsePayload.TYPE) || !ClientPlayNetworking.canSend(BlockUsePredictionPayload.TYPE)) { ++ if (!ClientPlayNetworking.canSend(BlockUsePayload.TYPE) || !canPredict()) { + resetBlockUse(); + return; + } @@ -562,17 +614,20 @@ index 0000000..854a58b + } + + public static void attempted() { ++ if (held && Minecraft.getInstance().player.getMainHandItem().isEmpty()) { ++ INPUT.retainBlockUse(); ++ } + if (held && (successfulAttempt || INPUT.blockActive())) { + INPUT.attemptedBlock(System.nanoTime(), successfulAttempt); + } + } + + public static boolean repeatsFromFrames() { -+ return INPUT.repeatsFromFrames(holdingFood()); ++ return INPUT.repeatsFromFrames(); + } + + public static boolean frameReady() { -+ return repeatsFromFrames() && INPUT.frameReady(System.nanoTime(), holdingFood(), PATH.active()); ++ return repeatsFromFrames() && INPUT.frameReady(System.nanoTime(), PATH.active()); + } + + public static void refresh() { @@ -606,8 +661,6 @@ index 0000000..854a58b + public static void placed(final BlockState state, final BlockPos support, final Vec3 hit) { + placedBlock = true; + if (held) { -+ // Core sends this first air phase after successful placement; retain its client deadline. -+ INPUT.placedBlock(System.nanoTime(), !Minecraft.getInstance().player.getMainHandItem().isEmpty()); + PLACEMENT.remember(state, support, hit); + } + } @@ -623,18 +676,23 @@ index 0000000..854a58b + cursor = null; + placedBlock = false; + predictedResult = BlockUsePrediction.Result.FAILED; ++ airAllowed = false; + } + + public static void predicted(final boolean success) { + successfulAttempt |= success; + predictedResult = !success ? BlockUsePrediction.Result.FAILED + : placedBlock ? BlockUsePrediction.Result.PLACED : BlockUsePrediction.Result.INTERACTED; ++ if (held && predictedResult == BlockUsePrediction.Result.PLACED) { ++ airAllowed = INPUT.placedBlock(System.nanoTime(), !Minecraft.getInstance().player.getMainHandItem().isEmpty()); ++ } + } + + public static void sendPrediction(final int sequence) { + if (cursor != null && BedrockInputPermissions.isBedrockConnection() && Minecraft.getInstance().getConnection() != null -+ && ClientPlayNetworking.canSend(BlockUsePredictionPayload.TYPE)) { -+ ClientPlayNetworking.send(new BlockUsePredictionPayload(new BlockUsePrediction(sequence, predictedResult, cursor))); ++ && canPredict()) { ++ ClientPlayNetworking.send(new BlockUsePredictionPayload(new BlockUsePrediction(sequence, predictedResult, cursor, airAllowed), ++ ClientPlayNetworking.canSend(BlockUsePredictionPayload.AIR_TYPE))); + } + clearPrediction(); + } @@ -777,10 +835,10 @@ index 0000000..52df66b +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePredictionPayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePredictionPayload.java new file mode 100644 -index 0000000..a98d448 +index 0000000..3ee51e7 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BlockUsePredictionPayload.java -@@ -0,0 +1,24 @@ +@@ -0,0 +1,33 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -794,23 +852,32 @@ index 0000000..a98d448 +import net.minecraft.resources.Identifier; +import net.raphimc.viabedrock.api.model.BlockUsePrediction; + -+public record BlockUsePredictionPayload(BlockUsePrediction use) implements CustomPacketPayload { ++public record BlockUsePredictionPayload(BlockUsePrediction use, boolean withAirAdmission) implements CustomPacketPayload { + public static final Type TYPE = new Type<>(Identifier.parse(BlockUsePrediction.CHANNEL)); + public static final StreamCodec CODEC = CustomPacketPayload.codec( + (payload, buffer) -> BlockUsePrediction.TYPE.write(buffer, payload.use), + buffer -> new BlockUsePredictionPayload(BlockUsePrediction.TYPE.read(buffer))); + ++ public static final Type AIR_TYPE = new Type<>(Identifier.parse(BlockUsePrediction.AIR_CHANNEL)); ++ public static final StreamCodec AIR_CODEC = CustomPacketPayload.codec( ++ (payload, buffer) -> BlockUsePrediction.AIR_TYPE.write(buffer, payload.use), ++ buffer -> new BlockUsePredictionPayload(BlockUsePrediction.AIR_TYPE.read(buffer), true)); ++ ++ public BlockUsePredictionPayload(final BlockUsePrediction use) { ++ this(use, false); ++ } ++ + @Override + public Type type() { -+ return TYPE; ++ return this.withAirAdmission ? AIR_TYPE : TYPE; + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java new file mode 100644 -index 0000000..334d0bf +index 0000000..246e21b --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/ItemUseState.java -@@ -0,0 +1,104 @@ +@@ -0,0 +1,114 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -837,10 +904,6 @@ index 0000000..334d0bf + return !this.airAttempted || now - this.airStartedAt > AIR_DELAY_NANOS; + } + -+ public boolean canBegin(final long now, final boolean frameRepeat) { -+ return frameRepeat && this.blockActive || this.canAttempt(now); -+ } -+ + public boolean admitAirUse(final long now, final InteractionHand hand, final boolean food, final boolean blockItem, final boolean canConsume) { + if (hand != InteractionHand.MAIN_HAND) { + return !this.airActive && !this.blockActive; @@ -848,12 +911,15 @@ index 0000000..334d0bf + if (this.blockActive) { + return false; + } -+ if (!this.airActive && !food && !blockItem) { -+ return true; -+ } + if (!this.canAttempt(now)) { ++ if (!this.airActive && (food || blockItem)) { ++ this.blockActive = true; ++ } + return false; + } ++ if (!this.airActive && !food && !blockItem) { ++ return true; ++ } + if (this.airActive || food && canConsume) { + this.airActive = true; + } else { @@ -863,10 +929,12 @@ index 0000000..334d0bf + return true; + } + -+ public void placedBlock(final long now, final boolean hasRemainder) { -+ if (!this.airActive && !this.blockActive && hasRemainder) { ++ public boolean placedBlock(final long now, final boolean hasRemainder) { ++ if (!this.airActive && !this.blockActive && hasRemainder && this.canAttempt(now)) { + this.recordAirUse(now); ++ return true; + } ++ return false; + } + + private void recordAirUse(final long now) { @@ -874,10 +942,19 @@ index 0000000..334d0bf + this.airStartedAt = now; + } + -+ public HitResult.Type hitType(final HitResult.Type target, final InteractionHand hand) { ++ public HitResult.Type hitType(final long now, final HitResult.Type target, final InteractionHand hand) { ++ if (!this.blockActive && target == HitResult.Type.ENTITY && !this.canAttempt(now)) { ++ return HitResult.Type.MISS; ++ } + return this.airActive || this.blockActive && hand != InteractionHand.MAIN_HAND ? HitResult.Type.MISS : target; + } + ++ public void retainBlockUse() { ++ if (!this.airActive) { ++ this.blockActive = true; ++ } ++ } ++ + public void attemptedBlock(final long now, final boolean success) { + if (!this.airActive && (!this.blockActive || success)) { + this.blockStartedAt = now; @@ -890,14 +967,14 @@ index 0000000..334d0bf + return this.blockActive; + } + -+ public boolean repeatsFromFrames(final boolean food) { -+ return this.airActive || this.blockActive || food; ++ public boolean repeatsFromFrames() { ++ return this.airActive || this.blockActive; + } + -+ public boolean frameReady(final long now, final boolean food, final boolean placementLine) { ++ public boolean frameReady(final long now, final boolean placementLine) { + return this.blockActive + ? !this.blockDelayed || now - this.blockStartedAt > (placementLine ? LINE_DELAY_NANOS : BLOCK_DELAY_NANOS) -+ : this.canAttempt(now) && (this.airActive || food); ++ : this.canAttempt(now) && this.airActive; + } + + public void cancelPlacement() { @@ -992,7 +1069,7 @@ index f7c3bf4..416a16f 100644 "features.creative.MixinCreativeModeTabs", diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPathTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPathTest.java new file mode 100644 -index 0000000..cc7d3e8 +index 0000000..e4fe3f2 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/BlockPlacementPathTest.java @@ -0,0 +1,128 @@ @@ -1079,12 +1156,12 @@ index 0000000..cc7d3e8 + final var input = new ItemUseState(); + path.attempted(new BlockPos(0, -60, -1), true, false); + input.attemptedBlock(1_000_000_000L, true); -+ assertFalse(input.frameReady(1_300_000_000L, false, path.active())); -+ assertTrue(input.frameReady(1_300_000_001L, false, path.active())); ++ assertFalse(input.frameReady(1_300_000_000L, path.active())); ++ assertTrue(input.frameReady(1_300_000_001L, path.active())); + path.attempted(new BlockPos(0, -60, -2), true, false); + input.attemptedBlock(1_310_000_000L, true); -+ assertFalse(input.frameReady(1_510_000_000L, false, path.active())); -+ assertTrue(input.frameReady(1_510_000_001L, false, path.active())); ++ assertFalse(input.frameReady(1_510_000_000L, path.active())); ++ assertTrue(input.frameReady(1_510_000_001L, path.active())); + } + + @Test @@ -1126,10 +1203,10 @@ index 0000000..cc7d3e8 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java new file mode 100644 -index 0000000..a97e4b1 +index 0000000..29a02ab --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/input/ItemUseStateTest.java -@@ -0,0 +1,167 @@ +@@ -0,0 +1,221 @@ +package com.viaversion.viafabricplus.bedrock.input; + +import org.junit.jupiter.api.Test; @@ -1148,7 +1225,9 @@ index 0000000..a97e4b1 + assertTrue(timing.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + timing.release(); + assertFalse(timing.admitAirUse(1_131_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ timing.release(); + assertFalse(timing.admitAirUse(1_200_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ timing.release(); + assertTrue(timing.admitAirUse(1_616_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + assertFalse(timing.canAttempt(1_700_000_000L)); + timing.clear(); @@ -1159,9 +1238,12 @@ index 0000000..a97e4b1 + void heldFoodUsesTheFirstEligibleInputFrameInsteadOfARepeatedTickCount() { + final var timing = new ItemUseState(); + final List usedAt = new ArrayList<>(); -+ for (final int milliseconds : new int[] {0, 47, 94, 141, 188, 236, 284, 332, 380, 428, 475, 542, 609, 676}) { ++ assertFalse(timing.frameReady(1_000_000_000L, false)); ++ assertTrue(timing.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ usedAt.add(0); ++ for (final int milliseconds : new int[] {47, 94, 141, 188, 236, 284, 332, 380, 428, 475, 542, 609, 676}) { + final long now = 1_000_000_000L + milliseconds * 1_000_000L; -+ if (timing.frameReady(now, true, false) && timing.admitAirUse(now, InteractionHand.MAIN_HAND, true, false, true)) { ++ if (timing.frameReady(now, false) && timing.admitAirUse(now, InteractionHand.MAIN_HAND, true, false, true)) { + usedAt.add(milliseconds); + } + } @@ -1172,17 +1254,17 @@ index 0000000..a97e4b1 + void foodHoldKeepsAirUseAcrossBlockAndEntityTargetsUntilRelease() { + final var input = new ItemUseState(); + assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, true)); -+ assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); -+ assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); ++ assertEquals(HitResult.Type.MISS, input.hitType(1_500_000_000L, HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertEquals(HitResult.Type.MISS, input.hitType(1_500_000_000L, HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); + input.attemptedBlock(1_100_000_000L, true); -+ assertFalse(input.frameReady(1_200_000_000L, false, false)); -+ assertTrue(input.frameReady(1_200_000_001L, false, false)); ++ assertFalse(input.frameReady(1_200_000_000L, false)); ++ assertTrue(input.frameReady(1_200_000_001L, false)); + assertTrue(input.admitAirUse(1_200_000_001L, InteractionHand.MAIN_HAND, false, false, true)); + assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.OFF_HAND, false, false, true)); + input.cancelPlacement(); -+ assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertEquals(HitResult.Type.MISS, input.hitType(1_500_000_000L, HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + input.release(); -+ assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertEquals(HitResult.Type.BLOCK, input.hitType(1_500_000_000L, HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); + assertFalse(input.canAttempt(1_400_000_001L)); + assertTrue(input.canAttempt(1_400_000_002L)); + } @@ -1191,23 +1273,23 @@ index 0000000..a97e4b1 + void placementHoldDoesNotStartEatingWhenTheSelectedItemChanges() { + final var input = new ItemUseState(); + input.attemptedBlock(1_000_000_000L, true); -+ assertFalse(input.frameReady(1_200_000_001L, true, false)); -+ assertFalse(input.frameReady(1_300_000_000L, true, false)); -+ assertTrue(input.frameReady(1_300_000_001L, true, false)); -+ assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); -+ assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.OFF_HAND)); ++ assertFalse(input.frameReady(1_200_000_001L, false)); ++ assertFalse(input.frameReady(1_300_000_000L, false)); ++ assertTrue(input.frameReady(1_300_000_001L, false)); ++ assertEquals(HitResult.Type.BLOCK, input.hitType(1_500_000_000L, HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertEquals(HitResult.Type.MISS, input.hitType(1_500_000_000L, HitResult.Type.BLOCK, InteractionHand.OFF_HAND)); + assertFalse(input.admitAirUse(1_300_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + input.attemptedBlock(1_310_000_000L, false); -+ assertTrue(input.frameReady(1_311_000_000L, true, false)); ++ assertTrue(input.frameReady(1_311_000_000L, false)); + input.attemptedBlock(1_320_000_000L, true); -+ assertFalse(input.frameReady(1_619_999_999L, false, false)); -+ assertFalse(input.frameReady(1_620_000_000L, false, false)); -+ assertTrue(input.frameReady(1_620_000_001L, false, false)); ++ assertFalse(input.frameReady(1_619_999_999L, false)); ++ assertFalse(input.frameReady(1_620_000_000L, false)); ++ assertTrue(input.frameReady(1_620_000_001L, false)); + input.release(); + assertTrue(input.admitAirUse(1_630_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + assertFalse(input.canAttempt(1_800_000_000L)); + input.clear(); -+ assertEquals(HitResult.Type.ENTITY, input.hitType(HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); ++ assertEquals(HitResult.Type.ENTITY, input.hitType(1_500_000_000L, HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); + assertTrue(input.admitAirUse(1_800_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + } + @@ -1216,15 +1298,13 @@ index 0000000..a97e4b1 + final var input = new ItemUseState(); + assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, false)); + input.attemptedBlock(1_010_000_000L, false); -+ assertEquals(HitResult.Type.BLOCK, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); -+ assertTrue(input.frameReady(1_052_000_000L, true, false)); -+ assertTrue(input.canBegin(1_052_000_000L, true)); -+ assertFalse(input.canBegin(1_052_000_000L, false)); ++ assertEquals(HitResult.Type.BLOCK, input.hitType(1_500_000_000L, HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertTrue(input.frameReady(1_052_000_000L, false)); + assertFalse(input.admitAirUse(1_052_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + input.attemptedBlock(1_105_000_000L, true); -+ assertFalse(input.frameReady(1_404_999_999L, false, false)); -+ assertFalse(input.frameReady(1_405_000_000L, false, false)); -+ assertTrue(input.frameReady(1_405_000_001L, false, false)); ++ assertFalse(input.frameReady(1_404_999_999L, false)); ++ assertFalse(input.frameReady(1_405_000_000L, false)); ++ assertTrue(input.frameReady(1_405_000_001L, false)); + input.release(); + assertTrue(input.admitAirUse(1_500_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + } @@ -1234,9 +1314,9 @@ index 0000000..a97e4b1 + final var input = new ItemUseState(); + assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + assertTrue(input.admitAirUse(1_201_000_000L, InteractionHand.MAIN_HAND, true, false, false)); -+ assertEquals(HitResult.Type.MISS, input.hitType(HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); -+ assertFalse(input.frameReady(1_401_000_000L, false, false)); -+ assertTrue(input.frameReady(1_401_000_001L, false, false)); ++ assertEquals(HitResult.Type.MISS, input.hitType(1_500_000_000L, HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertFalse(input.frameReady(1_401_000_000L, false)); ++ assertTrue(input.frameReady(1_401_000_001L, false)); + } + + @Test @@ -1245,56 +1325,107 @@ index 0000000..a97e4b1 + assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, false, true, false)); + input.attemptedBlock(1_001_000_000L, false); + assertTrue(input.blockActive()); -+ assertTrue(input.frameReady(1_016_000_000L, false, false)); -+ assertTrue(input.canBegin(1_016_000_000L, true)); -+ assertFalse(input.canBegin(1_016_000_000L, false)); ++ assertTrue(input.frameReady(1_016_000_000L, false)); + assertFalse(input.admitAirUse(1_016_000_000L, InteractionHand.MAIN_HAND, false, true, false)); + assertFalse(input.admitAirUse(1_016_000_000L, InteractionHand.OFF_HAND, true, false, true)); + input.attemptedBlock(1_080_000_000L, false); -+ assertTrue(input.frameReady(1_096_000_000L, false, false)); ++ assertTrue(input.frameReady(1_096_000_000L, false)); + input.attemptedBlock(1_100_000_000L, true); -+ assertFalse(input.frameReady(1_400_000_000L, false, false)); -+ assertTrue(input.frameReady(1_400_000_001L, false, false)); ++ assertFalse(input.frameReady(1_400_000_000L, false)); ++ assertTrue(input.frameReady(1_400_000_001L, false)); + } + + @Test + void successfulFirstPlacementSharesItsAirDeadlineButHeldPlacementDoesNotRestartIt() { + final var input = new ItemUseState(); -+ input.placedBlock(1_000_000_000L, true); ++ assertTrue(input.placedBlock(1_000_000_000L, true)); + input.attemptedBlock(1_001_000_000L, true); -+ assertFalse(input.canBegin(1_200_000_000L, false)); -+ assertTrue(input.canBegin(1_200_000_001L, false)); -+ input.placedBlock(1_350_000_000L, true); ++ assertFalse(input.placedBlock(1_350_000_000L, true)); + input.attemptedBlock(1_351_000_000L, true); + input.release(); -+ assertTrue(input.canBegin(1_400_000_000L, false)); + assertTrue(input.admitAirUse(1_400_000_000L, InteractionHand.MAIN_HAND, true, false, true)); -+ assertFalse(input.canBegin(1_600_000_000L, false)); + } + + @Test + void placingTheLastBlockDoesNotCreateAnAirDeadline() { + final var input = new ItemUseState(); -+ input.placedBlock(1_000_000_000L, false); ++ assertFalse(input.placedBlock(1_000_000_000L, false)); + input.attemptedBlock(1_001_000_000L, true); + input.release(); + assertTrue(input.admitAirUse(1_050_000_000L, InteractionHand.MAIN_HAND, true, false, true)); + } + + @Test ++ void freshSuccessfulBlockBeforeTheAirDeadlineDoesNotRefreshOrAdmitThatAirPhase() { ++ final var input = new ItemUseState(); ++ assertTrue(input.placedBlock(1_000_000_000L, true)); ++ input.attemptedBlock(1_001_000_000L, true); ++ input.release(); ++ assertFalse(input.placedBlock(1_172_000_000L, true)); ++ input.attemptedBlock(1_173_000_000L, true); ++ assertFalse(input.frameReady(1_200_000_000L, false)); ++ input.release(); ++ assertTrue(input.admitAirUse(1_201_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ } ++ ++ @Test ++ void freshAirSuppressionRetainsUntimedBlockContinuationUntilRelease() { ++ final var input = new ItemUseState(); ++ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ input.release(); ++ assertFalse(input.admitAirUse(1_172_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ assertTrue(input.frameReady(1_173_000_000L, false)); ++ assertFalse(input.admitAirUse(1_201_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ input.attemptedBlock(1_202_000_000L, false); ++ assertTrue(input.frameReady(1_203_000_000L, false)); ++ input.release(); ++ assertTrue(input.admitAirUse(1_204_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ } ++ ++ @Test ++ void emptyHandContinuationUsesFramesWithoutTakingAnExistingAirHold() { ++ final var input = new ItemUseState(); ++ input.retainBlockUse(); ++ input.attemptedBlock(1_000_000_000L, false); ++ assertTrue(input.frameReady(1_001_000_000L, false)); ++ assertFalse(input.admitAirUse(1_002_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ input.release(); ++ assertFalse(input.frameReady(1_003_000_000L, false)); ++ assertTrue(input.admitAirUse(1_010_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ input.retainBlockUse(); ++ assertEquals(HitResult.Type.MISS, input.hitType(1_500_000_000L, HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertFalse(input.frameReady(1_210_000_000L, false)); ++ assertTrue(input.frameReady(1_210_000_001L, false)); ++ } ++ ++ @Test ++ void freshBlockDispatchDoesNotBypassTheExistingEntityAndAirDeadline() { ++ final var input = new ItemUseState(); ++ assertTrue(input.admitAirUse(1_000_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ input.release(); ++ assertEquals(HitResult.Type.BLOCK, input.hitType(1_172_000_000L, HitResult.Type.BLOCK, InteractionHand.MAIN_HAND)); ++ assertEquals(HitResult.Type.MISS, input.hitType(1_172_000_000L, HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); ++ assertFalse(input.admitAirUse(1_172_000_000L, InteractionHand.MAIN_HAND, true, false, true)); ++ input.release(); ++ assertEquals(HitResult.Type.MISS, input.hitType(1_200_000_000L, HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); ++ assertEquals(HitResult.Type.ENTITY, input.hitType(1_200_000_001L, HitResult.Type.ENTITY, InteractionHand.MAIN_HAND)); ++ assertTrue(input.admitAirUse(1_200_000_001L, InteractionHand.MAIN_HAND, true, false, true)); ++ } ++ ++ @Test + void monotonicClockRolloverKeepsBothDeadlines() { + final var timing = new ItemUseState(); + final long start = Long.MAX_VALUE - 150_000_000L; + timing.attemptedBlock(start, true); + timing.release(); + assertTrue(timing.admitAirUse(start, InteractionHand.MAIN_HAND, true, false, true)); -+ assertFalse(timing.frameReady(start + 200_000_000L, true, false)); -+ assertTrue(timing.frameReady(start + 200_000_001L, true, false)); ++ assertFalse(timing.frameReady(start + 200_000_000L, false)); ++ assertTrue(timing.frameReady(start + 200_000_001L, false)); + timing.release(); + timing.attemptedBlock(start, true); -+ assertFalse(timing.frameReady(start + 299_999_999L, false, false)); -+ assertFalse(timing.frameReady(start + 300_000_000L, false, false)); -+ assertTrue(timing.frameReady(start + 300_000_001L, false, false)); ++ assertFalse(timing.frameReady(start + 299_999_999L, false)); ++ assertFalse(timing.frameReady(start + 300_000_000L, false)); ++ assertTrue(timing.frameReady(start + 300_000_001L, false)); + } +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacementTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/inventory/TrapdoorPlacementTest.java diff --git a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md index d7669c92..d86a8bfa 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0038-predict-held-trapdoor-placement.pr.md @@ -4,8 +4,14 @@ Coordinate food retries and block placement through one client input owner. Inventory consumption and completion remain server-owned. Food and the first block-item air phase share a strict 200 ms monotonic deadline. -Fresh calls retain that deadline after release. -Frame repeats run after camera input. +Release retains the air deadline, but fresh block dispatch does not wait for it. +Fresh entity admission still waits for that deadline. +A consumed-click call resets the previous mode, path, and core session. +Tick and frame continuation retain that ownership. +Empty-hand rejection also continues from frames, including a fresh press after stack exhaustion. +A suppressed initial food/block air phase keeps untimed block continuation until release. +Frame repeats run after camera input and require an already owned mode. +The queued click establishes initial ownership, so a frame cannot preempt it. Select food from its transported component, including custom items carried as paper. A refused first food request retains block continuation while still recording the air deadline. The first rejected block placement also performs one air use and acquires untimed block continuation. @@ -20,7 +26,7 @@ Missing optional block-use channels do not clear the air-use hold or deadline. Placement has a separate strict delay: 300 ms before a line is established and 200 ms afterward. Successful local use restarts it; failed repeats do not. The frame owner suppresses Java's parallel four-tick path. -A first successful placement records the air deadline when blocks remain. +A first successful placement records the air deadline when blocks remain and the previous air deadline permits it. Core already sends that air phase, so the client does not send another transaction. Held placement success does not refresh the air deadline. Last-block placement and block activation do not create an air deadline. @@ -40,7 +46,9 @@ Returning to a trapdoor restores its snap. Separate trapdoors and the original support remain valid targets. Send physical use lifetime and sequence-owned prediction before the concluding Java packet. -Prediction format 2 carries the result and native cursor, including coordinates outside the selected support cube. +Negotiated prediction format 3 carries the result, native cursor, and air admission. +Coordinates can remain outside the selected support cube. +The client retains format 2 for older cores, which cannot honor the new admission field. Use the active Bedrock connection for native outlines and occlusion, including ViaProxy sessions with a local Java protocol target. The visible raycast remains unchanged. @@ -59,7 +67,7 @@ The cursor keeps its original world location, yielding a negative relative Z coo Player-collision failures retain the next cell. Returning to the old aim stops those attempts; a fresh press after release can use the top face. -Ten input-state regressions cover ownership, hand/target routing, initial rejection, clocks, last-stack placement, release, connection reset, and rollover. +Fourteen input-state regressions cover ownership, hand/target routing, initial rejection, clocks, last-stack placement, release, connection reset, and rollover. Eight path regressions cover native float points, rejection, advancement, all six axes, release, line delays, precision limits, and failed-attempt history. Five trapdoor regressions cover orientation, materials, independent properties, targets, frozen hits, and ordinary blocks. Unit cases for all axes do not establish live native parity in every direction. @@ -68,9 +76,13 @@ Installed direct and ViaProxy controls cover the formerly failing Stone cases, r First-rejected Stone and trapdoor controls send one native air payload, then immediate simulation retries without consuming items. Their air fields and fresh/simulation triggers match native, excluding player-coordinate equality. Both routes also verify unattached block use, first-success air use, last-stack exclusion, and activation without air use. +Rapid controls observe three fresh calls across successful placement, rejection, activation, last-stack placement, and food. +Before the air deadline, successful and rejected block calls still dispatch while their air phase stays suppressed. +Activation leaves air admission available, and the last stack creates no air deadline. +The direct boundary supplement uses the same installed JAR as its full session. Both routes also retain the six timed/refused-food controls, three earlier hold-mode controls, release/Pause, and eleven food-admission cases. The [food-use validation](../../viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.md) records admission and use-cycle limits. -The full four-project build passes 3,302 tests, with 325 skipped and no failures or errors. +The full four-project build passes 3,308 tests, with 325 skipped and no failures or errors. Installed JAR checksums match the build. Earlier trapdoor fixtures cover camera and material changes, cancellation, reconciliation, and specific rejection histories. @@ -83,5 +95,8 @@ Live build-height limits, moving placement, and air/entity target dispatch need Other initial use families, mixed-use state, and complete input phases also remain open. An unchanged stack does not prove repeat-timing parity. Frame cadence changes retry and instant-food counts, so these controls do not establish equal coordinates or complete scheduling parity. -Short native rapid-click trials did not sample every physical edge and do not establish edge parity. +A native fresh callback dispatches rejected block use about 27 ms before the air deadline, with no second air transaction. +Short native input spans still miss some physical edges and do not establish full edge parity. +Dynamic capability changes and legacy fallback need broader installed controls. +Short food presses while Java still owns an active use cycle need separate native comparisons. The original intermittent animation-without-feeding report remains open. From 4d582933a07c347d112ce52b373237666dd2990f Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 04:16:16 +0100 Subject: [PATCH 21/47] fix(inventory): retain pending search edits across screens Retain the latest local search text and explicit clear within the connection before waiting for an opening-bound reply. Restore text through a fresh command when another presentation opens, and discard the cache on disconnect. Keep pending item commands and late replies bound to their original screen. Match Bedrock 1.26.60.5 Escape behavior by leaving search focus before closing. Add controller and real-editor regressions, and verify rebuilt direct and ViaProxy clients with four-second search-processing stalls. Record the separate rapid-reopen closure as unresolved. --- docs/native-player-inventory.md | 9 +- ...9-present-authored-native-containers.patch | 207 ++++++++++++++++-- ...9-present-authored-native-containers.pr.md | 9 + 3 files changed, 210 insertions(+), 15 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index c1556987..2ada099c 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -104,6 +104,12 @@ The add-on owns input delivery and presentation over authoritative slots and cur Keep player destinations, offhand permissions, item locks, and creative actions explicit. A fallback acknowledgment must not consume a newer menu's request. +Retain the latest local search edit within its connection before waiting for a command reply. +Closing a screen retires its commands but preserves unsent text and explicit clearing. +A replacement presentation submits that text with its own opening, generation, revision, and request sequence. +Disconnect clears the client's retained edit. +In Bedrock 1.26.60.5, Escape first removes search focus. The next Escape closes the inventory. + Native 1.26.60.5 classic-screen controls distinguish recipe selection from crafting. A normal recipe click fills the grid. Taking its output sends a separate craft request. A workbench recipe Shift-click uses `CraftRecipeAuto` with explicit ingredient descriptors, followed by results, consumption, and placement actions. @@ -217,7 +223,8 @@ Installed direct and ViaProxy controls confirm these pointer, capacity, and reop Recipe families beyond the captured plank, cake, and honey controls, recipes with more than two outputs, and complete installed native parity remain unfinished. Native query lifetime across disconnects and world changes remains unverified. -Query edits that race menu closure still need native comparison. +Under a forced search-processing stall, a rapid player-inventory reopen can close again. +The close producer remains unverified. 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/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.patch b/patches/viafabricplus-bedrock/upstreamable/0029-present-authored-native-containers.patch index a72e3acd..b7888c4b 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 2bd6f7803d6a4335a151f97c670d73891449cda0 Mon Sep 17 00:00:00 2001 +From c45a175ec9fe85be6e58dda45e7e799090b0ef44 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 @@ -690,6 +690,22 @@ EditBox screen regression reproduces lost text input before the fix, then accepts another query containing the inventory key after refocusing. This fixes Java focus bookkeeping; it does not change native search or inventory action semantics. + +Retain the latest local search edit at connection scope before serializing +its opening-bound command. Restore only that text into a fresh presentation, +which sends a new search under its own opening, generation and revision. +Closing a menu may discard pending commands and queued transport work; it +must not discard unsent edits or an explicit clear. Disconnect clears the +client retention. Late replies keep their existing opening guards. + +Official Bedrock 1.26.60.5 against BDS 1.26.60.6/protocol 2225 retains the +latest query and explicit clear after pointer close and reopening. Its first +Escape while editing removes search focus; the next Escape closes the +inventory. Reproduce that focus order in the actual editor. An installed +Java control with search handling delayed by four seconds loses visible +text before this change. Controller boundary tests cover queued text and +clearing across close/reopen, late replies, blocked item actions while a +new search is pending, and connection replacement. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + @@ -703,10 +719,10 @@ inventory action semantics. .../render/MixinNativeGuiItemState.java | 20 + .../render/MixinNativeGuiOversizedItem.java | 31 + .../container/BedrockFurnacePreferences.java | 60 + - .../BedrockNativeContainerScreen.java | 1330 ++++++++++++++++ + .../BedrockNativeContainerScreen.java | 1331 ++++++++++++++++ .../container/BedrockNativeContainers.java | 282 ++++ .../container/BedrockNativeCooking.java | 147 ++ - .../BedrockNativePlayerInventory.java | 163 ++ + .../BedrockNativePlayerInventory.java | 170 ++ .../container/BedrockPlayerInventory.java | 92 ++ .../NativeContainerActionPayload.java | 30 + .../NativeContainerCurrentPayload.java | 30 + @@ -768,10 +784,11 @@ inventory action semantics. .../NativeInventoryItemStyleTest.java | 84 + .../NativeInventoryProgressTest.java | 83 + .../container/NativePlayerHelpTextTest.java | 34 + + .../NativePlayerInventoryLifetimeTest.java | 142 ++ .../NativePlayerPresentationTest.java | 153 ++ - .../container/NativePlayerScreenTest.java | 571 +++++++ + .../container/NativePlayerScreenTest.java | 576 +++++++ .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 80 files changed, 10779 insertions(+), 62 deletions(-) + 81 files changed, 10934 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 @@ -845,6 +862,7 @@ inventory action semantics. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryItemStyleTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativeInventoryProgressTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerHelpTextTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerInventoryLifetimeTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerPresentationTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenLifecycleTest.java @@ -1313,10 +1331,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..15de47d +index 0000000..7f60cd2 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainerScreen.java -@@ -0,0 +1,1330 @@ +@@ -0,0 +1,1331 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -2428,8 +2446,9 @@ index 0000000..15de47d + this.fallback(new IllegalArgumentException("Native recipe drop input is not supported")); + return true; + } -+ if (this.search != null && this.search.isFocused() && event.key() != InputConstants.KEY_ESCAPE) { -+ this.search.keyPressed(event); ++ if (this.search != null && this.search.isFocused()) { ++ if (event.key() == InputConstants.KEY_ESCAPE) this.setFocused(null); ++ else this.search.keyPressed(event); + return true; + } + if (this.waitingForPreference()) { @@ -3090,10 +3109,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..96d2187 +index 0000000..6081bf8 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativePlayerInventory.java -@@ -0,0 +1,163 @@ +@@ -0,0 +1,170 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3126,6 +3145,7 @@ index 0000000..96d2187 + private static ClientPacketListener owner; + private static AbstractContainerMenu menu; + private static NativePlayerPresentation presentation; ++ private static String retainedQuery; + private static AbstractContainerMenu retiredMenu; + private static long retiredOpening; + private static long retiredGeneration; @@ -3153,6 +3173,7 @@ index 0000000..96d2187 + private static void clear() { + if (presentation != null) presentation.retire(); + presentation = null; ++ retainedQuery = null; + menu = null; + owner = null; + retiredMenu = null; @@ -3190,6 +3211,7 @@ index 0000000..96d2187 + if (presentation != null) presentation.retire(); + menu = expected; + presentation = new NativePlayerPresentation(message, previews, System::nanoTime); ++ if (retainedQuery != null) presentation.query(retainedQuery, BedrockNativePlayerInventory::send); + } + } catch (final RuntimeException unsupported) { + ViaFabricPlusBedrock.impl().logger().warn("Unsupported native player inventory presentation", unsupported); @@ -3242,7 +3264,11 @@ index 0000000..96d2187 + } + + static void query(final ClientPacketListener listener, final AbstractContainerMenu expected, final String value) { -+ if (current(listener, expected) && presentation != null) presentation.query(value, BedrockNativePlayerInventory::send); ++ if (current(listener, expected) && presentation != null) { ++ // Local edits outlive a closed menu even when its last command has no reply yet. ++ retainedQuery = value; ++ presentation.query(value, BedrockNativePlayerInventory::send); ++ } + } + + static void retire(final NativePlayerPresentation candidate) { @@ -11387,6 +11413,154 @@ index 0000000..b9a8f60 + assertThrows(IllegalArgumentException.class, () -> NativePlayerHelpText.resolve(scene, token -> null)); + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerInventoryLifetimeTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerInventoryLifetimeTest.java +new file mode 100644 +index 0000000..3875605 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerInventoryLifetimeTest.java +@@ -0,0 +1,142 @@ ++/* ++ * 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.viaversion.util.GsonUtil; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; ++import net.minecraft.client.Minecraft; ++import net.minecraft.client.gui.Gui; ++import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; ++import net.minecraft.client.multiplayer.ClientPacketListener; ++import net.minecraft.client.player.LocalPlayer; ++import net.minecraft.network.Connection; ++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.raphimc.viabedrock.api.model.NativePlayerInventoryAction; ++import net.raphimc.viabedrock.api.model.NativePlayerInventoryMessage; ++import net.raphimc.viabedrock.api.model.NativePlayerInventoryResult; ++import net.raphimc.viabedrock.api.resourcepack.ui.PlayerInventoryUi; ++import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage; ++import org.junit.jupiter.api.BeforeAll; ++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.*; ++ ++class NativePlayerInventoryLifetimeTest { ++ @BeforeAll ++ static void bootstrap() throws Exception { ++ NativePlayerPresentationTest.bootstrap(); ++ } ++ ++ @ParameterizedTest ++ @ValueSource(strings = {"honey block", ""}) ++ void closingBeforeTheSearchReplyRetainsTheLatestEditOnlyWithinItsConnection(final String latest) throws Exception { ++ final var game = mock(Minecraft.class); ++ final var player = mock(LocalPlayer.class); ++ final var menu = new InventoryMenu(new Inventory(player, new EntityEquipment()), true, player); ++ final var owner = mock(ClientPacketListener.class); ++ final var connection = mock(Connection.class); ++ final var screen = mock(AbstractContainerScreen.class); ++ final var gui = mock(Gui.class); ++ when(owner.getConnection()).thenReturn(connection); ++ when(connection.isConnected()).thenReturn(true); ++ when(game.getConnection()).thenReturn(owner); ++ when(screen.getMenu()).thenReturn(menu); ++ when(gui.screen()).thenReturn(screen); ++ set(Minecraft.class, game, "player", player); ++ set(Minecraft.class, game, "gui", gui); ++ set(Player.class, player, "inventoryMenu", menu); ++ final List sent = new ArrayList<>(); ++ try (final var singleton = mockStatic(Minecraft.class); ++ final var network = mockStatic(ClientPlayNetworking.class); ++ final var screens = mockStatic(BedrockPlayerInventory.class); ++ final var containers = mockStatic(BedrockNativeContainers.class)) { ++ singleton.when(Minecraft::getInstance).thenReturn(game); ++ containers.when(BedrockNativeContainers::enabled).thenReturn(true); ++ network.when(() -> ClientPlayNetworking.canSend(NativePlayerInventoryActionPayload.TYPE)).thenReturn(true); ++ network.when(() -> ClientPlayNetworking.send(any(NativePlayerInventoryActionPayload.class))).thenAnswer(call -> { ++ sent.add(((NativePlayerInventoryActionPayload) call.getArgument(0)).action()); ++ return null; ++ }); ++ try { ++ set(BedrockNativePlayerInventory.class, null, "owner", owner); ++ screens.when(() -> BedrockPlayerInventory.opening(owner, menu)).thenReturn(1L); ++ accept(owner, message(1, 8, "oak planks")); ++ final var original = presentation(); ++ BedrockNativePlayerInventory.query(owner, menu, "h"); ++ BedrockNativePlayerInventory.query(owner, menu, latest); ++ assertEquals(latest, original.query()); ++ assertEquals(1, sent.size()); ++ BedrockNativePlayerInventory.retire(original); ++ ++ screens.when(() -> BedrockPlayerInventory.opening(owner, menu)).thenReturn(2L); ++ accept(owner, message(2, 9, "h")); ++ final var reopened = presentation(); ++ assertNotSame(original, reopened); ++ assertEquals(latest, reopened.query()); ++ assertEquals(2, sent.size()); ++ final var replacement = sent.getLast(); ++ assertEquals(latest, replacement.query()); ++ assertEquals(2, replacement.opening()); ++ assertEquals(9, replacement.generation()); ++ assertFalse(original.result(new NativePlayerInventoryResult(1, 8, 1, 2, true))); ++ assertEquals(latest, reopened.query()); ++ assertFalse(reopened.send(NativePlayerInventoryAction.Kind.PRIMARY, 0, "", null, sent::add)); ++ ++ clear(); ++ final var nextOwner = mock(ClientPacketListener.class); ++ when(nextOwner.getConnection()).thenReturn(connection); ++ when(game.getConnection()).thenReturn(nextOwner); ++ set(BedrockNativePlayerInventory.class, null, "owner", nextOwner); ++ screens.when(() -> BedrockPlayerInventory.opening(nextOwner, menu)).thenReturn(1L); ++ accept(nextOwner, message(1, 1, "sand")); ++ assertEquals("sand", presentation().query()); ++ assertEquals(2, sent.size()); ++ } finally { ++ clear(); ++ } ++ } ++ } ++ ++ private static NativePlayerInventoryMessage message(final long opening, final long generation, final String query) { ++ final var state = new PlayerInventoryUi.State(false, false, 2, 2, 5, 2, true, query, "Recipes", "Crafting", ++ List.of(1, 0, 0, 0), List.of(), "", -1); ++ final var ui = new PlayerInventoryUi(new ResourcePackStorage(List.of()).getJsonUiProgram()); ++ return new NativePlayerInventoryMessage(0, opening, generation, 1, ui.exportProgram(false).toString(), ++ GsonUtil.getGson().toJson(state), Map.of()); ++ } ++ ++ private static void accept(final ClientPacketListener owner, final NativePlayerInventoryMessage message) throws Exception { ++ final var method = BedrockNativePlayerInventory.class.getDeclaredMethod("accept", ClientPacketListener.class, NativePlayerInventoryMessage.class); ++ method.setAccessible(true); ++ method.invoke(null, owner, message); ++ } ++ ++ private static NativePlayerPresentation presentation() throws Exception { ++ final var field = BedrockNativePlayerInventory.class.getDeclaredField("presentation"); ++ field.setAccessible(true); ++ return (NativePlayerPresentation) field.get(null); ++ } ++ ++ private static void clear() throws Exception { ++ final var method = BedrockNativePlayerInventory.class.getDeclaredMethod("clear"); ++ method.setAccessible(true); ++ method.invoke(null); ++ } ++ ++ private static void set(final Class type, final Object owner, final String name, final Object value) throws Exception { ++ final var field = type.getDeclaredField(name); ++ field.setAccessible(true); ++ field.set(owner, value); ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerPresentationTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerPresentationTest.java new file mode 100644 index 0000000..d3902fa @@ -11548,10 +11722,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..ff0af89 +index 0000000..127e576 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java -@@ -0,0 +1,571 @@ +@@ -0,0 +1,576 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -11688,6 +11862,11 @@ index 0000000..ff0af89 + network.verify(() -> BedrockNativePlayerInventory.query(owner, menu, "ae")); + assertFalse(screen.closing()); + verifyNoInteractions(game.gameMode); ++ assertTrue(screen.keyPressed(new KeyEvent(InputConstants.KEY_ESCAPE, 0, 0))); ++ assertNull(screen.getFocused()); ++ assertFalse(screen.closing()); ++ assertTrue(screen.keyPressed(new KeyEvent(InputConstants.KEY_ESCAPE, 0, 0))); ++ assertTrue(screen.closing()); + } finally { screen.discard(); } + } + } 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 d4337c12..bd4d1cb2 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 @@ -32,7 +32,16 @@ The consumer regression keeps raw binding quantities and genuine quickcraft prev Player and workbench snapshots retain actual menu slots, recipe identities, and full item previews. Commands carry the opening, generation, snapshot revision, and request sequence. Buffered search edits survive an acknowledgment that advances the container generation. +The connection retains the latest local edit before command serialization, including an explicit clear. +Closing retires pending commands. A replacement presentation sends retained text under its own opening and generation. +Disconnect clears this client state; late replies cannot replace it. Search focus clears through the parent GUI owner when clicking elsewhere, hiding the editor, or exiting. +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 retain search text and explicit clearing after pointer close and reopening. +The first Escape removes editor focus; the next closes the inventory. +Controller regressions cover closing before a search reply, replacement commands, explicit clearing, late replies, and connection replacement. +Rebuilt direct and ViaProxy controls delay search handling by four seconds, close with unsent edits, and retain the latest text and explicit clear after reopening. +Both routes preserve the 32-log stack and the native two-step Escape behavior. +A rapid player-inventory reopen during this forced stall can close again. Its close producer remains unresolved. A real-editor screen regression clicks a recipe, returns to search, and types the inventory key without closing the screen. Installed direct and ViaProxy controls craft once, enter three replacement queries in the same opening, and retain completed output after reopening. Each route records one server-accepted craft request. Late replies and hover exits cannot affect a replacement opening or selection. From 46d060711d30dbba11a26a0c8635b3452d741433 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 05:14:08 +0100 Subject: [PATCH 22/47] fix(inventory): reject opens during native close waits Match Bedrock close-wait admission: reject a player opening received before the old close acknowledgment and require fresh input afterward. Retire native state on client-initiated acknowledgments without forwarding another Java GUI close. Preserve genuine server-initiated closure. Fold the behavior and packet/lifecycle regressions into the owning inventory patch. Native, direct and ViaProxy controls use four-second acknowledgment delays. Keep client-side admission timing open because Java can install a local screen before its correlated rejection arrives. --- docs/native-player-inventory.md | 9 +- ...erver-authoritative-bedrock-requests.patch | 133 ++++++++++++++---- ...erver-authoritative-bedrock-requests.pr.md | 16 +++ ...edrock-recipes-and-place-ingredients.patch | 43 +++--- ...ain-bedrock-creative-catalog-entries.patch | 56 ++++---- ...9-present-authored-native-containers.pr.md | 3 +- 6 files changed, 174 insertions(+), 86 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 2ada099c..60a4f50e 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -23,7 +23,10 @@ An authored presentation swap retains that token. An ordinary reopen receives a The Bedrock open acknowledgment has no client token. Core serializes native openings and retains retired requests until their acknowledgments arrive. -A new opening waits for the previous close acknowledgment. +Native ignores the inventory key while a previous player or workbench close awaits acknowledgment. +Core rejects a player opening received during that wait by its own opening token. +It requires fresh input after acknowledgment instead of queuing the rejected opening. +Client-initiated close acknowledgments retire native state without forwarding another Java screen close. Bind clicks, data, and closes to the connection, menu object, menu ID, and opening token. A stale reply must not operate on a replacement screen. Keep global player-inventory reconciliation independent of a retired temporary menu. @@ -223,8 +226,8 @@ Installed direct and ViaProxy controls confirm these pointer, capacity, and reop Recipe families beyond the captured plank, cake, and honey controls, recipes with more than two outputs, and complete installed native parity remain unfinished. Native query lifetime across disconnects and world changes remains unverified. -Under a forced search-processing stall, a rapid player-inventory reopen can close again. -The close producer remains unverified. +Java can briefly display its local inventory before a correlated close-wait rejection arrives. +Client-side admission timing remains unverified. 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.patch b/patches/viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.patch index 21df092e..ab7ef006 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 220b535527f2c106a7e0e5df6dc7f5f16d7dee67 Mon Sep 17 00:00:00 2001 +From ccc44e9cb92bfdf4740792ad8cec809ea850a358 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 @@ -72,7 +72,7 @@ Use authoritative requests for hand swaps when negotiated by START_GAME. Queue o Allocate legacy inventory transaction IDs with a separate negative even counter. Static analysis of the matching Bedrock 1.26.51.1 executable confirms allocation starts at -4 and decreases by two; item-stack requests use the independent odd sequence. Keep native captures and disassembly private. -Process temporary player inventory CLOSE acknowledgements without forwarding another Java screen close. Java owns its creative and player screen; forwarding the native acknowledgement prematurely closes it and causes subsequent inputs to affect the wrong UI. Keep genuine server-initiated closures intact. +Process client-initiated container CLOSE acknowledgements without forwarding another Java screen close. Java already dismissed the owning screen; forwarding its native acknowledgement can close a later UI. Retire pending native state and service queued work before consuming the acknowledgement. Keep genuine server-initiated closures intact. Write stonecutter ingredients as typed HolderSet fields instead of raw VarInts. Both encodings produce the same Java wire bytes, but ViaBackwards @@ -504,7 +504,7 @@ the newer construction/admission controls. Remove the superseded network name reader and its three tests so two independent state machines cannot drift. Native asset precedence and full ordinary names remain open. -Own player inventory openings by the actual installed client screen over a negotiated payload shared by direct and proxy routes. Java reuses menu zero, so retain retired native opens until acknowledgment and correlate server closes to a monotonic client opening. Serialize rapid reopenings, discard retired screen clicks, and preserve later global inventory interactions. Reject malformed or stale lifecycle messages without changing native ownership. +Own player inventory openings by the actual installed client screen over a negotiated payload shared by direct and proxy routes. Java reuses menu zero, so retain retired native opens until acknowledgment and correlate server closes to a monotonic client opening. Retain uncorrelated in-flight openings, reject new player openings while a previous close is pending, discard retired screen clicks, and preserve later global inventory interactions. Reject malformed or stale lifecycle messages without changing native ownership. Verify nonempty component readers inside complete native registry and late-block construction. Standalone reader controls cannot establish how @@ -762,6 +762,20 @@ created-output transfers for crafting batches. Ordinary transfer admission continues to reject crafting actions. Native-shaped regression requests cover reordered success responses, a rejected later craft, whole-batch rejection and refusal of unrelated destructive actions. + +Official Bedrock 1.26.60.5 against BDS 1.26.60.6/protocol 2225 ignores +a new inventory key while a previous player or workbench close awaits its +acknowledgement. A raw relay delays that acknowledgement by four seconds. +No OpenInventory interaction is sent for the early input, and the screen +stays closed after the acknowledgement until a fresh key press. + +Reject an installed player opening received during that close wait through +its own opening token. Do not queue it to open after the acknowledgement. +A later input can open normally once the pending native container retires. +Packet and tracker regressions cover both close sources, consumed client +acknowledgements, genuine server closes, and the fresh-input requirement. +Java can still briefly display its local screen before receiving the +correlated rejection; client-side frame admission remains a separate gate. --- build.gradle | 3 +- .../api/model/PlayerInventoryScreenClose.java | 46 + @@ -817,7 +831,7 @@ rejection and refusal of unrelated destructive actions. .../protocol/model/recipe/ItemRecipeKey.java | 35 + .../protocol/packet/ClientPlayerPackets.java | 60 +- .../protocol/packet/InteractionPackets.java | 51 +- - .../protocol/packet/InventoryPackets.java | 1526 +++- + .../protocol/packet/InventoryPackets.java | 1525 +++- .../protocol/packet/JoinPackets.java | 2 +- .../protocol/packet/MultiStatePackets.java | 34 +- .../protocol/packet/WorldPackets.java | 12 + @@ -829,7 +843,7 @@ rejection and refusal of unrelated destructive actions. .../storage/InventoryRequestStorage.java | 232 + .../storage/InventoryRequestTracker.java | 795 +++ .../protocol/storage/InventoryTracker.java | 282 +- - .../storage/PlayerInventoryScreenTracker.java | 202 + + .../storage/PlayerInventoryScreenTracker.java | 207 + .../protocol/types/item/BedrockItemType.java | 68 +- .../types/item/NetworkItemStackContext.java | 18 + .../item/NetworkItemStackDescriptorType.java | 79 +- @@ -874,7 +888,7 @@ rejection and refusal of unrelated destructive actions. .../packet/ContainerLifecyclePacketTest.java | 395 ++ .../protocol/packet/DropTransactionTest.java | 46 + .../packet/HotbarSelectionPacketTest.java | 155 + - .../protocol/packet/InventoryPacketsTest.java | 254 + + .../protocol/packet/InventoryPacketsTest.java | 290 + .../packet/MountInventoryPacketTest.java | 163 + .../NativeContainerOpeningPacketTest.java | 226 +- .../packet/Protocol2225InventoryTest.java | 225 + @@ -895,13 +909,13 @@ rejection and refusal of unrelated destructive actions. .../NativeContainerActionBackendTest.java | 95 + .../storage/NativeContainerResponseTest.java | 287 + .../PlayerInventoryScreenCodecTest.java | 71 + - .../PlayerInventoryScreenTrackerTest.java | 271 + + .../PlayerInventoryScreenTrackerTest.java | 294 + .../storage/StonecutterRecipePacketTest.java | 53 + .../types/inventory/ItemStackCodecTest.java | 12 +- .../item/NetworkItemConstructionTest.java | 171 + .../types/item/NetworkItemCountTest.java | 35 +- .../types/item/NetworkItemStackFixtures.java | 69 + - 138 files changed, 28833 insertions(+), 2047 deletions(-) + 138 files changed, 28896 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 @@ -9625,7 +9639,7 @@ index 223db3d9..d8e307df 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..375f5f0c 100644 +index d22d7616..48721470 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 @@ @@ -9789,7 +9803,7 @@ index d22d7616..375f5f0c 100644 final CommandBlockSession command = wrapper.user().get(CommandBlockSession.class); if (command != null && command.matches((byte) (int) wrapper.get(Types.VAR_INT, 0))) { -@@ -116,20 +187,34 @@ public final class InventoryPackets { +@@ -116,20 +187,33 @@ public final class InventoryPackets { return; } @@ -9821,14 +9835,13 @@ index d22d7616..375f5f0c 100644 + final boolean correlatedClose = container instanceof InventoryContainer && screens != null && screens.ownsClose(); inventoryTracker.setCurrentContainerClosed(serverInitiated); - if (!serverInitiated && inventoryTracker.getCurrentForm() != null) { -+ if (correlatedClose) { -+ wrapper.cancel(); -+ } -+ if (!serverInitiated && (container instanceof InventoryContainer || inventoryTracker.getCurrentForm() != null)) { ++ // A client-initiated close already dismissed its Java screen. Its acknowledgment ++ // cannot close a later screen, but must still retire native state above. ++ if (correlatedClose || !serverInitiated) { wrapper.cancel(); } } -@@ -202,347 +287,863 @@ public final class InventoryPackets { +@@ -202,347 +286,863 @@ public final class InventoryPackets { } public static void register(final BedrockProtocol protocol) { @@ -11001,7 +11014,7 @@ index d22d7616..375f5f0c 100644 protocol.registerServerbound( ServerboundPackets26_3.EDIT_BOOK, null, -@@ -593,33 +1194,55 @@ public final class InventoryPackets { +@@ -593,33 +1193,55 @@ public final class InventoryPackets { updatedTag.put("pages", updatedPages); book.setTag(updatedTag); }); @@ -11079,7 +11092,7 @@ index d22d7616..375f5f0c 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 +1254,7 @@ public final class InventoryPackets { +@@ -631,7 +1253,7 @@ public final class InventoryPackets { final String raw = customName.getValue(); return new ContainerTitle(TextUtil.stringToTextComponent(user.get(ResourcePackStorage.class).getTexts().translate(raw)), raw, false); } @@ -11088,7 +11101,7 @@ index d22d7616..375f5f0c 100644 } private static String actorTitle(final Entity actor, final ActorDataIds field) { -@@ -657,21 +1280,23 @@ public final class InventoryPackets { +@@ -657,21 +1279,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; @@ -11116,7 +11129,7 @@ index d22d7616..375f5f0c 100644 return; } final var resources = wrapper.user().get(ResourcePackStorage.class); -@@ -680,22 +1305,33 @@ public final class InventoryPackets { +@@ -680,22 +1304,33 @@ public final class InventoryPackets { } final var state = wrapper.user().get(NativeContainerUiState.class); final var ui = resources.getContainerUi(); @@ -11154,7 +11167,7 @@ index d22d7616..375f5f0c 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 +1622,148 @@ public final class InventoryPackets { +@@ -986,13 +1621,148 @@ public final class InventoryPackets { } } @@ -11305,7 +11318,7 @@ index d22d7616..375f5f0c 100644 for (String line : BedrockTextUtils.split(text, "\n")) { final TextInput textInput = new TextInput(TextUtil.stringToTextComponent(line)); textInput.setMaxLength(Integer.MAX_VALUE); -@@ -1000,14 +1771,53 @@ public final class InventoryPackets { +@@ -1000,14 +1770,53 @@ public final class InventoryPackets { dialog.getInputs().add(new Input("dummy", textInput)); } } else { // VB compatibility @@ -13941,10 +13954,10 @@ index 7472880e..19ffffe3 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java new file mode 100644 -index 00000000..e7a9d17d +index 00000000..7256dcde --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java -@@ -0,0 +1,202 @@ +@@ -0,0 +1,207 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -14020,6 +14033,11 @@ index 00000000..e7a9d17d + this.requests().clearQueuedScreenInteractions(); + this.latestOpening = request.opening(); + this.requested = true; ++ if (this.inventory().getPendingCloseContainer() != null) { ++ // Native ignores a new inventory key until the previous close is acknowledged. ++ this.retire(); ++ return; ++ } + this.requestedSince = System.nanoTime(); + } else { + if (request.opening() != this.latestOpening || !this.requested) { @@ -34709,10 +34727,10 @@ index 00000000..50ae8ed9 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java new file mode 100644 -index 00000000..7dccb3cc +index 00000000..d00218b7 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/InventoryPacketsTest.java -@@ -0,0 +1,254 @@ +@@ -0,0 +1,290 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -34739,6 +34757,7 @@ index 00000000..7dccb3cc +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.raphimc.viabedrock.api.model.container.ChestContainer; ++import net.raphimc.viabedrock.api.model.container.Container; +import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; +import net.raphimc.viabedrock.api.model.entity.Entity; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; @@ -34764,6 +34783,7 @@ index 00000000..7dccb3cc +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 { + @@ -34789,6 +34809,40 @@ index 00000000..7dccb3cc + } + + @Test ++ void workbenchCloseAcknowledgementRetiresTheNativeMenuWithoutClosingReplacementJavaScreens() { ++ final CloseFixture fixture = new CloseFixture(); ++ final Container workbench = mock(Container.class); ++ when(workbench.type()).thenReturn(ContainerType.WORKBENCH); ++ fixture.inventory.setCurrentContainer(workbench); ++ fixture.inventory.markPendingClose(workbench); ++ final PacketWrapper response = fixture.closePacket(ContainerType.NONE, false); ++ response.set(Types.VAR_INT, 0, 2); ++ ++ InventoryPackets.handleContainerClose(response); ++ ++ assertTrue(response.isCancelled()); ++ assertFalse(fixture.inventory.serverInitiated); ++ assertNull(fixture.inventory.getCurrentContainer()); ++ assertNull(fixture.inventory.getPendingCloseContainer()); ++ } ++ ++ @Test ++ void genuineServerWorkbenchClosureStillClosesTheJavaScreen() { ++ final CloseFixture fixture = new CloseFixture(); ++ final Container workbench = mock(Container.class); ++ when(workbench.type()).thenReturn(ContainerType.WORKBENCH); ++ fixture.inventory.setCurrentContainer(workbench); ++ final PacketWrapper response = fixture.closePacket(ContainerType.WORKBENCH, true); ++ response.set(Types.VAR_INT, 0, 2); ++ ++ InventoryPackets.handleContainerClose(response); ++ ++ assertFalse(response.isCancelled()); ++ assertTrue(fixture.inventory.serverInitiated); ++ assertNull(fixture.inventory.getCurrentContainer()); ++ } ++ ++ @Test + void genuineServerInventoryClosureStillClosesTheJavaScreen() { + final CloseFixture fixture = new CloseFixture(); + fixture.inventory.setCurrentContainer(fixture.inventory.getInventoryContainer()); @@ -40144,10 +40198,10 @@ index 00000000..66e78fc9 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTrackerTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTrackerTest.java new file mode 100644 -index 00000000..7882c712 +index 00000000..65e1be48 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTrackerTest.java -@@ -0,0 +1,271 @@ +@@ -0,0 +1,294 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -40201,7 +40255,7 @@ index 00000000..7882c712 + } + + @Test -+ void serializesReopeningTheSameJavaMenuAfterNativeClose() throws Exception { ++ void requiresFreshPlayerOpenAfterThePreviousCloseAcknowledgement() throws Exception { + try (var f = new Fixture()) { + f.request(1, true); + f.acknowledge(); @@ -40210,8 +40264,11 @@ index 00000000..7882c712 + assertEquals(1, f.opens()); + f.request(1, false); + f.request(2, true); ++ assertEquals(List.of(2L), f.closedScreens()); + assertEquals(1, f.opens()); + f.inventory.setCurrentContainerClosed(false); ++ assertEquals(1, f.opens()); ++ f.request(3, true); + assertEquals(2, f.opens()); + f.acknowledge(); + assertTrue(f.inventory.currentContainerGeneration() > oldGeneration); @@ -40219,6 +40276,26 @@ index 00000000..7882c712 + } + + @Test ++ void workbenchCloseWaitRejectsPlayerOpenWithoutQueuingItForTheAcknowledgement() throws Exception { ++ try (var f = new Fixture()) { ++ final var workbench = mock(Container.class); ++ when(workbench.type()).thenReturn(ContainerType.WORKBENCH); ++ f.inventory.setCurrentContainer(workbench); ++ f.inventory.markPendingClose(workbench); ++ f.request(1, true); ++ assertEquals(List.of(1L), f.closedScreens()); ++ assertEquals(0, f.opens()); ++ assertFalse(f.screens.acceptsClicks()); ++ f.inventory.setCurrentContainerClosed(false); ++ assertEquals(0, f.opens()); ++ f.request(2, true); ++ assertEquals(1, f.opens()); ++ f.acknowledge(); ++ assertTrue(f.screens.acceptsClicks()); ++ } ++ } ++ ++ @Test + void timeoutRetainsTheUncorrelatedAcknowledgmentTombstone() throws Exception { + try (var f = new Fixture()) { + f.request(1, true); 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 a8a417a6..441dc36a 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 @@ -13,6 +13,22 @@ Own predictions and responses by request, container object, and opening lifetime Regression coverage includes `ContainerTest`, `HorseContainerTest`, `NativeArmorPermissionsTest`, `NativeContainerActionSessionTest`, and `InventoryTransferBatchTest`. Late replies and replacement menus have targeted regressions. +Client-initiated close acknowledgments retire native state without closing another Java screen. +Genuine server-initiated closes retain their ordinary or correlated close path. +An opening received while a previous close awaits acknowledgment is rejected by its own token. +It needs fresh input after acknowledgment, rather than opening automatically when the wait ends. + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls establish this admission behavior on protocol 2225. +A raw relay delays the close acknowledgment by four seconds for both player and workbench closures. +Early E input sends no open interaction and leaves the screen closed after acknowledgment. +The workbench control also captures a fresh E input opening normally after acknowledgment. +Packet and tracker regressions cover acknowledgment consumption, server closes, early rejection, and fresh input. +Java can briefly show its local screen before the rejection arrives. Client-side frame admission remains unverified. + +Rebuilt direct and ViaProxy clients use the same delayed-acknowledgment relay for player and workbench close controls. +Both routes remain closed after early input without queuing an open interaction. +Fresh-input controls reopen normally, with the 32-log stack unchanged. + Scoped manual crafting batches reuse ordered prediction and response reconciliation. Native-shaped regressions cover reordered responses, rejection of a later craft, whole-batch rejection, and refusal of unrelated operations. 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. 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 5045d1ee..67a98d25 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 e5a2c73169b4716d835b00d94b971701542d529e Mon Sep 17 00:00:00 2001 +From eefc8ea306beadcc5a815378a442f3375501833a 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 @@ -947,7 +947,7 @@ arbitrary recipe families and original reports remain separate work. .../model/recipe/RecipeOwnershipTest.java | 52 ++ .../packet/FurnaceOptionsPacketTest.java | 162 ++++ .../packet/InventoryOptionsPacketTest.java | 211 +++++ - .../protocol/packet/InventoryPacketsTest.java | 51 +- + .../protocol/packet/InventoryPacketsTest.java | 49 +- .../NativeFurnaceOptionsPacketTest.java | 214 +++++ .../NativeRecipeConcreteCounterTest.java | 165 ++++ .../rewriter/NativeRecipePaletteTest.java | 203 +++++ @@ -972,7 +972,7 @@ arbitrary recipe families and original reports remain separate work. .../types/model/FurnaceOptionsTypeTest.java | 75 ++ .../types/model/InventoryOptionsTypeTest.java | 46 + .../types/recipe/CraftingDataReaderTest.java | 84 +- - 90 files changed, 13026 insertions(+), 233 deletions(-) + 90 files changed, 13024 insertions(+), 233 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 @@ -3965,7 +3965,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 375f5f0c..176bdfd5 100644 +index 48721470..08fc5932 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.*; @@ -4054,7 +4054,7 @@ index 375f5f0c..176bdfd5 100644 } static void handleContainerClose(final PacketWrapper wrapper) { -@@ -526,7 +586,13 @@ public final class InventoryPackets { +@@ -525,7 +585,13 @@ public final class InventoryPackets { if (container instanceof LecternContainer lectern) { lectern.sendInitialState(); } @@ -4069,7 +4069,7 @@ index 375f5f0c..176bdfd5 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.CONTAINER_CLOSE, -@@ -563,6 +629,9 @@ public final class InventoryPackets { +@@ -562,6 +628,9 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeContent(container); @@ -4079,7 +4079,7 @@ index 375f5f0c..176bdfd5 100644 if (container instanceof StonecutterContainer stonecutter) { stonecutter.refreshInput(); wrapper.cancel(); -@@ -570,6 +639,7 @@ public final class InventoryPackets { +@@ -569,6 +638,7 @@ public final class InventoryPackets { } PacketFactory.writeJavaContainerSetContent(wrapper, container); PacketFactory.sendJavaCursorForHudUpdate(wrapper.user(), container); @@ -4087,7 +4087,7 @@ index 375f5f0c..176bdfd5 100644 } else { wrapper.cancel(); } -@@ -598,6 +668,12 @@ public final class InventoryPackets { +@@ -597,6 +667,12 @@ public final class InventoryPackets { container.markServerUpdate(); wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeSlot(container, slot); @@ -4100,7 +4100,7 @@ index 375f5f0c..176bdfd5 100644 if (container instanceof StonecutterContainer stonecutter && slot == 3) { stonecutter.refreshInput(); wrapper.cancel(); -@@ -621,6 +697,7 @@ public final class InventoryPackets { +@@ -620,6 +696,7 @@ public final class InventoryPackets { } wrapper.write( VersionedTypes.V26_3.item, container.getJavaItem(slot)); // item @@ -4108,7 +4108,7 @@ index 375f5f0c..176bdfd5 100644 } else { wrapper.cancel(); } -@@ -682,6 +759,14 @@ public final class InventoryPackets { +@@ -681,6 +758,14 @@ public final class InventoryPackets { inventoryTracker.removeDynamicContainer(containerName); } }); @@ -4123,7 +4123,7 @@ index 375f5f0c..176bdfd5 100644 protocol.registerClientbound( ClientboundBedrockPackets.CRAFTING_DATA, null, -@@ -691,7 +776,27 @@ public final class InventoryPackets { +@@ -690,7 +775,27 @@ public final class InventoryPackets { wrapper.user().get(CraftingDataTracker.class); final CraftingDataReader.Data data = CraftingDataReader.read(wrapper); tracker.updateCraftingDataList(data.recipes(), data.clearRecipes()); @@ -4151,7 +4151,7 @@ index 375f5f0c..176bdfd5 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.CONTAINER_SET_DATA, -@@ -904,6 +1009,46 @@ public final class InventoryPackets { +@@ -903,6 +1008,46 @@ public final class InventoryPackets { PacketFactory.sendJavaContainerSetContent(wrapper.user(), container); } }); @@ -4198,7 +4198,7 @@ index 375f5f0c..176bdfd5 100644 protocol.registerServerbound( ServerboundPackets26_3.CONTAINER_BUTTON_CLICK, null, -@@ -1727,10 +1872,10 @@ public final class InventoryPackets { +@@ -1726,10 +1871,10 @@ public final class InventoryPackets { private static BedrockItem readCreativeItem(final PacketWrapper wrapper, final ItemRewriter itemRewriter) throws InformativeException { @@ -4211,7 +4211,7 @@ index 375f5f0c..176bdfd5 100644 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 { +@@ -1739,13 +1884,12 @@ public final class InventoryPackets { if (id == 0) { return BedrockItem.empty(); } @@ -9685,10 +9685,10 @@ index 00000000..590c47d4 + +} 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 +index d00218b7..add7f0d2 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; +@@ -37,6 +37,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; @@ -9703,16 +9703,7 @@ index 7dccb3cc..44b29c83 100644 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 { +@@ -143,13 +151,50 @@ class InventoryPacketsTest { writeCreativeDescriptor(descriptors, 529, fireworkData); final PacketWrapper wrapper = new PacketWrapperImpl(0, descriptors, null); 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 0610d046..06714b6e 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 455e1690491b995eb190e70e5efd5175034b9b7b Mon Sep 17 00:00:00 2001 +From 1eee75d42aacbdb548a56d1ca0dc937c0bdac279 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 @@ -390,7 +390,7 @@ Native query lifetime across disconnects and world changes remains unverified. .../container/NativeRecipeQuantityTest.java | 61 ++ .../ui/PlayerInventoryUiTest.java | 126 ++++ .../protocol/model/CreativeCatalogTest.java | 382 ++++++++++ - .../protocol/packet/InventoryPacketsTest.java | 267 +++++-- + .../protocol/packet/InventoryPacketsTest.java | 266 +++++-- .../rewriter/PlayerRecipeModelTest.java | 517 ++++++++++++++ ...egisteredPlayerRecipeAvailabilityTest.java | 5 +- .../RegisteredPlayerRecipeSearchTest.java | 3 +- @@ -399,7 +399,7 @@ Native query lifetime across disconnects and world changes remains unverified. .../NativePlayerCreativeAuxiliaryTest.java | 333 +++++++++ .../storage/NativePlayerCreativePickTest.java | 138 ++++ .../storage/NativeRecipeCraftingTest.java | 491 +++++++++++++ - 35 files changed, 5730 insertions(+), 171 deletions(-) + 35 files changed, 5729 insertions(+), 171 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 @@ -2383,7 +2383,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 c7461506..64053175 100644 +index a0269a99..de7d0af7 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -22,6 +22,7 @@ import net.raphimc.viabedrock.api.resourcepack.ui.ServerFormUi; @@ -2423,7 +2423,7 @@ index c7461506..64053175 100644 import java.util.logging.Level; public final class InventoryPackets { -@@ -358,22 +366,33 @@ public final class InventoryPackets { +@@ -357,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); @@ -2464,7 +2464,7 @@ index c7461506..64053175 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.CONTAINER_OPEN, -@@ -1148,41 +1167,7 @@ public final class InventoryPackets { +@@ -1147,41 +1166,7 @@ public final class InventoryPackets { protocol.registerServerbound( ServerboundPackets26_3.SET_CREATIVE_MODE_SLOT, null, @@ -2507,7 +2507,7 @@ index c7461506..64053175 100644 protocol.registerServerbound( ServerboundPackets26_3.CUSTOM_CLICK_ACTION, ServerboundBedrockPackets.MODAL_FORM_RESPONSE, -@@ -1294,7 +1279,7 @@ public final class InventoryPackets { +@@ -1293,7 +1278,7 @@ public final class InventoryPackets { if (container == null) { if (containerId == ContainerID.CONTAINER_ID_INVENTORY.getValue()) { wrapper.user().get(InventoryRequestTracker.class) @@ -2516,7 +2516,7 @@ index c7461506..64053175 100644 } wrapper.cancel(); return; -@@ -1867,31 +1852,129 @@ public final class InventoryPackets { +@@ -1866,31 +1851,129 @@ public final class InventoryPackets { return "container." + name; } @@ -3813,7 +3813,7 @@ index 00000000..cde67af9 + +} 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 +index 7256dcde..3406b08d 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java @@ -30,6 +30,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { @@ -3825,9 +3825,9 @@ index e7a9d17d..d3356aa9 100644 private boolean advertised; public PlayerInventoryScreenTracker(final UserConnection user) { -@@ -74,11 +76,16 @@ public final class PlayerInventoryScreenTracker extends StoredObject { - this.latestOpening = request.opening(); - this.requested = true; +@@ -79,11 +81,16 @@ public final class PlayerInventoryScreenTracker extends StoredObject { + return; + } this.requestedSince = System.nanoTime(); + final var presentation = this.user().get(NativePlayerRecipeState.class); + if (presentation != null) { @@ -3842,7 +3842,7 @@ index e7a9d17d..d3356aa9 100644 this.requests().clearQueuedScreenInteractions(); } this.service(); -@@ -95,7 +102,22 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -100,7 +107,22 @@ public final class PlayerInventoryScreenTracker extends StoredObject { public boolean acceptsClicks() { return this.requested && (this.inFlight == this.latestOpening @@ -3866,7 +3866,7 @@ index e7a9d17d..d3356aa9 100644 } /** Called after the native container is installed, before any queued Java clicks are replayed. */ -@@ -106,6 +128,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -111,6 +133,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { final long opening = this.inFlight; this.inFlight = 0; this.nativeOpening = opening; @@ -3875,7 +3875,7 @@ index e7a9d17d..d3356aa9 100644 if (this.enabled() && this.requested && opening == this.latestOpening) { this.requests().keepPlayerInventoryOpen(); return; -@@ -119,6 +143,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -124,6 +148,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { public void closed(final boolean serverInitiated) { final long opening = this.nativeOpening; this.nativeOpening = 0; @@ -3884,7 +3884,7 @@ index e7a9d17d..d3356aa9 100644 if (serverInitiated && opening != 0 && opening == this.latestOpening) { this.retire(); } -@@ -128,6 +154,7 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -133,6 +159,7 @@ public final class PlayerInventoryScreenTracker extends StoredObject { public void retire() { if (this.requested) { this.requested = false; @@ -3892,7 +3892,7 @@ index e7a9d17d..d3356aa9 100644 if (this.enabled()) { final var packet = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, this.user()); packet.write(Types.STRING, PlayerInventoryScreenClose.CHANNEL); -@@ -142,6 +169,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -147,6 +174,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { public void recovering() { this.retire(); this.nativeOpening = 0; @@ -3901,7 +3901,7 @@ index e7a9d17d..d3356aa9 100644 } public void tick(final long now) { -@@ -177,6 +206,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -182,6 +211,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { if (inventory.getCurrentContainer() instanceof InventoryContainer) { // A temporary offhand opening can become the actual installed player screen. this.nativeOpening = this.latestOpening; @@ -3910,7 +3910,7 @@ index e7a9d17d..d3356aa9 100644 return; } final boolean send = this.requests().beginInventoryOpen(); -@@ -191,6 +222,13 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -196,6 +227,13 @@ public final class PlayerInventoryScreenTracker extends StoredObject { packet.sendToServer(BedrockProtocol.class); } @@ -4735,10 +4735,10 @@ 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 19fe63ab..8cc84c93 100644 +index 1cfb02f6..d8eb367f 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; +@@ -17,16 +17,25 @@ package net.raphimc.viabedrock.protocol.packet; import com.viaversion.nbt.tag.CompoundTag; import com.viaversion.viaversion.api.connection.UserConnection; import com.viaversion.viaversion.api.minecraft.data.StructuredDataKey; @@ -4753,7 +4753,7 @@ index 19fe63ab..8cc84c93 100644 import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import net.raphimc.viabedrock.api.model.container.ChestContainer; -+import net.raphimc.viabedrock.api.model.container.Container; + import net.raphimc.viabedrock.api.model.container.Container; +import net.raphimc.viabedrock.api.model.container.player.ArmorContainer; import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; +import net.raphimc.viabedrock.api.model.container.player.OffhandContainer; @@ -4764,7 +4764,7 @@ index 19fe63ab..8cc84c93 100644 import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerEnumName; import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerID; import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.ContainerType; -@@ -33,6 +43,7 @@ import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.InventorySou +@@ -34,6 +43,7 @@ import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.InventorySou import net.raphimc.viabedrock.protocol.model.BedrockItem; import net.raphimc.viabedrock.protocol.model.inventory.BedrockInventoryTransaction; import net.raphimc.viabedrock.protocol.model.inventory.InventoryActionData; @@ -4772,7 +4772,7 @@ index 19fe63ab..8cc84c93 100644 import net.raphimc.viabedrock.protocol.storage.InventoryRequestTracker; import net.raphimc.viabedrock.protocol.storage.InventoryTracker; import net.raphimc.viabedrock.protocol.types.BedrockTypes; -@@ -48,7 +59,9 @@ import org.junit.jupiter.api.Test; +@@ -49,7 +59,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeAll; import java.lang.reflect.Proxy; @@ -4782,7 +4782,7 @@ index 19fe63ab..8cc84c93 100644 import java.util.Map; import java.util.UUID; -@@ -67,6 +80,31 @@ class InventoryPacketsTest { +@@ -67,6 +79,31 @@ class InventoryPacketsTest { Class.forName(StructuredDataKey.class.getName()); } @@ -4814,7 +4814,7 @@ index 19fe63ab..8cc84c93 100644 @Test void nativePlayerInventoryCloseAcknowledgementDoesNotCloseTheJavaCreativeScreen() { final CloseFixture fixture = new CloseFixture(); -@@ -266,62 +304,174 @@ class InventoryPacketsTest { +@@ -300,62 +337,174 @@ class InventoryPacketsTest { } @Test @@ -5038,7 +5038,7 @@ index 19fe63ab..8cc84c93 100644 } } -@@ -332,7 +482,7 @@ class InventoryPacketsTest { +@@ -366,7 +515,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); @@ -5047,7 +5047,7 @@ index 19fe63ab..8cc84c93 100644 private CloseFixture() { this.storage.put(InventoryTracker.class, this.inventory); -@@ -364,4 +514,21 @@ class InventoryPacketsTest { +@@ -398,4 +547,21 @@ class InventoryPacketsTest { } } 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 bd4d1cb2..9cd22de8 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 @@ -41,7 +41,8 @@ The first Escape removes editor focus; the next closes the inventory. Controller regressions cover closing before a search reply, replacement commands, explicit clearing, late replies, and connection replacement. Rebuilt direct and ViaProxy controls delay search handling by four seconds, close with unsent edits, and retain the latest text and explicit clear after reopening. Both routes preserve the 32-log stack and the native two-step Escape behavior. -A rapid player-inventory reopen during this forced stall can close again. Its close producer remains unresolved. +The [core inventory patch](../../viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.pr.md) owns close-wait admission and close acknowledgments. +Java can briefly display a local inventory before its correlated rejection arrives; client-side admission timing remains unverified. A real-editor screen regression clicks a recipe, returns to search, and types the inventory key without closing the screen. Installed direct and ViaProxy controls craft once, enter three replacement queries in the same opening, and retain completed output after reopening. Each route records one server-accepted craft request. Late replies and hover exits cannot affect a replacement opening or selection. From fc6e30eb73b44e18881a405286d665c74121c5aa Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 06:16:24 +0100 Subject: [PATCH 23/47] fix(inventory): await native player screen acknowledgments Keep the world and movement active until the native player container opens. Negotiate acknowledgment-bound admission separately from legacy requests, and retain opening identity through repeated input and Creative redirects. Cancel pending admission when another screen opens or the player menu changes. Late acknowledgments cannot install an abandoned screen. Preserve read-only recipe snapshots without using them as permission to show the GUI. Native 1.26.60.5 and rebuilt direct/proxy controls use four-second open delays. Both routes pass seven controls; the full build passes 3,323 tests with 325 skipped and no failures or errors. --- docs/native-player-inventory.md | 24 +- ...erver-authoritative-bedrock-requests.patch | 217 ++++++++- ...erver-authoritative-bedrock-requests.pr.md | 14 +- ...ain-bedrock-creative-catalog-entries.patch | 34 +- ...9-present-authored-native-containers.patch | 416 ++++++++++++++++-- ...9-present-authored-native-containers.pr.md | 13 +- 6 files changed, 635 insertions(+), 83 deletions(-) diff --git a/docs/native-player-inventory.md b/docs/native-player-inventory.md index 60a4f50e..8bb23ae8 100644 --- a/docs/native-player-inventory.md +++ b/docs/native-player-inventory.md @@ -16,9 +16,9 @@ Java screens do not reproduce those custom layouts. ## Screen lifetime -Open the Bedrock inventory only after Java installs the actual screen. -Constructing a screen does not open a server container. -Each installed opening receives a monotonic connection token. +Request the Bedrock inventory at the actual GUI admission attempt. +Constructing a screen alone does not open a server container. +Each requested opening receives a monotonic connection token. An authored presentation swap retains that token. An ordinary reopen receives a new one. The Bedrock open acknowledgment has no client token. @@ -49,7 +49,15 @@ 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. +A negotiated player opening keeps the world visible until its native container acknowledgment arrives. +Movement continues during that wait. +Repeated E sends another open interaction under the same opening token. +Escape or chat cancels the pending intent. +A late acknowledgment cannot replace that other screen or reopen the inventory after exit. +This admission applies with Bedrock inventory screens enabled or disabled. +Older translators retain their existing screen behavior. + +A requested 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. Item requests still require the acknowledged container and its generation. @@ -226,9 +234,11 @@ Installed direct and ViaProxy controls confirm these pointer, capacity, and reop Recipe families beyond the captured plank, cake, and honey controls, recipes with more than two outputs, and complete installed native parity remain unfinished. Native query lifetime across disconnects and world changes remains unverified. -Java can briefly display its local inventory before a correlated close-wait rejection arrives. -Client-side admission timing remains unverified. -Native constructor timing relative to the open acknowledgment and frame-level population timing remain unverified. +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls delay the initial open acknowledgment by four seconds on protocol 2225. +They establish visible admission, repeated input, movement, and cancellation through Escape or chat. +Rebuilt direct and ViaProxy controls retain the world during that same delay in Survival, Creative, and workbench openings. +Both routes preserve the pending opening through repeated E and close abandoned native containers after cancellation. +Native model constructor timing 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. The original inventory-background, rollback, food-use, and locator-bar reports remain unresolved. 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 ab7ef006..cb72a005 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 ccc44e9cb92bfdf4740792ad8cec809ea850a358 Mon Sep 17 00:00:00 2001 +From b27caf00cff9f20385e2186139e821ce84219a2d 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 @@ -774,12 +774,29 @@ its own opening token. Do not queue it to open after the acknowledgement. A later input can open normally once the pending native container retires. Packet and tracker regressions cover both close sources, consumed client acknowledgements, genuine server closes, and the fresh-input requirement. -Java can still briefly display its local screen before receiving the -correlated rejection; client-side frame admission remains a separate gate. +The optional acknowledgment-bound client admission prevents the local +screen from appearing before this correlated rejection arrives. + +Official 1.26.60.5 also keeps the world visible until the initial player +ContainerOpen arrives. A four-second raw relay delay confirms movement +continues, repeated E reissues the interaction without replacing the +pending intent, and Escape or chat +cancels the intent without a late acknowledgment stealing that screen. + +Negotiate acknowledgment-bound requests separately from legacy requests. +Publish one opening-bound ready message only after installing the native +container, or adopting an already acknowledged temporary inventory. +Read-only recipe models are not an admission signal. Legacy clients keep +the original request channel and never receive unsolicited ready messages. + +For acknowledgment-bound input, another request for the same pending +opening reissues OpenInventory without allocating a new screen token. +Keep legacy requests idempotent and publish readiness once per opening. --- build.gradle | 3 +- .../api/model/PlayerInventoryScreenClose.java | 46 + - .../model/PlayerInventoryScreenRequest.java | 52 + + .../api/model/PlayerInventoryScreenReady.java | 46 + + .../model/PlayerInventoryScreenRequest.java | 53 + .../api/model/container/ChestContainer.java | 66 +- .../api/model/container/Container.java | 1117 ++- .../model/container/ContainerOpenContext.java | 19 + @@ -843,7 +860,7 @@ correlated rejection; client-side frame admission remains a separate gate. .../storage/InventoryRequestStorage.java | 232 + .../storage/InventoryRequestTracker.java | 795 +++ .../protocol/storage/InventoryTracker.java | 282 +- - .../storage/PlayerInventoryScreenTracker.java | 207 + + .../storage/PlayerInventoryScreenTracker.java | 254 + .../protocol/types/item/BedrockItemType.java | 68 +- .../types/item/NetworkItemStackContext.java | 18 + .../item/NetworkItemStackDescriptorType.java | 79 +- @@ -909,14 +926,15 @@ correlated rejection; client-side frame admission remains a separate gate. .../NativeContainerActionBackendTest.java | 95 + .../storage/NativeContainerResponseTest.java | 287 + .../PlayerInventoryScreenCodecTest.java | 71 + - .../PlayerInventoryScreenTrackerTest.java | 294 + + .../PlayerInventoryScreenTrackerTest.java | 345 + .../storage/StonecutterRecipePacketTest.java | 53 + .../types/inventory/ItemStackCodecTest.java | 12 +- .../item/NetworkItemConstructionTest.java | 171 + .../types/item/NetworkItemCountTest.java | 35 +- .../types/item/NetworkItemStackFixtures.java | 69 + - 138 files changed, 28896 insertions(+), 2047 deletions(-) + 139 files changed, 29041 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/PlayerInventoryScreenReady.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 create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/container/HorseContainer.java @@ -1077,12 +1095,64 @@ index 00000000..652570b8 + } + +} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenReady.java b/src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenReady.java +new file mode 100644 +index 00000000..ea9cb624 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenReady.java +@@ -0,0 +1,46 @@ ++/* ++ * 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 net.raphimc.viabedrock.protocol.data.ProtocolConstants; ++ ++/** Admits one requested player screen after its native container acknowledgment. */ ++public record PlayerInventoryScreenReady(long opening) { ++ ++ public static final String CHANNEL = "viabedrock:player_inventory_screen_ready_v1"; ++ public static final Type TYPE = new Type<>(PlayerInventoryScreenReady.class) { ++ ++ @Override ++ public PlayerInventoryScreenReady read(final ByteBuf buffer) { ++ if (buffer.readableBytes() > 18 || Types.VAR_INT.readPrimitive(buffer) != 1 ++ || Types.VAR_INT.readPrimitive(buffer) != ProtocolConstants.BEDROCK_PROTOCOL_VERSION) { ++ throw new IllegalArgumentException("Unsupported inventory screen message"); ++ } ++ final var result = new PlayerInventoryScreenReady(Types.VAR_LONG.readPrimitive(buffer)); ++ if (buffer.isReadable()) { ++ throw new IllegalArgumentException("Trailing inventory screen fields"); ++ } ++ return result; ++ } ++ ++ @Override ++ public void write(final ByteBuf buffer, final PlayerInventoryScreenReady value) { ++ Types.VAR_INT.writePrimitive(buffer, 1); ++ Types.VAR_INT.writePrimitive(buffer, ProtocolConstants.BEDROCK_PROTOCOL_VERSION); ++ Types.VAR_LONG.writePrimitive(buffer, value.opening); ++ } ++ }; ++ ++ public PlayerInventoryScreenReady { ++ if (opening <= 0) { ++ throw new IllegalArgumentException("Invalid inventory screen opening"); ++ } ++ } ++ ++} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenRequest.java b/src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenRequest.java new file mode 100644 -index 00000000..f52cd9a9 +index 00000000..0ebbfc0a --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/api/model/PlayerInventoryScreenRequest.java -@@ -0,0 +1,52 @@ +@@ -0,0 +1,53 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -1099,6 +1169,7 @@ index 00000000..f52cd9a9 +public record PlayerInventoryScreenRequest(long opening, boolean open) { + + public static final String CHANNEL = "viabedrock:player_inventory_screen_request_v1"; ++ public static final String DEFERRED_CHANNEL = "viabedrock:player_inventory_screen_request_ack_v1"; + public static final Type TYPE = new Type<>(PlayerInventoryScreenRequest.class) { + + @Override @@ -11388,7 +11459,7 @@ index 5af0eca2..fa556209 100644 itemDefinitions.addFromNetworkTag(itemEntry.identifier(), itemEntry.componentData()); } 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 67129d78..57b24cc8 100644 +index 67129d78..c04231bd 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/MultiStatePackets.java @@ -17,17 +17,14 @@ @@ -11419,11 +11490,11 @@ index 67129d78..57b24cc8 100644 } return; } -+ if (channel.equals(PlayerInventoryScreenRequest.CHANNEL)) { ++ if (channel.equals(PlayerInventoryScreenRequest.CHANNEL) || channel.equals(PlayerInventoryScreenRequest.DEFERRED_CHANNEL)) { + final var screens = wrapper.user().get(PlayerInventoryScreenTracker.class); + if (screens != null && screens.enabled()) { + try { -+ screens.request(wrapper.read(PlayerInventoryScreenRequest.TYPE)); ++ screens.request(wrapper.read(PlayerInventoryScreenRequest.TYPE), channel.equals(PlayerInventoryScreenRequest.DEFERRED_CHANNEL)); + } catch (final IllegalArgumentException | IndexOutOfBoundsException malformed) { + // Invalid messages cannot own a native opening or close another screen. + } @@ -13954,10 +14025,10 @@ index 7472880e..19ffffe3 100644 } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java new file mode 100644 -index 00000000..7256dcde +index 00000000..9f2ccf71 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java -@@ -0,0 +1,207 @@ +@@ -0,0 +1,254 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -13975,6 +14046,7 @@ index 00000000..7256dcde +import java.util.concurrent.TimeUnit; +import net.raphimc.viabedrock.api.model.PlayerInventoryScreenClose; +import net.raphimc.viabedrock.api.model.PlayerInventoryScreenRequest; ++import net.raphimc.viabedrock.api.model.PlayerInventoryScreenReady; +import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; +import net.raphimc.viabedrock.protocol.BedrockProtocol; +import net.raphimc.viabedrock.protocol.ServerboundBedrockPackets; @@ -13991,6 +14063,9 @@ index 00000000..7256dcde + private long requestedSince; + private long nativeOpening; + private boolean advertised; ++ private boolean deferredAdvertised; ++ private boolean deferred; ++ private long readyOpening; + + public PlayerInventoryScreenTracker(final UserConnection user) { + super(user); @@ -14005,34 +14080,60 @@ index 00000000..7256dcde + public void refreshCapability() { + if (this.user().getProtocolInfo().getClientState() != State.PLAY) { + this.advertised = false; ++ this.deferredAdvertised = false; + this.retire(); + return; + } + final boolean enabled = this.enabled(); -+ if (enabled == this.advertised) { -+ return; ++ if (enabled != this.advertised) { ++ this.advertised = enabled; ++ this.advertise(PlayerInventoryScreenRequest.CHANNEL, enabled); ++ } ++ final boolean deferred = this.deferredEnabled(); ++ if (deferred != this.deferredAdvertised) { ++ this.deferredAdvertised = deferred; ++ this.advertise(PlayerInventoryScreenRequest.DEFERRED_CHANNEL, deferred); ++ if (!deferred && this.deferred) { ++ this.retire(); ++ } + } -+ this.advertised = enabled; -+ final var packet = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, this.user()); -+ packet.write(Types.STRING, enabled ? "minecraft:register" : "minecraft:unregister"); -+ packet.write(Types.REMAINING_BYTES, PlayerInventoryScreenRequest.CHANNEL.getBytes(StandardCharsets.UTF_8)); -+ packet.send(BedrockProtocol.class); + if (!enabled) { + this.retire(); + } + } + + public void request(final PlayerInventoryScreenRequest request) { -+ if (!this.enabled()) { ++ this.request(request, false); ++ } ++ ++ private void advertise(final String channel, final boolean enabled) { ++ final var packet = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, this.user()); ++ packet.write(Types.STRING, enabled ? "minecraft:register" : "minecraft:unregister"); ++ packet.write(Types.REMAINING_BYTES, channel.getBytes(StandardCharsets.UTF_8)); ++ packet.send(BedrockProtocol.class); ++ } ++ ++ public boolean deferredEnabled() { ++ final var channels = this.user().get(ChannelStorage.class); ++ return this.enabled() && channels.hasChannel(PlayerInventoryScreenReady.CHANNEL); ++ } ++ ++ public void request(final PlayerInventoryScreenRequest request, final boolean deferred) { ++ if (!this.enabled() || deferred && !this.deferredEnabled()) { + return; + } + if (request.open()) { + if (request.opening() <= this.latestOpening) { ++ if (deferred && this.deferred && this.requested && request.opening() == this.latestOpening ++ && this.inFlight == this.latestOpening) { ++ this.sendOpen(); ++ } + return; + } + this.requests().clearQueuedScreenInteractions(); + this.latestOpening = request.opening(); + this.requested = true; ++ this.deferred = deferred; + if (this.inventory().getPendingCloseContainer() != null) { + // Native ignores a new inventory key until the previous close is acknowledged. + this.retire(); @@ -14073,6 +14174,7 @@ index 00000000..7256dcde + this.nativeOpening = opening; + if (this.enabled() && this.requested && opening == this.latestOpening) { + this.requests().keepPlayerInventoryOpen(); ++ this.ready(); + return; + } + // Old screen clicks were discarded when its request retired. Later global offhand @@ -14142,6 +14244,7 @@ index 00000000..7256dcde + if (inventory.getCurrentContainer() instanceof InventoryContainer) { + // A temporary offhand opening can become the actual installed player screen. + this.nativeOpening = this.latestOpening; ++ this.ready(); + return; + } + final boolean send = this.requests().beginInventoryOpen(); @@ -14149,6 +14252,10 @@ index 00000000..7256dcde + if (!send) { + return; + } ++ this.sendOpen(); ++ } ++ ++ private void sendOpen() { + final var packet = PacketWrapper.create(ServerboundBedrockPackets.INTERACT, this.user()); + packet.write(Types.UNSIGNED_BYTE, (short) InteractPacketPayload_Action.OpenInventory.getValue()); + packet.write(BedrockTypes.UNSIGNED_VAR_LONG, this.user().get(EntityTracker.class).getClientPlayer().runtimeId()); @@ -14156,6 +14263,17 @@ index 00000000..7256dcde + packet.sendToServer(BedrockProtocol.class); + } + ++ private void ready() { ++ if (!this.deferred || this.readyOpening == this.latestOpening || !this.deferredEnabled()) { ++ return; ++ } ++ this.readyOpening = this.latestOpening; ++ final var packet = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, this.user()); ++ packet.write(Types.STRING, PlayerInventoryScreenReady.CHANNEL); ++ packet.write(PlayerInventoryScreenReady.TYPE, new PlayerInventoryScreenReady(this.latestOpening)); ++ packet.send(BedrockProtocol.class); ++ } ++ + private InventoryTracker inventory() { + return this.user().get(InventoryTracker.class); + } @@ -40198,10 +40316,10 @@ index 00000000..66e78fc9 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTrackerTest.java b/src/test/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTrackerTest.java new file mode 100644 -index 00000000..65e1be48 +index 00000000..702c97f7 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTrackerTest.java -@@ -0,0 +1,294 @@ +@@ -0,0 +1,345 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -40213,6 +40331,7 @@ index 00000000..65e1be48 +import com.viaversion.viaversion.api.protocol.packet.PacketType; +import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; +import com.viaversion.viaversion.api.protocol.packet.State; ++import com.viaversion.viaversion.api.type.Types; +import com.viaversion.viaversion.protocol.packet.PacketWrapperImpl; +import java.util.ArrayList; +import java.util.HashMap; @@ -40221,6 +40340,7 @@ index 00000000..65e1be48 +import java.util.concurrent.TimeUnit; +import net.raphimc.viabedrock.api.model.PlayerInventoryScreenClose; +import net.raphimc.viabedrock.api.model.PlayerInventoryScreenRequest; ++import net.raphimc.viabedrock.api.model.PlayerInventoryScreenReady; +import net.raphimc.viabedrock.api.model.container.Container; +import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; +import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity; @@ -40235,6 +40355,48 @@ index 00000000..65e1be48 +class PlayerInventoryScreenTrackerTest { + + @Test ++ void deferredScreenBecomesReadyOnlyAfterItsNativeAcknowledgement() throws Exception { ++ try (var f = new Fixture()) { ++ f.user.get(ChannelStorage.class).addChannels(List.of(PlayerInventoryScreenReady.CHANNEL)); ++ f.screens.request(new PlayerInventoryScreenRequest(1, true), true); ++ assertTrue(f.readyScreens().isEmpty()); ++ assertEquals(1, f.opens()); ++ f.screens.request(new PlayerInventoryScreenRequest(1, true), true); ++ assertEquals(2, f.opens()); ++ assertTrue(f.readyScreens().isEmpty()); ++ f.acknowledge(); ++ assertEquals(List.of(1L), f.readyScreens()); ++ f.screens.tick(System.nanoTime()); ++ f.screens.request(new PlayerInventoryScreenRequest(1, true), true); ++ assertEquals(List.of(1L), f.readyScreens()); ++ } ++ } ++ ++ @Test ++ void cancelledDeferredScreenNeverBecomesReadyOnLateAcknowledgement() throws Exception { ++ try (var f = new Fixture()) { ++ f.user.get(ChannelStorage.class).addChannels(List.of(PlayerInventoryScreenReady.CHANNEL)); ++ f.screens.request(new PlayerInventoryScreenRequest(1, true), true); ++ f.screens.request(new PlayerInventoryScreenRequest(1, false), true); ++ f.acknowledge(); ++ assertTrue(f.readyScreens().isEmpty()); ++ assertNotNull(f.inventory.getPendingCloseContainer()); ++ } ++ } ++ ++ @Test ++ void oldClientsNeverReceiveReadyMessagesAndCannotRequestDeferredAdmission() throws Exception { ++ try (var f = new Fixture()) { ++ f.screens.request(new PlayerInventoryScreenRequest(1, true), true); ++ assertEquals(0, f.opens()); ++ f.request(1, true); ++ f.acknowledge(); ++ assertEquals(1, f.opens()); ++ assertTrue(f.readyScreens().isEmpty()); ++ } ++ } ++ ++ @Test + void retiresAnEarlyCloseBeforeOpeningTheNewScreen() throws Exception { + try (var f = new Fixture()) { + f.request(1, true); @@ -40483,6 +40645,13 @@ index 00000000..65e1be48 + return this.sent.stream().filter(p -> p.getPacketType() == ServerboundBedrockPackets.INTERACT).count(); + } + ++ private List readyScreens() { ++ return this.sent.stream().filter(p -> p.getPacketType() != ServerboundBedrockPackets.INTERACT ++ && p.getPacketType() != ServerboundBedrockPackets.CONTAINER_CLOSE ++ && PlayerInventoryScreenReady.CHANNEL.equals(p.get(Types.STRING, 0))) ++ .map(p -> p.get(PlayerInventoryScreenReady.TYPE, 0).opening()).toList(); ++ } ++ + private List closedScreens() { + return this.sent.stream().filter(p -> p.getPacketType() != ServerboundBedrockPackets.CONTAINER_CLOSE + && p.getPacketType() != ServerboundBedrockPackets.INTERACT) 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 441dc36a..a5cc4d0e 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 @@ -23,7 +23,19 @@ A raw relay delays the close acknowledgment by four seconds for both player and Early E input sends no open interaction and leaves the screen closed after acknowledgment. The workbench control also captures a fresh E input opening normally after acknowledgment. Packet and tracker regressions cover acknowledgment consumption, server closes, early rejection, and fresh input. -Java can briefly show its local screen before the rejection arrives. Client-side frame admission remains unverified. +The optional acknowledgment-bound request channel keeps the player screen hidden until its own native acknowledgment. +Legacy clients retain their original request channel. +Repeated E reissues the native interaction under the same opening token. +Cancellation and late replies retain their original ownership. +Tracker regressions cover readiness, cancellation, repeated input, and legacy requests. + +The same native builds also delay the initial player and workbench open acknowledgments by four seconds. +Both keep the world visible during the wait. +Movement continues, and Escape or chat cancels the pending player inventory. +Native closes the abandoned container after its late acknowledgment without replacing the other screen. +These controls establish visible admission, not internal constructor or frame-level population timing. +Rebuilt direct and ViaProxy controls cover initial Survival, Creative, and workbench openings through the delayed relay. +Their complete journals retain two open interactions for repeated E and one native close after each cancellation. Rebuilt direct and ViaProxy clients use the same delayed-acknowledgment relay for player and workbench close controls. Both routes remain closed after early input without queuing an open interaction. 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 06714b6e..62adee08 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 1eee75d42aacbdb548a56d1ca0dc937c0bdac279 Mon Sep 17 00:00:00 2001 +From db1e8cefd4db3bd7db6f7fc9b460085c08fc552a 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 @@ -2669,7 +2669,7 @@ index a0269a99..de7d0af7 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 c47f9dde..cf71a5b3 100644 +index 885579ea..d36de2f5 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 @@ @@ -3813,19 +3813,19 @@ index 00000000..cde67af9 + +} 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 7256dcde..3406b08d 100644 +index 9f2ccf71..2db2d17d 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/PlayerInventoryScreenTracker.java -@@ -30,6 +30,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -31,6 +31,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { private long inFlight; private long requestedSince; private long nativeOpening; + private long nativeGeneration; + private InventoryTracker nativeInventory; private boolean advertised; - - public PlayerInventoryScreenTracker(final UserConnection user) { -@@ -79,11 +81,16 @@ public final class PlayerInventoryScreenTracker extends StoredObject { + private boolean deferredAdvertised; + private boolean deferred; +@@ -109,11 +111,16 @@ public final class PlayerInventoryScreenTracker extends StoredObject { return; } this.requestedSince = System.nanoTime(); @@ -3842,7 +3842,7 @@ index 7256dcde..3406b08d 100644 this.requests().clearQueuedScreenInteractions(); } this.service(); -@@ -100,7 +107,22 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -130,7 +137,22 @@ public final class PlayerInventoryScreenTracker extends StoredObject { public boolean acceptsClicks() { return this.requested && (this.inFlight == this.latestOpening @@ -3866,7 +3866,7 @@ index 7256dcde..3406b08d 100644 } /** Called after the native container is installed, before any queued Java clicks are replayed. */ -@@ -111,6 +133,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -141,6 +163,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { final long opening = this.inFlight; this.inFlight = 0; this.nativeOpening = opening; @@ -3874,8 +3874,8 @@ index 7256dcde..3406b08d 100644 + this.nativeGeneration = this.nativeInventory.currentContainerGeneration(); if (this.enabled() && this.requested && opening == this.latestOpening) { this.requests().keepPlayerInventoryOpen(); - return; -@@ -124,6 +148,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { + this.ready(); +@@ -155,6 +179,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { public void closed(final boolean serverInitiated) { final long opening = this.nativeOpening; this.nativeOpening = 0; @@ -3884,7 +3884,7 @@ index 7256dcde..3406b08d 100644 if (serverInitiated && opening != 0 && opening == this.latestOpening) { this.retire(); } -@@ -133,6 +159,7 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -164,6 +190,7 @@ public final class PlayerInventoryScreenTracker extends StoredObject { public void retire() { if (this.requested) { this.requested = false; @@ -3892,7 +3892,7 @@ index 7256dcde..3406b08d 100644 if (this.enabled()) { final var packet = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, this.user()); packet.write(Types.STRING, PlayerInventoryScreenClose.CHANNEL); -@@ -147,6 +174,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -178,6 +205,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { public void recovering() { this.retire(); this.nativeOpening = 0; @@ -3901,17 +3901,17 @@ index 7256dcde..3406b08d 100644 } public void tick(final long now) { -@@ -182,6 +211,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { +@@ -213,6 +242,8 @@ public final class PlayerInventoryScreenTracker extends StoredObject { if (inventory.getCurrentContainer() instanceof InventoryContainer) { // A temporary offhand opening can become the actual installed player screen. this.nativeOpening = this.latestOpening; + this.nativeInventory = inventory; + this.nativeGeneration = inventory.currentContainerGeneration(); + this.ready(); return; } - final boolean send = this.requests().beginInventoryOpen(); -@@ -196,6 +227,13 @@ public final class PlayerInventoryScreenTracker extends StoredObject { - packet.sendToServer(BedrockProtocol.class); +@@ -243,6 +274,13 @@ public final class PlayerInventoryScreenTracker extends StoredObject { + packet.send(BedrockProtocol.class); } + private void closePresentation() { 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 b7888c4b..6fc32d0c 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 c45a175ec9fe85be6e58dda45e7e799090b0ef44 Mon Sep 17 00:00:00 2001 +From 26d813419eb1a72b141bf7ec88242bb67c82d6ec 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 @@ -445,7 +445,12 @@ supplied; physical mode admission and derived cooking controllers stay separate gates. The screen regression covers post-resync pointer data, unequal Java/native scales, selection, matching refreshes and stale screens. -Send player inventory lifecycle messages only after Gui installs the real screen. Share the negotiated route between direct Bedrock and ViaProxy, retain one opening through a native presentation swap, and give ordinary reopens a new token despite Java's persistent menu zero. Accept server close notifications only for the matching connection and screen. Remove the constructor trigger and keep legacy direct opening at the installed-screen boundary. +Request negotiated player inventory openings at the actual Gui admission +attempt. Constructors alone never send an open. Share the route between +direct Bedrock and ViaProxy, retain one opening through presentation +swaps, and give ordinary reopens a new token despite Java's persistent +menu zero. Accept close notifications only for the matching connection +and opening. Keep legacy direct opening at the installed-screen boundary. Use shared native count text for authored slot labels, retaining raw numeric binding quantities and genuine Java quickcraft preview overrides. Bedrock @@ -706,11 +711,38 @@ Java control with search handling delayed by four seconds loses visible text before this change. Controller boundary tests cover queued text and clearing across close/reopen, late replies, blocked item actions while a new search is pending, and connection replacement. + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 protocol-2225 controls delay +ContainerOpen by four seconds. Native retains the world and gameplay +movement until acknowledgment. Repeated E resends the interaction with +the same pending intent. Escape and chat cancel it; a late acknowledgment +closes the abandoned native container without replacing those screens. + +Negotiate acknowledgment-bound player inventory requests separately from +the legacy bridge. Intercept actual GUI installation and retain the desired +screen behind the world until its own ready token arrives. Repeated input +retains its token, another screen retires it, and late or old-connection +readiness cannot display it. Keep read-only snapshots against that pending +owner without displaying them or enabling item actions before acknowledgment. + +Apply the gate independently of the authored inventory visual setting. +Older translators retain their existing immediate-screen behavior because +they do not advertise the acknowledgment-bound request channel. Workbench +screens already enter through the server open packet and need no extra gate. + +Retain the admitted opening through initialization redirects, including +Java InventoryScreen redirecting to Creative before GUI installation +returns. The installed control exposed an unnecessary close/reopen cycle; +a controller regression preserves its token and checks for one request. + +Check the current player menu before accepting readiness. Replacement +player menus retire the pending token instead of displaying a screen +captured from the old player. A controller regression covers that boundary. --- .../bedrock/ViaFabricPlusBedrock.java | 4 + .../access/IBedrockGuiItemStyle.java | 14 + .../MixinNativeContainerGeometry.java | 46 + - .../MixinNativeContainerScreenAdmission.java | 28 + + .../MixinNativeContainerScreenAdmission.java | 33 + .../features/misc/MixinInventoryScreen.java | 61 - .../render/MixinNativeContainerHud.java | 28 + .../MixinNativeContainerPresentation.java | 26 + @@ -720,10 +752,10 @@ new search is pending, and connection replacement. .../render/MixinNativeGuiOversizedItem.java | 31 + .../container/BedrockFurnacePreferences.java | 60 + .../BedrockNativeContainerScreen.java | 1331 ++++++++++++++++ - .../container/BedrockNativeContainers.java | 282 ++++ + .../container/BedrockNativeContainers.java | 283 ++++ .../container/BedrockNativeCooking.java | 147 ++ - .../BedrockNativePlayerInventory.java | 170 ++ - .../container/BedrockPlayerInventory.java | 92 ++ + .../BedrockNativePlayerInventory.java | 171 ++ + .../container/BedrockPlayerInventory.java | 144 ++ .../NativeContainerActionPayload.java | 30 + .../NativeContainerCurrentPayload.java | 30 + .../container/NativeContainerDataPayload.java | 30 + @@ -761,8 +793,9 @@ new search is pending, and connection replacement. .../NativePlayerInventoryResultPayload.java | 22 + .../container/NativePlayerPresentation.java | 195 +++ .../PlayerInventoryScreenClosePayload.java | 22 + - .../PlayerInventoryScreenLifecycle.java | 74 + - .../PlayerInventoryScreenRequestPayload.java | 22 + + .../PlayerInventoryScreenLifecycle.java | 98 ++ + .../PlayerInventoryScreenReadyPayload.java | 22 + + .../PlayerInventoryScreenRequestPayload.java | 28 + .../settings/impl/BedrockSettings.java | 4 + .../viafabricplus-bedrock/lang/en_us.json | 1 + .../viafabricplus-bedrock.mixins.json | 9 +- @@ -787,8 +820,9 @@ new search is pending, and connection replacement. .../NativePlayerInventoryLifetimeTest.java | 142 ++ .../NativePlayerPresentationTest.java | 153 ++ .../container/NativePlayerScreenTest.java | 576 +++++++ - .../PlayerInventoryScreenLifecycleTest.java | 95 ++ - 81 files changed, 10934 insertions(+), 62 deletions(-) + .../PlayerInventoryAdmissionTest.java | 147 ++ + .../PlayerInventoryScreenLifecycleTest.java | 139 ++ + 83 files changed, 11236 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 @@ -843,6 +877,7 @@ new search is pending, and connection replacement. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerPresentation.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenClosePayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenLifecycle.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenReadyPayload.java 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 @@ -865,6 +900,7 @@ new search is pending, and connection replacement. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerInventoryLifetimeTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerPresentationTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/NativePlayerScreenTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryAdmissionTest.java 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 @@ -963,10 +999,10 @@ index 0000000..6298650 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinNativeContainerScreenAdmission.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinNativeContainerScreenAdmission.java new file mode 100644 -index 0000000..37cf62b +index 0000000..4d00852 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/inventory/MixinNativeContainerScreenAdmission.java -@@ -0,0 +1,28 @@ +@@ -0,0 +1,33 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -984,6 +1020,11 @@ index 0000000..37cf62b + +@Mixin(Gui.class) +public abstract class MixinNativeContainerScreenAdmission { ++ @Inject(method = "setScreen", at = @At("HEAD"), cancellable = true) ++ private void deferPlayerInventory(final Screen screen, final CallbackInfo ci) { ++ if (!BedrockNativeContainers.admitPlayerScreen(screen)) ci.cancel(); ++ } ++ + @Inject(method = "setScreen", at = @At("RETURN")) + private void playerInventoryLifecycle(final Screen screen, final CallbackInfo ci) { + BedrockNativeContainers.playerScreenInstalled(); @@ -2668,10 +2709,10 @@ index 0000000..7f60cd2 +} 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..2118e9f +index 0000000..8776ae3 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativeContainers.java -@@ -0,0 +1,282 @@ +@@ -0,0 +1,283 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -2939,6 +2980,7 @@ index 0000000..2118e9f + } + + public static void playerScreenInstalled() { BedrockPlayerInventory.installed(); } ++ public static boolean admitPlayerScreen(final Screen screen) { return BedrockPlayerInventory.admit(screen); } + + /** Called after Java resynchronizes the pointer and before the first frame. */ + public static void screenInstalled(final Screen screen) { @@ -3109,10 +3151,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..6081bf8 +index 0000000..6f46cb7 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockNativePlayerInventory.java -@@ -0,0 +1,170 @@ +@@ -0,0 +1,171 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3192,8 +3234,9 @@ index 0000000..6081bf8 + 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; ++ if (owner != listener || client.getConnection() != listener || client.player == null) return; ++ final var candidate = client.gui.screen() == null ? BedrockPlayerInventory.pendingScreen(listener) : client.gui.screen(); ++ if (!(candidate instanceof AbstractContainerScreen displayed)) return; + final var expected = message.state().workbench() ? client.player.containerMenu : client.player.inventoryMenu; + if (message.menuId() != expected.containerId || expected.slots.size() != 46 + || message.state().workbench() && (!(expected instanceof CraftingMenu) || displayed.getMenu() != expected) @@ -3285,10 +3328,10 @@ index 0000000..6081bf8 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockPlayerInventory.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockPlayerInventory.java new file mode 100644 -index 0000000..de80dc4 +index 0000000..6f7e980 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/BedrockPlayerInventory.java -@@ -0,0 +1,92 @@ +@@ -0,0 +1,144 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -3310,6 +3353,7 @@ index 0000000..de80dc4 +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.raphimc.viabedrock.api.BedrockProtocolVersion; ++import net.raphimc.viabedrock.api.model.PlayerInventoryScreenRequest; +import net.raphimc.viabedrock.protocol.BedrockProtocol; +import net.raphimc.viabedrock.protocol.ServerboundBedrockPackets; +import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.InteractPacketPayload_Action; @@ -3317,25 +3361,30 @@ index 0000000..de80dc4 +import net.raphimc.viabedrock.protocol.storage.PlayerInventoryScreenTracker; +import net.raphimc.viabedrock.protocol.types.BedrockTypes; + -+/** Bridges the real installed player inventory through both direct translation and ViaProxy. */ ++/** Owns requested and installed player inventory screens on direct and ViaProxy connections. */ +final class BedrockPlayerInventory { + private static final PlayerInventoryScreenLifecycle SCREENS = + new PlayerInventoryScreenLifecycle<>(); ++ private static boolean admitting; + + private BedrockPlayerInventory() {} + + static void register() { + PayloadTypeRegistry.clientboundPlay().register(PlayerInventoryScreenClosePayload.TYPE, PlayerInventoryScreenClosePayload.CODEC); ++ PayloadTypeRegistry.clientboundPlay().register(PlayerInventoryScreenReadyPayload.TYPE, PlayerInventoryScreenReadyPayload.CODEC); + PayloadTypeRegistry.serverboundPlay().register(PlayerInventoryScreenRequestPayload.TYPE, PlayerInventoryScreenRequestPayload.CODEC); ++ PayloadTypeRegistry.serverboundPlay().register(PlayerInventoryScreenRequestPayload.DEFERRED_TYPE, PlayerInventoryScreenRequestPayload.DEFERRED_CODEC); + ClientPlayConnectionEvents.INIT.register((owner, client) -> { + SCREENS.begin(owner); + ClientPlayNetworking.registerReceiver(PlayerInventoryScreenClosePayload.TYPE, (payload, context) -> + client.execute(() -> closeInstalled(owner, SCREENS.close(owner, payload.close().opening())))); ++ ClientPlayNetworking.registerReceiver(PlayerInventoryScreenReadyPayload.TYPE, (payload, context) -> ++ client.execute(() -> ready(owner, payload.ready().opening()))); + }); + ClientPlayConnectionEvents.DISCONNECT.register((owner, client) -> SCREENS.clear(owner)); + ClientTickEvents.END_CLIENT_TICK.register(client -> { + final var owner = client.getConnection(); -+ closeInstalled(owner, SCREENS.capability(owner, available())); ++ closeInstalled(owner, SCREENS.capability(owner, SCREENS.pending(owner) ? deferredAvailable() : available())); + installed(); + }); + } @@ -3346,12 +3395,54 @@ index 0000000..de80dc4 + if (owner == null || client.player == null) return; + final var displayed = client.gui.screen(); + final var menu = client.player.inventoryMenu; ++ if (displayed == null && SCREENS.pending(owner) && SCREENS.opening(owner, menu) != 0) return; + final boolean playerInventory = displayed instanceof AbstractContainerScreen container + && (container.getMenu() == menu && client.player.containerMenu == menu + || displayed instanceof CreativeModeInventoryScreen && client.player.containerMenu == container.getMenu()); + SCREENS.installed(owner, playerInventory ? displayed : null, playerInventory ? menu : null, -+ playerInventory && BedrockNativeContainers.replacing(menu), available(), -+ request -> ClientPlayNetworking.send(new PlayerInventoryScreenRequestPayload(request)), BedrockPlayerInventory::legacyOpen); ++ playerInventory && (admitting || BedrockNativeContainers.replacing(menu)), available(), ++ BedrockPlayerInventory::send, BedrockPlayerInventory::legacyOpen); ++ } ++ ++ static boolean admit(final Screen screen) { ++ final var client = Minecraft.getInstance(); ++ final var owner = client.getConnection(); ++ if (owner == null || client.player == null) return true; ++ final var menu = client.player.inventoryMenu; ++ final boolean playerInventory = screen instanceof AbstractContainerScreen container ++ && (container.getMenu() == menu || screen instanceof CreativeModeInventoryScreen); ++ if (SCREENS.pending(owner)) { ++ // Native repeats the interaction while retaining the same pending screen intent. ++ if (playerInventory) { ++ SCREENS.request(owner, screen, menu, BedrockPlayerInventory::send); ++ return false; ++ } ++ SCREENS.installed(owner, null, null, false, available(), BedrockPlayerInventory::send, () -> {}); ++ } ++ if (playerInventory && client.gui.screen() == null && deferredAvailable() ++ && SCREENS.opening(owner, menu) == 0 && !BedrockNativeContainers.replacing(menu)) { ++ SCREENS.request(owner, screen, menu, BedrockPlayerInventory::send); ++ return false; ++ } ++ return true; ++ } ++ ++ static Screen pendingScreen(final ClientPacketListener owner) { return SCREENS.pendingScreen(owner); } ++ ++ static void ready(final ClientPacketListener owner, final long opening) { ++ final var client = Minecraft.getInstance(); ++ if (client.getConnection() != owner || client.player == null || client.gui.screen() != null ++ || SCREENS.opening(owner, client.player.inventoryMenu) != opening) return; ++ final var screen = SCREENS.ready(owner, opening); ++ if (screen == null) return; ++ // InventoryScreen can redirect to Creative during init before setScreen returns. ++ admitting = true; ++ try { client.gui.setScreen(screen); } ++ finally { admitting = false; } ++ } ++ ++ private static void send(final PlayerInventoryScreenRequest request) { ++ ClientPlayNetworking.send(new PlayerInventoryScreenRequestPayload(request, deferredAvailable())); + } + + static long opening(final ClientPacketListener owner, final AbstractContainerMenu menu) { @@ -3362,6 +3453,10 @@ index 0000000..de80dc4 + return ClientPlayNetworking.canSend(PlayerInventoryScreenRequestPayload.TYPE); + } + ++ private static boolean deferredAvailable() { ++ return ClientPlayNetworking.canSend(PlayerInventoryScreenRequestPayload.DEFERRED_TYPE); ++ } ++ + private static void closeInstalled(final ClientPacketListener owner, final Screen screen) { + final var client = Minecraft.getInstance(); + if (screen != null && client.getConnection() == owner && client.player != null && client.gui.screen() == screen) @@ -6424,10 +6519,10 @@ index 0000000..806cc97 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenLifecycle.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenLifecycle.java new file mode 100644 -index 0000000..0d6c034 +index 0000000..9c6cdbd --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenLifecycle.java -@@ -0,0 +1,74 @@ +@@ -0,0 +1,98 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -6438,7 +6533,7 @@ index 0000000..0d6c034 +import java.util.function.Consumer; +import net.raphimc.viabedrock.api.model.PlayerInventoryScreenRequest; + -+/** Installed-screen ownership survives presentation swaps, but never an ordinary reopen of menu zero. */ ++/** Opening ownership survives presentation swaps, but never an ordinary reopen of menu zero. */ +final class PlayerInventoryScreenLifecycle { + private O owner; + private S screen; @@ -6446,6 +6541,7 @@ index 0000000..0d6c034 + private long sequence; + private long opening; + private boolean bridged; ++ private boolean pending; + + void begin(final O owner) { + this.owner = owner; @@ -6455,6 +6551,7 @@ index 0000000..0d6c034 + // Keep tokens monotonic across listener replacement and physical reconnects. + this.opening = 0; + this.bridged = false; ++ this.pending = false; + } + + void installed(final O owner, final S screen, final M menu, final boolean replacing, @@ -6470,6 +6567,7 @@ index 0000000..0d6c034 + this.menu = null; + this.opening = 0; + this.bridged = false; ++ this.pending = false; + if (close) send.accept(new PlayerInventoryScreenRequest(previous, false)); + if (screen == null) return; + this.screen = screen; @@ -6487,9 +6585,30 @@ index 0000000..0d6c034 + this.menu = null; + this.opening = 0; + this.bridged = false; ++ this.pending = false; + return screen; + } + ++ void request(final O owner, final S screen, final M menu, final Consumer send) { ++ if (owner != this.owner) return; ++ if (this.pending) { ++ send.accept(new PlayerInventoryScreenRequest(this.opening, true)); ++ return; ++ } ++ this.installed(owner, screen, menu, false, true, send, () -> {}); ++ this.pending = true; ++ } ++ ++ boolean pending(final O owner) { return owner == this.owner && this.pending; } ++ ++ S pendingScreen(final O owner) { return this.pending(owner) ? this.screen : null; } ++ ++ S ready(final O owner, final long opening) { ++ if (!this.pending(owner) || !this.bridged || opening != this.opening) return null; ++ this.pending = false; ++ return this.screen; ++ } ++ + S capability(final O owner, final boolean available) { + return !available && this.bridged ? this.close(owner, this.opening) : null; + } @@ -6502,12 +6621,40 @@ index 0000000..0d6c034 + if (owner == this.owner) this.begin(null); + } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenReadyPayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenReadyPayload.java +new file mode 100644 +index 0000000..d63ffe6 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenReadyPayload.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.container; ++ ++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.PlayerInventoryScreenReady; ++ ++public record PlayerInventoryScreenReadyPayload(PlayerInventoryScreenReady ready) implements CustomPacketPayload { ++ public static final Type TYPE = new Type<>(Identifier.parse(PlayerInventoryScreenReady.CHANNEL)); ++ public static final StreamCodec CODEC = CustomPacketPayload.codec( ++ (payload, buffer) -> PlayerInventoryScreenReady.TYPE.write(buffer, payload.ready), ++ buffer -> new PlayerInventoryScreenReadyPayload(PlayerInventoryScreenReady.TYPE.read(buffer))); ++ ++ @Override ++ public Type type() { return TYPE; } ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenRequestPayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenRequestPayload.java new file mode 100644 -index 0000000..a1d1f14 +index 0000000..5d3f834 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenRequestPayload.java -@@ -0,0 +1,22 @@ +@@ -0,0 +1,28 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -6521,14 +6668,20 @@ index 0000000..a1d1f14 +import net.minecraft.resources.Identifier; +import net.raphimc.viabedrock.api.model.PlayerInventoryScreenRequest; + -+public record PlayerInventoryScreenRequestPayload(PlayerInventoryScreenRequest request) implements CustomPacketPayload { ++public record PlayerInventoryScreenRequestPayload(PlayerInventoryScreenRequest request, boolean deferred) implements CustomPacketPayload { + public static final Type TYPE = new Type<>(Identifier.parse(PlayerInventoryScreenRequest.CHANNEL)); ++ public static final Type DEFERRED_TYPE = new Type<>(Identifier.parse(PlayerInventoryScreenRequest.DEFERRED_CHANNEL)); + public static final StreamCodec CODEC = CustomPacketPayload.codec( + (payload, buffer) -> PlayerInventoryScreenRequest.TYPE.write(buffer, payload.request), + buffer -> new PlayerInventoryScreenRequestPayload(PlayerInventoryScreenRequest.TYPE.read(buffer))); ++ public static final StreamCodec DEFERRED_CODEC = CustomPacketPayload.codec( ++ (payload, buffer) -> PlayerInventoryScreenRequest.TYPE.write(buffer, payload.request), ++ buffer -> new PlayerInventoryScreenRequestPayload(PlayerInventoryScreenRequest.TYPE.read(buffer), true)); ++ ++ public PlayerInventoryScreenRequestPayload(final PlayerInventoryScreenRequest request) { this(request, false); } + + @Override -+ public Type type() { return TYPE; } ++ public Type type() { return this.deferred ? DEFERRED_TYPE : 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 a2c1af8..a0b21b8 100644 @@ -12302,12 +12455,165 @@ index 0000000..127e576 + new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT, 0)); + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryAdmissionTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryAdmissionTest.java +new file mode 100644 +index 0000000..c115bc0 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryAdmissionTest.java +@@ -0,0 +1,147 @@ ++/* ++ * 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 java.util.ArrayList; ++import java.util.List; ++import java.lang.reflect.Field; ++import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; ++import net.minecraft.client.Minecraft; ++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.multiplayer.ClientPacketListener; ++import net.minecraft.client.player.LocalPlayer; ++import net.minecraft.world.entity.player.Player; ++import net.minecraft.world.inventory.InventoryMenu; ++import org.junit.jupiter.api.BeforeAll; ++import org.junit.jupiter.api.Test; ++import org.mockito.MockedStatic; ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class PlayerInventoryAdmissionTest { ++ @BeforeAll ++ static void bootstrap() throws Exception { NativePlayerPresentationTest.bootstrap(); } ++ ++ @Test ++ void playerInventoryLeavesGameplayVisibleUntilItsOwnAcknowledgement() throws Exception { ++ try (var f = new Fixture()) { ++ assertFalse(BedrockPlayerInventory.admit(f.screen)); ++ assertNull(f.gui.screen()); ++ verify(f.gui, never()).setScreen(any()); ++ assertFalse(BedrockPlayerInventory.admit(f.screen)); ++ assertEquals(2, f.sent.size()); ++ assertEquals(f.sent.getFirst().request().opening(), f.sent.getLast().request().opening()); ++ assertTrue(f.sent.getFirst().deferred()); ++ final long opening = f.sent.getFirst().request().opening(); ++ BedrockPlayerInventory.ready(f.owner, opening + 1); ++ verify(f.gui, never()).setScreen(any()); ++ BedrockPlayerInventory.ready(f.owner, opening); ++ verify(f.gui).setScreen(f.screen); ++ BedrockPlayerInventory.ready(f.owner, opening); ++ verify(f.gui, times(1)).setScreen(f.screen); ++ } ++ } ++ ++ @Test ++ void chatOrPauseCancelsPendingAdmissionWithoutLateScreenReplacement() throws Exception { ++ try (var f = new Fixture()) { ++ assertFalse(BedrockPlayerInventory.admit(f.screen)); ++ final long opening = f.sent.getFirst().request().opening(); ++ final var other = mock(Screen.class); ++ assertTrue(BedrockPlayerInventory.admit(other)); ++ assertEquals(2, f.sent.size()); ++ assertFalse(f.sent.getLast().request().open()); ++ assertEquals(opening, f.sent.getLast().request().opening()); ++ when(f.gui.screen()).thenReturn(other); ++ BedrockPlayerInventory.ready(f.owner, opening); ++ when(f.gui.screen()).thenReturn(null); ++ BedrockPlayerInventory.ready(f.owner, opening); ++ verify(f.gui, never()).setScreen(any()); ++ } ++ } ++ ++ @Test ++ void initializationRedirectKeepsTheAcknowledgedOpeningInsteadOfClosingAndRequestingAgain() throws Exception { ++ try (var f = new Fixture()) { ++ final var redirected = mock(AbstractContainerScreen.class); ++ doReturn(f.menu).when(redirected).getMenu(); ++ field(Player.class, "containerMenu").set(f.player, f.menu); ++ doAnswer(call -> { ++ when(f.gui.screen()).thenReturn(redirected); ++ BedrockPlayerInventory.installed(); ++ return null; ++ }).when(f.gui).setScreen(f.screen); ++ assertFalse(BedrockPlayerInventory.admit(f.screen)); ++ final long opening = f.sent.getFirst().request().opening(); ++ BedrockPlayerInventory.ready(f.owner, opening); ++ assertEquals(1, f.sent.size()); ++ assertEquals(opening, BedrockPlayerInventory.opening(f.owner, f.menu)); ++ assertSame(redirected, f.lifecycle.close(f.owner, opening)); ++ } ++ } ++ ++ @Test ++ void replacementPlayerMenuRetiresPendingAdmissionBeforeItsAcknowledgementCanDisplayTheOldScreen() throws Exception { ++ try (var f = new Fixture()) { ++ assertFalse(BedrockPlayerInventory.admit(f.screen)); ++ final long opening = f.sent.getFirst().request().opening(); ++ field(Player.class, "inventoryMenu").set(f.player, mock(InventoryMenu.class)); ++ BedrockPlayerInventory.ready(f.owner, opening); ++ verify(f.gui, never()).setScreen(any()); ++ BedrockPlayerInventory.installed(); ++ assertEquals(2, f.sent.size()); ++ assertFalse(f.sent.getLast().request().open()); ++ assertEquals(opening, f.sent.getLast().request().opening()); ++ assertNull(BedrockPlayerInventory.pendingScreen(f.owner)); ++ } ++ } ++ ++ private static final class Fixture implements AutoCloseable { ++ private final Minecraft game = mock(Minecraft.class); ++ private final LocalPlayer player = mock(LocalPlayer.class); ++ private final InventoryMenu menu = mock(InventoryMenu.class); ++ private final ClientPacketListener owner = mock(ClientPacketListener.class); ++ private final Gui gui = mock(Gui.class); ++ private final AbstractContainerScreen screen = mock(AbstractContainerScreen.class); ++ private final List sent = new ArrayList<>(); ++ private final MockedStatic singleton = mockStatic(Minecraft.class); ++ private final MockedStatic network = mockStatic(ClientPlayNetworking.class); ++ private final PlayerInventoryScreenLifecycle lifecycle; ++ ++ @SuppressWarnings("unchecked") ++ private Fixture() throws Exception { ++ singleton.when(Minecraft::getInstance).thenReturn(game); ++ when(game.getConnection()).thenReturn(owner); ++ doReturn(menu).when(screen).getMenu(); ++ field(Minecraft.class, "player").set(game, player); ++ field(Minecraft.class, "gui").set(game, gui); ++ field(Player.class, "inventoryMenu").set(player, menu); ++ lifecycle = (PlayerInventoryScreenLifecycle) field(BedrockPlayerInventory.class, "SCREENS").get(null); ++ lifecycle.begin(owner); ++ network.when(() -> ClientPlayNetworking.canSend(PlayerInventoryScreenRequestPayload.TYPE)).thenReturn(true); ++ network.when(() -> ClientPlayNetworking.canSend(PlayerInventoryScreenRequestPayload.DEFERRED_TYPE)).thenReturn(true); ++ network.when(() -> ClientPlayNetworking.send(any(PlayerInventoryScreenRequestPayload.class))).thenAnswer(call -> { ++ sent.add(call.getArgument(0)); ++ return null; ++ }); ++ } ++ ++ @Override ++ public void close() { ++ lifecycle.clear(owner); ++ network.close(); ++ singleton.close(); ++ } ++ } ++ ++ private static Field field(final Class type, final String name) throws Exception { ++ final var field = type.getDeclaredField(name); ++ field.setAccessible(true); ++ return field; ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenLifecycleTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenLifecycleTest.java new file mode 100644 -index 0000000..c07439c +index 0000000..55223ac --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/screen/container/PlayerInventoryScreenLifecycleTest.java -@@ -0,0 +1,95 @@ +@@ -0,0 +1,139 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -12323,6 +12629,50 @@ index 0000000..c07439c + +class PlayerInventoryScreenLifecycleTest { + @Test ++ void pendingInventoryRepeatsItsInteractionAndAdmitsOnlyItsAcknowledgement() { ++ final var f = new Fixture(); ++ final var intended = new Object(); ++ f.screens.request(f.owner, intended, f.menu, f.sent::add); ++ f.screens.request(f.owner, new Object(), f.menu, f.sent::add); ++ assertTrue(f.screens.pending(f.owner)); ++ assertSame(intended, f.screens.pendingScreen(f.owner)); ++ assertNull(f.screens.ready(f.owner, 2)); ++ assertEquals(List.of(new PlayerInventoryScreenRequest(1, true), new PlayerInventoryScreenRequest(1, true)), f.sent); ++ assertSame(intended, f.screens.ready(f.owner, 1)); ++ f.install(intended, false, true); ++ assertFalse(f.screens.pending(f.owner)); ++ assertNull(f.screens.ready(f.owner, 1)); ++ assertEquals(2, f.sent.size()); ++ } ++ ++ @Test ++ void anotherScreenCancelsThePendingInventoryAndLateAcknowledgementCannotAdmitIt() { ++ final var f = new Fixture(); ++ f.screens.request(f.owner, new Object(), f.menu, f.sent::add); ++ f.install(null, false, true); ++ assertNull(f.screens.ready(f.owner, 1)); ++ assertNull(f.screens.pendingScreen(f.owner)); ++ assertEquals(List.of(new PlayerInventoryScreenRequest(1, true), new PlayerInventoryScreenRequest(1, false)), f.sent); ++ final var next = new Object(); ++ f.screens.request(f.owner, next, f.menu, f.sent::add); ++ assertNull(f.screens.ready(f.owner, 1)); ++ assertSame(next, f.screens.ready(f.owner, 2)); ++ } ++ ++ @Test ++ void capabilityWithdrawalAndNewConnectionDiscardPendingAdmission() { ++ final var f = new Fixture(); ++ f.screens.request(f.owner, new Object(), f.menu, f.sent::add); ++ f.screens.capability(f.owner, false); ++ assertFalse(f.screens.pending(f.owner)); ++ assertNull(f.screens.ready(f.owner, 1)); ++ f.screens.request(f.owner, new Object(), f.menu, f.sent::add); ++ f.screens.begin(new Object()); ++ assertNull(f.screens.ready(f.owner, 2)); ++ assertFalse(f.screens.pending(f.owner)); ++ } ++ ++ @Test + void ordinaryReopenGetsANewIdentityEvenWithTheSamePersistentMenu() { + final var f = new Fixture(); + final var first = new Object(); 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 9cd22de8..86ab03c7 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 @@ -42,7 +42,18 @@ Controller regressions cover closing before a search reply, replacement commands Rebuilt direct and ViaProxy controls delay search handling by four seconds, close with unsent edits, and retain the latest text and explicit clear after reopening. Both routes preserve the 32-log stack and the native two-step Escape behavior. The [core inventory patch](../../viabedrock/deferred/0003-translate-server-authoritative-bedrock-requests.pr.md) owns close-wait admission and close acknowledgments. -Java can briefly display a local inventory before its correlated rejection arrives; client-side admission timing remains unverified. +The optional acknowledgment-bound bridge keeps the world visible until the native player open acknowledgment. +This gate also applies with Bedrock inventory screens disabled. +Repeated E retains its token and reissues the native interaction. +Escape or chat cancels the pending intent, and late readiness cannot replace another screen. +Controller regressions cover actual screen admission, cancellation, old tokens, capability withdrawal, and connection replacement. +Initialization redirects retain the acknowledged token, including Java inventory switching to Creative during initialization. +Replacement player menus retire the pending opening before its acknowledgment can display the old screen. +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls establish these visible behaviors with a four-second raw relay delay on protocol 2225. +Movement continues during the wait. Internal model constructor timing and complete frame-level population parity remain unverified. +Rebuilt direct and ViaProxy controls retain the world through delayed Survival, Creative, and workbench openings. +Both routes retain movement, repeated input, Pause and Chat ownership, and the 32-log stack. +The journals verify the repeated open interactions and native cleanup after cancellation. A real-editor screen regression clicks a recipe, returns to search, and types the inventory key without closing the screen. Installed direct and ViaProxy controls craft once, enter three replacement queries in the same opening, and retain completed output after reopening. Each route records one server-accepted craft request. Late replies and hover exits cannot affect a replacement opening or selection. From 70c266aa48183ff2f67bdc3c7b8590795bca4aec Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 06:59:56 +0100 Subject: [PATCH 24/47] fix(rendering): preserve floors beneath beds with MoreCulling Use live native face occlusion while accepted Bedrock metadata owns the world. Preserve MoreCulling's model cache for Java sessions and keep the integration optional, without changing the mod's global settings. The pinned graphics modpack reproduces the missing floor with a full cached bed face. Native 1.26.60.5, rebuilt direct/proxy clients, a completed resource reload, and a plain client establish the corrected control. The full build passes 3,325 tests with 325 skipped. Keep the shield, custom-skin pose, and broader graphics compatibility reports open. --- docs/bedrock-coverage.md | 3 +- patches/viafabricplus-bedrock/series.json | 16 ++ ...ve-native-occlusion-with-moreculling.patch | 182 ++++++++++++++++++ ...ve-native-occlusion-with-moreculling.pr.md | 32 +++ 4 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.patch create mode 100644 patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.pr.md diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index fce9075d..6dd070f2 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -41,7 +41,8 @@ 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 and overlapping menus. -- The player confirms that the oversized shield, distorted skin pose, and terrain showing through the floor under a bed occur in 0.4.3 screenshots. +- The oversized shield and distorted custom-skin pose in the confirmed 0.4.3 screenshots remain unresolved. +- The 0.4.3 bed-floor report matches a reproduced [MoreCulling interaction](../patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.pr.md). Current direct and proxy controls preserve the floor. The affected player's mod list remains unknown. - 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/patches/viafabricplus-bedrock/series.json b/patches/viafabricplus-bedrock/series.json index fe516045..59b1186b 100644 --- a/patches/viafabricplus-bedrock/series.json +++ b/patches/viafabricplus-bedrock/series.json @@ -355,6 +355,22 @@ "reason": "Publish the connection capability state used to recognize both direct and remotely translated Bedrock clients before food admission." } ] + }, + { + "file": "0040-preserve-native-occlusion-with-moreculling.patch", + "title": "fix(blocks): preserve native occlusion with MoreCulling", + "prerequisites": [ + { + "project": "viafabricplus-bedrock", + "patch": "0017-apply-custom-block-shapes.patch", + "reason": "Retain authoritative per-state face occlusion before adapting external culling caches." + }, + { + "project": "viafabricplus-bedrock", + "patch": "0014-reproduce-the-native-bedrock-classic-lightmap.patch", + "reason": "Use the accepted, protocol-bound rendering metadata lifecycle on direct and ViaProxy connections." + } + ] } ], "deferred": [ diff --git a/patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.patch b/patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.patch new file mode 100644 index 00000000..60adeb77 --- /dev/null +++ b/patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.patch @@ -0,0 +1,182 @@ +From f99bfa7fab8df18ee6e265cd95f73a2b945326c9 Mon Sep 17 00:00:00 2001 +From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> +Date: Mon, 2 Nov 2026 06:35:10 +0100 +Subject: [PATCH] fix(blocks): preserve native occlusion with MoreCulling + +MoreCulling 1.9.0-beta.1 derives full cached bottom faces from the converted +bed model even though the live block state has empty occlusion. With the +pinned 26.3 graphics modpack and BDS 1.26.60.6/protocol 2225, those faces +hide the stone floor beneath the bed. Disabling its block-state culling +and rebuilding both affected sections restores the floor. Re-enabling +the setting reproduces the hole without changing the final block states. + +Adapt MoreCulling's face query to the live block-state occlusion while +accepted, protocol-bound Bedrock rendering metadata owns the world. +This uses the same native face hooks as ordinary terrain and custom +carrier blocks on direct and ViaProxy connections. Keep the host cache +intact so Java sessions and resource removal retain their own behavior. + +Gate the optional mixin on MoreCulling presence and require no mod API +dependency. Preserve the native solid-neighbor faces rather than disabling +the mod or its culling globally. Regression coverage exercises bed halves, +solid neighbors, and entry/exit through accepted metadata. Other culling +shapes, shield poses, and full third-party graphics parity remain open. +--- + .../bedrock/block/BedrockBlockOcclusion.java | 20 +++++++ + .../bedrock/injection/BedrockMixinPlugin.java | 3 + + .../MixinMoreCullingOcclusion.java | 24 ++++++++ + .../viafabricplus-bedrock.mixins.json | 1 + + .../block/BedrockBlockOcclusionTest.java | 57 +++++++++++++++++++ + 5 files changed, 105 insertions(+) + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/block/BedrockBlockOcclusion.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/moreculling/MixinMoreCullingOcclusion.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/block/BedrockBlockOcclusionTest.java + +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/block/BedrockBlockOcclusion.java b/src/main/java/com/viaversion/viafabricplus/bedrock/block/BedrockBlockOcclusion.java +new file mode 100644 +index 0000000..067b974 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/block/BedrockBlockOcclusion.java +@@ -0,0 +1,20 @@ ++/* ++ * 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.block; ++ ++import com.viaversion.viafabricplus.bedrock.render.light.BedrockRenderingMetadata; ++import net.minecraft.core.Direction; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.phys.shapes.VoxelShape; ++ ++/** Accepted native terrain retains its face occlusion independently of model-derived caches. */ ++public final class BedrockBlockOcclusion { ++ private BedrockBlockOcclusion() {} ++ ++ public static VoxelShape select(final BlockState state, final Direction direction, final VoxelShape cached) { ++ return BedrockRenderingMetadata.active() ? state.getFaceOcclusionShape(direction) : cached; ++ } ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/BedrockMixinPlugin.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/BedrockMixinPlugin.java +index 3155d70..efcda2c 100644 +--- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/BedrockMixinPlugin.java ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/BedrockMixinPlugin.java +@@ -16,6 +16,9 @@ import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + public final class BedrockMixinPlugin implements IMixinConfigPlugin { + @Override + public boolean shouldApplyMixin(final String targetClassName, final String mixinClassName) { ++ if (mixinClassName.startsWith("com.viaversion.viafabricplus.bedrock.injection.mixin.compat.moreculling.")) { ++ return FabricLoader.getInstance().isModLoaded("moreculling"); ++ } + if (mixinClassName.startsWith("com.viaversion.viafabricplus.bedrock.injection.mixin.compat.emf.")) { + return FabricLoader.getInstance().isModLoaded("entity_model_features"); + } +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/moreculling/MixinMoreCullingOcclusion.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/moreculling/MixinMoreCullingOcclusion.java +new file mode 100644 +index 0000000..d65280d +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/compat/moreculling/MixinMoreCullingOcclusion.java +@@ -0,0 +1,24 @@ ++/* ++ * 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.compat.moreculling; ++ ++import com.viaversion.viafabricplus.bedrock.block.BedrockBlockOcclusion; ++import net.minecraft.core.Direction; ++import net.minecraft.world.level.block.state.BlockBehaviour; ++import net.minecraft.world.level.block.state.BlockState; ++import net.minecraft.world.phys.shapes.VoxelShape; ++import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.injection.At; ++import org.spongepowered.asm.mixin.injection.Inject; ++import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; ++ ++@Mixin(BlockBehaviour.BlockStateBase.class) ++public abstract class MixinMoreCullingOcclusion { ++ @Inject(method = "moreculling$getFaceCullingShape", at = @At("RETURN"), cancellable = true, remap = false, require = 0) ++ private void nativeOcclusion(final Direction direction, final CallbackInfoReturnable cir) { ++ cir.setReturnValue(BedrockBlockOcclusion.select((BlockState) (Object) this, direction, cir.getReturnValue())); ++ } ++} +diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json +index ac083ae..590dad9 100644 +--- a/src/main/resources/viafabricplus-bedrock.mixins.json ++++ b/src/main/resources/viafabricplus-bedrock.mixins.json +@@ -6,6 +6,7 @@ + "compatibilityLevel": "JAVA_25", + "priority": 1001, + "client": [ ++ "compat.moreculling.MixinMoreCullingOcclusion", + "compat.emf.MixinEmfModelRoots", + "features.render.MixinNativeFluidMesh", + "features.render.MixinNativeFluidNeighbors", +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/block/BedrockBlockOcclusionTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/block/BedrockBlockOcclusionTest.java +new file mode 100644 +index 0000000..3ad95de +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/block/BedrockBlockOcclusionTest.java +@@ -0,0 +1,57 @@ ++/* ++ * 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.block; ++ ++import com.viaversion.viafabricplus.bedrock.render.light.BedrockRenderingMetadata; ++import net.minecraft.SharedConstants; ++import net.minecraft.core.Direction; ++import net.minecraft.server.Bootstrap; ++import net.minecraft.world.level.block.Blocks; ++import net.minecraft.world.level.block.state.properties.BedPart; ++import net.minecraft.world.level.block.BedBlock; ++import net.minecraft.world.phys.shapes.BooleanOp; ++import net.minecraft.world.phys.shapes.Shapes; ++import org.junit.jupiter.api.BeforeAll; ++import org.junit.jupiter.api.Test; ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class BedrockBlockOcclusionTest { ++ @BeforeAll ++ static void bootstrap() { ++ SharedConstants.tryDetectVersion(); ++ Bootstrap.bootStrap(); ++ } ++ ++ @Test ++ void modelDerivedBedFacesNeverHideTheFloorInAnAcceptedBedrockWorld() { ++ try (var metadata = mockStatic(BedrockRenderingMetadata.class)) { ++ metadata.when(BedrockRenderingMetadata::active).thenReturn(true); ++ for (var part : BedPart.values()) { ++ final var bed = Blocks.BED.red().defaultBlockState().setValue(BedBlock.PART, part); ++ final var face = BedrockBlockOcclusion.select(bed, Direction.DOWN, Shapes.block()); ++ assertTrue(Shapes.joinIsNotEmpty(Shapes.block(), face, BooleanOp.ONLY_FIRST), ++ "The floor under the " + part + " must remain visible"); ++ } ++ final var stone = BedrockBlockOcclusion.select(Blocks.STONE.defaultBlockState(), Direction.DOWN, Shapes.empty()); ++ assertFalse(Shapes.joinIsNotEmpty(Shapes.block(), stone, BooleanOp.ONLY_FIRST), ++ "A solid neighbor must still occlude the complete floor face"); ++ } ++ } ++ ++ @Test ++ void leavingTheAcceptedWorldRestoresTheModsCachedFaceWithoutMutatingIt() { ++ try (var metadata = mockStatic(BedrockRenderingMetadata.class)) { ++ final var bed = Blocks.BED.red().defaultBlockState(); ++ final var cached = Shapes.block(); ++ assertSame(cached, BedrockBlockOcclusion.select(bed, Direction.DOWN, cached)); ++ metadata.when(BedrockRenderingMetadata::active).thenReturn(true); ++ assertTrue(BedrockBlockOcclusion.select(bed, Direction.DOWN, cached).isEmpty()); ++ metadata.when(BedrockRenderingMetadata::active).thenReturn(false); ++ assertSame(cached, BedrockBlockOcclusion.select(bed, Direction.DOWN, cached)); ++ } ++ } ++} +-- +2.56.0 + diff --git a/patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.pr.md b/patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.pr.md new file mode 100644 index 00000000..076e7d74 --- /dev/null +++ b/patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.pr.md @@ -0,0 +1,32 @@ +# fix(blocks): preserve native occlusion with MoreCulling + +## Purpose + +Keep the terrain beneath Bedrock beds visible when MoreCulling derives an opaque face from the converted model. +Use the existing native face occlusion while accepted Bedrock metadata owns the world. +Preserve MoreCulling's cache and ordinary Java behavior outside that context. + +## Design constraints + +This optional integration requires the custom block face hooks and accepted rendering metadata. +It changes the face query without disabling MoreCulling or altering its global settings. +The mod remains optional and adds no compile dependency. + +## Validation and remaining limits + +The pinned Fabulously Optimized profile includes MoreCulling 1.9.0-beta.1 and Sodium 0.9.2 on Java 26.3. +With BDS 1.26.60.6/protocol 2225, the live bed has an empty occlusion face and MoreCulling caches a full bottom face. +Disabling block-state culling and rebuilding the affected sections restores the stone floor. +Enabling the same setting and rebuilding reproduces the hole without changing the final block states. + +BedrockBlockOcclusionTest covers both bed halves, solid neighbors, and entry into and exit from the accepted context. +Official Bedrock 1.26.60.5 keeps the stone floor visible beneath the same bed in the same BDS scene. +Rebuilt direct and ViaProxy clients preserve that floor with the complete graphics modpack and MoreCulling enabled. +Both live probes retain the host's full cached face while the adapted bed face remains empty. +The stone neighbor retains a full face. +An installed ViaProxy resource reload completes with the floor still visible and the same query results. +A current plain client without MoreCulling also reaches gameplay and retains the floor. +Other models, graphics mod combinations, and complete native visual parity remain open. +The original oversized-shield and custom-skin pose reports remain unresolved. + +The [owning patch](0040-preserve-native-occlusion-with-moreculling.patch) records the implementation rationale. From 7ca7f21e4f4fc133334f76437e9ec974c2ca1bdc Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:35:00 +0100 Subject: [PATCH 25/47] docs(movement): record current projectile rollback controls Record protocol-2225 native, direct and proxy projectile controls, including two proxy combinations with 300 ms inbound delay. Native also receives corrections after wind impulses, so correction counts alone do not establish the reported persistent rollback loop. Keep the original report open and state the input cadence, trajectory and latency limits of these controls. --- docs/bedrock-coverage.md | 2 +- .../0025-send-completed-local-physics-to-viabedrock.pr.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 6dd070f2..359ff9b8 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -43,7 +43,7 @@ Retain these reports until a matching reproduction establishes the result: - An update from 0.3.61 to 0.4.2 caused reported inventory lag spikes and overlapping menus. - The oversized shield and distorted custom-skin pose in the confirmed 0.4.3 screenshots remain unresolved. - The 0.4.3 bed-floor report matches a reproduced [MoreCulling interaction](../patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.pr.md). Current direct and proxy controls preserve the floor. The affected player's mod list remains unknown. -- An ender pearl followed by a wind charge can cause repeated rollbacks. +- An ender pearl followed by a wind charge can cause repeated rollbacks. Selected [current native and Java controls](../patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md) settle, including two proxy combinations with added delay. Faster combinations and movement during throws remain unverified. - Food can repeat its use animation without feeding the player. - The locator bar can be absent. diff --git a/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md b/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md index d67a0038..3925b713 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md @@ -22,6 +22,13 @@ Regression coverage includes `BedrockPhysicalMotionLifecycleTest`, `BedrockPlaye Native and live controls establish selected movement, correction, and flight behavior. Uncommon mounts, network conditions, and mixed mechanics remain open. +Bedrock 1.26.60.5 and BDS 1.26.60.6 controls cover pearl-only, wind-only, and two pearl/wind angles. +Both current Java routes produce the expected projectiles and settle after landing. +Two ViaProxy combinations also settle with 300 ms of inbound Java-channel delay. +Native packets contain movement corrections after wind impulses, so correction counts alone do not identify the reported rollback loop. +These controls use physical 150 ms presses. The proxy combinations place five or six server ticks between uses. +They do not establish identical trajectories, faster combinations, movement during throws, or resolution of the original persistent rollback report. + Bedrock 1.26.60.5 and BDS 1.26.60.6 establish four stationary ten-block drops. Native, direct, and ViaProxy lose seven health on dry ground and at both partial overlaps with a solid/water boundary. A fully wet landing loses none. These controls cover fall damage with full blocks and source water. Other fluid movement and correction behavior remain separate. The [owning patch](0025-send-completed-local-physics-to-viabedrock.patch) retains implementation rationale and versioned evidence. From b661d9372357769d00752adc7fb34d6d10e43c77 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 08:22:23 +0100 Subject: [PATCH 26/47] fix(integration): select BDS HTTP signaling targets Normalize NetherNet targets through the replay runner target adapter before starting ViaProxy. A raw nethernet URL was parsed as a hostname, while the LAN prefix selected discovery signaling instead of the HTTP signaling used by current BDS. Verify the corrected route with installed protocol-2225 gameplay and reconnect controls. --- src/integration/cli.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/integration/cli.ts b/src/integration/cli.ts index fbb673eb..f9c13873 100644 --- a/src/integration/cli.ts +++ b/src/integration/cli.ts @@ -11,6 +11,7 @@ import { Effect } from "effect"; import { createCaptureUi } from "../capture/ui.ts"; import { activeDisplay, displayEnv, ensureDisplay, stopDisplay } from "../lab/display.ts"; import { root } from "../model.ts"; +import { viaProxyTarget } from "../replay/targets.ts"; import { artifact } from "../client-artifacts.ts"; import { installPrism, prismProcess } from "../prism.ts"; import { installEntityProbe, waitForProbe } from "./entity-probe.ts"; @@ -357,9 +358,11 @@ async function viaProxy( "--bind-address", `127.0.0.1:${port}`, "--target-address", - transport === "nethernet" - ? `nethernet://127.0.0.1:${bedrockPort}` - : `127.0.0.1:${bedrockPort}`, + viaProxyTarget( + transport === "nethernet" + ? `nethernet://127.0.0.1:${bedrockPort}` + : `127.0.0.1:${bedrockPort}`, + ), "--target-version", `Bedrock ${version}`, "--auth-method", From 655878ebccd4a02da214185db2296b732a7a7121 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 08:25:07 +0100 Subject: [PATCH 27/47] test(movement): cover moving pearl and wind-charge recovery Retain pre-use motion samples for a walking projectile sequence. Require walking before the pearl, its landing at the native impact position, both consumed items, a wind launch and a quiet landing. Reject stationary approaches and projectile motion masquerading as walking. Record protocol-2225 native, direct and proxy controls with incoming delay while keeping the original persistent rollback report and exact input timing unresolved. Verify the new installed gameplay case and reconnect with current built clients. --- docs/bedrock-complex-gameplay.md | 1 + docs/bedrock-coverage.md | 2 +- ...ompleted-local-physics-to-viabedrock.pr.md | 10 +++-- src/integration/gameplay-probe.ts | 13 ++++++ src/integration/player-use-observation.ts | 45 ++++++++++++++++--- src/integration/ranged-observation.ts | 1 + test-packs/entity-probe/src/player-uses.ts | 14 +++--- test/integration-complex-gameplay.test.ts | 4 +- ...integration-player-use-observation.test.ts | 36 +++++++++++++++ 9 files changed, 108 insertions(+), 18 deletions(-) diff --git a/docs/bedrock-complex-gameplay.md b/docs/bedrock-complex-gameplay.md index 6325c0e1..883c3634 100644 --- a/docs/bedrock-complex-gameplay.md +++ b/docs/bedrock-complex-gameplay.md @@ -35,6 +35,7 @@ bun run test:integration -- --route java-bedrock --gameplay-complex ``` Select individual cases through the [test-pack guide](bedrock-test-packs.md). +Select `pearl-wind-charge` and `pearl-wind-charge-moving` to compare stationary and walking projectile combinations. The suite exercises ranged use, potion effects, fireballs, powder snow, fluids, and creative ascent. It is a runnable subset, not an exhaustive Bedrock simulation. The Java/Geyser fixture does not implement every BDS complex case. diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 359ff9b8..0743a9bf 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -43,7 +43,7 @@ Retain these reports until a matching reproduction establishes the result: - An update from 0.3.61 to 0.4.2 caused reported inventory lag spikes and overlapping menus. - The oversized shield and distorted custom-skin pose in the confirmed 0.4.3 screenshots remain unresolved. - The 0.4.3 bed-floor report matches a reproduced [MoreCulling interaction](../patches/viafabricplus-bedrock/upstreamable/0040-preserve-native-occlusion-with-moreculling.pr.md). Current direct and proxy controls preserve the floor. The affected player's mod list remains unknown. -- An ender pearl followed by a wind charge can cause repeated rollbacks. Selected [current native and Java controls](../patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md) settle, including two proxy combinations with added delay. Faster combinations and movement during throws remain unverified. +- An ender pearl followed by a wind charge can cause repeated rollbacks. Selected [current native and Java controls](../patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md) settle, including walking projectile combinations with added incoming delay. Timing for short presses and the original persistent loop remain unverified. - Food can repeat its use animation without feeding the player. - The locator bar can be absent. diff --git a/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md b/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md index 3925b713..28bb5b34 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md @@ -24,10 +24,14 @@ Native and live controls establish selected movement, correction, and flight beh Bedrock 1.26.60.5 and BDS 1.26.60.6 controls cover pearl-only, wind-only, and two pearl/wind angles. Both current Java routes produce the expected projectiles and settle after landing. -Two ViaProxy combinations also settle with 300 ms of inbound Java-channel delay. +Selected native, direct, and ViaProxy combinations also settle with 300 ms of incoming delivery delay, including combinations with held movement input. Native packets contain movement corrections after wind impulses, so correction counts alone do not identify the reported rollback loop. -These controls use physical 150 ms presses. The proxy combinations place five or six server ticks between uses. -They do not establish identical trajectories, faster combinations, movement during throws, or resolution of the original persistent rollback report. +The selected moving controls place four server ticks between native uses, two on direct Java, and three through ViaProxy. +Short native input attempts do not consistently produce both throws. These controls do not establish equal trajectories or timing for individual presses. +The original persistent rollback report remains open. +The permanent `pearl-wind-charge-moving` case requires walking before either projectile moves the player. +It also requires both uses and impacts, consumption, a wind launch, and a quiet landing. +Its pearl landing must match the impact position, so walking away from the initial position cannot substitute for the teleport result. Bedrock 1.26.60.5 and BDS 1.26.60.6 establish four stationary ten-block drops. Native, direct, and ViaProxy lose seven health on dry ground and at both partial overlaps with a solid/water boundary. A fully wet landing loses none. These controls cover fall damage with full blocks and source water. Other fluid movement and correction behavior remain separate. diff --git a/src/integration/gameplay-probe.ts b/src/integration/gameplay-probe.ts index 61f5ad00..25468790 100644 --- a/src/integration/gameplay-probe.ts +++ b/src/integration/gameplay-probe.ts @@ -805,6 +805,19 @@ export async function driveGameplay( await uiMouse(ui, "right", 60); await Bun.sleep(5000); return; + case "pearl-wind-charge-moving": { + await Promise.all([ + ui(["ui", "key-hold", "w", "1200", "--client", "java"]), + (async () => { + await Bun.sleep(200); + await uiMouse(ui, "right", 80); + await ui(["ui", "key-hold", "2", "40", "--client", "java"]); + await uiMouse(ui, "right", 80); + })(), + ]); + await Bun.sleep(5000); + return; + } case "eat-moving-repeat": case "eat-moving-last": { const duration = id === "eat-moving-last" ? 3500 : 8000; diff --git a/src/integration/player-use-observation.ts b/src/integration/player-use-observation.ts index 095ff2f1..11c2cd9b 100644 --- a/src/integration/player-use-observation.ts +++ b/src/integration/player-use-observation.ts @@ -2,7 +2,7 @@ import type { KnockbackFrame } from "./knockback-observation.ts"; import type { RangedAction } from "./ranged-observation.ts"; export interface PlayerUseObservation { - kind: "pearl-wind" | "food"; + kind: "pearl-wind" | "pearl-wind-moving" | "food"; initialFood: number; remainingFood: number; remainingPearls: number; @@ -10,7 +10,12 @@ export interface PlayerUseObservation { absorption: number; start: { x: number; y: number; z: number }; events: { item: string; action: RangedAction; tick: number }[]; - impacts: { type: string; tick: number; playerPosition: { x: number; y: number; z: number } }[]; + impacts: { + type: string; + tick: number; + playerPosition: { x: number; y: number; z: number }; + impactPosition?: { x: number; y: number; z: number }; + }[]; frames: KnockbackFrame[]; error?: string; } @@ -18,9 +23,14 @@ export interface PlayerUseObservation { function validFrames(observation: PlayerUseObservation): boolean { return ( !observation.error && - [observation.start, ...observation.impacts.map((impact) => impact.playerPosition)].every((v) => - [v.x, v.y, v.z].every(Number.isFinite), - ) && + [ + observation.start, + ...observation.impacts.flatMap((impact) => + impact.impactPosition + ? [impact.playerPosition, impact.impactPosition] + : [impact.playerPosition], + ), + ].every((v) => [v.x, v.y, v.z].every(Number.isFinite)) && observation.events.every((event) => Number.isInteger(event.tick)) && observation.impacts.every((impact) => Number.isInteger(impact.tick)) && observation.frames.length > 1 && @@ -35,7 +45,11 @@ function validFrames(observation: PlayerUseObservation): boolean { /** Require both native uses and impacts, a teleport, a wind jump, then a quiet landing. */ export function pearlWindChargePasses(observation: PlayerUseObservation): boolean { - if (!validFrames(observation) || observation.kind !== "pearl-wind") return false; + if ( + !validFrames(observation) || + (observation.kind !== "pearl-wind" && observation.kind !== "pearl-wind-moving") + ) + return false; const uses = observation.events.filter((event) => event.action === "use"); const [pearl, wind] = uses; const pearlHit = observation.impacts.find((impact) => impact.type === "minecraft:ender_pearl"); @@ -60,6 +74,25 @@ export function pearlWindChargePasses(observation: PlayerUseObservation): boolea ) < 0.4 ) return false; + if (observation.kind === "pearl-wind-moving") { + // Observe walking before either projectile can teleport or propel the player. + const before = observation.frames.filter((frame) => frame.tick < pearl.tick).slice(-6); + const first = before[0]; + const last = before.at(-1); + if ( + !first || + !last || + !pearlHit.impactPosition || + before.filter((frame) => Math.hypot(frame.velocity.x, frame.velocity.z) > 0.05).length < 2 || + Math.hypot(last.position.x - first.position.x, last.position.z - first.position.z) <= 0.2 || + Math.hypot( + pearlHit.playerPosition.x - pearlHit.impactPosition.x, + pearlHit.playerPosition.y - pearlHit.impactPosition.y, + pearlHit.playerPosition.z - pearlHit.impactPosition.z, + ) > 0.02 + ) + return false; + } const after = observation.frames.filter((frame) => frame.tick >= windHit.tick); const tail = after.slice(-20); const end = tail.at(-1); diff --git a/src/integration/ranged-observation.ts b/src/integration/ranged-observation.ts index 6215ee89..a01fd30c 100644 --- a/src/integration/ranged-observation.ts +++ b/src/integration/ranged-observation.ts @@ -1,5 +1,6 @@ export const complexGameplayCaseIds = [ "pearl-wind-charge", + "pearl-wind-charge-moving", "eat-moving-repeat", "eat-moving-last", "bow-release", diff --git a/test-packs/entity-probe/src/player-uses.ts b/test-packs/entity-probe/src/player-uses.ts index bef8abf8..d294f40c 100644 --- a/test-packs/entity-probe/src/player-uses.ts +++ b/test-packs/entity-probe/src/player-uses.ts @@ -23,13 +23,13 @@ export function registerPlayerUseGameplay(context: ProbeContext) { const observation = observationFor(player); if (!observation) return; observation.impacts.push({ type: event.projectile.typeId, tick: system.currentTick, - playerPosition: { ...player.location } }); + playerPosition: { ...player.location }, impactPosition: { ...event.location } }); }); system.runInterval(() => { const current = getActive(); if (!current || current.fixture.closed) return; const observation = current.fixture.playerUse; - if (!observation || observation.events.length === 0) return; + if (!observation || (observation.events.length === 0 && observation.kind !== "pearl-wind-moving")) return; const player = world.getAllPlayers().find((candidate) => candidate.name === current.playerName); if (!player) return; try { @@ -39,13 +39,13 @@ export function registerPlayerUseGameplay(context: ProbeContext) { } catch (error) { observation.error = String(error); } }, 1); - for (const id of ["pearl-wind-charge", "eat-moving-repeat", "eat-moving-last"] as const) { + for (const id of ["pearl-wind-charge", "pearl-wind-charge-moving", "eat-moving-repeat", "eat-moving-last"] as const) { define(id, "player-use", async (player): Promise<{ playerUse: PlayerUseObservation; closed: boolean }> => { await prepareArena(player); const from = position(-8, -1, -8), to = position(8, -1, 32); player.dimension.runCommand(`fill ${Math.floor(from.x)} ${from.y} ${Math.floor(from.z)} ${Math.floor(to.x)} ${to.y} ${Math.floor(to.z)} stone`); player.dimension.runCommand(`fill ${Math.floor(from.x)} ${from.y + 1} ${Math.floor(from.z)} ${Math.floor(to.x)} ${to.y + 11} ${Math.floor(to.z)} air`); - const kind = id === "pearl-wind-charge" ? "pearl-wind" : "food"; + const kind = id === "pearl-wind-charge-moving" ? "pearl-wind-moving" : id === "pearl-wind-charge" ? "pearl-wind" : "food"; const initialFood = kind === "food" ? id === "eat-moving-last" ? 1 : 6 : 0; if (kind === "food") inventory(player).setItem(0, new ItemStack("minecraft:golden_apple", initialFood)); else { @@ -65,9 +65,9 @@ export function registerPlayerUseGameplay(context: ProbeContext) { observation.remainingPearls = countItem(inventory(player), "minecraft:ender_pearl"); observation.remainingWindCharges = countItem(inventory(player), "minecraft:wind_charge"); observation.absorption = player.getEffect("absorption")?.duration ?? 0; - return { passed: observation.kind === "pearl-wind" ? pearlWindChargePasses(observation) : movingFoodUsePasses(observation), - observed: observation, expected: observation.kind === "pearl-wind" - ? "One pearl teleport, one immediate wind-charge impact, an upward impulse and a settled landing." + return { passed: observation.kind === "food" ? movingFoodUsePasses(observation) : pearlWindChargePasses(observation), + observed: observation, expected: observation.kind !== "food" + ? "A pearl teleport, a wind-charge launch, and a settled landing. The moving case also requires walking before the throw." : "Food completes while moving, grants absorption and stops when the last item is consumed." }; }); } diff --git a/test/integration-complex-gameplay.test.ts b/test/integration-complex-gameplay.test.ts index 9dda27b7..6241b4f7 100644 --- a/test/integration-complex-gameplay.test.ts +++ b/test/integration-complex-gameplay.test.ts @@ -90,7 +90,7 @@ test("player-use outcomes exclude other players and stop with the active fixture const source = { ...player, typeId: "minecraft:player", location: { x: 0, y: 70, z: 1 } }; const use = (p: object) => callbacks.get("itemUse")!({ source: p, itemStack: { typeId: "minecraft:ender_pearl" } }); const impact = (p: object) => callbacks.get("projectileHitBlock")!({ source: p, - projectile: { typeId: "minecraft:ender_pearl" } }); + projectile: { typeId: "minecraft:ender_pearl" }, location: { x: 0.5, y: 70, z: 2 } }); use({ ...source, name: "OtherPlayer" }); impact({ ...source, name: "OtherPlayer" }); expect(observation.events).toHaveLength(0); @@ -99,6 +99,8 @@ test("player-use outcomes exclude other players and stop with the active fixture impact(source); expect(observation.events).toHaveLength(1); expect(observation.impacts).toHaveLength(1); + expect(observation.impacts[0]).toMatchObject({ playerPosition: { x: 0, y: 70, z: 1 }, + impactPosition: { x: 0.5, y: 70, z: 2 } }); fixture.closed = true; use(source); impact(source); diff --git a/test/integration-player-use-observation.test.ts b/test/integration-player-use-observation.test.ts index 60a2b14b..9a2fbb3f 100644 --- a/test/integration-player-use-observation.test.ts +++ b/test/integration-player-use-observation.test.ts @@ -94,6 +94,42 @@ test("pearl and wind charge require both native outcomes and a quiet landing", ( } }); +test("moving pearl and wind charge require walking before projectile movement", () => { + const moving = pearlWind(); + moving.kind = "pearl-wind-moving"; + moving.events.forEach((event) => { event.tick += 4; }); + moving.impacts.forEach((impact) => { impact.tick += 4; }); + moving.frames.forEach((frame) => { frame.tick += 4; }); + moving.impacts[0]!.impactPosition = { ...moving.impacts[0]!.playerPosition }; + moving.frames.unshift( + { tick: 1, position: { x: 0, y: 70, z: 0 }, velocity: { x: 0, y: 0, z: 0.1 }, onGround: true }, + { tick: 2, position: { x: 0, y: 70, z: 0.15 }, velocity: { x: 0, y: 0, z: 0.15 }, onGround: true }, + { tick: 3, position: { x: 0, y: 70, z: 0.35 }, velocity: { x: 0, y: 0, z: 0.2 }, onGround: true }, + { tick: 4, position: { x: 0, y: 70, z: 0.55 }, velocity: { x: 0, y: 0, z: 0.2 }, onGround: true }, + ); + expect(pearlWindChargePasses(moving)).toBe(true); + + const stationary = structuredClone(moving); + for (const frame of stationary.frames.filter((frame) => frame.tick < 5)) { + frame.position.z = 0; + frame.velocity.z = 0; + } + expect(pearlWindChargePasses(stationary)).toBe(false); + + const teleported = structuredClone(moving); + for (const frame of teleported.frames.filter((frame) => frame.tick < 5)) frame.velocity.z = 0; + expect(pearlWindChargePasses(teleported)).toBe(false); + + const noPearlTeleport = structuredClone(moving); + noPearlTeleport.impacts[0]!.playerPosition.z = 0.55; + expect(pearlWindChargePasses(noPearlTeleport)).toBe(false); + + const noApproach = pearlWind(); + noApproach.kind = "pearl-wind-moving"; + noApproach.impacts[0]!.impactPosition = { ...noApproach.impacts[0]!.playerPosition }; + expect(pearlWindChargePasses(noApproach)).toBe(false); +}); + test("moving eating must complete and consume the same number of items", () => { expect(movingFoodUsePasses(food())).toBe(true); for (const alter of [ From c659c9613bdc44a9438d7e5be6a3902d53121d4a Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 09:20:51 +0100 Subject: [PATCH 28/47] fix(ui): translate action form dialog labels Bind the accepted resource pack translator to action form elements before Java dialog presentation. MCStructs Bedrock 2.0.1 already translates the form title and body, but leaves action button labels on the default translator. Add a packet-to-dialog regression and compare the same language pack on native Bedrock 1.26.60.5 and both rebuilt Java routes. Preserve literal labels and original response indices. Keep the original Lifeboat report open pending a matching form capture. --- docs/bedrock-coverage.md | 1 + ...rate-authored-native-ui-presentation.patch | 86 +++++++++++++++---- ...rate-authored-native-ui-presentation.pr.md | 6 ++ 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 0743a9bf..5b88a02e 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: +- Lifeboat dialog text was reported as untranslated. A generic [action-form dialog label defect](../patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md) is fixed. The original report still needs a matching form capture. - 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 and overlapping menus. - The oversized shield and distorted custom-skin pose in the confirmed 0.4.3 screenshots remain unresolved. 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 0919c3c6..17b7324b 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 cc0940d826c6524c35f956e00f26d521e6da0004 Mon Sep 17 00:00:00 2001 +From 95b41a4a5c2de68bb8cbdb08157f8ec649bbbd18 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 @@ -669,6 +669,10 @@ 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. + +Bind the accepted pack translator to every ActionForm element before retaining the form. MCStructs Bedrock 2.0.1 propagates translators through CustomForm, but ActionForm leaves its element labels on the default translator. Java dialog fallback otherwise translates the title and body while exposing raw language keys on its buttons. Preserve original response ordinals and raw form values. + +Bedrock 1.26.60.5 with BDS 1.26.60.6, protocol 2225, resolves a language-only pack key on the action button and preserves the literal second button. Native selections return indices 0 and 1. A packet-to-dialog regression verifies the same title, body and button labels without requiring authored JSON UI. --- Data Asset Sources.md | 33 + build.gradle | 1 + @@ -711,7 +715,7 @@ former content-only shortcut. .../protocol/model/NativeItemName.java | 78 + .../protocol/packet/HudPackets.java | 51 +- .../protocol/packet/InteractionPackets.java | 2 +- - .../protocol/packet/InventoryPackets.java | 653 +- + .../protocol/packet/InventoryPackets.java | 659 +- .../protocol/packet/JoinPackets.java | 14 +- .../protocol/packet/MultiStatePackets.java | 38 + .../protocol/packet/NpcDialoguePackets.java | 417 +- @@ -745,14 +749,14 @@ former content-only shortcut. .../protocol/model/ItemRegistryTest.java | 272 + .../model/NativeItemDefinitionsTest.java | 207 + .../protocol/model/NativeItemNameTest.java | 61 + - .../packet/ModalFormTransitionTest.java | 123 + + .../packet/ModalFormTransitionTest.java | 165 + .../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, 36368 insertions(+), 362 deletions(-) + 82 files changed, 36416 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 @@ -4083,7 +4087,7 @@ index 999f2404..0acee785 100644 && "minecraft:written_book".equals(itemIdentifier)) { wrapper.cancel(); 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 39b8a7e4..d22d7616 100644 +index 39b8a7e4..7b3138e1 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -17,6 +17,14 @@ @@ -4128,7 +4132,7 @@ index 39b8a7e4..d22d7616 100644 import net.raphimc.viabedrock.protocol.types.BedrockTypes; import java.util.ArrayList; -@@ -90,6 +104,103 @@ public final class InventoryPackets { +@@ -90,6 +104,109 @@ public final class InventoryPackets { private static final int DIALOG_FAKE_BUTTON_WIDTH = 300; private static final String DIALOG_FAKE_BUTTON_TEXT = "This is not actually a button, but has to be one because dialogs don't support adding text only elements. Clicking it has the same effect as closing the dialog."; @@ -4200,6 +4204,12 @@ index 39b8a7e4..d22d7616 100644 + final ResourcePackStorage resourcePackStorage = + wrapper.user().get(ResourcePackStorage.class); + form.setTranslator(resourcePackStorage.getTexts()::translate); ++ if (form instanceof ActionForm actionForm) { ++ // ActionForm does not propagate its translator to element labels. ++ for (final FormElement element : actionForm.getElements()) { ++ element.setTranslator(form.getTranslator()); ++ } ++ } + inventoryTracker.closeContainerForForm(); + inventoryTracker.setCurrentForm(IntObjectPair.of(id, form)); + final boolean nativeCustom = form instanceof CustomForm && wrapper.user().get(ChannelStorage.class).hasChannel(NativeFormMessage.CUSTOM_CHANNEL); @@ -4232,7 +4242,7 @@ index 39b8a7e4..d22d7616 100644 public static void register(final BedrockProtocol protocol) { protocol.registerClientbound(ClientboundBedrockPackets.PLAYER_START_ITEM_COOLDOWN, ClientboundPackets26_3.COOLDOWN, wrapper -> { -@@ -109,7 +220,8 @@ public final class InventoryPackets { +@@ -109,7 +226,8 @@ public final class InventoryPackets { return; } final BlockPosition position = wrapper.read(BedrockTypes.BLOCK_POSITION); // position @@ -4242,7 +4252,7 @@ index 39b8a7e4..d22d7616 100644 if (inventoryTracker.isAnyScreenOpen()) { ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Server tried to open container while another container is open"); -@@ -117,11 +229,15 @@ public final class InventoryPackets { +@@ -117,11 +235,15 @@ public final class InventoryPackets { wrapper.cancel(); return; } @@ -4263,7 +4273,7 @@ index 39b8a7e4..d22d7616 100644 final Container container; switch (type) { -@@ -131,6 +247,9 @@ public final class InventoryPackets { +@@ -131,6 +253,9 @@ public final class InventoryPackets { return; } case CONTAINER -> container = new ChestContainer(wrapper.user(), containerId, title, position, 27); @@ -4273,7 +4283,7 @@ index 39b8a7e4..d22d7616 100644 case COMMAND_BLOCK -> { wrapper.cancel(); if (!WorldPackets.openNativeCommandBlock(wrapper.user(), containerId, blockEntity)) { -@@ -153,40 +272,16 @@ public final class InventoryPackets { +@@ -153,40 +278,16 @@ public final class InventoryPackets { inventoryTracker.setCurrentContainer(container); wrapper.write(Types.VAR_INT, (int) containerId); // container id @@ -4318,7 +4328,7 @@ index 39b8a7e4..d22d7616 100644 } }); protocol.registerClientbound(ClientboundBedrockPackets.INVENTORY_CONTENT, ClientboundPackets26_3.CONTAINER_SET_CONTENT, wrapper -> { -@@ -227,134 +322,10 @@ public final class InventoryPackets { +@@ -227,134 +328,10 @@ public final class InventoryPackets { wrapper.cancel(); } }); @@ -4457,7 +4467,7 @@ index 39b8a7e4..d22d7616 100644 protocol.registerClientbound(ClientboundBedrockPackets.CLOSE_FORM, ClientboundPackets26_3.CLEAR_DIALOG, wrapper -> { final InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); if (inventoryTracker.getCurrentForm() != null) { -@@ -452,6 +423,11 @@ public final class InventoryPackets { +@@ -452,6 +429,11 @@ public final class InventoryPackets { wrapper.cancel(); return; } @@ -4469,7 +4479,7 @@ index 39b8a7e4..d22d7616 100644 if (!container.handleClick(revision, slot, button, action)) { if (container.type() != ContainerType.INVENTORY) { PacketFactory.sendJavaContainerSetContent(wrapper.user(), inventoryTracker.getInventoryContainer()); -@@ -490,6 +466,15 @@ public final class InventoryPackets { +@@ -490,6 +472,15 @@ public final class InventoryPackets { return; } @@ -4485,7 +4495,7 @@ index 39b8a7e4..d22d7616 100644 inventoryTracker.setCurrentForm(null); if (payload.contains("exit") && payload.getBoolean("exit")) { wrapper.write(BedrockTypes.UNSIGNED_VAR_INT, formId); // id -@@ -633,6 +618,342 @@ public final class InventoryPackets { +@@ -633,6 +624,342 @@ public final class InventoryPackets { }); } @@ -4828,7 +4838,7 @@ index 39b8a7e4..d22d7616 100644 private static void sendBookEdits(final UserConnection user, final int slot, final List previousPages, final List pages, final String title) { for (int index = 0; index < pages.size(); index++) { -@@ -687,4 +1008,12 @@ public final class InventoryPackets { +@@ -687,4 +1014,12 @@ public final class InventoryPackets { private InventoryPackets() { } @@ -36937,10 +36947,10 @@ index 00000000..44536259 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/ModalFormTransitionTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/ModalFormTransitionTest.java new file mode 100644 -index 00000000..51cdb2bf +index 00000000..343bf7aa --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/ModalFormTransitionTest.java -@@ -0,0 +1,123 @@ +@@ -0,0 +1,165 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2026 RK_01/RaphiMC and contributors @@ -36953,11 +36963,17 @@ index 00000000..51cdb2bf +import com.viaversion.viaversion.api.type.Types; +import com.viaversion.viaversion.libs.gson.JsonObject; +import com.viaversion.viaversion.libs.fastutil.ints.IntObjectPair; ++import com.viaversion.viaversion.libs.mcstructs.converter.impl.v1_21_5.NbtConverter_v1_21_5; ++import com.viaversion.viaversion.libs.mcstructs.dialog.body.PlainMessageBody; ++import com.viaversion.viaversion.libs.mcstructs.dialog.impl.MultiActionDialog; ++import com.viaversion.viaversion.libs.mcstructs.dialog.serializer.DialogSerializer; +import com.viaversion.viaversion.protocol.packet.PacketWrapperImpl; +import io.netty.buffer.Unpooled; +import net.lenni0451.mcstructs_bedrock.forms.serializer.FormSerializer; +import net.raphimc.viabedrock.api.model.NativeFormMessage; +import net.raphimc.viabedrock.api.model.container.ChestContainer; ++import net.raphimc.viabedrock.api.resourcepack.ResourcePack; ++import net.raphimc.viabedrock.api.resourcepack.content.InMemoryContent; +import net.raphimc.viabedrock.api.resourcepack.ui.ServerFormUi; +import net.raphimc.viabedrock.api.util.PacketFactory; +import net.raphimc.viabedrock.protocol.BedrockProtocol; @@ -36982,6 +36998,42 @@ index 00000000..51cdb2bf +class ModalFormTransitionTest { + + @Test ++ void actionDialogTranslatesButtonsWithTheSamePackAsItsTitleAndBody() throws Exception { ++ final var user = mock(UserConnection.class); ++ final var inventory = new InventoryTracker(user); ++ when(user.get(InventoryTracker.class)).thenReturn(inventory); ++ when(user.get(ChannelStorage.class)).thenReturn(new ChannelStorage()); ++ final var content = new InMemoryContent(); ++ content.putString("manifest.json", """ ++ {"format_version":2,"header":{"uuid":"11111111-1111-1111-1111-111111111111", ++ "version":[1,0,0],"name":"Dialog language probe"}} ++ """); ++ content.putString("texts/en_US.lang", "probe.title=Panel title\nprobe.body=Panel body\nprobe.choice=Translated choice\n"); ++ final var packs = new ResourcePackStorage(List.of(new ResourcePack(content))); ++ when(user.get(ResourcePackStorage.class)).thenReturn(packs); ++ ++ final var input = Unpooled.buffer(); ++ try (var packets = mockStatic(PacketFactory.class)) { ++ BedrockTypes.UNSIGNED_VAR_INT.writePrimitive(input, 17); ++ BedrockTypes.STRING.write(input, """ ++ {"type":"form","title":"%probe.title","content":"%probe.body", ++ "buttons":[{"text":"%probe.choice"},{"text":"Literal choice"}]} ++ """); ++ final var request = new PacketWrapperImpl(100, input, user); ++ InventoryPackets.handleModalFormRequest(request); ++ final var dialog = DialogSerializer.V1_21_6.getDirectCodec().deserialize( ++ NbtConverter_v1_21_5.INSTANCE, request.get(Types.TRUSTED_COMPOUND_TAG_HOLDER, 0).value()).get(); ++ assertEquals("Panel title", dialog.getTitle().asUnformattedString()); ++ assertEquals(List.of("Panel body"), dialog.getBody().stream() ++ .map(body -> ((PlainMessageBody) body).getContents().asUnformattedString()).toList()); ++ assertEquals(List.of("Translated choice", "Literal choice"), ((MultiActionDialog) dialog).getActions().stream() ++ .map(button -> button.getLabel().asUnformattedString()).toList()); ++ } finally { ++ input.release(); ++ } ++ } ++ ++ @Test + void serverFormReplacesContainerAndRetainsFormThroughCloseAcknowledgement() throws Exception { + final var user = mock(UserConnection.class); + final var storage = new HashMap, Object>(); diff --git a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md index e70fafff..24ce9155 100644 --- a/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md +++ b/patches/viabedrock/upstreamable/0095-integrate-authored-native-ui-presentation.pr.md @@ -26,4 +26,10 @@ Component defaults, tooltip selection, source timing, and complete installed pix Packet, menu-lifetime, and action tests cover supported adapters. Complete installed player/cooking controllers and live native parity remain unfinished. +Action-form dialog labels use the accepted pack translator, as the title and body already do. +`ModalFormTransitionTest` covers translated and literal buttons without authored JSON UI. +Native Bedrock 1.26.60.5 and BDS 1.26.60.6 establish the expected text with the same English language pack. +Rebuilt direct and ViaProxy clients show the translated button in Java dialogs and return original response indices 0 and 1. +This verifies the generic fallback defect; the original Lifeboat dialog report still needs a matching form capture. + The [owning patch](0095-integrate-authored-native-ui-presentation.patch) retains implementation rationale and versioned evidence. From 1c1eaf4c6792e1daca81e5c12c1307c6e50ecc15 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 09:53:24 +0100 Subject: [PATCH 29/47] chore(deps): refresh ViaProxy upstream pin Replay the complete patch stacks against the latest upstream branches. ViaProxy adds Thai translations; the other three target pins and successful ViaFabricPlus build 2295 remain current. Verify all 154 patches, the full build with 3326 passing tests and 325 skipped, offline checks, the bundle, and artifact checksums. --- targets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/targets.json b/targets.json index 7688b97b..0d56070e 100644 --- a/targets.json +++ b/targets.json @@ -38,7 +38,7 @@ "fork": "StackAnvil/ViaProxy", "prAssignees": ["RaphiMC"], "baseBranch": "main", - "baseSha": "2d578e951a65d7278134e350c734f76f2c985fa2", + "baseSha": "3e81750e3b1ab5bd84eaafa83e372c48f9fea200", "java": "25", "publication": "MavenJava", "buildTask": "build" From 70c653d7e358e2f19c73c6d44d46ab5b8cc68723 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 10:14:10 +0100 Subject: [PATCH 30/47] fix(camera): reset queued blends after clear Use the player camera as the next blend origin when clear and set commands arrive before one rendered frame. Retain the rendered origin for ordinary interrupted free-camera blends. Fold the fix and two frame regressions into the owning camera patch. Official Bedrock 1.26.60.5/BDS 1.26.60.6, installed direct, and ViaProxy controls establish the origin behavior. The full build passes 3328 tests with 325 skipped; exact frame timing and broader camera parity remain open. --- ...render-core-resolved-camera-movement.patch | 107 +++++++++++++++++- ...render-core-resolved-camera-movement.pr.md | 6 +- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.patch b/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.patch index c8e66dd8..13b3c44a 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.patch @@ -1,4 +1,4 @@ -From d35cf5dcdee30561fb4616d2e217dd66dc2cf449 Mon Sep 17 00:00:00 2001 +From 6f32f144b2f85a70fb69e481e9f02dc0ea6a7c8c 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(camera): render core-resolved server camera movement @@ -124,12 +124,28 @@ of the native identity is reported as actor loss. This lets the shared runtime retire failed fixed-boom attachments without treating delayed render metadata as removal, while preserving existing target-tracker behavior until its physical center is available. + +Reset the interrupted blend origin when a LOCAL clear snapshot is drained, +even if a later free-camera command is waiting in the same render frame. +Otherwise the queue retains the previous detached pose and the next eased +command never returns through the player camera. Keep ordinary interrupted +free-camera blends anchored to the rendered pose. + +Official Bedrock 1.26.60.5 with BDS 1.26.60.6 on protocol 2225 establishes +this reset in a colored height-band fixture. Two same-tick clear/set trials +move from player height through the intermediate bands to the detached +target height; the no-clear control stays at the detached height. Complete +native packet framing confirms separate CLEAR and eased SET instructions. +The installed pre-fix direct Java route stays at the old detached height. +A client-frame regression fails at that stale position before the fix and +passes afterward, alongside the no-clear blend control. These observations +establish the blend origin, not complete frame timing or pixel parity. --- .../bedrock/ViaFabricPlusBedrock.java | 44 +++++ .../camera/BedrockCameraAimAssist.java | 70 +++++++ .../bedrock/camera/BedrockCameraFade.java | 40 ++++ .../bedrock/camera/BedrockCameraFov.java | 68 +++++++ - .../bedrock/camera/BedrockCameraPosition.java | 180 ++++++++++++++++++ + .../bedrock/camera/BedrockCameraPosition.java | 183 ++++++++++++++++++ .../bedrock/camera/BedrockCameraShake.java | 49 +++++ .../bedrock/camera/BedrockCameraWorld.java | 95 +++++++++ .../camera/CameraAimAssistControlPayload.java | 22 +++ @@ -151,7 +167,8 @@ behavior until its physical center is available. .../settings/impl/BedrockSettings.java | 4 + .../viafabricplus-bedrock/lang/en_us.json | 1 + .../viafabricplus-bedrock.mixins.json | 8 +- - 26 files changed, 959 insertions(+), 1 deletion(-) + .../camera/BedrockCameraPositionTest.java | 68 +++++++ + 27 files changed, 1030 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/camera/BedrockCameraAimAssist.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/camera/BedrockCameraFade.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/camera/BedrockCameraFov.java @@ -173,6 +190,7 @@ behavior until its physical center is available. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockCameraShake.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockFollowInput.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/sound/MixinBedrockCameraListener.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/camera/BedrockCameraPositionTest.java diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java index 0b69037..6d7f012 100644 @@ -434,10 +452,10 @@ index 0000000..49fab8a +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/camera/BedrockCameraPosition.java b/src/main/java/com/viaversion/viafabricplus/bedrock/camera/BedrockCameraPosition.java new file mode 100644 -index 0000000..09af5ad +index 0000000..c76154a --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/camera/BedrockCameraPosition.java -@@ -0,0 +1,180 @@ +@@ -0,0 +1,183 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -556,7 +574,10 @@ index 0000000..09af5ad + while (!PENDING.isEmpty()) { + final CameraPositionState snapshot = PENDING.removeFirst(); + state = snapshot; -+ if (snapshot.mode() != Mode.LOCAL) { ++ if (snapshot.mode() == Mode.LOCAL) { ++ // Clear resets the next blend even when both commands arrive before this frame. ++ rendered = normal; ++ } else { + // A snapshot lacks the old client's interrupted frame. Restore its settled target, + // rather than replaying an expired or newly negotiated cutscene from a guessed pose. + final boolean continuous = snapshot.live() && snapshot.sequence() == appliedSequence + 1; @@ -1354,5 +1375,79 @@ index 549dc2b..605b96f 100644 ], "injectors": { "defaultRequire": 1 +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/camera/BedrockCameraPositionTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/camera/BedrockCameraPositionTest.java +new file mode 100644 +index 0000000..0836de0 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/camera/BedrockCameraPositionTest.java +@@ -0,0 +1,68 @@ ++/* ++ * 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.camera; ++ ++import net.minecraft.SharedConstants; ++import net.minecraft.client.Minecraft; ++import net.minecraft.server.Bootstrap; ++import net.raphimc.viabedrock.api.model.CameraEase; ++import net.raphimc.viabedrock.api.model.CameraPose; ++import net.raphimc.viabedrock.api.model.CameraPositionState; ++import net.raphimc.viabedrock.api.model.CameraPositionState.Mode; ++import org.junit.jupiter.api.AfterEach; ++import org.junit.jupiter.api.BeforeAll; ++import org.junit.jupiter.api.Test; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.mockito.Mockito.mock; ++import static org.mockito.Mockito.mockStatic; ++ ++final class BedrockCameraPositionTest { ++ private static final CameraPose PLAYER = CameraPose.fromDegrees(new CameraPose.Vector(.5F, 71.62F, .5F), 0, 0, 70, Mode.LOCAL.ordinal()); ++ private static final CameraPose.Vector OLD = new CameraPose.Vector(-7.5F, 74.5F, -3.5F); ++ private static final CameraPose.Vector TARGET = new CameraPose.Vector(8.5F, 74.5F, -3.5F); ++ ++ @BeforeAll ++ static void bootstrap() { ++ SharedConstants.tryDetectVersion(); ++ Bootstrap.bootStrap(); ++ } ++ ++ @AfterEach ++ void clear() { ++ BedrockCameraPosition.clear(); ++ } ++ ++ @Test ++ void clearBeforeAnotherCommandInTheSameFrameStartsItsBlendAtThePlayerCamera() { ++ try (final var singleton = mockStatic(Minecraft.class)) { ++ singleton.when(Minecraft::getInstance).thenReturn(mock(Minecraft.class)); ++ BedrockCameraPosition.accept(command(1, Mode.FREE, OLD, 0)); ++ assertEquals(OLD, BedrockCameraPosition.frame(PLAYER).position()); ++ ++ BedrockCameraPosition.accept(command(2, Mode.LOCAL, CameraPose.Vector.ZERO, 0)); ++ BedrockCameraPosition.accept(command(3, Mode.FREE, TARGET, 2)); ++ final CameraPose first = BedrockCameraPosition.frame(PLAYER); ++ assertEquals(PLAYER.position(), first.position()); ++ } ++ } ++ ++ @Test ++ void anotherFreeCommandWithoutClearStartsItsBlendAtTheRenderedCamera() { ++ try (final var singleton = mockStatic(Minecraft.class)) { ++ singleton.when(Minecraft::getInstance).thenReturn(mock(Minecraft.class)); ++ BedrockCameraPosition.accept(command(1, Mode.FREE, OLD, 0)); ++ assertEquals(OLD, BedrockCameraPosition.frame(PLAYER).position()); ++ ++ BedrockCameraPosition.accept(command(2, Mode.FREE, TARGET, 2)); ++ assertEquals(OLD, BedrockCameraPosition.frame(PLAYER).position()); ++ } ++ } ++ ++ private static CameraPositionState command(final long sequence, final Mode mode, final CameraPose.Vector position, final float duration) { ++ return new CameraPositionState(sequence, 0, true, mode, position, 0, 0, duration, CameraEase.LINEAR, true, false); ++ } ++} -- 2.56.0 diff --git a/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.pr.md index ac927fda..8a30813a 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0023-render-core-resolved-camera-movement.pr.md @@ -2,7 +2,7 @@ ## Purpose -Subscribe to ViaBedrock's versioned camera fade channel on direct and ViaProxy connections. Draw the supplied color and opacity timeline while preserving the HUD, including when the normal HUD is hidden. Render the remaining elapsed state after late registration instead of restarting it. +Render ViaBedrock's resolved camera poses, blends, follow targets, FOV, fades, and audio context on direct and ViaProxy connections. Preserve overlay ordering and elapsed state when listeners register late. ## Design constraints @@ -18,6 +18,10 @@ Prerequisites: Run the affected project checks and the specific reproduction described by this patch. +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 compare a detached-camera blend with a clear immediately followed by that blend. Two native clear/set trials start at player height; the ordinary blend retains the rendered detached height. Complete packet journals retain distinct CLEAR and SET instructions. The pre-fix installed direct route retains the stale detached height after clear. Two client-frame regressions cover the queued clear/set boundary and the ordinary blend. + +Rebuilt direct and ViaProxy clients each pass clear-only, ordinary blend, and two clear/set trials with verified installed JARs. The four-project build passes 3,328 tests with 325 skipped and no failures or errors; bundle and artifact checksums pass. + Timeline and selected native/replay controls cover supported commands. Complete transition timing, aim assistance, collision, and visual composition remain partial. The [owning patch](0023-render-core-resolved-camera-movement.patch) retains implementation rationale and versioned evidence. From 3f4e48839d1423293d46cb775164727429339dee Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 10:34:58 +0100 Subject: [PATCH 31/47] docs(rendering): record native enchanted shield controls Verify genuine Unbreaking I offhand shields on official Bedrock 1.26.60.5, direct Java, and ViaProxy against BDS 1.26.60.6. Record plain, enchanted, blocking, and plain-return observations with complete packet journals and verified installed JARs. The controls support visible glint and coarse pose behavior without justifying a renderer change. Retain exact lighting, phase, unmatched appearances, and the original oversized-shield and custom-skin reports as open limits. --- docs/skin-flow.md | 2 +- .../0017-render-equipped-native-pack-models.pr.md | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/skin-flow.md b/docs/skin-flow.md index e3faa938..f67ce23c 100644 --- a/docs/skin-flow.md +++ b/docs/skin-flow.md @@ -56,6 +56,6 @@ The owning patch notes describe their specific validation and limits: ## Remaining limits Other wardrobe categories, rapid profile changes, and distinct LOD texture selection still need matching fixtures. -Enchanted shields, complete first-person appearance, live mounts, remote skins, and motion timing remain partial or unverified. +Exact enchanted-shield lighting and phase, complete first-person appearance, live mounts, remote skins, and motion timing remain partial or unverified. Platform purchases, complete account synchronization, and broader emote parity remain open. Keep current status in the [compatibility page](bedrock-coverage.md), not a chronological skin journal. diff --git a/patches/viafabricplus-bedrock/upstreamable/0017-render-equipped-native-pack-models.pr.md b/patches/viafabricplus-bedrock/upstreamable/0017-render-equipped-native-pack-models.pr.md index 4870b346..3c217f34 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0017-render-equipped-native-pack-models.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0017-render-equipped-native-pack-models.pr.md @@ -12,6 +12,10 @@ Select attachables using the rendered owner, slot, item binding, world-version c Regression coverage includes `BedrockAttachableBindingsTest`, `BedrockAttachableScopeTest`, `BedrockAttachableSelectionTest`, `BedrockEquipmentResourcesTest`. -Native authored wings and equipment controls cover selected binding paths. Enchanted shield rendering, broader equipment lifecycles, and complete native appearance remain incomplete. +Native authored wings and equipment controls cover selected binding paths. Broader equipment lifecycles and complete native appearance remain incomplete. + +Bedrock 1.26.60.5 and BDS 1.26.60.6 controls on protocol 2225 cover plain and Unbreaking I offhand shields. Native, direct Java, and ViaProxy each receive the same enchantment NBT, record two blocking cycles, and return to a plain shield. Both Java routes use verified add-on JARs; the running proxy also matches its built JAR. + +Glint changes over time on idle and blocking shields. Returning to a plain shield removes it. Coarse shield-contour measurements above the HUD differ from native by less than 0.5% of the viewport. These measurements exclude the lower screen and do not establish pixel parity. The native and Java appearances were not matched. Exact lighting, glint phase, and the reported oversized shield and distorted custom-skin pose remain unverified. No renderer change follows from these controls. The [owning patch](0017-render-equipped-native-pack-models.patch) retains implementation rationale and versioned evidence. From 316bbea0dc329a089b86cc0aba1fdeaf9cc9524d Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 11:35:23 +0100 Subject: [PATCH 32/47] fix(rendering): preserve native player postures and anchors Emit explicit poses for remote player metadata after native flags arrive, without overwriting local prediction outside the existing crawling correction. Keep native animated bodies at their own crouch anchor by removing Java's additional offset while preserving scale, passenger offsets, and fallback behavior. Fold each change into its owning core and client patch. Matching slim/wide/slim protocol-2225 controls reproduce ignored crouching and the extra model drop, then verify the fixes on direct and ViaProxy clients against Bedrock 1.26.60.5. Five new regressions and the full build pass: 3333 tests passed, 325 skipped. Full body rotation and the original affected custom-skin report remain open. --- ...rse-properties-and-composed-metadata.patch | 83 +++++++++++-- ...rse-properties-and-composed-metadata.pr.md | 6 + ...re-native-actor-and-resource-runtime.patch | 115 +++++++++++++++++- ...re-native-actor-and-resource-runtime.pr.md | 6 + 4 files changed, 199 insertions(+), 11 deletions(-) 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 96f69641..1cda1c28 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 af61a3324c2b4756edd6c0739826ce2dccf395fe Mon Sep 17 00:00:00 2001 +From 6fdbf2ac49f8051d39622acfeffef60293135c86 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 @@ -136,10 +136,29 @@ Translate shulker dye variants from Bedrock black-to-white order into Java white-to-black order, preserving the undyed variant. The native 1.26.51.1 shulker render controller establishes all seventeen variant textures. Actual metadata rewrite tests cover each color and the undyed state. + +Emit explicit Java poses for remote player metadata updates without +requiring a previous crawling transition. Shared sneaking/swimming flags +do not change a remote Java avatar's pose, so native player animations can +remain standing despite the retained Bedrock posture. Resolve the pose +from both retained native flag words after the sparse update. Preserve +local prediction outside the existing crawling transition correction. +Do not synthesize posture for name-only updates before any native flag +word is known. + +A synthetic colored classic-skin peer on protocol 2225 demonstrates the +missing crouch with official Bedrock 1.26.60.5 and BDS 1.26.60.6. Native +crouches the peer for its sneaking flag while the installed pre-fix direct +Java client remains standing. The fixture switches slim/wide/slim geometry +without changing its limb colors. It isolates rendering and metadata; +it does not establish movement simulation, complete body rotation, or the +original local custom-skin report. Regressions cover remote sneak/stand, +sparse flag composition, other established posture mappings, and retaining +local prediction outside crawling transitions. --- .../viabedrock/api/model/entity/Entity.java | 51 ++ .../api/model/entity/LivingEntity.java | 4 +- - .../api/model/entity/PlayerEntity.java | 40 +- + .../api/model/entity/PlayerEntity.java | 43 +- .../entity/properties/EntityProperty.java | 20 +- .../entity/properties/EntityPropertyList.java | 98 ++- .../viabedrock/protocol/BedrockProtocol.java | 1 + @@ -151,7 +170,7 @@ Actual metadata rewrite tests cover each color and the undyed state. .../storage/EntityPropertyTracker.java | 22 +- .../entity_flag_molang_query_mappings.json | 6 +- .../entity/EntityPropertyRenderTest.java | 75 +++ - .../model/entity/PlayerCrawlingPoseTest.java | 132 ++++ + .../model/entity/PlayerCrawlingPoseTest.java | 175 +++++ .../properties/EntityPropertyListTest.java | 92 +++ .../enums/bedrock/ActorFlagsMappingTest.java | 27 + .../ArmorStandMarkerMetadataTest.java | 177 ++++++ @@ -159,7 +178,7 @@ Actual metadata rewrite tests cover each color and the undyed state. .../rewriter/EntityMetadataRewriterTest.java | 318 ++++++++++ .../rewriter/EntityNameFormattingTest.java | 256 ++++++++ .../rewriter/PotionActorMetadataTest.java | 107 ++++ - 22 files changed, 2158 insertions(+), 210 deletions(-) + 22 files changed, 2204 insertions(+), 210 deletions(-) create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/entity/EntityPropertyRenderTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/entity/PlayerCrawlingPoseTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/entity/properties/EntityPropertyListTest.java @@ -288,7 +307,7 @@ index d79489fc..78f5d211 100644 this.updateAttributes(attributes, updateAttributes); updateAttributes.send(BedrockProtocol.class); diff --git a/src/main/java/net/raphimc/viabedrock/api/model/entity/PlayerEntity.java b/src/main/java/net/raphimc/viabedrock/api/model/entity/PlayerEntity.java -index ef95055b..96856c42 100644 +index ef95055b..fe3fb21a 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/entity/PlayerEntity.java +++ b/src/main/java/net/raphimc/viabedrock/api/model/entity/PlayerEntity.java @@ -27,6 +27,8 @@ import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets @@ -331,7 +350,7 @@ index ef95055b..96856c42 100644 setPlayerTeam.write(Types.VAR_INT, TeamCollisionRule.NEVER.ordinal()); // collision rule setPlayerTeam.write(Types.BOOL_OPTIONAL_VAR_INT, null); // color setPlayerTeam.write(Types.BYTE, (byte) 3); // flags -@@ -114,6 +123,33 @@ public class PlayerEntity extends LivingEntity { +@@ -114,6 +123,36 @@ public class PlayerEntity extends LivingEntity { this.abilities = abilities; } @@ -348,7 +367,10 @@ index ef95055b..96856c42 100644 + protected void finishEntityDataUpdate(final List javaEntityData) { + final var flags = this.entityFlags(); + final boolean crawling = flags.contains(ActorFlags.CRAWLING); -+ if (crawling || this.crawling) { ++ final boolean nativeFlagsKnown = this.entityData.containsKey(ActorDataIds.RESERVED_0) ++ || this.entityData.containsKey(ActorDataIds.RESERVED_092); ++ if (!(this instanceof ClientPlayerEntity) && nativeFlagsKnown || crawling || this.crawling) { ++ // Remote avatars do not derive their pose from Java's shared flags. + // Java uses pose 3 for the short crawling body. Its swimming flag still + // comes only from native SWIMMING, which controls different physics. + final int pose = crawling || flags.contains(ActorFlags.SWIMMING) ? 3 @@ -1756,10 +1778,10 @@ index 00000000..642d1546 +} diff --git a/src/test/java/net/raphimc/viabedrock/api/model/entity/PlayerCrawlingPoseTest.java b/src/test/java/net/raphimc/viabedrock/api/model/entity/PlayerCrawlingPoseTest.java new file mode 100644 -index 00000000..b0ad3f0d +index 00000000..886bee60 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/api/model/entity/PlayerCrawlingPoseTest.java -@@ -0,0 +1,132 @@ +@@ -0,0 +1,175 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors @@ -1858,6 +1880,49 @@ index 00000000..b0ad3f0d + } + } + ++ @Test ++ void remoteSneakingChangesPoseAndSurvivesAnUnrelatedFlagWordUpdate() { ++ final PlayerEntity player = player(); ++ assertEquals(5, value(update(player, flags(ActorDataIds.RESERVED_0, ActorFlags.SNEAKING)), EntityDataFields.POSE)); ++ assertEquals(5, value(update(player, flags(ActorDataIds.RESERVED_092)), EntityDataFields.POSE)); ++ assertEquals(0, value(update(player, flags(ActorDataIds.RESERVED_0)), EntityDataFields.POSE)); ++ } ++ ++ @Test ++ void remotePosturesDoNotRequireAnEarlierCrawlingTransition() { ++ final ActorFlags[] flags = {ActorFlags.SWIMMING, ActorFlags.GLIDING, ActorFlags.SLEEPING}; ++ final int[] poses = {3, 1, 2}; ++ final PlayerEntity player = player(); ++ for (int i = 0; i < flags.length; i++) { ++ final EntityData low = flags(ActorDataIds.RESERVED_0, flags[i].getValue() < Long.SIZE ? new ActorFlags[]{flags[i]} : new ActorFlags[0]); ++ final EntityData high = flags(ActorDataIds.RESERVED_092, flags[i].getValue() >= Long.SIZE ? new ActorFlags[]{flags[i]} : new ActorFlags[0]); ++ assertEquals(poses[i], value(update(player, low, high), EntityDataFields.POSE)); ++ assertEquals(0, value(update(player, flags(ActorDataIds.RESERVED_0), flags(ActorDataIds.RESERVED_092)), EntityDataFields.POSE)); ++ } ++ } ++ ++ @Test ++ void localPredictedPoseIsNotOverwrittenOutsideTheExistingCrawlingTransition() { ++ final UserConnection user = (UserConnection) Proxy.newProxyInstance(UserConnection.class.getClassLoader(), ++ new Class[]{UserConnection.class}, (proxy, method, args) -> null); ++ final var player = new ClientPlayerEntity(user, 1, new UUID(0, 1), new PlayerAbilities(1, (byte) 0, (byte) 0)); ++ player.entityData().put(ActorDataIds.RESERVED_0, flags(ActorDataIds.RESERVED_0, ActorFlags.SNEAKING)); ++ final List result = new ArrayList<>(); ++ player.finishEntityDataUpdate(result); ++ assertTrue(result.isEmpty()); ++ ++ player.entityData().put(ActorDataIds.RESERVED_092, flags(ActorDataIds.RESERVED_092, ActorFlags.CRAWLING)); ++ player.finishEntityDataUpdate(result); ++ assertEquals(3, value(result, EntityDataFields.POSE)); ++ result.clear(); ++ player.entityData().put(ActorDataIds.RESERVED_092, flags(ActorDataIds.RESERVED_092)); ++ player.finishEntityDataUpdate(result); ++ assertEquals(5, value(result, EntityDataFields.POSE)); ++ result.clear(); ++ player.finishEntityDataUpdate(result); ++ assertTrue(result.isEmpty()); ++ } ++ + private static PlayerEntity player() { + final UserConnection user = (UserConnection) Proxy.newProxyInstance(UserConnection.class.getClassLoader(), + new Class[]{UserConnection.class}, (proxy, method, args) -> null); diff --git a/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.pr.md b/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.pr.md index e06eb9e6..01b06a2e 100644 --- a/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.pr.md +++ b/patches/viabedrock/upstreamable/0002-preserve-sparse-properties-and-composed-metadata.pr.md @@ -16,6 +16,12 @@ Prerequisites: Regression coverage includes `EntityPropertyRenderTest`, `PlayerCrawlingPoseTest`, `EntityPropertyListTest`, `ActorFlagsMappingTest`. +Remote player updates emit their resolved Java pose even without an earlier crawling transition. Name-only updates do not synthesize posture before native flags arrive. The local player retains prediction outside the existing crawling correction. Regressions cover sneak/stand, sparse flag words, other established posture mappings, and local ownership. + +A synthetic colored classic-skin peer uses Bedrock 1.26.60.5/BDS 1.26.60.6 on protocol 2225. Native crouches when its sneaking flag changes; the pre-fix direct Java client remains standing. Slim/wide/slim skin replacements isolate posture from texture changes. These controls do not establish movement simulation, complete body rotation, or the original local custom-skin report. + +Rebuilt direct and ViaProxy controls restore crouching and the native foot anchor with identical skin and posture inputs. The combined four-project build passes 3,333 tests with 325 skipped and no failures or errors. Full stack replay, bundle, artifact checksums, and embedded core-class comparisons pass. + Property, pose, scale, and metadata regressions cover selected actors. Full native appearance and interaction parity require separate comparisons. The [owning patch](0002-preserve-sparse-properties-and-composed-metadata.patch) retains implementation rationale and versioned evidence. 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 21e472d4..962e608a 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 357c77aad1b996db375932d8e50869f1748e9522 Mon Sep 17 00:00:00 2001 +From ead24dedecf1aa7c7646c881770917a856ee017c 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 @@ -443,6 +443,20 @@ 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. + +Keep the authored native hierarchy's crouch anchor instead of adding +Java's separate two-pixel crouch render offset. Compensate only while an +animation pose or explicit visibility selects the native tree. Preserve +passenger offsets, scale, and the ordinary Java fallback. + +A protocol-2225 synthetic remote peer with a colored classic skin on +Bedrock 1.26.60.5/BDS 1.26.60.6 retains its foot anchor through standing +and crouching. After explicit remote posture metadata is restored in +core, the installed Java route crouches but drops its entire model by +Java's extra offset. A real-renderer regression reproduces that drop at +non-unit scale and verifies native animated/visibility-only trees and +fallback ownership. This establishes root anchoring, not complete body +rotation, movement timing, or the original custom-skin report. --- .gitignore | 4 +- build.gradle.kts | 10 + @@ -558,6 +572,7 @@ expression's recovery or complete public-server actor visual parity. .../render/BedrockPlayerAnimationState.java | 21 + .../render/BedrockPlayerBodyRotation.java | 35 + .../bedrock/render/BedrockPlayerModel.java | 53 + + .../bedrock/render/BedrockPlayerRenderer.java | 12 + .../bedrock/render/BedrockViewBobbing.java | 51 + .../render/BedrockWalkingAnimation.java | 35 + .../render/NativeActorAttackPayload.java | 24 + @@ -651,10 +666,11 @@ expression's recovery or complete public-server actor visual parity. .../render/BedrockModelTransformsTest.java | 97 ++ .../render/BedrockPlayerBodyRotationTest.java | 53 + .../render/BedrockPlayerModelTest.java | 145 +++ + .../render/BedrockPlayerRenderOffsetTest.java | 58 + .../render/BedrockViewBobbingTest.java | 93 ++ .../render/BedrockWalkingAnimationTest.java | 65 + .../render/NativeActorColorPayloadTest.java | 50 + - 210 files changed, 25065 insertions(+), 16 deletions(-) + 212 files changed, 25135 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 @@ -852,6 +868,7 @@ expression's recovery or complete public-server actor visual parity. create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockModelTransformsTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerBodyRotationTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerModelTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderOffsetTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockViewBobbingTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockWalkingAnimationTest.java create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/render/NativeActorColorPayloadTest.java @@ -14052,6 +14069,36 @@ index 0000000..4bedd7f + this.children.put("animated", this.animated.model().root()); + } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java +index a1e0f55..a4e6f37 100644 +--- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java +@@ -32,6 +32,7 @@ import net.minecraft.client.renderer.entity.state.AvatarRenderState; + import net.minecraft.client.renderer.rendertype.RenderType; + import net.minecraft.client.renderer.texture.OverlayTexture; + import net.minecraft.resources.Identifier; ++import net.minecraft.world.phys.Vec3; + + final class BedrockPlayerRenderer extends AvatarRenderer { + +@@ -61,6 +62,17 @@ final class BedrockPlayerRenderer extends AvatarRenderer { + return visible && !translucent && !state.isSpectator; + } + ++ @Override ++ public Vec3 getRenderOffset(final AvatarRenderState state) { ++ final Vec3 offset = super.getRenderOffset(state); ++ if (state.isCrouching && state instanceof BedrockPlayerAnimationState nativeState ++ && (!nativeState.bedrockAnimationPose().isEmpty() || nativeState.bedrockBodyVisibility().specified())) { ++ // The native hierarchy already supplies its crouch posture and root position. ++ return offset.add(0, state.scale * 2D / 16D, 0); ++ } ++ return offset; ++ } ++ + @Override + public void renderRightHand(final PoseStack poseStack, final SubmitNodeCollector collector, final int packedLight, + final Identifier skinTexture, final boolean hasSleeve) { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockViewBobbing.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockViewBobbing.java new file mode 100644 index 0000000..389807a @@ -27143,6 +27190,70 @@ index 0000000..166bca7 + } + } +} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderOffsetTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderOffsetTest.java +new file mode 100644 +index 0000000..2103746 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderOffsetTest.java +@@ -0,0 +1,58 @@ ++/* ++ * 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.render; ++ ++import com.viaversion.viafabricplus.bedrock.appearance.BedrockActorAnimation; ++import com.viaversion.viafabricplus.bedrock.appearance.BedrockBoneAnimation; ++import net.minecraft.client.renderer.entity.state.AvatarRenderState; ++import net.minecraft.world.phys.Vec3; ++import org.junit.jupiter.api.Test; ++ ++import java.util.List; ++import java.util.Map; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.mockito.Mockito.CALLS_REAL_METHODS; ++import static org.mockito.Mockito.mock; ++ ++final class BedrockPlayerRenderOffsetTest { ++ @Test ++ void nativeCrouchingKeepsThePassengerOffsetForAnimatedAndVisibilityOnlyBodies() { ++ final var renderer = mock(BedrockPlayerRenderer.class, CALLS_REAL_METHODS); ++ for (final boolean visibilityOnly : new boolean[]{false, true}) { ++ final var state = new State(); ++ state.isCrouching = true; ++ state.scale = 2; ++ state.passengerOffset = new Vec3(1, 2, 3); ++ if (visibilityOnly) state.visibility = new BedrockActorAnimation.Visibility(true, List.of()); ++ else state.pose = Map.of("body", new BedrockBoneAnimation.Transform(new BedrockBoneAnimation.Vector(0, 0, 0), ++ new BedrockBoneAnimation.Vector(45, 0, 0), new BedrockBoneAnimation.Vector(1, 1, 1), false)); ++ assertEquals(state.passengerOffset, renderer.getRenderOffset(state)); ++ } ++ } ++ ++ @Test ++ void fallbackKeepsTheJavaCrouchOffsetAndStandingKeepsThePassengerOffset() { ++ final var renderer = mock(BedrockPlayerRenderer.class, CALLS_REAL_METHODS); ++ final var state = new State(); ++ state.scale = 2; ++ state.passengerOffset = new Vec3(1, 2, 3); ++ state.isCrouching = true; ++ assertEquals(new Vec3(1, 1.75, 3), renderer.getRenderOffset(state)); ++ state.isCrouching = false; ++ assertEquals(state.passengerOffset, renderer.getRenderOffset(state)); ++ } ++ ++ private static final class State extends AvatarRenderState implements BedrockPlayerAnimationState { ++ private Map pose = Map.of(); ++ private BedrockActorAnimation.Visibility visibility = BedrockActorAnimation.Visibility.UNSPECIFIED; ++ ++ @Override public BedrockActorAnimation.Visibility bedrockBodyVisibility() { return this.visibility; } ++ @Override public void bedrockBodyVisibility(final BedrockActorAnimation.Visibility visibility) { this.visibility = visibility; } ++ @Override public Map bedrockAnimationPose() { return this.pose; } ++ @Override public void bedrockAnimationPose(final Map pose) { this.pose = pose; } ++ } ++} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockViewBobbingTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockViewBobbingTest.java new file mode 100644 index 0000000..0c92c0e 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 6f757570..71e98322 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 @@ -26,6 +26,12 @@ Prerequisites: Regression coverage includes `BedrockActorAnimationTest`, `BedrockActorQueriesTest`, `BedrockActorRenderSelectionTest`, `BedrockActorScaleTest`, and `BedrockActorPickingStateTest`. +Native player hierarchies retain their authored crouch anchor instead of receiving Java's additional two-pixel render offset. Animated and visibility-only native trees share this rule. Passenger offsets, scale, and the Java fallback remain intact. + +A colored slim/wide/slim remote-peer fixture uses Bedrock 1.26.60.5/BDS 1.26.60.6 on protocol 2225. After core restores explicit remote posture metadata, the pre-fix Java renderer crouches but drops its foot anchor. The real-renderer regression fails at that extra offset and passes after compensation. The fixture isolates metadata and rendering; complete body rotation and the original local custom-skin report remain open. + +Rebuilt direct and ViaProxy controls restore crouching and the native foot anchor with identical skin and posture inputs. The combined four-project build passes 3,333 tests with 325 skipped and no failures or errors. Full stack replay, bundle, artifact checksums, and embedded core-class comparisons pass. + Native current/older/current cat and audio controls establish selected resource paths. Complete pose alignment, audio, particle, query, and visual parity remain unfinished. Three native 26.60 non-cardinal picking controls establish the camera ray calculation. From b0674e9c9fd896a254c13cdb0d4c872b50d6ccf1 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 11:54:19 +0100 Subject: [PATCH 33/47] chore(deps): rebase Bedrock add-on onto upstream Replay the client stack on cef69c4, retaining upstream's newer NetherNet and libdatachannel dependencies alongside the existing local catalog changes. Leave the other project and ViaFabricPlus pins at their current upstream tips. Verify all 154 patches through the four-project build, the standalone first client patch, bundle generation, replay self-test, and artifact checksums. The build passes 3,333 tests with 325 skipped. --- .../upstreamable/0001-social-party-realms.patch | 8 ++++---- targets.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/patches/viafabricplus-bedrock/upstreamable/0001-social-party-realms.patch b/patches/viafabricplus-bedrock/upstreamable/0001-social-party-realms.patch index 4745316b..69d42589 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0001-social-party-realms.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0001-social-party-realms.patch @@ -1,4 +1,4 @@ -From 9b3efac0b6dd0dc450b29876779ed0f50864dd70 Mon Sep 17 00:00:00 2001 +From c93e143d3a76ec75449ec230ef18b79b7b5a02a4 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(bedrock): add social, party, and Realm Hub support @@ -275,7 +275,7 @@ index a38c2db..6936fa8 100644 + useJUnitPlatform() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml -index f3ca449..1fa6908 100644 +index dea5855..316622b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,8 @@ fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-l @@ -286,9 +286,9 @@ index f3ca449..1fa6908 100644 -minecraftauth = { module = "net.raphimc:MinecraftAuth", version = "5.0.2" } +viabedrock = { module = "net.raphimc:ViaBedrock", version = "0.0.32-SNAPSHOT" } +minecraftauth = { module = "net.raphimc:MinecraftAuth", version = "5.0.3-SNAPSHOT" } - netty-transport-nethernet = { module = "org.cloudburstmc.netty:netty-transport-nethernet", version = "2.0.0.CR4-20261001.235918-13" } + netty-transport-nethernet = { module = "org.cloudburstmc.netty:netty-transport-nethernet", version = "2.0.0.CR6-20261030.131938-23" } +netty-transport-raknet = { module = "dev.kastle.netty:netty-transport-raknet", version = "1.7.0" } - libdatachannel-java-arch-detect = { module = "dev.opencollab:libdatachannel-java-arch-detect", version = "0.24.5.0-20260921.140330-12" } + libdatachannel-java-arch-detect = { module = "dev.opencollab:libdatachannel-java-arch-detect", version = "0.24.6.0-20261029.091642-10" } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockFriendsService.java b/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockFriendsService.java index 1ddd9d4..24560dc 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/features/friends/BedrockFriendsService.java diff --git a/targets.json b/targets.json index 0d56070e..345d7326 100644 --- a/targets.json +++ b/targets.json @@ -27,7 +27,7 @@ "fork": "StackAnvil/viafabricplus-bedrock", "prAssignees": ["RaphiMC", "Exterminate5573", "florianreuth"], "baseBranch": "main", - "baseSha": "6c11a7edc29d766cd783e8ac7c9b14133155b4c9", + "baseSha": "cef69c4d1edb45ed78c4df137f2e27c42003d6c7", "java": "25", "publication": "Maven", "buildTask": "build" From be5a483a613668a72b1d121513da3e1efbace88b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:18:59 +0100 Subject: [PATCH 34/47] fix(render): seed native remote player body rotation Start a fresh native remote player's body control from zero instead of Java's spawn yaw. Reset the previous angle with that seed and retain established rotation when native state arrives after Java control starts. Fold the change into the shared actor runtime patch. Compare six fresh spawn identities against official Bedrock 1.26.60.5 on protocol 2225. Rebuilt direct and ViaProxy routes restore the observed body orientation with matching skin and spawn inputs. Add regressions for signed spawn angles and late native state. The full build passes 3,335 tests with 325 skipped; bundle and artifact checks also pass. --- ...re-native-actor-and-resource-runtime.patch | 87 ++++++++++++++++--- ...re-native-actor-and-resource-runtime.pr.md | 8 +- 2 files changed, 79 insertions(+), 16 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 962e608a..23238aa2 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 ead24dedecf1aa7c7646c881770917a856ee017c Mon Sep 17 00:00:00 2001 +From 42ee24005f515c2e7608ca92bb4ba8198a047d77 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 @@ -46,6 +46,24 @@ position subtraction and use the existing translated swing state. These checks establish the body calculation and client binding, not full network interpolation, swing timing or visible native scene parity. +Seed a fresh native remote player body from zero on its first control +tick. Java initializes body rotation from the spawn yaw, which prevents +the stationary native soft limit from reaching the observed offset. +Retain the existing body when native state arrives after Java control +has started. Scope initialization to the entity instance, and reset its +previous body angle with the initial seed to avoid interpolating from +the Java spawn value. + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 spawn +six fresh player identities at yaw 0, 180, 90, -90, -180 and 0. Each uses +the same slim skin, position, dimensions and camera. Native nonzero-yaw +spawns retain an offset between body and head. The installed pre-fix +Java client instead keeps them aligned, with native state present from +ticks zero or one. Strict journal decoding verifies the fresh identities, +skin bytes, spawn transforms, metadata and removal order. These controls +support the initial zero body seed, not a change to the existing turn +formula or complete network interpolation parity. + Read every weighted alias in an animation-reference object. The licensed 1.26.51.1 camel controller groups adult and baby animation aliases in a single object; rejecting objects with multiple members prevents its @@ -537,7 +555,7 @@ rotation, movement timing, or the original custom-skin report. .../render/MixinAvatarAnimationState.java | 40 + .../MixinBedrockModelPartTransforms.java | 34 + .../render/MixinBedrockWalkingAnimation.java | 35 + - .../render/MixinLivingEntityBodyRotation.java | 28 + + .../render/MixinLivingEntityBodyRotation.java | 37 + .../render/MixinLivingEntityRenderer.java | 26 + .../render/MixinNativeActorAttack.java | 29 + .../render/MixinNativeActorPickingEntity.java | 58 + @@ -570,7 +588,7 @@ rotation, movement timing, or the original custom-skin report. .../bedrock/render/BedrockModelLocators.java | 122 ++ .../render/BedrockModelTransforms.java | 82 ++ .../render/BedrockPlayerAnimationState.java | 21 + - .../render/BedrockPlayerBodyRotation.java | 35 + + .../render/BedrockPlayerBodyRotation.java | 43 + .../bedrock/render/BedrockPlayerModel.java | 53 + .../bedrock/render/BedrockPlayerRenderer.java | 12 + .../bedrock/render/BedrockViewBobbing.java | 51 + @@ -664,13 +682,13 @@ rotation, movement timing, or the original custom-skin report. .../render/BedrockMaterialGeometryTest.java | 87 ++ .../render/BedrockModelLocatorsTest.java | 112 ++ .../render/BedrockModelTransformsTest.java | 97 ++ - .../render/BedrockPlayerBodyRotationTest.java | 53 + + .../render/BedrockPlayerBodyRotationTest.java | 77 ++ .../render/BedrockPlayerModelTest.java | 145 +++ .../render/BedrockPlayerRenderOffsetTest.java | 58 + .../render/BedrockViewBobbingTest.java | 93 ++ .../render/BedrockWalkingAnimationTest.java | 65 + .../render/NativeActorColorPayloadTest.java | 50 + - 212 files changed, 25135 insertions(+), 16 deletions(-) + 212 files changed, 25176 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 @@ -12023,10 +12041,10 @@ index 0000000..95fa67a +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinLivingEntityBodyRotation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinLivingEntityBodyRotation.java new file mode 100644 -index 0000000..32cc817 +index 0000000..89c5db6 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinLivingEntityBodyRotation.java -@@ -0,0 +1,28 @@ +@@ -0,0 +1,37 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -12039,16 +12057,25 @@ index 0000000..32cc817 +import net.minecraft.client.player.RemotePlayer; +import net.minecraft.world.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntity.class) +public abstract class MixinLivingEntityBodyRotation { ++ @Unique ++ private BedrockPlayerBodyRotation nativeBodyRotation; ++ + @Inject(method = "tickHeadTurn", at = @At("HEAD"), cancellable = true) + private void nativePlayerBodyRotation(final float javaTarget, final CallbackInfo ci) { -+ if ((Object) this instanceof RemotePlayer player && BedrockActorState.state(player.getUUID()) != null) { -+ player.yBodyRot = BedrockPlayerBodyRotation.tick(player.yBodyRot, player.getYRot(), ++ if (!((Object) this instanceof RemotePlayer player)) return; ++ if (this.nativeBodyRotation == null) this.nativeBodyRotation = new BedrockPlayerBodyRotation(); ++ final boolean nativeActor = BedrockActorState.state(player.getUUID()) != null; ++ final float body = this.nativeBodyRotation.beginTick(nativeActor, player.yBodyRot); ++ if (nativeActor) { ++ if (body != player.yBodyRot) player.yBodyRotO = body; ++ player.yBodyRot = BedrockPlayerBodyRotation.tick(body, player.getYRot(), + (float) player.getX() - (float) player.xo, (float) player.getZ() - (float) player.zo, + player.getSwingAnimation(1F) > 0F, player.isPassenger()); + ci.cancel(); @@ -13971,10 +13998,10 @@ index 0000000..f10740a +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerBodyRotation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerBodyRotation.java new file mode 100644 -index 0000000..56d63f5 +index 0000000..be56b68 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerBodyRotation.java -@@ -0,0 +1,35 @@ +@@ -0,0 +1,43 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -13984,7 +14011,15 @@ index 0000000..56d63f5 + +/** The player body control uses different turn limits from the generic mob control. */ +public final class BedrockPlayerBodyRotation { -+ private BedrockPlayerBodyRotation() {} ++ private boolean ticked; ++ ++ public float beginTick(final boolean nativeActor, final float body) { ++ final boolean firstTick = !this.ticked; ++ this.ticked = true; ++ // Native players start body control at zero rather than inheriting their spawn yaw. ++ // A late native snapshot must preserve rotation already established by Java. ++ return firstTick && nativeActor ? 0F : body; ++ } + + public static float tick(final float body, final float yaw, final float dx, final float dz, + final boolean attacking, final boolean passenger) { @@ -26982,10 +27017,10 @@ index 0000000..51be474 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerBodyRotationTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerBodyRotationTest.java new file mode 100644 -index 0000000..cd7051b +index 0000000..45ef342 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerBodyRotationTest.java -@@ -0,0 +1,53 @@ +@@ -0,0 +1,77 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -27004,6 +27039,30 @@ index 0000000..cd7051b + +final class BedrockPlayerBodyRotationTest { + @Test ++ void settlesFreshNativePlayersFromZeroInsteadOfTheirSpawnYaw() { ++ final float[][] cases = {{0F, 0F}, {180F, -130F}, {90F, 40F}, {-90F, -40F}, {-180F, -130F}}; ++ for (final float[] value : cases) { ++ final var control = new BedrockPlayerBodyRotation(); ++ float body = value[0]; ++ for (int tick = 0; tick < 25; tick++) { ++ body = BedrockPlayerBodyRotation.tick(control.beginTick(true, body), value[0], 0F, 0F, false, false); ++ } ++ assertEquals(value[1], body); ++ } ++ } ++ ++ @Test ++ void preservesEstablishedRotationWhenNativeStateArrivesLate() { ++ final var control = new BedrockPlayerBodyRotation(); ++ assertEquals(135F, control.beginTick(false, 135F)); ++ assertEquals(120F, control.beginTick(false, 120F)); ++ float body = BedrockPlayerBodyRotation.tick(control.beginTick(true, 120F), 135F, 0F, 0F, false, false); ++ assertEquals(120F, body); ++ body = BedrockPlayerBodyRotation.tick(control.beginTick(true, body), 135F, .1F, 0F, false, false); ++ assertEquals(165F, body); ++ } ++ ++ @Test + void appliesThePlayerSoftLimitAfterAStandingHalfTurn() { + float body = -45F; + body = BedrockPlayerBodyRotation.tick(body, 135F, 0F, 0F, false, false); 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 71e98322..548c12bf 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 @@ -28,9 +28,13 @@ Regression coverage includes `BedrockActorAnimationTest`, `BedrockActorQueriesTe Native player hierarchies retain their authored crouch anchor instead of receiving Java's additional two-pixel render offset. Animated and visibility-only native trees share this rule. Passenger offsets, scale, and the Java fallback remain intact. -A colored slim/wide/slim remote-peer fixture uses Bedrock 1.26.60.5/BDS 1.26.60.6 on protocol 2225. After core restores explicit remote posture metadata, the pre-fix Java renderer crouches but drops its foot anchor. The real-renderer regression fails at that extra offset and passes after compensation. The fixture isolates metadata and rendering; complete body rotation and the original local custom-skin report remain open. +A colored slim/wide/slim remote-peer fixture uses Bedrock 1.26.60.5/BDS 1.26.60.6 on protocol 2225. After core restores explicit remote posture metadata, the pre-fix Java renderer crouches but drops its foot anchor. The real-renderer regression fails at that extra offset and passes after compensation. The fixture isolates metadata and rendering. Broader body rotation and the original local custom-skin report remain open. -Rebuilt direct and ViaProxy controls restore crouching and the native foot anchor with identical skin and posture inputs. The combined four-project build passes 3,333 tests with 325 skipped and no failures or errors. Full stack replay, bundle, artifact checksums, and embedded core-class comparisons pass. +Fresh spawn controls use the same slim skin with six new player identities at yaw 0, 180, 90, -90, -180, and 0. Official Bedrock 1.26.60.5/BDS 1.26.60.6 retains an angled body at nonzero yaw. The pre-fix Java client instead aligns body and head, despite native state present at ticks zero or one. A zero initial body seed restores the observed result on rebuilt direct and ViaProxy routes. Established Java rotation remains intact if native state arrives late. + +Strict protocol-2225 decoders verify matching skin bytes, spawn transforms, dimensions, fresh identities, removal order, and FOV60. Six marker projections per phase use three frames each. Both fixed routes have median marker centroids within 0.17% of each viewport axis and bounding coordinates within 0.42% of native. These stationary, floating peers establish selected spawn posture. Movement interpolation, idle-animation phase, lighting, and complete pose parity remain unverified. Two regressions cover signed spawn angles through settling and late native state. All 33 recorded process identities are gone. Cleanup restored the native profile, trust setting, external server lists, and owned server configuration exactly. + +Rebuilt direct and ViaProxy controls restore crouching and the native foot anchor with identical skin and posture inputs. The combined four-project build passes 3,335 tests with 325 skipped and no failures or errors. Full stack replay, bundle, artifact checksums, and embedded core-class comparisons pass. Native current/older/current cat and audio controls establish selected resource paths. Complete pose alignment, audio, particle, query, and visual parity remain unfinished. From 786943f3248d16204a3505123eb2aaa3ebdcf105 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:46:43 +0100 Subject: [PATCH 35/47] docs(render): record native turn and movement controls Record matching same-identity yaw, pitch-only, and movement controls on official Bedrock 1.26.60.5 and the current direct and ViaProxy clients. Keep the working rotation formula and packet semantics unchanged. Scope the evidence to settled poses and endpoints, exclude the obstructed tutorial-toast frames, and retain interpolation and mounted-behavior limits. Replace outdated build totals in the head-packet PR context. --- ...ad-rotation-in-java-26-3-field-order.pr.md | 19 ++++++------------- ...re-native-actor-and-resource-runtime.pr.md | 6 ++++++ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/patches/viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md b/patches/viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md index 6d278fed..91066440 100644 --- a/patches/viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md +++ b/patches/viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md @@ -2,21 +2,14 @@ Bedrock head-only movement produced corrupted Java player rotations. Java 26.3 reads the on-ground boolean before yaw and pitch. ViaBedrock wrote it after both angles. -In a recorded Hive scene, the gray seated player’s source pitch became its Java yaw. Its ground flag became a 1.40625-degree pitch. This explains a concrete pose error without changing the skin mesh. +The patch corrects the Java packet field order and preserves the Bedrock state handling. Two registered-handler cases serialize the output and decode the current Java wire layout. Both ground-state cases pass in the current verified build. -The patch corrects only the Java packet field order. It preserves the existing Bedrock head-only movement state handling. The native head/body application and default skin animation graph need separate evidence. +## Evidence and remaining limits -## Evidence and validation +The earlier Hive control uses Bedrock 1.26.51.1 on protocol 2193. The gray seated player's source pitch became its Java yaw, and its ground flag became a 1.40625-degree pitch. A fixed local replay restores the expected draw-call angles within the existing byte-angle quantization. This evidence establishes the wire-order error without a skin mesh change. -- Inspected the mapped Java 26.3 rotation packet reader and writer. -- Observed the actual entity and avatar render state for three seated Hive players. -- Both parameterized packet cases failed before the change and passed afterward. They exercise the registered Bedrock handler, serialize its Java output, and decode the current wire layout. -- The rotation and movement tests pass: three cases, no failures or skips. Main and test Checkstyle checks pass. +Current controls use official Bedrock 1.26.60.5/BDS 1.26.60.6 on protocol 2225. After a same-identity yaw cycle, a mode-3 packet supplies a 22.5-degree pitch, conflicting yaw and head yaw, and a different position. Native applies the pitch while retaining the earlier position and yaw. The current direct and ViaProxy clients reproduce this selected result. Their runtime audits retain position, actor yaw, and head yaw while pitch changes to 22.5 degrees. A normal packet restores the level head pose. -The native reference uses Bedrock 1.26.51.1, protocol 2193. Private captures, client assets, and runtime traces remain outside Git. +Strict decoders verify identical source spawn and movement payloads across the routes. The accepted Java fixtures disable their onboarding tutorial before observations. The [shared player-runtime controls](../../viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md) describe the complete sequence, marker measurements, and cleanup. -The full exported stack builds successfully: 11 converter tests, 333 core tests, and 447 add-on tests. All 791 cases pass with no failures, errors, or skips. The Prism bundle also builds. The core patch is placed before deferred patches; the metadata patch changes only its regenerated Git blob indexes. - -The fixed local Hive replay confirms the correction at the actual draw calls. The gray seated player now has Java pitch 23.90625 degrees and yaw 125.15625 degrees. Previously these were 1.40625 and 23.90625. Its relative head yaw changes from 76.75 to about -6.73 degrees. The yellow player’s pitch and yaw also decode correctly; the fully synchronized red player is unchanged. The remaining difference from the captured pitch is the existing byte-angle quantization. - -The replay accepts all 49 native skins with no rejected appearances. Retained player and chair metadata contain no seat rotation bounds in fields 57–60. All five artifact hashes stay unchanged through the replay, and the owned client, replay server, display, and audio guards are cleaned up. This validates the wire fix; it does not establish complete default skin animation parity. +This stationary, unmounted control supports the retained pitch-only handling for the selected inputs. Mounted head/body rules, native ground-state parity, interpolation timing, and complete skin animation parity remain open. Private captures, client assets, and runtime traces stay outside Git. No packet-semantic change follows from these controls. 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 548c12bf..d613995e 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 @@ -34,6 +34,12 @@ Fresh spawn controls use the same slim skin with six new player identities at ya Strict protocol-2225 decoders verify matching skin bytes, spawn transforms, dimensions, fresh identities, removal order, and FOV60. Six marker projections per phase use three frames each. Both fixed routes have median marker centroids within 0.17% of each viewport axis and bounding coordinates within 0.42% of native. These stationary, floating peers establish selected spawn posture. Movement interpolation, idle-animation phase, lighting, and complete pose parity remain unverified. Two regressions cover signed spawn angles through settling and late native state. All 33 recorded process identities are gone. Cleanup restored the native profile, trust setting, external server lists, and owned server configuration exactly. +Same-identity controls extend the fixture through a full yaw cycle, a pitch-only update, and east/west/center movement. Native and current direct/proxy routes retain the turn history and restore the center position. The camera and skin stay constant. + +Strict protocol-2225 decoders verify one spawn and identical 71 MovePlayer payloads per route. Three settled frames per phase compare six markers. Across 11 phases, both routes have median marker centroids within 0.30% of each viewport axis and bounding coordinates within 0.42% of native. The accepted Java fixtures disable their onboarding tutorial through its public API before observations. An earlier obstructed westward frame does not support pose comparison. All 33 recorded process identities are gone, and cleanup restored the native profile and server configuration exactly. + +These controls establish selected settled poses and position endpoints. Exact interpolation timing, movement curves, mounted head updates, ground state, full locomotion animation, and the original custom-skin report remain open. The [pitch-only packet control](../../viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md) describes the retained head rotation semantics. + Rebuilt direct and ViaProxy controls restore crouching and the native foot anchor with identical skin and posture inputs. The combined four-project build passes 3,335 tests with 325 skipped and no failures or errors. Full stack replay, bundle, artifact checksums, and embedded core-class comparisons pass. Native current/older/current cat and audio controls establish selected resource paths. Complete pose alignment, audio, particle, query, and visual parity remain unfinished. From 105a966307c33727f46729c19ff362fd7c4998f1 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 13:27:36 +0100 Subject: [PATCH 36/47] fix(players): align picking boxes at remote spawn Initialize retained remote player state from AddPlayer's feet through the existing eye-origin conversion. Preserve visible spawn coordinates and keep picking, relative click positions and spatial receive history consistent before the first movement packet. Add a registered-handler regression and protocol-2225 native controls for two fresh peers. Pre-fix direct Java misses both initial upper-body picks; rebuilt direct and ViaProxy clients match all four native attack outcomes. The full build passes 3,336 tests with 325 skipped. Bundle, artifact and embedded core checks pass, and all owned fixtures are restored. --- patches/viabedrock/series.json | 21 ++ ...bridge-native-actor-picking-hitboxes.pr.md | 2 + ...-align-retained-remote-spawn-origins.patch | 229 ++++++++++++++++++ ...-align-retained-remote-spawn-origins.pr.md | 17 ++ 4 files changed, 269 insertions(+) create mode 100644 patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.patch create mode 100644 patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md diff --git a/patches/viabedrock/series.json b/patches/viabedrock/series.json index 88dc6774..959a3644 100644 --- a/patches/viabedrock/series.json +++ b/patches/viabedrock/series.json @@ -556,6 +556,27 @@ "reason": "Land Mockito and the shared JVM packet-test suite configuration used to invoke the registered effect handler." } ] + }, + { + "file": "0106-align-retained-remote-spawn-origins.patch", + "title": "fix(players): align retained remote spawn origins", + "prerequisites": [ + { + "project": "viabedrock", + "patch": "0001-update-bedrock-to-1-26-60.patch", + "reason": "Land protocol-2225 item descriptors and the shared packet-test configuration before the spawn regression." + }, + { + "project": "viabedrock", + "patch": "0015-transport-native-resources-and-sound-commands.patch", + "reason": "Land Mockito for the registered-handler packet fixture." + }, + { + "project": "viabedrock", + "patch": "0018-preserve-sparse-actor-spatial-updates.patch", + "reason": "Retain the initial spatial receive cache before asserting its canonical player origin." + } + ] } ], "deferred": [ diff --git a/patches/viabedrock/upstreamable/0099-bridge-native-actor-picking-hitboxes.pr.md b/patches/viabedrock/upstreamable/0099-bridge-native-actor-picking-hitboxes.pr.md index 502cf1f3..98487566 100644 --- a/patches/viabedrock/upstreamable/0099-bridge-native-actor-picking-hitboxes.pr.md +++ b/patches/viabedrock/upstreamable/0099-bridge-native-actor-picking-hitboxes.pr.md @@ -24,4 +24,6 @@ Survival placement transactions travel inside `PlayerAuthInput`; the journal com Eye precision, other actor shapes, and overlapping multipart targets remain open. Successful native attacks emit two `Animate` packets, while these Java controls emit one. Animation packet counts and timing remain open. +The [remote-player spawn control](0106-align-retained-remote-spawn-origins.pr.md) covers initial physical proxies and their first stationary movement update. + The [owning patch](0099-bridge-native-actor-picking-hitboxes.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.patch b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.patch new file mode 100644 index 00000000..f8286b2f --- /dev/null +++ b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.patch @@ -0,0 +1,229 @@ +From f8b6dc11ac96bc6b58005404bcc76e8ece23e3a5 Mon Sep 17 00:00:00 2001 +From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> +Date: Mon, 2 Nov 2026 13:09:03 +0100 +Subject: [PATCH] fix(players): align retained remote spawn origins + +AddPlayer supplies the remote player's feet, while MovePlayer supplies its +eye position. Initialize the retained transform through setJavaPosition +so picking proxies, relative click positions and spatial receive history +use the same eye-origin convention from the first frame. Keep the Java +spawn coordinates at the original feet position. + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 spawn two +fresh player identities and send a stationary normal movement update for +each. Native upper-body attacks succeed before and after both updates. +The installed pre-fix direct client misses both initial attacks: its +physical picking proxies begin 1.62001 blocks below their correct origin. +The same ray hits after the movement update, although the visible player +remains stationary. Complete journals and read-only client bounds audits +establish the mismatch. The synthetic peer is not a server damage target; +these controls establish client picking and outgoing attack admission. + +The registered-handler regression decodes Java spawn and movement output, +checks visible feet and the retained eye origin, and preserves the initial +receive cache through the first stationary movement update. It fails on +the lowered retained origin before the correction. +--- + .../protocol/packet/OtherPlayerPackets.java | 3 +- + .../packet/PlayerSpawnPositionPacketTest.java | 176 ++++++++++++++++++ + 2 files changed, 178 insertions(+), 1 deletion(-) + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerSpawnPositionPacketTest.java + +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java +index 2162e5ee..e4758ac5 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java +@@ -109,7 +109,8 @@ public final class OtherPlayerPackets { + final int buildPlatform = wrapper.read(BedrockTypes.INT_LE); // build platform + + final PlayerEntity entity = entityTracker.addEntity(new PlayerEntity(wrapper.user(), entityRuntimeId, entityTracker.getNextJavaEntityId(), uuid, abilities)); +- entity.setPosition(position); ++ // AddPlayer supplies feet rather than MovePlayer's eye origin. ++ entity.setJavaPosition(position); + entity.setRotation(rotation); + entity.updateName(username); + wrapper.user().get(PlayerListStorage.class).setGameType(uuid, abilities.entityUniqueId(), gameType); +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerSpawnPositionPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerSpawnPositionPacketTest.java +new file mode 100644 +index 00000000..c9ceacf1 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerSpawnPositionPacketTest.java +@@ -0,0 +1,176 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.packet; ++ ++import com.viaversion.viaversion.api.connection.UserConnection; ++import com.viaversion.viaversion.api.minecraft.entities.EntityTypes26_3; ++import com.viaversion.viaversion.api.protocol.Protocol; ++import com.viaversion.viaversion.api.protocol.packet.PacketType; ++import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; ++import com.viaversion.viaversion.api.protocol.packet.State; ++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.ByteBuf; ++import io.netty.buffer.Unpooled; ++import net.raphimc.viabedrock.protocol.BedrockProtocol; ++import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; ++import net.raphimc.viabedrock.protocol.data.enums.bedrock.generated.GameType; ++import net.raphimc.viabedrock.protocol.data.generated.java.EntityDataFields; ++import net.raphimc.viabedrock.protocol.model.BedrockItem; ++import net.raphimc.viabedrock.protocol.model.PlayerAbilities; ++import net.raphimc.viabedrock.protocol.model.Position3f; ++import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; ++import net.raphimc.viabedrock.protocol.storage.EntityPropertyTracker; ++import net.raphimc.viabedrock.protocol.storage.EntityTracker; ++import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; ++import net.raphimc.viabedrock.protocol.storage.PlayerListStorage; ++import net.raphimc.viabedrock.protocol.types.BedrockTypes; ++import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackDescriptorType; ++import org.junit.jupiter.api.Test; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Map; ++import java.util.UUID; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class PlayerSpawnPositionPacketTest { ++ ++ @Test ++ void remoteSpawnAndItsFirstStationaryMoveShareTheVisibleFeetOrigin() throws Exception { ++ Class.forName("com.viaversion.viaversion.api.minecraft.data.StructuredDataKey"); ++ final var user = mock(UserConnection.class); ++ final var tracker = new EntityTracker(user); ++ final var session = mock(GameSessionStorage.class); ++ final var items = mock(ItemRewriter.class); ++ when(user.get(EntityTracker.class)).thenReturn(tracker); ++ when(user.get(EntityPropertyTracker.class)).thenReturn(mock(EntityPropertyTracker.class)); ++ when(user.get(GameSessionStorage.class)).thenReturn(session); ++ when(user.get(PlayerListStorage.class)).thenReturn(new PlayerListStorage()); ++ when(user.get(ItemRewriter.class)).thenReturn(items); ++ when(session.getLevelGameType()).thenReturn(GameType.Survival); ++ when(items.createNetworkItem(0, (short) 0, (byte) 0, 0)).thenReturn(BedrockItem.empty()); ++ when(items.newItemType()).thenReturn(new NetworkItemStackDescriptorType(0, false, items)); ++ final var fields = BedrockProtocol.MAPPINGS.getClass().getDeclaredField("javaEntityDataFields"); ++ fields.setAccessible(true); ++ final Object oldFields = fields.get(BedrockProtocol.MAPPINGS); ++ final var entityId = EntityTypes26_3.class.getDeclaredField("id"); ++ entityId.setAccessible(true); ++ final int oldEntityId = entityId.getInt(EntityTypes26_3.PLAYER); ++ final List sent = new ArrayList<>(); ++ try (var factory = mockStatic(PacketWrapper.class)) { ++ entityId.setInt(EntityTypes26_3.PLAYER, 42); ++ fields.set(BedrockProtocol.MAPPINGS, Map.of(EntityTypes26_3.PLAYER, List.of(EntityDataFields.PLAYER_MODE_CUSTOMISATION))); ++ factory.when(() -> PacketWrapper.create(any(PacketType.class), eq(user))).thenAnswer(call -> packet(call.getArgument(0), null, user, sent)); ++ final var protocol = new TestProtocol(); ++ final var feet = new Position3f(1F, 64F, 3F); ++ final var nativeOrigin = new Position3f(1F, 65.62001F, 3F); ++ final var input = Unpooled.buffer(); ++ final var output = Unpooled.buffer(); ++ try { ++ BedrockTypes.UUID.write(input, new UUID(0, 21)); ++ BedrockTypes.STRING.write(input, "Peer"); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 101L); ++ BedrockTypes.STRING.write(input, ""); ++ BedrockTypes.POSITION_3F.write(input, feet); ++ BedrockTypes.POSITION_3F.write(input, Position3f.ZERO); ++ BedrockTypes.POSITION_3F.write(input, Position3f.ZERO); ++ input.writeZero(8); ++ BedrockTypes.VAR_INT.write(input, GameType.Survival.getValue()); ++ BedrockTypes.UNSIGNED_VAR_INT.write(input, 0); ++ BedrockTypes.UNSIGNED_VAR_INT.write(input, 0); ++ BedrockTypes.UNSIGNED_VAR_INT.write(input, 0); ++ BedrockTypes.PLAYER_ABILITIES.write(input, new PlayerAbilities(21, (byte) 0, (byte) 0)); ++ BedrockTypes.UNSIGNED_VAR_INT.write(input, 0); ++ input.writeBoolean(false); ++ BedrockTypes.STRING.write(input, ""); ++ input.writeIntLE(8); ++ final var spawn = packet(ClientboundBedrockPackets.ADD_PLAYER, input, user, sent); ++ protocol.translate(spawn); ++ assertFalse(input.isReadable()); ++ final var player = tracker.getEntityByRid(101); ++ assertNotNull(player); ++ assertEquals(feet, player.javaPosition()); ++ assertEquals(nativeOrigin, player.position()); ++ final var initialOrigin = player.position(); ++ assertEquals(initialOrigin, player.lastReceivedPosition()); ++ final var emitted = sent.stream().filter(p -> p.getPacketType() == ClientboundPackets26_3.ADD_ENTITY).findFirst().orElseThrow(); ++ emitted.writeToBuffer(output); ++ assertEquals(ClientboundPackets26_3.ADD_ENTITY.getId(), Types.VAR_INT.readPrimitive(output)); ++ assertEquals(player.javaId(), Types.VAR_INT.readPrimitive(output)); ++ assertEquals(player.javaUuid(), Types.UUID.read(output)); ++ Types.VAR_INT.readPrimitive(output); // entity type comes from the installed test mapping ++ assertEquals(1D, Types.DOUBLE.read(output)); ++ assertEquals(64D, Types.DOUBLE.read(output)); ++ assertEquals(3D, Types.DOUBLE.read(output)); ++ Types.LOW_PRECISION_VECTOR.read(output); ++ output.skipBytes(3); // pitch, yaw, head yaw ++ assertEquals(0, Types.VAR_INT.readPrimitive(output)); ++ assertFalse(output.isReadable()); ++ output.clear(); ++ input.clear(); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 101L); ++ BedrockTypes.POSITION_3F.write(input, nativeOrigin); ++ BedrockTypes.POSITION_3F.write(input, Position3f.ZERO); ++ input.writeByte(0).writeBoolean(true); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 0L); ++ input.writeBoolean(false); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 1L); ++ final var move = packet(ClientboundBedrockPackets.MOVE_PLAYER, input, user, sent); ++ protocol.translate(move); ++ assertFalse(input.isReadable()); ++ assertEquals(initialOrigin, player.position()); ++ assertEquals(feet, player.javaPosition()); ++ move.writeToBuffer(output); ++ assertEquals(ClientboundPackets26_3.ENTITY_POSITION_SYNC.getId(), Types.VAR_INT.readPrimitive(output)); ++ assertEquals(player.javaId(), Types.VAR_INT.readPrimitive(output)); ++ assertEquals(1, Types.VAR_INT.readPrimitive(output)); // stepped interpolation ++ assertEquals(1, Types.VAR_INT.readPrimitive(output)); // one position ++ assertEquals(1D, Types.DOUBLE.read(output)); ++ assertEquals(64D, Types.DOUBLE.read(output), .00001D); ++ assertEquals(3D, Types.DOUBLE.read(output)); ++ assertEquals(3, Types.VAR_INT.readPrimitive(output)); // interpolation steps ++ assertEquals(0F, Types.FLOAT.read(output)); ++ assertEquals(0F, Types.FLOAT.read(output)); ++ assertTrue(Types.BOOLEAN.read(output)); ++ assertFalse(output.isReadable()); ++ } finally { ++ input.release(); ++ output.release(); ++ } ++ } finally { ++ fields.set(BedrockProtocol.MAPPINGS, oldFields); ++ entityId.setInt(EntityTypes26_3.PLAYER, oldEntityId); ++ } ++ } ++ ++ private static PacketWrapper packet(final PacketType type, final ByteBuf input, final UserConnection user, final List sent) { ++ return new PacketWrapperImpl(type, input, user) { ++ @Override ++ public void send(final Class protocol, final boolean skipCurrentPipeline) { ++ sent.add(this); ++ } ++ }; ++ } ++ ++ private static final class TestProtocol extends BedrockProtocol { ++ ++ private TestProtocol() { ++ OtherPlayerPackets.register(this); ++ } ++ ++ private void translate(final PacketWrapper packet) throws Exception { ++ final var mapping = this.clientboundMappings.mappedPacket(State.PLAY, packet.getId()); ++ mapping.applyType(packet); ++ mapping.handler().handle(packet); ++ } ++ ++ } ++ ++} +-- +2.56.0 diff --git a/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md new file mode 100644 index 00000000..a87c0300 --- /dev/null +++ b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md @@ -0,0 +1,17 @@ +# Align retained remote player spawn origins + +AddPlayer supplies feet. MovePlayer supplies an eye position. Java already spawns the visible player at the supplied feet, but the retained transform treated that value as an eye origin. Physical picking proxies therefore started one eye offset below the visible player until its first movement packet. + +The handler converts spawn feet through `setJavaPosition` before it retains the player state. The Java spawn coordinates stay at feet. Picking, relative clicks, and the initial spatial receive cache share the existing eye-origin convention. + +## Validation and remaining limits + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 cover two fresh peers and their stationary movement updates. Native admits all four upper-body attacks. Pre-fix direct Java misses both initial attacks, then admits the same aim after each update. Read-only bounds audits show misplaced proxies while the visible player remains stationary. + +Rebuilt direct and ViaProxy routes admit all four attacks with verified JARs. In-world samples confirm that each initial proxy aligns with its visible owner, including the normal 0.1-block picking padding. Strict journal decoders verify complete framing, matching skin bytes, and identical peer spawn and movement payloads across all four recordings. + +The registered-handler regression fails before the correction. It decodes Java spawn and movement output and checks visible feet, the retained eye origin, and the initial spatial receive cache. The full four-project build passes 3,336 tests with 325 skipped and no failures or errors. All 155 patches replay. Bundle generation, ten artifact and manifest checksums, and the 1,647 embedded core-class comparisons pass. + +All 33 recorded process identities are gone. Cleanup restored the native world, profile options, trust setting, external server lists, and owned server configuration exactly. + +The fixture uses synthetic floating standing peers. It establishes selected client picking and outgoing attack admission. Server damage, other spawn poses, arbitrary actors, mounts, and complete skin or interaction parity remain unverified. From 2d23f85a52dc47b80667e4192742fa3872f5663f Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 13:51:32 +0100 Subject: [PATCH 37/47] chore(deps): refresh upstream build and patch replay Pin verified ViaFabricPlus Jenkins build 2296 and its exact Maven publication. The four patch-target upstream branch tips remain unchanged. Regenerate cartography patch context against the current stack so a fresh clone can replay it without cached historical blobs. The resulting ViaBedrock source tree is unchanged. --- ...ranslate-bedrock-cartography-in-core.patch | 20 +++++++++---------- viafabricplus.json | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) 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 7f92d44a..644a1f70 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 b7cea07eef2932a131d522d54a551b79ea2bbcc1 Mon Sep 17 00:00:00 2001 +From 74b667fd47e69ec962e3ddcb5323867b5be7504c 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 c43f45dd..091ae27a 100644 +index 05c17d96..59355265 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/InventoryPackets.java @@ -74,6 +74,7 @@ import net.lenni0451.mcstructs_bedrock.text.utils.BedrockTextUtils; @@ -653,9 +653,9 @@ index c43f45dd..091ae27a 100644 + cartography.closeNameDialog(); + } inventoryTracker.setCurrentContainerClosed(serverInitiated); - if (correlatedClose) { - wrapper.cancel(); -@@ -535,6 +539,7 @@ public final class InventoryPackets { + // A client-initiated close already dismissed its Java screen. Its acknowledgment + // cannot close a later screen, but must still retire native state above. +@@ -540,6 +544,7 @@ public final class InventoryPackets { container = new SmithingContainer( wrapper.user(), containerId, title, containerPosition); @@ -663,7 +663,7 @@ index c43f45dd..091ae27a 100644 case LOOM -> container = new LoomContainer(wrapper.user(), containerId, title, containerPosition); case STONECUTTER -> container = -@@ -599,6 +604,13 @@ public final class InventoryPackets { +@@ -604,6 +609,13 @@ public final class InventoryPackets { } container.resolveBlockBinding(); inventoryTracker.setCurrentContainer(container, openContext); @@ -677,7 +677,7 @@ index c43f45dd..091ae27a 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 { +@@ -657,6 +669,13 @@ public final class InventoryPackets { (byte) containerId, containerName, storageItem); if (container != null && container.setItems(items)) { container.markServerUpdate(); @@ -691,7 +691,7 @@ index c43f45dd..091ae27a 100644 wrapper.user().get(InventoryRequestTracker.class) .handleAuthoritativeContent(container); wrapper.user().get(InventoryRequestTracker.class) -@@ -822,6 +841,9 @@ public final class InventoryPackets { +@@ -827,6 +846,9 @@ public final class InventoryPackets { .refreshCraftingOutput(); tracker.sendJavaUpdateRecipes(wrapper.user()); NativeCookingUiState.sendUpdate(wrapper.user()); @@ -701,7 +701,7 @@ index c43f45dd..091ae27a 100644 }); protocol.registerClientbound( ClientboundBedrockPackets.UNLOCKED_RECIPES, -@@ -1148,6 +1170,9 @@ public final class InventoryPackets { +@@ -1153,6 +1175,9 @@ public final class InventoryPackets { if (wrapper.user().get(InventoryTracker.class).getCurrentContainer() instanceof AnvilContainer anvil) { anvil.setRenameText(name); @@ -711,7 +711,7 @@ index c43f45dd..091ae27a 100644 } }); protocol.registerServerbound( -@@ -1182,6 +1207,13 @@ public final class InventoryPackets { +@@ -1187,6 +1212,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/viafabricplus.json b/viafabricplus.json index 08dd0029..ea7f25cd 100644 --- a/viafabricplus.json +++ b/viafabricplus.json @@ -1,9 +1,9 @@ { - "build": 2295, - "commit": "546c4fbf79df1036574a3f0bfc20bb17c5df9d92", + "build": 2296, + "commit": "a1ca6882cecd08d40d9e530a793fccd9b4796937", "version": "5.1.4-SNAPSHOT", - "mavenVersion": "5.1.4-20261101.155810-1", - "sha256": "fb28fd936403ba6314f87f48573a79933bafd31ef35e4ebe26fda4a2dc037265", + "mavenVersion": "5.1.4-20261102.124314-2", + "sha256": "493475c494365b81b09ed8963d330e9da1f51fe3343e49e512c6d72b49d48551", "pomSha256": "87720823df2792894748d3082992009d93186c45b283734880ead4c8b9763918", "apiSha256": "a479914e0364f24b9bf1f40437eabb4017e8492ea89fed236758671d1c3439a9", "apiPomSha256": "506feba24cec28c82ac1c16565c40fdc3b7dc3b819cbfcc0f50fbdc8ffd8985b" From 257c778becb36bc240c4783eb3e68e4073eab3eb Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 14:36:34 +0100 Subject: [PATCH 38/47] fix(players): separate sparse movement cache from spawn Initialize fresh remote-player actor movement caches from constructor zeroes, independently of visible spawn and normal player movement. Keep the existing feet-to-eye conversion that aligns picking proxies. Fold the correction and extended registered-handler regression into the spawn patch. Current native live controls, original initializer instructions, and rebuilt direct/proxy recordings establish the selected cache behavior. Record the separate body-orientation mismatch after large vertical excursions and the excluded pre-gameplay proxy timeout. --- docs/bedrock-coverage.md | 2 +- ...reserve-sparse-actor-spatial-updates.pr.md | 7 +- ...-align-retained-remote-spawn-origins.patch | 109 +++++++++++++----- ...-align-retained-remote-spawn-origins.pr.md | 24 ++-- 4 files changed, 98 insertions(+), 44 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 5b88a02e..3edf371b 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -30,7 +30,7 @@ No complete area has verified Bedrock parity. | Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Short input edges during active Java use, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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. | +| Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Body orientation after large vertical excursions still differs. 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. | | Fog and picking | Fog commands, inherited legacy biome bindings, pack-defined defaults, Overworld clock modulation, End dragon distance rules, native actor picking transport, and native float camera rays. Disabling the daylight rule freezes explicit Overworld clocks. | Both routes match selected native fog colors and distances, three non-cardinal rays, and paired armor-stand and block reach controls. Eye precision, other actor shapes, multipart target ordering, spatial blending, timed command transitions, directional sunrise tint, sky sampling, and other render modes remain open. | | Custom content | Resource-defined actors, materials, particles, sound, block geometry, lighting, and collision. | Unsupported material methods, missing assets, carrier capacity, and visual differences remain. Live-server admission does not establish parity. | | Platforms | Linux build, replay, and selected native comparisons. | Windows and macOS launcher/account/rendering paths need their own results. See [platform testing](platform-testing.md). | diff --git a/patches/viabedrock/upstreamable/0018-preserve-sparse-actor-spatial-updates.pr.md b/patches/viabedrock/upstreamable/0018-preserve-sparse-actor-spatial-updates.pr.md index 96798642..a96e2c18 100644 --- a/patches/viabedrock/upstreamable/0018-preserve-sparse-actor-spatial-updates.pr.md +++ b/patches/viabedrock/upstreamable/0018-preserve-sparse-actor-spatial-updates.pr.md @@ -27,8 +27,8 @@ Live prediction and other actor writes no longer replace that source. Both absolute and sparse packets update the retained stream before the local-player admission check. An ignored local transform therefore still supplies the next sparse packet. Unknown and removed actors receive no update, and each actor instance owns its retained state. -Initial actor placement supplies the bootstrap before movement packets arrive. -Spawn angles use the native wrap and byte conversion before they enter the received cache. +Actor spawns that run the native bootstrap retain their initial placement and packed angles before movement packets arrive. +The [remote-player spawn control](0106-align-retained-remote-spawn-origins.pr.md) establishes the AddPlayer exception: its actor movement cache keeps constructor zeros, independently of the visible spawn and normal MovePlayer updates. The live spawn rotation keeps its original float values. The pinned 1.26.51.1 executable routes delta packets through dispatcher `141732ef0`, callback slot `710`, and derived client handler `14133ef30`. @@ -52,7 +52,7 @@ The native cache comparison runs without skips. Offline tooling, static, and patch metadata checks pass. These results do not claim a fresh live trajectory or a complete interpolation port. -The native spawn path calls `14103ff60` after proxy construction. +The actor spawn bootstrap examined by the instruction fixture is `14103ff60`. That function stores the runtime ID and position, then wraps and packs the three angles into bytes. It divides by 360 before multiplication by 256. The previous bootstrap retained full spawn angles, so the first sparse packet used different omitted angles. @@ -62,6 +62,7 @@ A further 1,024 controls execute two original inline initializers, the complete They cover all six omission bits, distinct live positions, wrap boundaries, and large finite angles. The fixture supplies allocation, native frame data, ready-level and actor lookup callbacks, local-control storage, and the CRT remainder operation. Surrounding spawn-handler flow, complete actor construction, and live packet delivery remain outside this comparison. +These instruction controls do not establish that every spawn handler calls the bootstrap. The current combined build passes 2,540 tests, with 215 optional skips and no failures or errors. All five packet-cache tests run without skips, including both native references. diff --git a/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.patch b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.patch index f8286b2f..55befc49 100644 --- a/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.patch +++ b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.patch @@ -1,39 +1,28 @@ -From f8b6dc11ac96bc6b58005404bcc76e8ece23e3a5 Mon Sep 17 00:00:00 2001 +From a8873599993f3914cb07f9bc28d24355b09cb6b7 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 13:09:03 +0100 Subject: [PATCH] fix(players): align retained remote spawn origins -AddPlayer supplies the remote player's feet, while MovePlayer supplies its -eye position. Initialize the retained transform through setJavaPosition -so picking proxies, relative click positions and spatial receive history -use the same eye-origin convention from the first frame. Keep the Java -spawn coordinates at the original feet position. +AddPlayer supplies feet while MovePlayer supplies an eye position. Initialize the live transform through setJavaPosition so picking proxies and relative clicks align with visible feet from the first frame. Keep Java spawn coordinates at the supplied feet. -Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 spawn two -fresh player identities and send a stationary normal movement update for -each. Native upper-body attacks succeed before and after both updates. -The installed pre-fix direct client misses both initial attacks: its -physical picking proxies begin 1.62001 blocks below their correct origin. -The same ray hits after the movement update, although the visible player -remains stationary. Complete journals and read-only client bounds audits -establish the mismatch. The synthetic peer is not a server damage target; -these controls establish client picking and outgoing attack admission. +Keep the absolute/delta actor receive cache independent from that live spawn transform. Current Bedrock 1.26.60.5/BDS 1.26.60.6 protocol 2225 controls leave fresh player sparse coordinates and angles at zero. A normal MovePlayer does not populate that cache; an actor absolute update does. Preserve constructor flags and duration while initializing the fresh player cache. -The registered-handler regression decodes Java spawn and movement output, -checks visible feet and the retained eye origin, and preserves the initial -receive cache through the first stationary movement update. It fails on -the lowered retained origin before the correction. +Native and pre-fix direct journals carry identical peer payloads. Sparse Z-only updates hide the native fresh player while Java incorrectly retains its spawn height. A separate native control supplies Y/Z with X omitted, then X alone: the player starts at the zero-X projection and returns to center only after explicit X. Fresh replacement peers repeat the result. Absolute/normal/delta mixtures retain the separate actor cache. + +The registered regression decodes Java spawn, normal movement and sparse movement. It preserves visible feet and the retained eye origin, and rejects spawn or normal movement leaking into omitted sparse axes and angles. Its fixture uses an empty held-item codec without relying on deferred inventory APIs. + +Earlier native upper-body attack controls establish initial picking and outgoing attack admission before and after stationary updates. Their synthetic peers do not establish server damage. Full movement trajectories, arbitrary actors, mounts and complete rendering parity remain unverified. --- - .../protocol/packet/OtherPlayerPackets.java | 3 +- - .../packet/PlayerSpawnPositionPacketTest.java | 176 ++++++++++++++++++ - 2 files changed, 178 insertions(+), 1 deletion(-) + .../protocol/packet/OtherPlayerPackets.java | 6 +- + .../packet/PlayerSpawnPositionPacketTest.java | 228 ++++++++++++++++++ + 2 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerSpawnPositionPacketTest.java diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java -index 2162e5ee..e4758ac5 100644 +index 2162e5ee..8b37d578 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java -@@ -109,7 +109,8 @@ public final class OtherPlayerPackets { +@@ -109,8 +109,12 @@ public final class OtherPlayerPackets { final int buildPlatform = wrapper.read(BedrockTypes.INT_LE); // build platform final PlayerEntity entity = entityTracker.addEntity(new PlayerEntity(wrapper.user(), entityRuntimeId, entityTracker.getNextJavaEntityId(), uuid, abilities)); @@ -41,14 +30,18 @@ index 2162e5ee..e4758ac5 100644 + // AddPlayer supplies feet rather than MovePlayer's eye origin. + entity.setJavaPosition(position); entity.setRotation(rotation); ++ // AddPlayer does not seed the separate absolute/delta actor movement cache. ++ entity.retainSpatialUpdate(Position3f.ZERO, Position3f.ZERO, ++ entity.lastReceivedMovementFlags(), entity.lastReceivedInterpolationTicks()); entity.updateName(username); wrapper.user().get(PlayerListStorage.class).setGameType(uuid, abilities.entityUniqueId(), gameType); + diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerSpawnPositionPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerSpawnPositionPacketTest.java new file mode 100644 -index 00000000..c9ceacf1 +index 00000000..61176fef --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/PlayerSpawnPositionPacketTest.java -@@ -0,0 +1,176 @@ +@@ -0,0 +1,228 @@ +/* + * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -62,6 +55,7 @@ index 00000000..c9ceacf1 +import com.viaversion.viaversion.api.protocol.packet.PacketType; +import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; +import com.viaversion.viaversion.api.protocol.packet.State; ++import com.viaversion.viaversion.api.type.Type; +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; @@ -80,7 +74,6 @@ index 00000000..c9ceacf1 +import net.raphimc.viabedrock.protocol.storage.GameSessionStorage; +import net.raphimc.viabedrock.protocol.storage.PlayerListStorage; +import net.raphimc.viabedrock.protocol.types.BedrockTypes; -+import net.raphimc.viabedrock.protocol.types.item.NetworkItemStackDescriptorType; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; @@ -94,7 +87,7 @@ index 00000000..c9ceacf1 +class PlayerSpawnPositionPacketTest { + + @Test -+ void remoteSpawnAndItsFirstStationaryMoveShareTheVisibleFeetOrigin() throws Exception { ++ void remoteSpawnKeepsVisibleFeetAndAnIndependentActorMovementCache() throws Exception { + Class.forName("com.viaversion.viaversion.api.minecraft.data.StructuredDataKey"); + final var user = mock(UserConnection.class); + final var tracker = new EntityTracker(user); @@ -106,8 +99,19 @@ index 00000000..c9ceacf1 + when(user.get(PlayerListStorage.class)).thenReturn(new PlayerListStorage()); + when(user.get(ItemRewriter.class)).thenReturn(items); + when(session.getLevelGameType()).thenReturn(GameType.Survival); -+ when(items.createNetworkItem(0, (short) 0, (byte) 0, 0)).thenReturn(BedrockItem.empty()); -+ when(items.newItemType()).thenReturn(new NetworkItemStackDescriptorType(0, false, items)); ++ when(items.newItemType()).thenReturn(new Type<>(BedrockItem.class) { ++ @Override ++ public BedrockItem read(final ByteBuf buffer) { ++ assertEquals(0L, buffer.readLong()); // This fixture only supplies an empty held-item descriptor. ++ return BedrockItem.empty(); ++ } ++ ++ @Override ++ public void write(final ByteBuf buffer, final BedrockItem value) { ++ assertTrue(value.isEmpty()); ++ buffer.writeZero(8); ++ } ++ }); + final var fields = BedrockProtocol.MAPPINGS.getClass().getDeclaredField("javaEntityDataFields"); + fields.setAccessible(true); + final Object oldFields = fields.get(BedrockProtocol.MAPPINGS); @@ -131,7 +135,7 @@ index 00000000..c9ceacf1 + BedrockTypes.STRING.write(input, ""); + BedrockTypes.POSITION_3F.write(input, feet); + BedrockTypes.POSITION_3F.write(input, Position3f.ZERO); -+ BedrockTypes.POSITION_3F.write(input, Position3f.ZERO); ++ BedrockTypes.POSITION_3F.write(input, new Position3f(22.5F, 90F, 90F)); + input.writeZero(8); + BedrockTypes.VAR_INT.write(input, GameType.Survival.getValue()); + BedrockTypes.UNSIGNED_VAR_INT.write(input, 0); @@ -150,7 +154,8 @@ index 00000000..c9ceacf1 + assertEquals(feet, player.javaPosition()); + assertEquals(nativeOrigin, player.position()); + final var initialOrigin = player.position(); -+ assertEquals(initialOrigin, player.lastReceivedPosition()); ++ assertEquals(Position3f.ZERO, player.lastReceivedPosition()); ++ assertEquals(Position3f.ZERO, player.lastReceivedRotation()); + final var emitted = sent.stream().filter(p -> p.getPacketType() == ClientboundPackets26_3.ADD_ENTITY).findFirst().orElseThrow(); + emitted.writeToBuffer(output); + assertEquals(ClientboundPackets26_3.ADD_ENTITY.getId(), Types.VAR_INT.readPrimitive(output)); @@ -191,6 +196,45 @@ index 00000000..c9ceacf1 + assertEquals(0F, Types.FLOAT.read(output)); + assertTrue(Types.BOOLEAN.read(output)); + assertFalse(output.isReadable()); ++ ++ input.clear(); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 101L); ++ BedrockTypes.POSITION_3F.write(input, new Position3f(2F, 66.62001F, 4F)); ++ BedrockTypes.POSITION_3F.write(input, new Position3f(22.5F, 90F, 90F)); ++ input.writeByte(0).writeBoolean(true); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 0L); ++ input.writeBoolean(false); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 2L); ++ protocol.translate(packet(ClientboundBedrockPackets.MOVE_PLAYER, input, user, sent)); ++ assertFalse(input.isReadable()); ++ assertEquals(new Position3f(2F, 65F, 4F), player.javaPosition()); ++ ++ // A sparse actor update does not inherit AddPlayer or MovePlayer coordinates. ++ input.clear(); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 101L); ++ input.writeBoolean(false).writeBoolean(true).writeFloatLE(nativeOrigin.y()); ++ input.writeBoolean(true).writeFloatLE(4F); ++ input.writeBoolean(false).writeBoolean(false).writeBoolean(false); ++ input.writeBoolean(true).writeBoolean(false).writeBoolean(false).writeBoolean(false); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 3L); ++ final var delta = packet(ClientboundBedrockPackets.MOVE_ENTITY_DELTA, input, user, sent); ++ protocol.translate(delta); ++ assertFalse(input.isReadable()); ++ assertEquals(new Position3f(0F, 64F, 4F), player.javaPosition()); ++ output.clear(); ++ delta.writeToBuffer(output); ++ assertEquals(ClientboundPackets26_3.ENTITY_POSITION_SYNC.getId(), Types.VAR_INT.readPrimitive(output)); ++ assertEquals(player.javaId(), Types.VAR_INT.readPrimitive(output)); ++ assertEquals(1, Types.VAR_INT.readPrimitive(output)); ++ assertEquals(1, Types.VAR_INT.readPrimitive(output)); ++ assertEquals(0D, Types.DOUBLE.read(output)); ++ assertEquals(64D, Types.DOUBLE.read(output), .00001D); ++ assertEquals(4D, Types.DOUBLE.read(output)); ++ assertEquals(3, Types.VAR_INT.readPrimitive(output)); ++ assertEquals(0F, Types.FLOAT.read(output)); ++ assertEquals(0F, Types.FLOAT.read(output)); ++ assertTrue(Types.BOOLEAN.read(output)); ++ assertFalse(output.isReadable()); + } finally { + input.release(); + output.release(); @@ -214,6 +258,7 @@ index 00000000..c9ceacf1 + + private TestProtocol() { + OtherPlayerPackets.register(this); ++ EntityPackets.register(this); + } + + private void translate(final PacketWrapper packet) throws Exception { diff --git a/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md index a87c0300..e34ed1e2 100644 --- a/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md +++ b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md @@ -1,17 +1,25 @@ # Align retained remote player spawn origins -AddPlayer supplies feet. MovePlayer supplies an eye position. Java already spawns the visible player at the supplied feet, but the retained transform treated that value as an eye origin. Physical picking proxies therefore started one eye offset below the visible player until its first movement packet. +AddPlayer supplies feet. MovePlayer supplies an eye position. Convert spawn feet through `setJavaPosition` so the live transform, picking proxies, and relative clicks use the eye origin. Keep Java spawn coordinates at the supplied feet. -The handler converts spawn feet through `setJavaPosition` before it retains the player state. The Java spawn coordinates stay at feet. Picking, relative clicks, and the initial spatial receive cache share the existing eye-origin convention. +The absolute/delta actor movement cache has a separate lifetime. AddPlayer leaves its coordinates and packed angles at constructor zeros. Normal MovePlayer updates do not populate it. An actor absolute or sparse update does. Preserve constructor movement flags and duration when initializing this player-specific cache. Other actor spawn bootstraps retain their existing behavior. -## Validation and remaining limits +## Validation -Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 cover two fresh peers and their stationary movement updates. Native admits all four upper-body attacks. Pre-fix direct Java misses both initial attacks, then admits the same aim after each update. Read-only bounds audits show misplaced proxies while the visible player remains stationary. +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls on protocol 2225 distinguish live player movement from the actor receive cache. A fresh player disappears from the original view after a Z-only sparse update; pre-fix Java incorrectly keeps its spawn height. Normal player movement does not change the source for the next sparse update. Actor absolute movement does, including across normal and head-only player updates. Replacement players start with independent caches. -Rebuilt direct and ViaProxy routes admit all four attacks with verified JARs. In-world samples confirm that each initial proxy aligns with its visible owner, including the normal 0.1-block picking padding. Strict journal decoders verify complete framing, matching skin bytes, and identical peer spawn and movement payloads across all four recordings. +Corrected direct and proxy routes match all three Z-only disappearance phases in the longer sequence. Their journals carry the same peer payloads as native. Read-only game audits verify the Java movement and cache-derived output through all twelve phases. The complete rendered pose still differs in later phases, as recorded below. -The registered-handler regression fails before the correction. It decodes Java spawn and movement output and checks visible feet, the retained eye origin, and the initial spatial receive cache. The full four-project build passes 3,336 tests with 325 skipped and no failures or errors. All 155 patches replay. Bundle generation, ten artifact and manifest checksums, and the 1,647 embedded core-class comparisons pass. +A second control supplies Y/Z while omitting X and angles, then supplies X alone. Native first renders the player at the zero-X projection and returns it to center only after explicit X. Two fresh identities repeat this result. Rebuilt direct and ViaProxy clients match all ten phases. Strict decoders verify complete journals and identical accepted peer spawn, skin, normal, absolute, and sparse packet bytes. Three settled frames per phase place median marker centroids within 0.20% of each viewport axis and boundaries within 0.49%. Read-only game audits verify the resulting Java positions and angles with native state present. -All 33 recorded process identities are gone. Cleanup restored the native world, profile options, trust setting, external server lists, and owned server configuration exactly. +The verified 1.26.60.5 executable contains the matching cache initializer instructions. Six controls execute both original initializer ranges on differently prefilled storage. They verify zero coordinates and angle bytes, flags 255, and zero duration. Ghidra decompilation separately confirms the stores and the distinct spawn bootstrap. These instruction controls exclude allocation and surrounding actor construction; the live controls establish the player-specific behavior. -The fixture uses synthetic floating standing peers. It establishes selected client picking and outgoing attack admission. Server damage, other spawn poses, arbitrary actors, mounts, and complete skin or interaction parity remain unverified. +Earlier upper-body attack controls establish the live spawn-origin correction. Native admits four attacks before and after two stationary updates. Pre-fix direct Java misses both initial attacks because its physical proxies sit 1.62001 blocks too low. Rebuilt direct and proxy routes admit all four. Those synthetic peers establish client picking and outgoing attack admission, without server damage claims. + +The registered-handler regression decodes Java spawn, stationary movement, and sparse movement after a conflicting normal move. It checks visible feet, the retained eye origin, and omitted coordinates and angles. It fails before the cache correction. Its empty held-item fixture also compiles without deferred inventory APIs. The full build passes 3,336 tests with 325 skipped and no failures. All 155 patches replay from fresh upstream clones. Bundle generation, ten artifact/manifest hashes, and all 1,647 embedded core-class comparisons pass. + +## Remaining limits + +The longer Z-only sequence exposes a body-orientation mismatch after a large vertical excursion and later movement. The cache correction does not establish complete pose or trajectory parity. Native numeric world transforms, interpolation timing, other spawn poses, arbitrary actors, mounts, server damage, and complete skin or interaction parity remain unverified. + +One extended proxy join timed out during resource-pack negotiation before gameplay. It is excluded from validation. The fresh-server retry passes; reconnect robustness remains unverified. From 7701c01afb6bc4360309dd67b737f4836782de8f Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:18:11 +0100 Subject: [PATCH 39/47] docs(movement): identify native interpolation divergence Record current native admission and coordinate observations for the remote-player body mismatch. The selected normal moves pass all native gates and retain three interpolation steps; Java snaps large moves and also uses different intermediate positions. Keep mode-preserving interpolation as unfinished work. Core currently emits the same Java path for normal and reset modes, so an unconditional distance-cutoff bypass can alter teleport and respawn behavior. Production sources remain unchanged. --- .../0106-align-retained-remote-spawn-origins.pr.md | 2 +- .../0008-share-native-actor-and-resource-runtime.pr.md | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md index e34ed1e2..f74e5649 100644 --- a/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md +++ b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md @@ -20,6 +20,6 @@ The registered-handler regression decodes Java spawn, stationary movement, and s ## Remaining limits -The longer Z-only sequence exposes a body-orientation mismatch after a large vertical excursion and later movement. The cache correction does not establish complete pose or trajectory parity. Native numeric world transforms, interpolation timing, other spawn poses, arbitrary actors, mounts, server damage, and complete skin or interaction parity remain unverified. +The longer Z-only sequence exposes a body-orientation mismatch after a large vertical excursion and later movement. The [actor runtime evidence](../../viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md) identifies the host distance cutoff and different intermediate steps. The cache correction does not establish complete pose or trajectory parity. Native interpolation timing, other spawn poses, arbitrary actors, mounts, server damage, and complete skin or interaction parity remain unverified. One extended proxy join timed out during resource-pack negotiation before gameplay. It is excluded from validation. The fresh-server retry passes; reconnect robustness remains unverified. 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 d613995e..c2e336f0 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 @@ -38,6 +38,12 @@ Same-identity controls extend the fixture through a full yaw cycle, a pitch-only Strict protocol-2225 decoders verify one spawn and identical 71 MovePlayer payloads per route. Three settled frames per phase compare six markers. Across 11 phases, both routes have median marker centroids within 0.30% of each viewport axis and bounding coordinates within 0.42% of native. The accepted Java fixtures disable their onboarding tutorial through its public API before observations. An earlier obstructed westward frame does not support pose comparison. All 33 recorded process identities are gone, and cleanup restored the native profile and server configuration exactly. +The [mixed movement control](../../viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md) exposes a separate interpolation defect after a large vertical move. Current native observation confirms that all selected normal moves pass readiness, admission, and region checks. Native retains three position steps across the large move. Java instead applies its 64-block distance cutoff, snaps to the target, and resets the previous position. Body control then receives different displacement history. + +The host interpolation curve also differs during small moves. One native transition advances X through 2.1667, 1.8333, and 1.5. Java advances through 2.1667, 1.75, and 1.5. Current Ghidra decompilation and read-only debugger samples establish the native branches and coordinate changes. Complete journals verify identical peer payloads. Debugger timing and native numeric body angles remain outside this comparison. + +The interpolation correction must retain native movement modes. Core currently emits the same three-step Java path for remote normal, teleport, and respawn moves. An unconditional distance-cutoff bypass can therefore change reset behavior. Native step math, mode admission, mounting, component lifetimes, and complete trajectories remain unfinished. + These controls establish selected settled poses and position endpoints. Exact interpolation timing, movement curves, mounted head updates, ground state, full locomotion animation, and the original custom-skin report remain open. The [pitch-only packet control](../../viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md) describes the retained head rotation semantics. Rebuilt direct and ViaProxy controls restore crouching and the native foot anchor with identical skin and posture inputs. The combined four-project build passes 3,335 tests with 325 skipped and no failures or errors. Full stack replay, bundle, artifact checksums, and embedded core-class comparisons pass. From f798d94fa864d8fcaf04cba7fc757e54fe1cedc6 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:04:27 +0100 Subject: [PATCH 40/47] fix(players): preserve native remote interpolation Retain source modes, raw inputs, exact Java targets and actor lifetimes in negotiated movement receipts. Apply native float steps through the remote entity interpolation phase instead of Java's distance cutoff and curve. Keep independent position, body and head counters, including forced queues. Pair head-only packets explicitly so they preserve pending movement and apply raw fractional pitch. Retain ordinary Java history and host fallback for reset modes, riding, local authority and unavailable native state. Record current native instruction controls, direct and proxy comparisons, and remaining admission, timing and skin limits. Full builds pass 3,352 tests with 326 skipped; all 157 patches replay from fresh upstream clones. Keep the newly reported Realms connectivity failure open pending a matching reproduction with the current networking dependencies. --- docs/bedrock-coverage.md | 3 +- patches/viabedrock/series.json | 21 + ...-align-retained-remote-spawn-origins.pr.md | 2 +- ...ansport-remote-player-movement-modes.patch | 648 +++++++++++ ...ansport-remote-player-movement-modes.pr.md | 17 + patches/viafabricplus-bedrock/series.json | 26 + ...re-native-actor-and-resource-runtime.pr.md | 8 +- ...polate-native-remote-player-movement.patch | 1002 +++++++++++++++++ ...polate-native-remote-player-movement.pr.md | 31 + 9 files changed, 1750 insertions(+), 8 deletions(-) create mode 100644 patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.patch create mode 100644 patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md create mode 100644 patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch create mode 100644 patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 3edf371b..71bb2bce 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -30,7 +30,7 @@ No complete area has verified Bedrock parity. | Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Short input edges during active Java use, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Body orientation after large vertical excursions still differs. 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. | +| Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Remote interpolation readiness, exact timing, reset modes, and mounted behavior remain unverified. 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. | | Fog and picking | Fog commands, inherited legacy biome bindings, pack-defined defaults, Overworld clock modulation, End dragon distance rules, native actor picking transport, and native float camera rays. Disabling the daylight rule freezes explicit Overworld clocks. | Both routes match selected native fog colors and distances, three non-cardinal rays, and paired armor-stand and block reach controls. Eye precision, other actor shapes, multipart target ordering, spatial blending, timed command transitions, directional sunrise tint, sky sampling, and other render modes remain open. | | Custom content | Resource-defined actors, materials, particles, sound, block geometry, lighting, and collision. | Unsupported material methods, missing assets, carrier capacity, and visual differences remain. Live-server admission does not establish parity. | | Platforms | Linux build, replay, and selected native comparisons. | Windows and macOS launcher/account/rendering paths need their own results. See [platform testing](platform-testing.md). | @@ -47,6 +47,7 @@ Retain these reports until a matching reproduction establishes the result: - An ender pearl followed by a wind charge can cause repeated rollbacks. Selected [current native and Java controls](../patches/viafabricplus-bedrock/upstreamable/0025-send-completed-local-physics-to-viabedrock.pr.md) settle, including walking projectile combinations with added incoming delay. Timing for short presses and the original persistent loop remain unverified. - Food can repeat its use animation without feeding the player. - The locator bar can be absent. +- A Realms disconnect report shows NetherNet connectivity loss and repeated STUN 401 responses. Current networking updates still need a matching reproduction. Release fixes and successful local controls do not close an unmatched report. Record the client build, mods, server, input path, and reproduction with a new result. diff --git a/patches/viabedrock/series.json b/patches/viabedrock/series.json index 959a3644..e0989bdb 100644 --- a/patches/viabedrock/series.json +++ b/patches/viabedrock/series.json @@ -577,6 +577,27 @@ "reason": "Retain the initial spatial receive cache before asserting its canonical player origin." } ] + }, + { + "file": "0107-transport-remote-player-movement-modes.patch", + "title": "feat(players): transport remote movement modes and targets", + "prerequisites": [ + { + "project": "viabedrock", + "patch": "0001-update-bedrock-to-1-26-60.patch", + "reason": "Use protocol 2225 types and the shared JVM test dependencies." + }, + { + "project": "viabedrock", + "patch": "0064-evaluate-named-actor-properties.patch", + "reason": "Publish native actor snapshots and per-actor lifetimes before pairing remote movement." + }, + { + "project": "viabedrock", + "patch": "0092-retain-completed-client-prediction-frames.patch", + "reason": "Retain actor spatial admission and reset handling without changing their independent collision history." + } + ] } ], "deferred": [ diff --git a/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md index f74e5649..b9907112 100644 --- a/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md +++ b/patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md @@ -20,6 +20,6 @@ The registered-handler regression decodes Java spawn, stationary movement, and s ## Remaining limits -The longer Z-only sequence exposes a body-orientation mismatch after a large vertical excursion and later movement. The [actor runtime evidence](../../viafabricplus-bedrock/upstreamable/0008-share-native-actor-and-resource-runtime.pr.md) identifies the host distance cutoff and different intermediate steps. The cache correction does not establish complete pose or trajectory parity. Native interpolation timing, other spawn poses, arbitrary actors, mounts, server damage, and complete skin or interaction parity remain unverified. +The longer Z-only sequence also exercises interpolation after a large vertical excursion. The [remote movement integration](../../viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md) covers that correction and its current limits. The cache correction alone does not establish pose or trajectory parity. Full readiness, timing, other spawn poses, mounts, server damage, and complete skin or interaction parity remain unverified. One extended proxy join timed out during resource-pack negotiation before gameplay. It is excluded from validation. The fresh-server retry passes; reconnect robustness remains unverified. diff --git a/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.patch b/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.patch new file mode 100644 index 00000000..4f3c6b00 --- /dev/null +++ b/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.patch @@ -0,0 +1,648 @@ +From 104f1c47db9bb16d6f77ef331cc3ca6ef3b075f1 Mon Sep 17 00:00:00 2001 +From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> +Date: Mon, 2 Nov 2026 15:43:18 +0100 +Subject: [PATCH] feat(players): transport remote movement modes and targets + +Official Bedrock 1.26.60.5, protocol 2225, smooths admitted normal moves +through three float-rounded positions, including large excursions. +Java position packets lose the source mode and apply a different curve. +Java rotation-only packets also quantize pitch and omit the head target. + +Publish a negotiated receipt before each remote player's translated +position or rotation packet. Retain native inputs, source mode, known +riding, UUID, lifetime, raw actor flags and unsigned duration. Carry the +exact Java target separately, including its packet family. Preserve the +existing float/double Y arithmetic and byte-angle translation so pairing +does not depend on reconstructing either packet family's rounding. + +Keep raw head-only pitch and head yaw. Its supplied position and +conflicting main yaw remain inert. Current native mode helper +0x1402d92c0 leaves pending position and main-rotation targets intact, +applies pitch immediately, and refreshes the independent head counter. +The client can reproduce this branch without changing legacy packets. + +Consume receipts once in connection order. Retire unmatched batches and +bound pending state so modes cannot cross packet, actor or connection +lifetimes. Exclude local players, generic actors and unavailable native +state. Keep absolute/delta receive caches and collision history separate +from normal player movement and client interpolation. +--- + .../api/model/RemotePlayerMovement.java | 88 +++++++ + .../model/RemotePlayerMovementTracker.java | 47 ++++ + .../viabedrock/api/model/entity/Entity.java | 29 ++ + .../protocol/packet/EntityPackets.java | 3 + + .../protocol/packet/OtherPlayerPackets.java | 4 + + .../api/model/RemotePlayerMovementTest.java | 100 +++++++ + .../RemotePlayerMovementPacketTest.java | 248 ++++++++++++++++++ + 7 files changed, 519 insertions(+) + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/RemotePlayerMovement.java + create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/RemotePlayerMovementTracker.java + create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/RemotePlayerMovementTest.java + create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/RemotePlayerMovementPacketTest.java + +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/RemotePlayerMovement.java b/src/main/java/net/raphimc/viabedrock/api/model/RemotePlayerMovement.java +new file mode 100644 +index 00000000..c0f695d8 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/RemotePlayerMovement.java +@@ -0,0 +1,88 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock ++ * Copyright (C) 2026 ViaVersion 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 net.raphimc.viabedrock.protocol.data.ProtocolConstants; ++import net.raphimc.viabedrock.protocol.model.Position3f; ++ ++import java.util.Objects; ++import java.util.UUID; ++ ++/** Native inputs paired with their next translated Java position or rotation packet. */ ++public record RemotePlayerMovement(int entityId, UUID uuid, UUID lifetime, int mode, boolean riding, ++ Position3f position, Position3f rotation, float originOffset, ++ short flags, long duration, boolean onGround, JavaTarget javaTarget) { ++ ++ public static final int ACTOR = -1; ++ public static final String CHANNEL = "viabedrock:remote_player_movement"; ++ public static final int MAX_PAYLOAD_BYTES = 128; ++ public static final Type TYPE = new Type<>(RemotePlayerMovement.class) { ++ @Override ++ public RemotePlayerMovement read(final ByteBuf buffer) { ++ if (buffer.readableBytes() > MAX_PAYLOAD_BYTES || Types.VAR_INT.readPrimitive(buffer) != 1 ++ || Types.VAR_INT.readPrimitive(buffer) != ProtocolConstants.BEDROCK_PROTOCOL_VERSION) { ++ throw new IllegalArgumentException("Unsupported remote player movement"); ++ } ++ final var value = new RemotePlayerMovement(Types.VAR_INT.readPrimitive(buffer), Types.UUID.read(buffer), ++ Types.UUID.read(buffer), buffer.readByte(), buffer.readBoolean(), ++ new Position3f(buffer.readFloat(), buffer.readFloat(), buffer.readFloat()), ++ new Position3f(buffer.readFloat(), buffer.readFloat(), buffer.readFloat()), buffer.readFloat(), ++ buffer.readUnsignedByte(), buffer.readLong(), buffer.readBoolean(), ++ new JavaTarget(buffer.readBoolean(), buffer.readDouble(), buffer.readDouble(), buffer.readDouble(), ++ buffer.readFloat(), buffer.readFloat(), buffer.readBoolean())); ++ if (buffer.isReadable()) { ++ throw new IllegalArgumentException("Trailing remote player movement data"); ++ } ++ return value; ++ } ++ ++ @Override ++ public void write(final ByteBuf buffer, final RemotePlayerMovement value) { ++ Types.VAR_INT.writePrimitive(buffer, 1); ++ Types.VAR_INT.writePrimitive(buffer, ProtocolConstants.BEDROCK_PROTOCOL_VERSION); ++ Types.VAR_INT.writePrimitive(buffer, value.entityId); ++ Types.UUID.write(buffer, value.uuid); ++ Types.UUID.write(buffer, value.lifetime); ++ buffer.writeByte(value.mode).writeBoolean(value.riding); ++ buffer.writeFloat(value.position.x()).writeFloat(value.position.y()).writeFloat(value.position.z()); ++ buffer.writeFloat(value.rotation.x()).writeFloat(value.rotation.y()).writeFloat(value.rotation.z()); ++ buffer.writeFloat(value.originOffset).writeByte(value.flags).writeLong(value.duration).writeBoolean(value.onGround); ++ final var target = value.javaTarget; ++ buffer.writeBoolean(target.position).writeDouble(target.x).writeDouble(target.y).writeDouble(target.z); ++ buffer.writeFloat(target.yaw).writeFloat(target.pitch).writeBoolean(target.onGround); ++ } ++ }; ++ ++ public RemotePlayerMovement { ++ Objects.requireNonNull(uuid); ++ Objects.requireNonNull(lifetime); ++ Objects.requireNonNull(javaTarget); ++ if (entityId < 0 || mode < ACTOR || mode > 3 || flags < 0 || flags > 255 ++ || !finite(position) || !finite(rotation) || !Float.isFinite(originOffset) ++ || javaTarget.position != (mode != 3)) { ++ throw new IllegalArgumentException("Invalid remote player movement"); ++ } ++ } ++ ++ public static boolean finite(final Position3f value) { ++ return value != null && Float.isFinite(value.x()) && Float.isFinite(value.y()) && Float.isFinite(value.z()); ++ } ++ ++ public record JavaTarget(boolean position, double x, double y, double z, float yaw, float pitch, boolean onGround) { ++ ++ public JavaTarget { ++ if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(z) || !Float.isFinite(yaw) || !Float.isFinite(pitch) ++ || !position && (x != 0 || y != 0 || z != 0)) { ++ throw new IllegalArgumentException("Invalid translated remote player target"); ++ } ++ } ++ ++ } ++ ++} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/RemotePlayerMovementTracker.java b/src/main/java/net/raphimc/viabedrock/api/model/RemotePlayerMovementTracker.java +new file mode 100644 +index 00000000..1e88b1de +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/api/model/RemotePlayerMovementTracker.java +@@ -0,0 +1,47 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model; ++ ++import java.util.ArrayDeque; ++import java.util.UUID; ++ ++/** Connection-scoped FIFO pairing; an unmatched packet cannot reuse an old mode. */ ++public final class RemotePlayerMovementTracker { ++ ++ private static final int MAX_PENDING = 256; ++ private final ArrayDeque pending = new ArrayDeque<>(); ++ private boolean disabled; ++ ++ public void accept(final RemotePlayerMovement movement) { ++ if (this.disabled) { ++ return; ++ } ++ if (this.pending.size() == MAX_PENDING) { ++ this.pending.clear(); ++ this.disabled = true; ++ return; ++ } ++ this.pending.addLast(movement); ++ } ++ ++ public RemotePlayerMovement consume(final int entityId, final RemotePlayerMovement.JavaTarget target, final UUID uuid, final UUID lifetime) { ++ final var next = this.pending.pollFirst(); ++ if (next == null) { ++ return null; ++ } ++ if (next.entityId() != entityId || !next.javaTarget().equals(target) || !next.uuid().equals(uuid) || !next.lifetime().equals(lifetime)) { ++ this.pending.clear(); ++ return null; ++ } ++ return next; ++ } ++ ++ public void clear() { ++ this.pending.clear(); ++ this.disabled = false; ++ } ++ ++} +diff --git a/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java b/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java +index b47f5471..2dd368c8 100644 +--- a/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java ++++ b/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java +@@ -29,6 +29,7 @@ import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets + import net.raphimc.viabedrock.ViaBedrock; + import net.raphimc.viabedrock.api.model.MovementEffect; + import net.raphimc.viabedrock.api.model.ActorSpatialCommand; ++import net.raphimc.viabedrock.api.model.RemotePlayerMovement; + import net.raphimc.viabedrock.api.model.NativeActorMessage; + import net.raphimc.viabedrock.api.model.NativeActorHitboxes; + import net.raphimc.viabedrock.api.util.EnumUtil; +@@ -591,6 +592,34 @@ public class Entity { + } + } + ++ /** Preserve the source mode without changing the legacy Java translation or actor receive cache. */ ++ public final void sendRemotePlayerMovement(final int mode, final boolean riding, final short flags, final long duration) { ++ this.sendRemotePlayerMovement(mode, riding, flags, duration, this.rotation); ++ } ++ ++ public final void sendRemotePlayerMovement(final int mode, final boolean riding, final short flags, final long duration, final Position3f sourceRotation) { ++ final var channels = this.user == null ? null : this.user.get(ChannelStorage.class); ++ if (!(this instanceof PlayerEntity) || this instanceof ClientPlayerEntity || channels == null ++ || !channels.hasChannel(RemotePlayerMovement.CHANNEL) ++ || !this.nativeStateAvailable() || !RemotePlayerMovement.finite(this.position) ++ || !RemotePlayerMovement.finite(sourceRotation) ++ || this.user.get(EntityTracker.class).getEntityByUid(this.uniqueId()) != this) { ++ return; ++ } ++ this.sendNativeActorSnapshot(); ++ final boolean position = mode != 3; ++ final double y = mode == RemotePlayerMovement.ACTOR ? this.javaPosition().y() : (double) this.position.y() - this.eyeOffset(); ++ final var target = new RemotePlayerMovement.JavaTarget(position, position ? this.position.x() : 0, position ? y : 0, position ? this.position.z() : 0, ++ position ? this.javaRotation().y() : MathUtil.byte2Float(MathUtil.float2Byte(this.rotation.y())), ++ position ? this.javaRotation().x() : MathUtil.byte2Float(MathUtil.float2Byte(this.rotation.x())), this.isOnGround()); ++ final var movement = new RemotePlayerMovement(this.javaId(), this.javaUuid(), ++ this.nativeLifetime, mode, riding, this.position, sourceRotation, this.eyeOffset(), flags, duration, this.isOnGround(), target); ++ final var payload = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, this.user); ++ payload.write(Types.STRING, RemotePlayerMovement.CHANNEL); ++ payload.write(RemotePlayerMovement.TYPE, movement); ++ payload.send(BedrockProtocol.class); ++ } ++ + public final ActorSpatialCommand nativeSpatialCommand() { + final var tracker = this.user == null ? null : this.user.get(EntityTracker.class); + // The horse writer sets VehicleInputIntent.mIsClientPredicted only for a saddled, tamed local controller. +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 4578a73f..6f65acbc 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +@@ -38,6 +38,7 @@ 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.RemotePlayerMovement; + import net.raphimc.viabedrock.api.model.MovementCorrectionTiming; + import net.raphimc.viabedrock.api.model.NativeAnimationMessage; + import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity; +@@ -294,6 +295,7 @@ public final class EntityPackets { + entity.setPosition(position); + entity.setRotation(new Position3f(pitch, yaw, headYaw)); + entity.setOnGround(onGround); ++ entity.sendRemotePlayerMovement(RemotePlayerMovement.ACTOR, false, flags, 0); + + wrapper.write(Types.VAR_INT, entity.javaId()); // entity id + wrapper.write(Types.VAR_INT, 1); // Stepped +@@ -380,6 +382,7 @@ public final class EntityPackets { + PacketFactory.sendJavaRotateHead(wrapper.user(), entity); + } + entity.setOnGround(onGround); ++ entity.sendRemotePlayerMovement(RemotePlayerMovement.ACTOR, false, flags, interpolationTicks); + + wrapper.write(Types.VAR_INT, entity.javaId()); // entity id + wrapper.write(Types.VAR_INT, 1); // Stepped +diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java +index 8b37d578..1bd8cc1b 100644 +--- a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java ++++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java +@@ -186,6 +186,8 @@ public final class OtherPlayerPackets { + } + if (mode == PositionMode.OnlyHeadRot) { + entity.setRotation(new Position3f(rotation.x(), entity.rotation().y(), entity.rotation().z())); ++ entity.sendRemotePlayerMovement(mode.getValue(), ridingEntityRuntimeId != 0 ++ && entityTracker.getEntityByRid(ridingEntityRuntimeId) != null, (short) (onGround ? 1 : 0), 0, rotation); + wrapper.setPacketType(ClientboundPackets26_3.MOVE_ENTITY_ROT); + wrapper.write(Types.VAR_INT, entity.javaId()); // entity id + wrapper.write(Types.BOOLEAN, entity.isOnGround()); // on ground +@@ -208,6 +210,8 @@ public final class OtherPlayerPackets { + return; + } + ++ entity.sendRemotePlayerMovement(mode.getValue(), ridingEntityRuntimeId != 0 ++ && entityTracker.getEntityByRid(ridingEntityRuntimeId) != null, (short) (onGround ? 1 : 0), 3); + wrapper.write(Types.VAR_INT, entity.javaId()); // entity id + wrapper.write(Types.VAR_INT, 1); // Stepped + wrapper.write(Types.VAR_INT, 1); // 1 step +diff --git a/src/test/java/net/raphimc/viabedrock/api/model/RemotePlayerMovementTest.java b/src/test/java/net/raphimc/viabedrock/api/model/RemotePlayerMovementTest.java +new file mode 100644 +index 00000000..0b199629 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/api/model/RemotePlayerMovementTest.java +@@ -0,0 +1,100 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.api.model; ++ ++import io.netty.buffer.Unpooled; ++import net.raphimc.viabedrock.protocol.model.Position3f; ++import org.junit.jupiter.api.Test; ++ ++import java.util.UUID; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class RemotePlayerMovementTest { ++ ++ private static final UUID PLAYER = new UUID(1, 2); ++ private static final UUID LIFETIME = new UUID(3, 4); ++ ++ @Test ++ void wireRoundTripPreservesResetModesAndUnsignedActorDuration() { ++ final var bytes = Unpooled.buffer(); ++ try { ++ for (int mode = RemotePlayerMovement.ACTOR; mode <= 3; mode++) { ++ final var source = movement(mode, PLAYER, LIFETIME); ++ RemotePlayerMovement.TYPE.write(bytes, source); ++ assertTrue(bytes.readableBytes() <= RemotePlayerMovement.MAX_PAYLOAD_BYTES); ++ assertEquals(source, RemotePlayerMovement.TYPE.read(bytes)); ++ assertFalse(bytes.isReadable()); ++ bytes.clear(); ++ } ++ RemotePlayerMovement.TYPE.write(bytes, movement(0, PLAYER, LIFETIME)); ++ bytes.setByte(0, 2); ++ assertThrows(IllegalArgumentException.class, () -> RemotePlayerMovement.TYPE.read(bytes)); ++ bytes.clear(); ++ RemotePlayerMovement.TYPE.write(bytes, movement(0, PLAYER, LIFETIME)); ++ bytes.writeByte(0); ++ assertThrows(IllegalArgumentException.class, () -> RemotePlayerMovement.TYPE.read(bytes)); ++ } finally { ++ bytes.release(); ++ } ++ } ++ ++ @Test ++ void pairingPreservesOrderAndConsumesEachModeOnce() { ++ final var tracker = new RemotePlayerMovementTracker(); ++ for (int mode = 0; mode <= 3; mode++) { ++ tracker.accept(movement(mode, PLAYER, LIFETIME)); ++ } ++ for (int mode = 0; mode <= 3; mode++) { ++ final var expected = movement(mode, PLAYER, LIFETIME); ++ assertEquals(expected, tracker.consume(17, expected.javaTarget(), PLAYER, LIFETIME)); ++ } ++ assertNull(tracker.consume(17, movement(0, PLAYER, LIFETIME).javaTarget(), PLAYER, LIFETIME)); ++ } ++ ++ @Test ++ void unmatchedPacketOrReusedIdentityCannotApplyAnOlderMode() { ++ for (int mismatch = 0; mismatch < 3; mismatch++) { ++ final var tracker = new RemotePlayerMovementTracker(); ++ final var source = movement(0, PLAYER, LIFETIME); ++ tracker.accept(source); ++ tracker.accept(movement(2, PLAYER, LIFETIME)); ++ final var target = source.javaTarget(); ++ assertNull(tracker.consume(mismatch == 0 ? 99 : 17, target, ++ mismatch == 1 ? new UUID(5, 6) : PLAYER, mismatch == 2 ? new UUID(7, 8) : LIFETIME)); ++ assertNull(tracker.consume(17, target, PLAYER, LIFETIME)); ++ tracker.accept(source); ++ assertEquals(source, tracker.consume(17, target, PLAYER, LIFETIME)); ++ } ++ } ++ ++ @Test ++ void overflowUsesLegacyMovementUntilTheConnectionIsCleared() { ++ final var tracker = new RemotePlayerMovementTracker(); ++ final var source = movement(0, PLAYER, LIFETIME); ++ for (int i = 0; i < 257; i++) { ++ tracker.accept(source); ++ } ++ assertNull(tracker.consume(17, source.javaTarget(), PLAYER, LIFETIME)); ++ tracker.accept(source); ++ assertNull(tracker.consume(17, source.javaTarget(), PLAYER, LIFETIME)); ++ tracker.clear(); ++ tracker.accept(source); ++ assertEquals(source, tracker.consume(17, source.javaTarget(), PLAYER, LIFETIME)); ++ tracker.accept(source); ++ tracker.clear(); ++ assertNull(tracker.consume(17, source.javaTarget(), PLAYER, LIFETIME)); ++ } ++ ++ private static RemotePlayerMovement movement(final int mode, final UUID uuid, final UUID lifetime) { ++ return new RemotePlayerMovement(17, uuid, lifetime, mode, true, ++ new Position3f(1.5F, 73.24002F, 4.5F), new Position3f(22.5F, 90, 45), ++ 1.62001F, (short) 255, -1L, true, ++ new RemotePlayerMovement.JavaTarget(mode != 3, mode == 3 ? 0 : 1.5D, mode == 3 ? 0 : 71.62001073360443D, ++ mode == 3 ? 0 : 4.5D, 90, 22.5F, true)); ++ } ++ ++} +diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/RemotePlayerMovementPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/RemotePlayerMovementPacketTest.java +new file mode 100644 +index 00000000..8cbe8670 +--- /dev/null ++++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/RemotePlayerMovementPacketTest.java +@@ -0,0 +1,248 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock ++ * Copyright (C) 2026 ViaVersion and contributors ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++package net.raphimc.viabedrock.protocol.packet; ++ ++import com.viaversion.viaversion.api.connection.ProtocolInfo; ++import com.viaversion.viaversion.api.connection.UserConnection; ++import com.viaversion.viaversion.api.minecraft.entities.EntityTypes26_3; ++import com.viaversion.viaversion.api.protocol.packet.PacketType; ++import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; ++import com.viaversion.viaversion.api.protocol.packet.State; ++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.ByteBuf; ++import io.netty.buffer.Unpooled; ++import net.raphimc.viabedrock.api.model.NativeActorMessage; ++import net.raphimc.viabedrock.api.model.RemotePlayerMovement; ++import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity; ++import net.raphimc.viabedrock.api.model.entity.Entity; ++import net.raphimc.viabedrock.api.model.entity.PlayerEntity; ++import net.raphimc.viabedrock.api.util.PacketFactory; ++import net.raphimc.viabedrock.api.util.MathUtil; ++import net.raphimc.viabedrock.protocol.BedrockProtocol; ++import net.raphimc.viabedrock.protocol.ClientboundBedrockPackets; ++import net.raphimc.viabedrock.protocol.model.PlayerAbilities; ++import net.raphimc.viabedrock.protocol.model.Position3f; ++import net.raphimc.viabedrock.protocol.storage.ChannelStorage; ++import net.raphimc.viabedrock.protocol.storage.EntityTracker; ++import net.raphimc.viabedrock.protocol.types.BedrockTypes; ++import org.junit.jupiter.api.Test; ++import org.mockito.MockedStatic; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.Set; ++import java.util.UUID; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class RemotePlayerMovementPacketTest { ++ ++ @Test ++ void registeredMovementHandlersDeliverModesAndExactJavaTargets() throws Exception { ++ final var fixture = new Fixture(); ++ try (var packets = mockStatic(PacketWrapper.class); ++ var helpers = mockStatic(PacketFactory.class)) { ++ fixture.capture(packets); ++ for (int mode = 0; mode <= 3; mode++) { ++ fixture.receipts.clear(); ++ final var input = Unpooled.buffer(); ++ try { ++ fixture.playerMove(input, mode, mode == 1 ? 102 : 0); ++ final var packet = fixture.translate(ClientboundBedrockPackets.MOVE_PLAYER, input); ++ assertFalse(input.isReadable()); ++ final var receipt = fixture.receipts.getFirst(); ++ assertEquals(1, fixture.receipts.size()); ++ assertEquals(mode, receipt.mode()); ++ assertEquals(mode == 1, receipt.riding()); ++ assertEquals(fixture.player.nativeActorSnapshot().lifetime(), receipt.lifetime()); ++ assertEquals(fixture.player.javaUuid(), receipt.uuid()); ++ assertEquals(receipt.javaTarget(), target(packet)); ++ assertEquals(Position3f.ZERO, fixture.player.lastReceivedPosition()); ++ if (mode == 3) { ++ assertEquals(new Position3f(22.75F, 135, -135), receipt.rotation()); ++ assertEquals(90F, receipt.javaTarget().yaw()); ++ assertEquals(22.5F, receipt.javaTarget().pitch()); ++ assertEquals(new Position3f(1.5F, 73.24002F, 4.5F), receipt.position()); ++ } ++ } finally { ++ input.release(); ++ } ++ } ++ for (boolean delta : new boolean[]{false, true}) { ++ fixture.receipts.clear(); ++ final var input = Unpooled.buffer(); ++ try { ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, fixture.player.runtimeId()); ++ if (delta) { ++ input.writeBoolean(true); ++ BedrockTypes.FLOAT_LE.write(input, 2.5F); ++ input.writeBoolean(false).writeBoolean(false); ++ input.writeBoolean(false).writeBoolean(false).writeBoolean(false); ++ input.writeBoolean(true).writeBoolean(false).writeBoolean(false).writeBoolean(true); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, -1L); ++ } else { ++ input.writeByte(1); ++ BedrockTypes.POSITION_3F.write(input, new Position3f(1.5F, 73.24002F, 4.5F)); ++ input.writeByte(16).writeByte(64).writeByte(32); ++ } ++ final var packet = fixture.translate(delta ? ClientboundBedrockPackets.MOVE_ENTITY_DELTA : ClientboundBedrockPackets.MOVE_ENTITY_ABSOLUTE, input); ++ final var receipt = fixture.receipts.getFirst(); ++ assertEquals(1, fixture.receipts.size()); ++ assertEquals(RemotePlayerMovement.ACTOR, receipt.mode()); ++ assertEquals(delta ? -1L : 0L, receipt.duration()); ++ assertEquals(delta ? 249 : 1, receipt.flags()); ++ assertEquals(receipt.javaTarget(), target(packet)); ++ assertFalse(input.isReadable()); ++ } finally { ++ input.release(); ++ } ++ } ++ } ++ } ++ ++ @Test ++ void unnegotiatedOrUnpublishedActorsKeepLegacyPackets() throws Exception { ++ for (int state = 0; state < 4; state++) { ++ final var fixture = new Fixture(); ++ if (state == 0) { ++ fixture.channels.removeChannels(Set.of(RemotePlayerMovement.CHANNEL)); ++ } ++ if (state == 1) { ++ when(fixture.tracker.nativeActorsJoined()).thenReturn(false); ++ } ++ if (state == 2) { ++ when(fixture.tracker.getEntityByUid(fixture.player.uniqueId())).thenReturn(null); ++ } ++ if (state == 3) { ++ final var local = new ClientPlayerEntity(fixture.user, 101, new UUID(5, 6), new PlayerAbilities(23, (byte) 0, (byte) 0)); ++ when(fixture.tracker.getEntityByRid(101)).thenReturn(local); ++ } ++ try (var packets = mockStatic(PacketWrapper.class); ++ var helpers = mockStatic(PacketFactory.class)) { ++ fixture.capture(packets); ++ final var input = Unpooled.buffer(); ++ try { ++ fixture.playerMove(input, 0, 0); ++ assertEquals(ClientboundPackets26_3.ENTITY_POSITION_SYNC, fixture.translate(ClientboundBedrockPackets.MOVE_PLAYER, input).getPacketType()); ++ assertTrue(fixture.receipts.isEmpty()); ++ } finally { ++ input.release(); ++ } ++ fixture.boat.sendRemotePlayerMovement(0, false, (short) 0, 3); ++ assertTrue(fixture.receipts.isEmpty()); ++ } ++ } ++ } ++ ++ private static RemotePlayerMovement.JavaTarget target(final PacketWrapper packet) throws Exception { ++ final var bytes = Unpooled.buffer(); ++ try { ++ packet.writeToBuffer(bytes); ++ final int type = Types.VAR_INT.readPrimitive(bytes); ++ assertEquals(7, Types.VAR_INT.readPrimitive(bytes)); ++ if (type == ClientboundPackets26_3.MOVE_ENTITY_ROT.getId()) { ++ final boolean ground = bytes.readBoolean(); ++ final var target = new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, ++ MathUtil.byte2Float(bytes.readByte()), MathUtil.byte2Float(bytes.readByte()), ground); ++ assertFalse(bytes.isReadable()); ++ return target; ++ } ++ assertEquals(ClientboundPackets26_3.ENTITY_POSITION_SYNC.getId(), type); ++ assertEquals(1, Types.VAR_INT.readPrimitive(bytes)); ++ assertEquals(1, Types.VAR_INT.readPrimitive(bytes)); ++ final double x = bytes.readDouble(); ++ final double y = bytes.readDouble(); ++ final double z = bytes.readDouble(); ++ assertEquals(3, Types.VAR_INT.readPrimitive(bytes)); ++ final var target = new RemotePlayerMovement.JavaTarget(true, x, y, z, bytes.readFloat(), bytes.readFloat(), bytes.readBoolean()); ++ assertFalse(bytes.isReadable()); ++ return target; ++ } finally { ++ bytes.release(); ++ } ++ } ++ ++ private static final class Fixture { ++ private final UserConnection user = mock(UserConnection.class); ++ private final EntityTracker tracker = spy(new EntityTracker(this.user)); ++ private final ChannelStorage channels = new ChannelStorage(); ++ private final PlayerEntity player; ++ private final Entity boat; ++ private final List receipts = new ArrayList<>(); ++ ++ private Fixture() { ++ final var info = mock(ProtocolInfo.class); ++ when(this.user.get(any())).thenAnswer(call -> mock((Class) call.getArgument(0))); ++ when(this.user.getProtocolInfo()).thenReturn(info); ++ when(info.getServerState()).thenReturn(State.PLAY); ++ when(this.user.get(EntityTracker.class)).thenReturn(this.tracker); ++ when(this.user.get(ChannelStorage.class)).thenReturn(this.channels); ++ when(this.tracker.nativeActorsJoined()).thenReturn(false); ++ this.channels.addChannels(Set.of(NativeActorMessage.CURRENT_CHANNEL, RemotePlayerMovement.CHANNEL)); ++ this.player = new PlayerEntity(this.user, 101, 7, new UUID(1, 2), new PlayerAbilities(21, (byte) 0, (byte) 0)); ++ this.tracker.addEntity(this.player, false); ++ when(this.tracker.getEntityByUid(this.player.uniqueId())).thenReturn(this.player); ++ this.boat = new Entity(this.user, 22, 102, "minecraft:boat", 8, new UUID(3, 4), EntityTypes26_3.OAK_BOAT); ++ this.tracker.addEntity(this.boat, false); ++ this.player.setPosition(Position3f.ZERO); ++ this.player.setRotation(Position3f.ZERO); ++ when(this.tracker.nativeActorsJoined()).thenReturn(true); ++ } ++ ++ private void capture(final MockedStatic packets) { ++ packets.when(() -> PacketWrapper.create(any(PacketType.class), eq(this.user))).thenAnswer(call -> { ++ final var packet = spy(new PacketWrapperImpl(call.getArgument(0), null, this.user)); ++ doAnswer(_ -> { ++ final var bytes = Unpooled.buffer(); ++ try { ++ packet.writeToBuffer(bytes); ++ assertEquals(ClientboundPackets26_3.CUSTOM_PAYLOAD.getId(), Types.VAR_INT.readPrimitive(bytes)); ++ if (Types.STRING.read(bytes).equals(RemotePlayerMovement.CHANNEL)) { ++ this.receipts.add(RemotePlayerMovement.TYPE.read(bytes)); ++ } ++ } finally { ++ bytes.release(); ++ } ++ return null; ++ }).when(packet).send(eq(BedrockProtocol.class), eq(true)); ++ return packet; ++ }); ++ } ++ ++ private void playerMove(final ByteBuf input, final int mode, final long riding) { ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, this.player.runtimeId()); ++ BedrockTypes.POSITION_3F.write(input, mode == 3 ? new Position3f(37, 123, 19) : new Position3f(1.5F, 73.24002F, 4.5F)); ++ BedrockTypes.POSITION_3F.write(input, mode == 3 ? new Position3f(22.75F, 135, -135) : new Position3f(22.5F, 90, 45)); ++ input.writeByte(mode).writeBoolean(true); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, riding); ++ input.writeBoolean(false); ++ BedrockTypes.UNSIGNED_VAR_LONG.write(input, 200L); ++ } ++ ++ private PacketWrapper translate(final ClientboundBedrockPackets type, final ByteBuf input) throws Exception { ++ final var packet = new PacketWrapperImpl(type, input, this.user); ++ new TestProtocol().translate(packet); ++ return packet; ++ } ++ } ++ ++ private static final class TestProtocol extends BedrockProtocol { ++ private TestProtocol() { ++ OtherPlayerPackets.register(this); ++ EntityPackets.register(this); ++ } ++ ++ private void translate(final PacketWrapper packet) throws Exception { ++ final var mapping = this.clientboundMappings.mappedPacket(State.PLAY, packet.getId()); ++ mapping.applyType(packet); ++ mapping.handler().handle(packet); ++ } ++ } ++ ++} +-- +2.56.0 diff --git a/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md b/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md new file mode 100644 index 00000000..70a33c64 --- /dev/null +++ b/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md @@ -0,0 +1,17 @@ +# Preserve remote player movement modes + +Java position packets discard the original normal, respawn, and teleport modes. Rotation-only packets also quantize pitch and omit the native head target. A negotiated receipt retains native coordinates, rotations, flags, duration, mode, UUID, and lifetime. Existing Java packets stay available to older clients. + +The receipt precedes its translated position or rotation packet. It carries the exact Java target separately, including packet family. This preserves the different Y arithmetic and byte-angle rounding without reconstructing either packet. A bounded connection queue consumes each receipt once. A mismatch clears pending receipts. Overflow disables pairing until connection reset. + +## Evidence and validation + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls use protocol 2225. Current native observation distinguishes normal interpolation from Java's large-distance snap and intermediate curve. Original mode-helper instructions also preserve pending movement during head-only updates and apply raw pitch immediately. + +Focused tests invoke the registered handlers and decode their actual output. They cover normal, respawn, teleport, head-only, known riding, absolute, and sparse updates. Fractional pitch and conflicting yaw/head values distinguish raw native inputs from the translated byte-angle target. Other cases cover unsigned duration, packet-family correlation, actor replacement, connection reset, overflow, capability gates, and local-player exclusion. The focused suite and both Checkstyle tasks pass. + +The [client integration](../../viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md) records current native instruction controls, installed comparisons, and complete build validation. + +## Limits + +This patch transports inputs. It does not change interpolation, actor receive caches, collision history, or body-control formulas. Full native readiness, region admission, scheduling, reset behavior, mounted movement, and pose parity remain open. diff --git a/patches/viafabricplus-bedrock/series.json b/patches/viafabricplus-bedrock/series.json index 59b1186b..d377ed4d 100644 --- a/patches/viafabricplus-bedrock/series.json +++ b/patches/viafabricplus-bedrock/series.json @@ -371,6 +371,32 @@ "reason": "Use the accepted, protocol-bound rendering metadata lifecycle on direct and ViaProxy connections." } ] + }, + { + "file": "0041-interpolate-native-remote-player-movement.patch", + "title": "fix(players): interpolate negotiated remote movement as native floats", + "prerequisites": [ + { + "project": "viabedrock", + "patch": "0107-transport-remote-player-movement-modes.patch", + "reason": "Publish the typed movement receipt and exact target/lifetime pairing API." + }, + { + "project": "viafabricplus-bedrock", + "patch": "0008-share-native-actor-and-resource-runtime.patch", + "reason": "Retain native actor lifetimes and body-control ownership before integrating remote interpolation." + }, + { + "project": "viafabricplus-bedrock", + "patch": "0020-apply-server-movement-and-camera-permissions.patch", + "reason": "Recognize negotiated Bedrock connections across direct and ViaProxy routes." + }, + { + "project": "viafabricplus-bedrock", + "patch": "0025-send-completed-local-physics-to-viabedrock.patch", + "reason": "Use the existing physical replay scope to prevent live remote interpolation during historical simulation." + } + ] } ], "deferred": [ 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 c2e336f0..76d51b04 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 @@ -38,13 +38,9 @@ Same-identity controls extend the fixture through a full yaw cycle, a pitch-only Strict protocol-2225 decoders verify one spawn and identical 71 MovePlayer payloads per route. Three settled frames per phase compare six markers. Across 11 phases, both routes have median marker centroids within 0.30% of each viewport axis and bounding coordinates within 0.42% of native. The accepted Java fixtures disable their onboarding tutorial through its public API before observations. An earlier obstructed westward frame does not support pose comparison. All 33 recorded process identities are gone, and cleanup restored the native profile and server configuration exactly. -The [mixed movement control](../../viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md) exposes a separate interpolation defect after a large vertical move. Current native observation confirms that all selected normal moves pass readiness, admission, and region checks. Native retains three position steps across the large move. Java instead applies its 64-block distance cutoff, snaps to the target, and resets the previous position. Body control then receives different displacement history. +The [remote movement integration](0041-interpolate-native-remote-player-movement.pr.md) covers interpolation after a large vertical excursion. It retains native movement modes and float steps while preserving host fallback and actor lifetimes. Rebuilt direct and proxy controls restore the selected settled marker projections, including leg visibility. Its evidence describes the observed current native steps, installed comparisons, and remaining limits. -The host interpolation curve also differs during small moves. One native transition advances X through 2.1667, 1.8333, and 1.5. Java advances through 2.1667, 1.75, and 1.5. Current Ghidra decompilation and read-only debugger samples establish the native branches and coordinate changes. Complete journals verify identical peer payloads. Debugger timing and native numeric body angles remain outside this comparison. - -The interpolation correction must retain native movement modes. Core currently emits the same three-step Java path for remote normal, teleport, and respawn moves. An unconditional distance-cutoff bypass can therefore change reset behavior. Native step math, mode admission, mounting, component lifetimes, and complete trajectories remain unfinished. - -These controls establish selected settled poses and position endpoints. Exact interpolation timing, movement curves, mounted head updates, ground state, full locomotion animation, and the original custom-skin report remain open. The [pitch-only packet control](../../viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md) describes the retained head rotation semantics. +These controls establish selected settled poses and position steps. Full readiness and region admission, exact timing, mounted head updates, ground state, and locomotion animation remain open. The original custom-skin report also remains unresolved. The [pitch-only packet control](../../viabedrock/upstreamable/0075-encode-head-rotation-in-java-26-3-field-order.pr.md) describes the retained head rotation semantics. Rebuilt direct and ViaProxy controls restore crouching and the native foot anchor with identical skin and posture inputs. The combined four-project build passes 3,335 tests with 325 skipped and no failures or errors. Full stack replay, bundle, artifact checksums, and embedded core-class comparisons pass. diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch new file mode 100644 index 00000000..8f15e45c --- /dev/null +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch @@ -0,0 +1,1002 @@ +From 6ae81e929d0207f136784e938f439e354b47d751 Mon Sep 17 00:00:00 2001 +From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> +Date: Mon, 2 Nov 2026 15:51:01 +0100 +Subject: [PATCH] fix(players): interpolate negotiated remote movement as + native floats + +Official Bedrock 1.26.60.5, protocol 2225, retains three float position +steps across admitted normal movement beyond Java's 64-block cutoff. +Its small-move X samples are 2.166666746, 1.833333373 and 1.5. Java +instead advances through 2.166666657, 1.75 and 1.5. Retain the native +eye origin and float arithmetic inside the entity interpolation phase. + +Decorate the real host handler only for remote players. Pair negotiated +receipts with exact Java packets, UUIDs and native lifetimes. Keep the +position, main-rotation and head counters independent. Preserve forced +target completion and queued handoff. Retain eye coordinates between +steps instead of rounding them through Java's feet origin every tick. + +A head-only packet applies raw pitch immediately and preserves live yaw, +pending position and body targets, and forced queues. Its head target +uses the maximum remaining position/body counter. Pair the rotation-only +Java packet explicitly so the host path cannot cancel pending movement. +Cancel the superseded Java head lerp through its existing public API. + +Return reset modes, known riding, local authority, unticking entities +and unpaired packets to the host path. Preserve ordinary Java movement +history. Retire native state on connection, world, entity, lifetime or +mount changes. Keep body formulas and collision-history ownership intact. + +Current native helper 0x1402d92c0, callback 0x143630d70, angle helper +0x143631440, target producer 0x1422c8d20 and queue kernel 0x143634ae0 +establish these component rules. Instruction controls exclude component +allocation and native scheduler admission. Live comparisons establish +selected steps and poses; complete timing and trajectory parity remain +open alongside reset, mounted and unknown-factory behavior. +--- + .../bedrock/ViaFabricPlusBedrock.java | 2 + + .../MixinRemotePlayerInterpolation.java | 22 ++ + ...xinRemotePlayerMovementPacketListener.java | 54 +++++ + .../input/BedrockRemotePlayerMovement.java | 81 ++++++++ + .../input/RemotePlayerMovementPayload.java | 22 ++ + .../movement/RemotePlayerInterpolation.java | 133 ++++++++++++ + .../RemotePlayerInterpolationHandler.java | 165 +++++++++++++++ + .../viafabricplus-bedrock.mixins.json | 2 + + ...ativeRemoteInterpolationReferenceTest.java | 74 +++++++ + .../RemotePlayerInterpolationHandlerTest.java | 190 ++++++++++++++++++ + .../RemotePlayerInterpolationTest.java | 114 +++++++++++ + 11 files changed, 859 insertions(+) + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerInterpolation.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerMovementPacketListener.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockRemotePlayerMovement.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/RemotePlayerMovementPayload.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolation.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/movement/NativeRemoteInterpolationReferenceTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java + create mode 100644 src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationTest.java + +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +index 8ed6c7a..131e784 100644 +--- a/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/ViaFabricPlusBedrock.java +@@ -90,6 +90,7 @@ import com.viaversion.viafabricplus.bedrock.input.RawInputPayload; + import com.viaversion.viafabricplus.bedrock.input.BedrockItemUse; + import com.viaversion.viafabricplus.bedrock.input.PlayerPredictionPayload; + import com.viaversion.viafabricplus.bedrock.input.BedrockPlayerCorrections; ++import com.viaversion.viafabricplus.bedrock.input.BedrockRemotePlayerMovement; + import com.viaversion.viafabricplus.bedrock.input.BedrockMovementEffects; + import com.viaversion.viafabricplus.bedrock.input.BedrockVehiclePredictions; + import com.viaversion.viafabricplus.bedrock.appearance.BedrockAppearanceStore; +@@ -217,6 +218,7 @@ public final class ViaFabricPlusBedrock implements ViaFabricPlusEntrypoint { + BedrockCommandBlockScreen.register(); + PayloadTypeRegistry.serverboundPlay().register(PlayerPredictionPayload.TYPE, PlayerPredictionPayload.CODEC); + BedrockPlayerCorrections.register(); ++ BedrockRemotePlayerMovement.register(); + BedrockMovementEffects.register(); + BedrockVehiclePredictions.register(); + PayloadTypeRegistry.clientboundPlay().register(InputPermissionsPayload.TYPE, InputPermissionsPayload.CODEC); +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerInterpolation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerInterpolation.java +new file mode 100644 +index 0000000..5836e51 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerInterpolation.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.injection.mixin.features.movement; ++ ++import com.llamalad7.mixinextras.injector.ModifyReturnValue; ++import com.viaversion.viafabricplus.bedrock.movement.RemotePlayerInterpolationHandler; ++import net.minecraft.client.player.RemotePlayer; ++import net.minecraft.world.entity.InterpolationHandler; ++import net.minecraft.world.entity.LivingEntity; ++import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.injection.At; ++ ++@Mixin(LivingEntity.class) ++public abstract class MixinRemotePlayerInterpolation { ++ @ModifyReturnValue(method = "createInterpolationHandler", at = @At("RETURN")) ++ private InterpolationHandler decorateRemotePlayerInterpolation(final InterpolationHandler host) { ++ return (Object) this instanceof RemotePlayer player ? new RemotePlayerInterpolationHandler(player, host) : host; ++ } ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerMovementPacketListener.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerMovementPacketListener.java +new file mode 100644 +index 0000000..c9330c6 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerMovementPacketListener.java +@@ -0,0 +1,54 @@ ++/* ++ * 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.movement; ++ ++import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; ++import com.llamalad7.mixinextras.injector.wrapoperation.Operation; ++import com.viaversion.viafabricplus.bedrock.input.BedrockRemotePlayerMovement; ++import com.viaversion.viafabricplus.bedrock.movement.RemotePlayerInterpolationHandler; ++import net.minecraft.client.Minecraft; ++import net.minecraft.client.multiplayer.ClientPacketListener; ++import net.minecraft.client.player.RemotePlayer; ++import net.minecraft.network.protocol.PacketUtils; ++import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket; ++import net.minecraft.network.protocol.game.ClientboundMoveEntityPacket; ++import org.spongepowered.asm.mixin.Mixin; ++ ++@Mixin(ClientPacketListener.class) ++public abstract class MixinRemotePlayerMovementPacketListener { ++ @WrapMethod(method = "handleEntityPositionSync") ++ private void applyNativeRemoteMovement(final ClientboundEntityPositionSyncPacket packet, final Operation original) { ++ final Minecraft client = Minecraft.getInstance(); ++ PacketUtils.ensureRunningOnSameThread(packet, (ClientPacketListener) (Object) this, client.packetProcessor()); ++ if (client.getConnection() == (Object) this && client.level != null) { ++ final var actor = client.level.getEntity(packet.id()); ++ final var movement = BedrockRemotePlayerMovement.consume(packet, actor); ++ if (actor instanceof RemotePlayer && actor.getInterpolation() instanceof RemotePlayerInterpolationHandler interpolation ++ && interpolation.receive(movement)) { ++ actor.getPositionCodec().setBase(packet.position().endPosition()); ++ actor.setOnGround(packet.onGround()); ++ return; ++ } ++ } ++ original.call(packet); ++ } ++ ++ @WrapMethod(method = "handleMoveEntity") ++ private void applyNativeRemoteHead(final ClientboundMoveEntityPacket packet, final Operation original) { ++ final Minecraft client = Minecraft.getInstance(); ++ PacketUtils.ensureRunningOnSameThread(packet, (ClientPacketListener) (Object) this, client.packetProcessor()); ++ if (client.getConnection() == (Object) this && client.level != null && !packet.hasPosition() && packet.hasRotation()) { ++ final var actor = packet.getEntity(client.level); ++ final var movement = BedrockRemotePlayerMovement.consume(packet, actor); ++ if (actor instanceof RemotePlayer && actor.getInterpolation() instanceof RemotePlayerInterpolationHandler interpolation ++ && interpolation.receive(movement)) { ++ actor.setOnGround(packet.isOnGround()); ++ return; ++ } ++ } ++ original.call(packet); ++ } ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockRemotePlayerMovement.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockRemotePlayerMovement.java +new file mode 100644 +index 0000000..5169027 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockRemotePlayerMovement.java +@@ -0,0 +1,81 @@ ++/* ++ * 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.input; ++ ++import com.viaversion.viafabricplus.bedrock.render.BedrockActorState; ++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.client.multiplayer.ClientLevel; ++import net.minecraft.client.multiplayer.ClientPacketListener; ++import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket; ++import net.minecraft.network.protocol.game.ClientboundMoveEntityPacket; ++import net.minecraft.world.entity.Entity; ++import net.raphimc.viabedrock.api.model.RemotePlayerMovement; ++import net.raphimc.viabedrock.api.model.RemotePlayerMovementTracker; ++ ++public final class BedrockRemotePlayerMovement { ++ private static final RemotePlayerMovementTracker TRACKER = new RemotePlayerMovementTracker(); ++ private static ClientPacketListener connection; ++ private static ClientLevel level; ++ ++ private BedrockRemotePlayerMovement() {} ++ ++ public static void register() { ++ PayloadTypeRegistry.clientboundPlay().register(RemotePlayerMovementPayload.TYPE, RemotePlayerMovementPayload.CODEC); ++ ClientPlayConnectionEvents.INIT.register((listener, client) -> { ++ TRACKER.clear(); ++ connection = listener; ++ level = null; ++ ClientPlayNetworking.registerReceiver(RemotePlayerMovementPayload.TYPE, (payload, context) -> { ++ if (connection == listener && context.client().getConnection() == listener && BedrockInputPermissions.isBedrockConnection()) { ++ bindWorld(context.client()); ++ if (level != null) TRACKER.accept(payload.movement()); ++ } ++ }); ++ }); ++ ClientPlayConnectionEvents.DISCONNECT.register((listener, client) -> { ++ if (connection == listener) { ++ TRACKER.clear(); ++ connection = null; ++ level = null; ++ } ++ }); ++ } ++ ++ private static void bindWorld(final Minecraft client) { ++ if (level != client.level) { ++ TRACKER.clear(); ++ level = client.level; ++ } ++ } ++ ++ public static RemotePlayerMovement consume(final ClientboundEntityPositionSyncPacket packet, final Entity actor) { ++ final Minecraft client = Minecraft.getInstance(); ++ if (!BedrockInputPermissions.isBedrockConnection() || client.getConnection() != connection) { ++ TRACKER.clear(); ++ return null; ++ } ++ bindWorld(client); ++ final var end = packet.position().endPosition(); ++ return TRACKER.consume(packet.id(), new RemotePlayerMovement.JavaTarget(true, end.x, end.y, end.z, ++ packet.yRot(), packet.xRot(), packet.onGround()), ++ actor == null ? null : actor.getUUID(), actor == null ? null : BedrockActorState.liveLifetime(actor.getUUID())); ++ } ++ ++ public static RemotePlayerMovement consume(final ClientboundMoveEntityPacket packet, final Entity actor) { ++ final Minecraft client = Minecraft.getInstance(); ++ if (!BedrockInputPermissions.isBedrockConnection() || client.getConnection() != connection) { ++ TRACKER.clear(); ++ return null; ++ } ++ bindWorld(client); ++ return TRACKER.consume(actor == null ? -1 : actor.getId(), new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, ++ packet.getYRot(), packet.getXRot(), packet.isOnGround()), ++ actor == null ? null : actor.getUUID(), actor == null ? null : BedrockActorState.liveLifetime(actor.getUUID())); ++ } ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/RemotePlayerMovementPayload.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/RemotePlayerMovementPayload.java +new file mode 100644 +index 0000000..e07848c +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/input/RemotePlayerMovementPayload.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.input; ++ ++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.RemotePlayerMovement; ++ ++public record RemotePlayerMovementPayload(RemotePlayerMovement movement) implements CustomPacketPayload { ++ public static final Type TYPE = new Type<>(Identifier.parse(RemotePlayerMovement.CHANNEL)); ++ public static final StreamCodec CODEC = CustomPacketPayload.codec( ++ (payload, buffer) -> RemotePlayerMovement.TYPE.write(buffer, payload.movement), ++ buffer -> new RemotePlayerMovementPayload(RemotePlayerMovement.TYPE.read(buffer))); ++ ++ @Override ++ public Type type() { return TYPE; } ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolation.java +new file mode 100644 +index 0000000..07ca2fe +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolation.java +@@ -0,0 +1,133 @@ ++/* ++ * 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.movement; ++ ++import net.raphimc.viabedrock.api.model.RemotePlayerMovement; ++import net.raphimc.viabedrock.protocol.model.Position3f; ++ ++/** Native float position, main-rotation, and head components with independent counters. */ ++public final class RemotePlayerInterpolation { ++ private Position3f target; ++ private Position3f rotation; ++ private int positionSteps; ++ private int rotationSteps; ++ private int headSteps; ++ private float headTarget; ++ private boolean mustComplete; ++ private Queued queued; ++ ++ public void receive(final RemotePlayerMovement movement) { ++ if (movement.mode() == 3) { ++ this.headTarget = movement.rotation().z(); ++ this.headSteps = Math.max(this.positionSteps, this.rotationSteps); ++ return; ++ } ++ if (movement.mode() == 0) { ++ this.target = movement.position(); ++ this.rotation = movement.rotation(); ++ this.positionSteps = 3; ++ this.rotationSteps = 3; ++ this.headTarget = movement.rotation().z(); ++ this.headSteps = 3; ++ return; ++ } ++ if (movement.mode() != RemotePlayerMovement.ACTOR) { ++ this.clear(); ++ return; ++ } ++ final int steps = Long.compareUnsigned(movement.duration(), 3) < 0 ? 3 : (int) movement.duration(); ++ final boolean forced = (movement.flags() & 8) != 0; ++ if (this.mustComplete && this.positionSteps != 0 && !forced) { ++ this.queued = new Queued(movement.position(), movement.rotation(), steps); ++ } else { ++ this.target = movement.position(); ++ this.positionSteps = steps; ++ this.mustComplete = forced; ++ this.queued = null; ++ } ++ // The remote-player category retains its independent three-step main rotation. ++ this.rotation = movement.rotation(); ++ this.rotationSteps = 3; ++ this.headTarget = movement.rotation().z(); ++ this.headSteps = 3; ++ } ++ ++ public Step tick(final Position3f position, final float pitch, final float yaw, final float previousPitch, final float previousYaw, final float headYaw) { ++ final boolean clearMotion = this.positionSteps > 0; ++ final Position3f next; ++ if (this.positionSteps > 0) { ++ final float inverse = 1F / this.positionSteps; ++ next = this.positionSteps == 1 ? this.target : new Position3f( ++ position.x() + (this.target.x() - position.x()) * inverse, ++ position.y() + (this.target.y() - position.y()) * inverse, ++ position.z() + (this.target.z() - position.z()) * inverse); ++ this.positionSteps--; ++ } else { ++ next = position; ++ } ++ float nextPitch = pitch; ++ float nextYaw = yaw; ++ float oldPitch = previousPitch; ++ float oldYaw = previousYaw; ++ if (this.rotationSteps > 0) { ++ final float inverse = 1F / this.rotationSteps; ++ final float rawPitch = this.rotationSteps == 1 ? this.rotation.x() : pitch + wrap(this.rotation.x() - pitch) * inverse; ++ final float rawYaw = this.rotationSteps == 1 ? this.rotation.y() : yaw + wrap(this.rotation.y() - yaw) * inverse; ++ nextPitch = wrap(rawPitch); ++ nextYaw = wrap(rawYaw); ++ oldPitch = wrap(previousPitch + (nextPitch - rawPitch)); ++ oldYaw = wrap(previousYaw + (nextYaw - rawYaw)); ++ this.rotationSteps--; ++ } ++ float nextHeadYaw = headYaw; ++ if (this.headSteps > 0) { ++ nextHeadYaw = this.headSteps == 1 ? this.headTarget : headYaw + wrap(this.headTarget - headYaw) * (1F / this.headSteps); ++ this.headSteps--; ++ } ++ if (this.queued != null) { ++ if (this.mustComplete && this.positionSteps != 0) { ++ this.queued = new Queued(this.queued.position, this.queued.rotation, Math.max(2, this.queued.steps) - 1); ++ } else { ++ this.target = this.queued.position; ++ this.rotation = this.queued.rotation; ++ this.positionSteps = this.queued.steps; ++ this.rotationSteps = this.queued.steps; ++ this.headTarget = 0F; ++ this.headSteps = 0; ++ this.mustComplete = false; ++ this.queued = null; ++ } ++ } ++ return new Step(next, nextPitch, nextYaw, oldPitch, oldYaw, nextHeadYaw, clearMotion); ++ } ++ ++ public boolean active() { ++ return this.positionSteps != 0 || this.rotationSteps != 0 || this.headSteps != 0; ++ } ++ ++ public Position3f target() { return this.target; } ++ public Position3f rotation() { return this.rotation; } ++ ++ public void clear() { ++ this.target = null; ++ this.rotation = null; ++ this.positionSteps = 0; ++ this.rotationSteps = 0; ++ this.headSteps = 0; ++ this.headTarget = 0F; ++ this.mustComplete = false; ++ this.queued = null; ++ } ++ ++ static float wrap(final float value) { ++ float result = (value + 180F) % 360F; ++ if (result < 0F) result += 360F; ++ return result - 180F; ++ } ++ ++ public record Step(Position3f position, float pitch, float yaw, float previousPitch, float previousYaw, float headYaw, boolean clearMotion) {} ++ private record Queued(Position3f position, Position3f rotation, int steps) {} ++} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java +new file mode 100644 +index 0000000..9961a14 +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java +@@ -0,0 +1,165 @@ ++/* ++ * 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.movement; ++ ++import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; ++import com.viaversion.viafabricplus.bedrock.movement.replay.PhysicalReplayContext; ++import com.viaversion.viafabricplus.bedrock.render.BedrockActorState; ++import net.minecraft.client.Minecraft; ++import net.minecraft.client.multiplayer.ClientLevel; ++import net.minecraft.client.multiplayer.ClientPacketListener; ++import net.minecraft.client.player.RemotePlayer; ++import net.minecraft.core.PositionAndRotation; ++import net.minecraft.world.entity.InterpolationHandler; ++import net.minecraft.world.entity.InterpolationTracker; ++import net.minecraft.world.entity.PositionPath; ++import net.minecraft.world.phys.Vec3; ++import net.raphimc.viabedrock.api.model.RemotePlayerMovement; ++import net.raphimc.viabedrock.protocol.model.Position3f; ++ ++import java.util.UUID; ++ ++/** Decorates the real host handler so ordinary Java and unsupported modes retain their existing path. */ ++public final class RemotePlayerInterpolationHandler implements InterpolationHandler { ++ private final RemotePlayer player; ++ private final InterpolationHandler host; ++ private final RemotePlayerInterpolation nativeInterpolation = new RemotePlayerInterpolation(); ++ private ClientPacketListener connection; ++ private ClientLevel level; ++ private UUID lifetime; ++ private float originOffset; ++ private Position3f eye; ++ private Vec3 appliedPosition; ++ ++ public RemotePlayerInterpolationHandler(final RemotePlayer player, final InterpolationHandler host) { ++ this.player = player; ++ this.host = host; ++ } ++ ++ public boolean receive(final RemotePlayerMovement movement) { ++ if (movement == null || movement.mode() == 1 || movement.mode() == 2 || movement.riding() || this.player.isPassenger() ++ || this.player.isLocalInstanceAuthoritative() || !BedrockInputPermissions.isBedrockConnection() ++ || !movement.uuid().equals(this.player.getUUID()) || movement.entityId() != this.player.getId() ++ || !movement.lifetime().equals(BedrockActorState.liveLifetime(this.player.getUUID()))) { ++ this.releaseNative(); ++ return false; ++ } ++ final Minecraft client = Minecraft.getInstance(); ++ if (client.level == null || client.getConnection() == null || !client.getConnection().getConnection().isConnected() || this.player.level() != client.level ++ || client.level.getEntity(this.player.getId()) != this.player || !client.level.isTickingEntity(this.player) ++ || client.player == null || this.player.hasIndirectPassenger(client.player) || this.player.isRemoved()) { ++ this.releaseNative(); ++ return false; ++ } ++ final boolean continuing = this.validContext() && movement.lifetime().equals(this.lifetime); ++ if (!continuing) this.nativeInterpolation.clear(); ++ this.host.cancel(); ++ this.connection = client.getConnection(); ++ this.level = client.level; ++ this.lifetime = movement.lifetime(); ++ this.originOffset = movement.originOffset(); ++ if (!continuing || !this.player.position().equals(this.appliedPosition)) this.captureEye(); ++ this.nativeInterpolation.receive(movement); ++ // Every legacy head packet for these sources precedes its paired position packet. ++ this.player.lerpHeadTo(this.player.getYHeadRot(), 0); ++ if (movement.mode() == 3) { ++ final float rawPitch = movement.rotation().x(); ++ final float rawYaw = this.player.getYRot(); ++ final float pitch = RemotePlayerInterpolation.wrap(rawPitch); ++ final float yaw = RemotePlayerInterpolation.wrap(rawYaw); ++ this.player.setXRot(pitch); ++ this.player.setYRot(yaw); ++ this.player.xRotO = RemotePlayerInterpolation.wrap((pitch - rawPitch) + this.player.xRotO); ++ this.player.yRotO = RemotePlayerInterpolation.wrap((yaw - rawYaw) + this.player.yRotO); ++ } ++ this.player.onInterpolationStart(this); ++ return true; ++ } ++ ++ private boolean validContext() { ++ final Minecraft client = Minecraft.getInstance(); ++ return this.lifetime != null && BedrockInputPermissions.isBedrockConnection() ++ && client.getConnection() == this.connection && client.level == this.level ++ && this.connection.getConnection().isConnected() ++ && this.player.level() == this.level && !this.player.isRemoved() && !this.player.isPassenger() ++ && !this.player.isLocalInstanceAuthoritative() && this.level.getEntity(this.player.getId()) == this.player ++ && this.lifetime.equals(BedrockActorState.liveLifetime(this.player.getUUID())); ++ } ++ ++ private void captureEye() { ++ this.eye = new Position3f((float) this.player.getX(), (float) this.player.getY() + this.originOffset, (float) this.player.getZ()); ++ this.appliedPosition = this.player.position(); ++ } ++ ++ @Override ++ public void interpolate() { ++ if (PhysicalReplayContext.replaying()) return; ++ if (this.lifetime == null) { ++ this.host.interpolate(); ++ return; ++ } ++ if (!this.validContext()) { ++ this.cancel(); ++ return; ++ } ++ if (!this.nativeInterpolation.active()) return; ++ if (!this.player.position().equals(this.appliedPosition)) this.captureEye(); ++ final var next = this.nativeInterpolation.tick(this.eye, this.player.getXRot(), this.player.getYRot(), this.player.xRotO, this.player.yRotO, this.player.getYHeadRot()); ++ if (next.clearMotion()) this.player.setDeltaMovement(Vec3.ZERO); ++ if (!next.position().equals(this.eye)) { ++ this.eye = next.position(); ++ this.player.setPos(this.eye.x(), (double) this.eye.y() - this.originOffset, this.eye.z()); ++ } ++ this.appliedPosition = this.player.position(); ++ this.player.setXRot(next.pitch()); ++ this.player.setYRot(next.yaw()); ++ this.player.xRotO = next.previousPitch(); ++ this.player.yRotO = next.previousYaw(); ++ this.player.setYHeadRot(next.headYaw()); ++ } ++ ++ @Override ++ public PositionAndRotation target() { ++ if (this.lifetime == null || this.nativeInterpolation.target() == null) return this.host.target(); ++ final var position = this.nativeInterpolation.target(); ++ final var rotation = this.nativeInterpolation.rotation(); ++ return PositionAndRotation.of(new Vec3(position.x(), (double) position.y() - this.originOffset, position.z()), rotation.y(), rotation.x()); ++ } ++ ++ @Override ++ public boolean interpolateTo(final PositionPath path, final float yaw, final float pitch, final boolean hasRotation) { ++ this.releaseNative(); ++ return this.host.interpolateTo(path, yaw, pitch, hasRotation); ++ } ++ ++ @Override ++ public InterpolationTracker interpolationTracker() { return this.host.interpolationTracker(); } ++ ++ @Override ++ public void applyPredictedMovement(final Vec3 movement) { ++ if (this.lifetime == null) this.host.applyPredictedMovement(movement); ++ } ++ ++ @Override ++ public boolean hasActiveInterpolation() { ++ return this.lifetime == null ? this.host.hasActiveInterpolation() : this.nativeInterpolation.active(); ++ } ++ ++ @Override ++ public void cancel() { ++ this.releaseNative(); ++ this.host.cancel(); ++ } ++ ++ private void releaseNative() { ++ this.nativeInterpolation.clear(); ++ this.lifetime = null; ++ this.connection = null; ++ this.level = null; ++ this.eye = null; ++ this.appliedPosition = null; ++ } ++} +diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json +index 590dad9..21b4eae 100644 +--- a/src/main/resources/viafabricplus-bedrock.mixins.json ++++ b/src/main/resources/viafabricplus-bedrock.mixins.json +@@ -209,6 +209,8 @@ + "features.movement.MixinFireworkRocketEntity", + "features.movement.MixinLocalPlayer", + "features.movement.MixinPlayerCorrectionPacketListener", ++ "features.movement.MixinRemotePlayerInterpolation", ++ "features.movement.MixinRemotePlayerMovementPacketListener", + "features.movement.MixinPlayer", + "features.sound.MixinBedrockMiningAudio", + "features.block.MixinBedrockCommandBlockSettings", +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/movement/NativeRemoteInterpolationReferenceTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/NativeRemoteInterpolationReferenceTest.java +new file mode 100644 +index 0000000..1a00bf6 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/NativeRemoteInterpolationReferenceTest.java +@@ -0,0 +1,74 @@ ++/* ++ * 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.movement; ++ ++import com.google.gson.JsonArray; ++import com.google.gson.JsonParser; ++import net.raphimc.viabedrock.protocol.model.Position3f; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; ++ ++import java.nio.file.Files; ++import java.nio.file.Path; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class NativeRemoteInterpolationReferenceTest { ++ @Test ++ @EnabledIfEnvironmentVariable(named = "STACKANVIL_REMOTE_INTERPOLATION_REFERENCE", matches = ".+") ++ void matchesCurrentNativePositionMainRotationAndHeadCounterInstructions() throws Exception { ++ final var reference = JsonParser.parseString(Files.readString(Path.of(System.getenv("STACKANVIL_REMOTE_INTERPOLATION_REFERENCE"), ++ "native-current-interpolation-tick-controls.json"))).getAsJsonObject(); ++ assertEquals("Bedrock1.26.60.5/protocol2225", reference.get("target").getAsString()); ++ assertEquals("988f6a61403b4381aa41e6c66d64cce441b14e561a9b10f647c2758ef3f2672a", reference.get("sha256").getAsString()); ++ int cases = 0; ++ for (final var element : reference.getAsJsonArray("rows")) { ++ final var row = element.getAsJsonObject(); ++ // Remote players retain a head component. Missing-component factories have a separate admission boundary. ++ if (!row.get("headPresent").getAsBoolean()) continue; ++ final var interpolation = new RemotePlayerInterpolation(); ++ set(interpolation, "target", position(row.getAsJsonArray("target"))); ++ set(interpolation, "rotation", new Position3f(190.25F, -190.5F, 175.25F)); ++ set(interpolation, "headTarget", 175.25F); ++ set(interpolation, "positionSteps", row.get("positionSteps").getAsInt()); ++ set(interpolation, "rotationSteps", row.get("rotationSteps").getAsInt()); ++ set(interpolation, "headSteps", row.get("headSteps").getAsInt()); ++ final var input = position(row.getAsJsonArray("inputPosition")); ++ final var step = interpolation.tick(input, -179.75F, 179.5F, 90.25F, -90.5F, 179.75F); ++ final var expectedPosition = row.get("positionRequest"); ++ assertEquals(expectedPosition.isJsonNull() ? input : position(expectedPosition.getAsJsonArray()), step.position()); ++ final var rotation = row.getAsJsonArray("rotation"); ++ assertEquals(rotation.get(0).getAsFloat(), step.pitch()); ++ assertEquals(rotation.get(1).getAsFloat(), step.yaw()); ++ assertEquals(rotation.get(2).getAsFloat(), step.previousPitch()); ++ assertEquals(rotation.get(3).getAsFloat(), step.previousYaw()); ++ assertEquals(row.getAsJsonArray("head").get(0).getAsFloat(), step.headYaw()); ++ assertEquals(row.getAsJsonArray("motion").get(0).getAsFloat() == 0F, step.clearMotion()); ++ final var counters = row.getAsJsonArray("postCounters"); ++ assertEquals(counters.get(0).getAsInt(), get(interpolation, "positionSteps")); ++ assertEquals(counters.get(1).getAsInt(), get(interpolation, "rotationSteps")); ++ assertEquals(counters.get(2).getAsInt(), get(interpolation, "headSteps")); ++ cases++; ++ } ++ assertEquals(256, cases); ++ } ++ ++ private static Position3f position(final JsonArray value) { ++ return new Position3f(value.get(0).getAsFloat(), value.get(1).getAsFloat(), value.get(2).getAsFloat()); ++ } ++ ++ private static void set(final RemotePlayerInterpolation interpolation, final String name, final Object value) throws Exception { ++ final var field = RemotePlayerInterpolation.class.getDeclaredField(name); ++ field.setAccessible(true); ++ field.set(interpolation, value); ++ } ++ ++ private static int get(final RemotePlayerInterpolation interpolation, final String name) throws Exception { ++ final var field = RemotePlayerInterpolation.class.getDeclaredField(name); ++ field.setAccessible(true); ++ return field.getInt(interpolation); ++ } ++} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java +new file mode 100644 +index 0000000..c88158b +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java +@@ -0,0 +1,190 @@ ++/* ++ * 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.movement; ++ ++import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; ++import com.viaversion.viafabricplus.bedrock.render.BedrockActorState; ++import net.minecraft.SharedConstants; ++import net.minecraft.client.Minecraft; ++import net.minecraft.client.multiplayer.ClientLevel; ++import net.minecraft.client.multiplayer.ClientPacketListener; ++import net.minecraft.client.player.LocalPlayer; ++import net.minecraft.client.player.RemotePlayer; ++import net.minecraft.network.Connection; ++import net.minecraft.server.Bootstrap; ++import net.minecraft.world.entity.InterpolationHandler; ++import net.minecraft.world.entity.PositionPath; ++import net.minecraft.world.phys.Vec3; ++import net.raphimc.viabedrock.api.model.RemotePlayerMovement; ++import net.raphimc.viabedrock.protocol.model.Position3f; ++import org.junit.jupiter.api.BeforeAll; ++import org.junit.jupiter.api.Test; ++import org.mockito.MockedStatic; ++ ++import java.util.UUID; ++import java.util.concurrent.atomic.AtomicReference; ++ ++import static org.junit.jupiter.api.Assertions.*; ++import static org.mockito.Mockito.*; ++ ++class RemotePlayerInterpolationHandlerTest { ++ @BeforeAll ++ static void initializeGame() { ++ SharedConstants.tryDetectVersion(); ++ Bootstrap.bootStrap(); ++ } ++ ++ @Test ++ void appliesNativeFloatStepsWithoutTheHostDistanceCutoffOrPreviousPositionReset() { ++ try (final var f = new Fixture()) { ++ assertTrue(f.handler.receive(f.movement(0, false))); ++ f.handler.interpolate(); ++ assertEquals(.5D, f.position.get().x); ++ assertEquals((double) 24.413341522216797F - 1.62001F, f.position.get().y); ++ f.handler.interpolate(); ++ assertEquals(1D, f.position.get().x); ++ f.handler.interpolate(); ++ assertEquals(1.5D, f.position.get().x); ++ assertEquals((double) 73.24002F - 1.62001F, f.position.get().y); ++ assertFalse(f.handler.hasActiveInterpolation()); ++ verify(f.player, times(3)).setDeltaMovement(Vec3.ZERO); ++ verify(f.player, never()).snapTo(any(Vec3.class), anyFloat(), anyFloat()); ++ verify(f.host, never()).interpolate(); ++ } ++ } ++ ++ @Test ++ void leavesOrdinaryJavaInterpolationHistoryIntactAcrossUnpairedPackets() { ++ try (final var f = new Fixture()) { ++ final var first = PositionPath.of(new Vec3(1, 2, 3)); ++ final var second = PositionPath.of(new Vec3(4, 5, 6)); ++ when(f.host.interpolateTo(any(), anyFloat(), anyFloat(), anyBoolean())).thenReturn(true); ++ assertFalse(f.handler.receive(null)); ++ assertTrue(f.handler.interpolateTo(first, 1, 2, true)); ++ assertFalse(f.handler.receive(null)); ++ assertTrue(f.handler.interpolateTo(second, 3, 4, true)); ++ f.handler.interpolate(); ++ verify(f.host, never()).cancel(); ++ verify(f.host).interpolateTo(first, 1, 2, true); ++ verify(f.host).interpolateTo(second, 3, 4, true); ++ verify(f.host).interpolate(); ++ } ++ } ++ ++ @Test ++ void headOnlyReceiptAppliesRawPitchAndKeepsTheActiveMovement() { ++ try (final var f = new Fixture()) { ++ assertTrue(f.handler.receive(f.movement(0, false))); ++ f.handler.interpolate(); ++ assertEquals(.5D, f.position.get().x); ++ clearInvocations(f.host, f.player); ++ final var head = new RemotePlayerMovement(17, f.uuid, f.lifetime, 3, false, ++ new Position3f(37, 123, 19), new Position3f(-22.75F, 135, -135.25F), 1.62001F, (short) 0, 0, false, ++ new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, 0, -22.5F, false)); ++ assertTrue(f.handler.receive(head)); ++ verify(f.player).setXRot(-22.75F); ++ verify(f.player).setYRot(0F); ++ assertEquals(.5D, f.position.get().x); ++ assertTrue(f.handler.hasActiveInterpolation()); ++ f.handler.interpolate(); ++ assertEquals(1D, f.position.get().x); ++ f.handler.interpolate(); ++ assertEquals(1.5D, f.position.get().x); ++ assertFalse(f.handler.hasActiveInterpolation()); ++ verify(f.host, never()).interpolateTo(any(), anyFloat(), anyFloat(), anyBoolean()); ++ verify(f.player).lerpHeadTo(0F, 0); ++ } ++ } ++ ++ @Test ++ void resetModesAndMountedPlayersReturnToTheHostPath() { ++ for (int mode = 0; mode <= 2; mode++) { ++ try (final var f = new Fixture()) { ++ assertTrue(f.handler.receive(f.movement(0, false))); ++ assertFalse(f.handler.receive(f.movement(mode, mode == 0))); ++ assertFalse(f.handler.hasActiveInterpolation()); ++ f.handler.interpolate(); ++ verify(f.host).interpolate(); ++ verify(f.player, never()).setPos(anyDouble(), anyDouble(), anyDouble()); ++ } ++ } ++ } ++ ++ @Test ++ void retiresPendingMovementWhenItsWorldConnectionActorOrNativeLifetimeChanges() { ++ for (int change = 0; change < 7; change++) { ++ try (final var f = new Fixture()) { ++ assertTrue(f.handler.receive(f.movement(0, false))); ++ switch (change) { ++ case 0 -> f.client.level = mock(ClientLevel.class); ++ case 1 -> when(f.client.getConnection()).thenReturn(mock(ClientPacketListener.class)); ++ case 2 -> when(f.level.getEntity(17)).thenReturn(mock(RemotePlayer.class)); ++ case 3 -> f.actors.when(() -> BedrockActorState.liveLifetime(f.uuid)).thenReturn(new UUID(5, 6)); ++ case 4 -> when(f.player.isPassenger()).thenReturn(true); ++ case 5 -> f.permissions.when(BedrockInputPermissions::isBedrockConnection).thenReturn(false); ++ case 6 -> when(f.connection.isConnected()).thenReturn(false); ++ default -> fail(); ++ } ++ f.handler.interpolate(); ++ assertFalse(f.handler.hasActiveInterpolation()); ++ verify(f.player, never()).setPos(anyDouble(), anyDouble(), anyDouble()); ++ } ++ } ++ } ++ ++ private static final class Fixture implements AutoCloseable { ++ private final UUID uuid = new UUID(1, 2); ++ private final UUID lifetime = new UUID(3, 4); ++ private final Minecraft client = mock(Minecraft.class); ++ private final ClientLevel level = mock(ClientLevel.class); ++ private final ClientPacketListener listener = mock(ClientPacketListener.class); ++ private final Connection connection = mock(Connection.class); ++ private final RemotePlayer player = mock(RemotePlayer.class); ++ private final InterpolationHandler host = mock(InterpolationHandler.class); ++ private final AtomicReference position = new AtomicReference<>(new Vec3(0, -(double) 1.62001F, 5.5)); ++ private final RemotePlayerInterpolationHandler handler = new RemotePlayerInterpolationHandler(this.player, this.host); ++ private final MockedStatic game = mockStatic(Minecraft.class); ++ private final MockedStatic actors = mockStatic(BedrockActorState.class); ++ private final MockedStatic permissions = mockStatic(BedrockInputPermissions.class); ++ ++ private Fixture() { ++ this.client.level = this.level; ++ this.client.player = mock(LocalPlayer.class); ++ this.game.when(Minecraft::getInstance).thenReturn(this.client); ++ this.permissions.when(BedrockInputPermissions::isBedrockConnection).thenReturn(true); ++ this.actors.when(() -> BedrockActorState.liveLifetime(this.uuid)).thenReturn(this.lifetime); ++ when(this.client.getConnection()).thenReturn(this.listener); ++ when(this.listener.getConnection()).thenReturn(this.connection); ++ when(this.connection.isConnected()).thenReturn(true); ++ when(this.player.getId()).thenReturn(17); ++ when(this.player.getUUID()).thenReturn(this.uuid); ++ when(this.player.level()).thenReturn(this.level); ++ when(this.level.getEntity(17)).thenReturn(this.player); ++ when(this.level.isTickingEntity(this.player)).thenReturn(true); ++ when(this.player.position()).thenAnswer(_ -> this.position.get()); ++ when(this.player.getX()).thenAnswer(_ -> this.position.get().x); ++ when(this.player.getY()).thenAnswer(_ -> this.position.get().y); ++ when(this.player.getZ()).thenAnswer(_ -> this.position.get().z); ++ doAnswer(call -> { ++ this.position.set(new Vec3(call.getArgument(0), call.getArgument(1), call.getArgument(2))); ++ return null; ++ }).when(this.player).setPos(anyDouble(), anyDouble(), anyDouble()); ++ } ++ ++ private RemotePlayerMovement movement(final int mode, final boolean riding) { ++ return new RemotePlayerMovement(17, this.uuid, this.lifetime, mode, riding, ++ new Position3f(1.5F, 73.24002F, 5.5F), Position3f.ZERO, 1.62001F, (short) 0, 3, false, ++ new RemotePlayerMovement.JavaTarget(mode != 3, 0, 0, 0, 0, 0, false)); ++ } ++ ++ @Override ++ public void close() { ++ this.permissions.close(); ++ this.actors.close(); ++ this.game.close(); ++ } ++ } ++} +diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationTest.java +new file mode 100644 +index 0000000..d3c0422 +--- /dev/null ++++ b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationTest.java +@@ -0,0 +1,114 @@ ++/* ++ * 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.movement; ++ ++import net.raphimc.viabedrock.api.model.RemotePlayerMovement; ++import net.raphimc.viabedrock.protocol.model.Position3f; ++import org.junit.jupiter.api.Test; ++ ++import java.util.UUID; ++ ++import static org.junit.jupiter.api.Assertions.*; ++ ++class RemotePlayerInterpolationTest { ++ @Test ++ void followsTheThreeCurrentNativeStateVectorStepsAcrossALargeMove() { ++ final var interpolation = new RemotePlayerInterpolation(); ++ interpolation.receive(movement(0, new Position3f(1.5F, 73.24002F, 5.5F), 0, 3)); ++ Position3f position = new Position3f(0, 0, 5.5F); ++ final Position3f[] expected = { ++ new Position3f(.5F, 24.413341522216797F, 5.5F), ++ new Position3f(1F, 48.826683044433594F, 5.5F), ++ new Position3f(1.5F, 73.24002075195312F, 5.5F) ++ }; ++ for (final var nativePosition : expected) { ++ final var step = interpolation.tick(position, 0, 0, 0, 0, 0); ++ assertEquals(nativePosition, step.position()); ++ assertTrue(step.clearMotion()); ++ position = step.position(); ++ } ++ assertFalse(interpolation.active()); ++ assertFalse(interpolation.tick(position, 0, 0, 0, 0, 0).clearMotion()); ++ } ++ ++ @Test ++ void preservesFloatRoundingThroughTheObservedSmallMove() { ++ final var interpolation = new RemotePlayerInterpolation(); ++ interpolation.receive(movement(0, new Position3f(1.5F, 73.24002F, 4.5F), 0, 3)); ++ Position3f position = new Position3f(2.5F, 73.24002F, 4.5F); ++ for (final float x : new float[]{2.1666667461395264F, 1.8333333730697632F, 1.5F}) { ++ position = interpolation.tick(position, 0, 0, 0, 0, 0).position(); ++ assertEquals(x, position.x()); ++ } ++ } ++ ++ @Test ++ void finishesAForcedActorTargetBeforeApplyingTheAgedQueuedTarget() { ++ final var interpolation = new RemotePlayerInterpolation(); ++ interpolation.receive(movement(RemotePlayerMovement.ACTOR, new Position3f(9, 0, 0), 8, 3)); ++ Position3f position = interpolation.tick(Position3f.ZERO, 0, 0, 0, 0, 0).position(); ++ assertEquals(3F, position.x()); ++ interpolation.receive(movement(RemotePlayerMovement.ACTOR, new Position3f(12, 0, 0), 0, 5)); ++ for (final float x : new float[]{6F, 9F, 9.75F, 10.5F, 11.25F, 12F}) { ++ position = interpolation.tick(position, 0, 0, 0, 0, 0).position(); ++ assertEquals(x, position.x()); ++ } ++ assertFalse(interpolation.active()); ++ } ++ ++ @Test ++ void takesTheShortRotationArcAndDoesNotMoveForANegativeNativeCounter() { ++ final var interpolation = new RemotePlayerInterpolation(); ++ final var source = movement(RemotePlayerMovement.ACTOR, new Position3f(9, 0, 0), 0, -1L); ++ interpolation.receive(new RemotePlayerMovement(source.entityId(), source.uuid(), source.lifetime(), source.mode(), false, ++ source.position(), new Position3f(10, 10, 10), source.originOffset(), source.flags(), source.duration(), false, source.javaTarget())); ++ float yaw = 350F; ++ float pitch = 350F; ++ for (int i = 0; i < 3; i++) { ++ final var step = interpolation.tick(Position3f.ZERO, pitch, yaw, pitch, yaw, 0); ++ assertEquals(Position3f.ZERO, step.position()); ++ assertFalse(step.clearMotion()); ++ assertTrue(Math.abs(step.yaw()) <= 10F); ++ pitch = step.pitch(); ++ yaw = step.yaw(); ++ } ++ assertEquals(10F, yaw); ++ assertTrue(interpolation.active()); ++ interpolation.clear(); ++ assertFalse(interpolation.active()); ++ } ++ ++ @Test ++ void headOnlyUpdatesRetainPendingPositionAndBodyTargets() { ++ final var interpolation = new RemotePlayerInterpolation(); ++ final var normal = movement(0, new Position3f(9, 0, 0), 0, 3); ++ interpolation.receive(new RemotePlayerMovement(17, normal.uuid(), normal.lifetime(), 0, false, ++ normal.position(), new Position3f(22.5F, 90, 90), normal.originOffset(), normal.flags(), normal.duration(), false, normal.javaTarget())); ++ final var first = interpolation.tick(Position3f.ZERO, 0, 0, 0, 0, 0); ++ assertEquals(new Position3f(3, 0, 0), first.position()); ++ assertEquals(30F, first.yaw()); ++ assertEquals(30F, first.headYaw()); ++ interpolation.receive(new RemotePlayerMovement(17, normal.uuid(), normal.lifetime(), 3, false, ++ new Position3f(37, 123, 19), new Position3f(-22.75F, 135, -135.25F), normal.originOffset(), (short) 0, 0, false, ++ new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, 90, -22.5F, false))); ++ final var second = interpolation.tick(first.position(), -22.75F, first.yaw(), 0, 0, first.headYaw()); ++ assertEquals(new Position3f(6, 0, 0), second.position()); ++ assertEquals(-.125F, second.pitch()); ++ assertEquals(60F, second.yaw()); ++ assertEquals(-52.625F, second.headYaw()); ++ final var last = interpolation.tick(second.position(), second.pitch(), second.yaw(), 0, 0, second.headYaw()); ++ assertEquals(new Position3f(9, 0, 0), last.position()); ++ assertEquals(22.5F, last.pitch()); ++ assertEquals(90F, last.yaw()); ++ assertEquals(-135.25F, last.headYaw()); ++ assertFalse(interpolation.active()); ++ } ++ ++ private static RemotePlayerMovement movement(final int mode, final Position3f target, final int flags, final long duration) { ++ return new RemotePlayerMovement(17, new UUID(1, 2), new UUID(3, 4), mode, false, target, Position3f.ZERO, 1.62001F, ++ (short) flags, duration, false, new RemotePlayerMovement.JavaTarget(mode != 3, 0, 0, 0, 0, 0, false)); ++ } ++} +-- +2.56.0 diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md new file mode 100644 index 00000000..5d5b5296 --- /dev/null +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md @@ -0,0 +1,31 @@ +# Interpolate remote player movement with native float steps + +Negotiated receipts retain the original player mode, native inputs, and actor lifetime. The client pairs each receipt with its exact translated position or rotation packet. Admitted remote movement uses independent native counters for position, body rotation, and head rotation. + +A decorator retains the real host handler. Unpaired packets, reset modes, known riding, local authority, and unticking entities use the host path. Ordinary Java movement keeps its interpolation history. Connection, world, entity, lifetime, and mount changes retire pending native movement. + +Head-only updates apply raw pitch immediately. They preserve live yaw, pending position and body targets, and forced queues. The head target uses the maximum remaining position/body counter. Explicit packet pairing prevents Java's rotation-only path from cancelling movement. Raw inputs avoid the legacy packet's byte-angle quantization. + +## Native evidence + +Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls use protocol 2225. Read-only StateVector samples show three steps after admitted normal movement, including an excursion beyond Java's distance cutoff. A small move produces X samples 2.166666746, 1.833333373, and 1.5. The [actor runtime evidence](0008-share-native-actor-and-resource-runtime.pr.md) links the earlier controls and their limits. + +Original instructions from the verified current executable establish the component rules. Mode helper `0x1402d92c0` passes 720 head-only cases, including pending counters and absent-component fallback. Callback `0x143630d70` and angle helper `0x143631440` pass 512 position, rotation, and head-counter cases. Producer `0x1422c8d20` and writer `0x142c74350` pass 432 target cases, including unsigned duration conversion and forced completion. Queue kernel `0x143634ae0` passes 108 handoff and signed-counter cases. Synthetic component stores and explicit boundary callbacks exclude allocation, scheduling, and full factory admission. + +A conditional Java/native test compares all 256 current callback cases with a head component. Default regressions cover observed position samples, overlapping head-only input, raw pitch, forced handoff, rotation wrapping, host fallback, and lifecycle replacement. + +## Installed comparisons + +Earlier direct and ViaProxy controls retain the observed large- and small-move steps. Twelve settled phases per route compare movement origins and poses, including the previously occluded leg. The new overlapping-head controls use the final functional source and two fresh player identities. + +The new fixture sends normal movement followed immediately by a head-only packet. Both rebuilt routes reach the native movement endpoint and retain the head target. A settled update keeps the raw 22.75-degree pitch. Sparse movement after the overlap still uses its independent receive cache. Read-only audits pass all eight position, pitch, yaw, and head-yaw checks per route. + +Strict journal decoders verify identical peer spawn, skin, normal, head-only, and sparse bytes against native. Camera instructions precede peer creation. Three settled images per phase compare six colored markers. All marker visibility checks agree. Median centroids stay within 0.34% of each viewport axis; boundaries stay within 0.66%. These projections do not establish frame timing or numeric native body angles. + +## Validation and limits + +The full four-project build passes 3,352 tests, with 326 skipped and no failures. The conditional native comparison also passes separately. All 157 patches replay from fresh upstream clones. Bundle generation, ten artifact/manifest hashes, and all 1,651 embedded core-class comparisons pass. The installed JAR hashes match the compared builds. Later commit-message edits preserve both exact source trees. + +All owned game, display, relay, and server processes are gone. Cleanup restores the backed-up native world, options, external server lists, and owned server configuration. + +Full factory, readiness, region, scheduler, and ground-state admission remain unverified. Reset modes retain existing host behavior. Mounted movement, broader head/body transitions, complete trajectories, and skin parity remain open. The original custom-skin report remains unresolved. From 774cbbc312f14d36e24972d00ca5aebc00498a60 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:39:26 +0100 Subject: [PATCH 41/47] docs(movement): record native reset and packet motion gaps Compare fourteen current native and installed direct-client phases with identical peer packets. Record immediate reset behavior, respawn pitch, previous-position differences, preserved head state and queued targets that resume after either reset. Identify the outer MovePlayer motion assignment before mode dispatch, including raw head-only positions that the current receipt omits. Keep these verified differences open; this evidence does not change production sources or establish complete movement parity. --- docs/bedrock-coverage.md | 2 +- .../0107-transport-remote-player-movement-modes.pr.md | 2 +- .../0041-interpolate-native-remote-player-movement.pr.md | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 71bb2bce..97b07fe2 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -30,7 +30,7 @@ No complete area has verified Bedrock parity. | Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Short input edges during active Java use, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Remote interpolation readiness, exact timing, reset modes, and mounted behavior remain unverified. 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. | +| Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Remote respawn/teleport resets, queued-target retention, and packet-level motion differ from native. Readiness, exact timing, and mounted behavior remain unverified. 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. | | Fog and picking | Fog commands, inherited legacy biome bindings, pack-defined defaults, Overworld clock modulation, End dragon distance rules, native actor picking transport, and native float camera rays. Disabling the daylight rule freezes explicit Overworld clocks. | Both routes match selected native fog colors and distances, three non-cardinal rays, and paired armor-stand and block reach controls. Eye precision, other actor shapes, multipart target ordering, spatial blending, timed command transitions, directional sunrise tint, sky sampling, and other render modes remain open. | | Custom content | Resource-defined actors, materials, particles, sound, block geometry, lighting, and collision. | Unsupported material methods, missing assets, carrier capacity, and visual differences remain. Live-server admission does not establish parity. | | Platforms | Linux build, replay, and selected native comparisons. | Windows and macOS launcher/account/rendering paths need their own results. See [platform testing](platform-testing.md). | diff --git a/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md b/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md index 70a33c64..e8ca6cde 100644 --- a/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md +++ b/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md @@ -14,4 +14,4 @@ The [client integration](../../viafabricplus-bedrock/upstreamable/0041-interpola ## Limits -This patch transports inputs. It does not change interpolation, actor receive caches, collision history, or body-control formulas. Full native readiness, region admission, scheduling, reset behavior, mounted movement, and pose parity remain open. +This patch transports inputs. It does not change interpolation, actor receive caches, collision history, or body-control formulas. Current head-only receipts retain live coordinates. Further native controls show that the outer player handler uses the raw input position to derive motion before mode dispatch. That input and its packet-level application remain missing. The client integration records the matching reset and queued-target differences. Full native readiness, region admission, scheduling, mounted movement, and pose parity remain open. diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md index 5d5b5296..fde4ad6a 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md @@ -28,4 +28,10 @@ The full four-project build passes 3,352 tests, with 326 skipped and no failures All owned game, display, relay, and server processes are gone. Cleanup restores the backed-up native world, options, external server lists, and owned server configuration. -Full factory, readiness, region, scheduler, and ground-state admission remain unverified. Reset modes retain existing host behavior. Mounted movement, broader head/body transitions, complete trajectories, and skin parity remain open. The original custom-skin report remains unresolved. +Current native reset controls expose remaining differences in the host fallback. Respawn applies position immediately, clears motion and interpolation counters, and resets pitch to zero. Teleport preserves pitch and resets previous position to its destination. Both retain the current head angle. A queued target survives either reset and resumes afterward. + +A fourteen-phase native/direct comparison verifies identical peer payloads and complete journals. Native resumes the queued target at X=2.5 after both resets. The current Java fallback instead stays at each reset destination and retains the supplied respawn pitch. These cases remain unfixed. The accepted native capture uses corrected teleport payloads and records seven reset-helper returns. An earlier malformed teleport trial and a later debugger pause are excluded. + +The outer native player handler also assigns motion before mode dispatch, including head-only packets. It subtracts the previous StateVector position from the raw packet position. Current head-only receipts retain live coordinates instead of that raw position. Packet-level motion and reset parity require broader integration; helper arithmetic alone does not establish them. + +Full factory, readiness, region, scheduler, and ground-state admission remain unverified. Mounted movement, broader head/body transitions, complete trajectories, and skin parity remain open. The original custom-skin report remains unresolved. From d2e12dea6d1e9e3f819d02e87ce9c51be74c39e3 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:16:05 +0100 Subject: [PATCH 42/47] fix(players): preserve native reset and packet motion state Retain raw head-only positions separately from their unchanged Java translation. Derive packet motion from exact previous native eye history. Apply respawn and teleport immediately, preserving live head state and queued movement while clearing active counters and motion. Keep their different pitch and previous-position behavior and native float origins. Fold the changes into the owning transport and interpolation patches. Three regressions fail before the correction and pass afterward. Full builds pass 3,358 tests with 326 skipped; all 157 patches replay from fresh upstream clones. Rebuilt direct and proxy routes pass fourteen reset and eight active-head state checks each with matching native peer packets. Record the remaining reset-pose projection difference. Selected state checks do not establish complete body-control timing or skin parity. --- docs/bedrock-coverage.md | 2 +- ...ansport-remote-player-movement-modes.patch | 33 +- ...ansport-remote-player-movement-modes.pr.md | 2 +- ...polate-native-remote-player-movement.patch | 292 +++++++++++++++--- ...polate-native-remote-player-movement.pr.md | 22 +- 5 files changed, 282 insertions(+), 69 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 97b07fe2..3a37b776 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -30,7 +30,7 @@ No complete area has verified Bedrock parity. | Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Short input edges during active Java use, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Remote respawn/teleport resets, queued-target retention, and packet-level motion differ from native. Readiness, exact timing, and mounted behavior remain unverified. 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. | +| Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Remote reset state and queued movement match selected native controls, but reset-pose projections still differ. Readiness, ground state, exact timing, and mounted behavior remain unverified. 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. | | Fog and picking | Fog commands, inherited legacy biome bindings, pack-defined defaults, Overworld clock modulation, End dragon distance rules, native actor picking transport, and native float camera rays. Disabling the daylight rule freezes explicit Overworld clocks. | Both routes match selected native fog colors and distances, three non-cardinal rays, and paired armor-stand and block reach controls. Eye precision, other actor shapes, multipart target ordering, spatial blending, timed command transitions, directional sunrise tint, sky sampling, and other render modes remain open. | | Custom content | Resource-defined actors, materials, particles, sound, block geometry, lighting, and collision. | Unsupported material methods, missing assets, carrier capacity, and visual differences remain. Live-server admission does not establish parity. | | Platforms | Linux build, replay, and selected native comparisons. | Windows and macOS launcher/account/rendering paths need their own results. See [platform testing](platform-testing.md). | diff --git a/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.patch b/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.patch index 4f3c6b00..006f4343 100644 --- a/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.patch +++ b/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.patch @@ -1,4 +1,4 @@ -From 104f1c47db9bb16d6f77ef331cc3ca6ef3b075f1 Mon Sep 17 00:00:00 2001 +From da3c1093042008fab5ed5df2366aed3c39f9d23b Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:43:18 +0100 Subject: [PATCH] feat(players): transport remote movement modes and targets @@ -15,8 +15,10 @@ exact Java target separately, including its packet family. Preserve the existing float/double Y arithmetic and byte-angle translation so pairing does not depend on reconstructing either packet family's rounding. -Keep raw head-only pitch and head yaw. Its supplied position and -conflicting main yaw remain inert. Current native mode helper +Keep raw head-only position, pitch and head yaw. Its supplied position +and conflicting main yaw remain inert for position and main yaw. The +outer native packet handler 0x146bd39a0 derives motion from raw packet +position minus the previous StateVector before mode dispatch. Current native mode helper 0x1402d92c0 leaves pending position and main-rotation targets intact, applies pitch immediately, and refreshes the independent head counter. The client can reproduce this branch without changing legacy packets. @@ -33,8 +35,8 @@ from normal player movement and client interpolation. .../protocol/packet/EntityPackets.java | 3 + .../protocol/packet/OtherPlayerPackets.java | 4 + .../api/model/RemotePlayerMovementTest.java | 100 +++++++ - .../RemotePlayerMovementPacketTest.java | 248 ++++++++++++++++++ - 7 files changed, 519 insertions(+) + .../RemotePlayerMovementPacketTest.java | 249 ++++++++++++++++++ + 7 files changed, 520 insertions(+) create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/RemotePlayerMovement.java create mode 100644 src/main/java/net/raphimc/viabedrock/api/model/RemotePlayerMovementTracker.java create mode 100644 src/test/java/net/raphimc/viabedrock/api/model/RemotePlayerMovementTest.java @@ -188,7 +190,7 @@ index 00000000..1e88b1de + +} diff --git a/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java b/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java -index b47f5471..2dd368c8 100644 +index b47f5471..a4b3ec09 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java +++ b/src/main/java/net/raphimc/viabedrock/api/model/entity/Entity.java @@ -29,6 +29,7 @@ import com.viaversion.viaversion.protocols.v26_2to26_3.packet.ClientboundPackets @@ -205,14 +207,14 @@ index b47f5471..2dd368c8 100644 + /** Preserve the source mode without changing the legacy Java translation or actor receive cache. */ + public final void sendRemotePlayerMovement(final int mode, final boolean riding, final short flags, final long duration) { -+ this.sendRemotePlayerMovement(mode, riding, flags, duration, this.rotation); ++ this.sendRemotePlayerMovement(mode, riding, flags, duration, this.position, this.rotation); + } + -+ public final void sendRemotePlayerMovement(final int mode, final boolean riding, final short flags, final long duration, final Position3f sourceRotation) { ++ public final void sendRemotePlayerMovement(final int mode, final boolean riding, final short flags, final long duration, final Position3f sourcePosition, final Position3f sourceRotation) { + final var channels = this.user == null ? null : this.user.get(ChannelStorage.class); + if (!(this instanceof PlayerEntity) || this instanceof ClientPlayerEntity || channels == null + || !channels.hasChannel(RemotePlayerMovement.CHANNEL) -+ || !this.nativeStateAvailable() || !RemotePlayerMovement.finite(this.position) ++ || !this.nativeStateAvailable() || !RemotePlayerMovement.finite(this.position) || !RemotePlayerMovement.finite(sourcePosition) + || !RemotePlayerMovement.finite(sourceRotation) + || this.user.get(EntityTracker.class).getEntityByUid(this.uniqueId()) != this) { + return; @@ -224,7 +226,7 @@ index b47f5471..2dd368c8 100644 + position ? this.javaRotation().y() : MathUtil.byte2Float(MathUtil.float2Byte(this.rotation.y())), + position ? this.javaRotation().x() : MathUtil.byte2Float(MathUtil.float2Byte(this.rotation.x())), this.isOnGround()); + final var movement = new RemotePlayerMovement(this.javaId(), this.javaUuid(), -+ this.nativeLifetime, mode, riding, this.position, sourceRotation, this.eyeOffset(), flags, duration, this.isOnGround(), target); ++ this.nativeLifetime, mode, riding, sourcePosition, sourceRotation, this.eyeOffset(), flags, duration, this.isOnGround(), target); + final var payload = PacketWrapper.create(ClientboundPackets26_3.CUSTOM_PAYLOAD, this.user); + payload.write(Types.STRING, RemotePlayerMovement.CHANNEL); + payload.write(RemotePlayerMovement.TYPE, movement); @@ -263,7 +265,7 @@ index 4578a73f..6f65acbc 100644 wrapper.write(Types.VAR_INT, entity.javaId()); // entity id wrapper.write(Types.VAR_INT, 1); // Stepped diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java -index 8b37d578..1bd8cc1b 100644 +index 8b37d578..0a90ef87 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/OtherPlayerPackets.java @@ -186,6 +186,8 @@ public final class OtherPlayerPackets { @@ -271,7 +273,7 @@ index 8b37d578..1bd8cc1b 100644 if (mode == PositionMode.OnlyHeadRot) { entity.setRotation(new Position3f(rotation.x(), entity.rotation().y(), entity.rotation().z())); + entity.sendRemotePlayerMovement(mode.getValue(), ridingEntityRuntimeId != 0 -+ && entityTracker.getEntityByRid(ridingEntityRuntimeId) != null, (short) (onGround ? 1 : 0), 0, rotation); ++ && entityTracker.getEntityByRid(ridingEntityRuntimeId) != null, (short) (onGround ? 1 : 0), 0, position, rotation); wrapper.setPacketType(ClientboundPackets26_3.MOVE_ENTITY_ROT); wrapper.write(Types.VAR_INT, entity.javaId()); // entity id wrapper.write(Types.BOOLEAN, entity.isOnGround()); // on ground @@ -392,10 +394,10 @@ index 00000000..0b199629 +} diff --git a/src/test/java/net/raphimc/viabedrock/protocol/packet/RemotePlayerMovementPacketTest.java b/src/test/java/net/raphimc/viabedrock/protocol/packet/RemotePlayerMovementPacketTest.java new file mode 100644 -index 00000000..8cbe8670 +index 00000000..c88feb12 --- /dev/null +++ b/src/test/java/net/raphimc/viabedrock/protocol/packet/RemotePlayerMovementPacketTest.java -@@ -0,0 +1,248 @@ +@@ -0,0 +1,249 @@ +/* + * This file is part of ViaBedrock - https://github.com/ViaVersionAddons/ViaBedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -466,7 +468,8 @@ index 00000000..8cbe8670 + assertEquals(new Position3f(22.75F, 135, -135), receipt.rotation()); + assertEquals(90F, receipt.javaTarget().yaw()); + assertEquals(22.5F, receipt.javaTarget().pitch()); -+ assertEquals(new Position3f(1.5F, 73.24002F, 4.5F), receipt.position()); ++ assertEquals(new Position3f(37, 123, 19), receipt.position()); ++ assertEquals(new Position3f(1.5F, 73.24002F, 4.5F), fixture.player.position()); + } + } finally { + input.release(); diff --git a/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md b/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md index e8ca6cde..b6f780ab 100644 --- a/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md +++ b/patches/viabedrock/upstreamable/0107-transport-remote-player-movement-modes.pr.md @@ -14,4 +14,4 @@ The [client integration](../../viafabricplus-bedrock/upstreamable/0041-interpola ## Limits -This patch transports inputs. It does not change interpolation, actor receive caches, collision history, or body-control formulas. Current head-only receipts retain live coordinates. Further native controls show that the outer player handler uses the raw input position to derive motion before mode dispatch. That input and its packet-level application remain missing. The client integration records the matching reset and queued-target differences. Full native readiness, region admission, scheduling, mounted movement, and pose parity remain open. +This patch transports inputs. It does not change interpolation, actor receive caches, collision history, or body-control formulas. Head-only receipts retain raw position for native packet-level motion while leaving live position and the legacy Java target unchanged. The client integration covers reset state and queued movement. Full native readiness, region admission, scheduling, mounted movement, and pose parity remain open. diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch index 8f15e45c..78af16b1 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch @@ -1,4 +1,4 @@ -From 6ae81e929d0207f136784e938f439e354b47d751 Mon Sep 17 00:00:00 2001 +From c44453d7d2ed4c62bb9a53023de714b328927201 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:51:01 +0100 Subject: [PATCH] fix(players): interpolate negotiated remote movement as @@ -22,30 +22,49 @@ uses the maximum remaining position/body counter. Pair the rotation-only Java packet explicitly so the host path cannot cancel pending movement. Cancel the superseded Java head lerp through its existing public API. -Return reset modes, known riding, local authority, unticking entities -and unpaired packets to the host path. Preserve ordinary Java movement -history. Retire native state on connection, world, entity, lifetime or -mount changes. Keep body formulas and collision-history ownership intact. +Apply respawn and teleport positions immediately. Both clear motion and +active interpolation counters while preserving the current head angle +and any queued target. Respawn resets pitch to zero and retains previous +position; teleport retains wrapped pitch and resets previous position. +Retain the native float feet-to-eye roundtrip on teleport. Queue handoff +still runs after a reset leaves all counters at zero. + +Native packet dispatch derives motion from raw source position minus +previous StateVector, including head-only packets. Retain exact native +eye history at the entity tick boundary instead of rounding it through +Java feet coordinates. Preserve source positions separately from their +exact translated Java targets. + +Return known riding, local authority and unpaired packets to the host +path. Normal interpolation retains the existing Java ticking guard; +resets and head-only updates have no native normal-mode readiness gate. +Preserve ordinary Java history. Retire native state on connection, world, +entity, lifetime or mount changes. Keep body formulas and the independent +absolute/delta receive cache and collision-history ownership intact. Current native helper 0x1402d92c0, callback 0x143630d70, angle helper 0x143631440, target producer 0x1422c8d20 and queue kernel 0x143634ae0 establish these component rules. Instruction controls exclude component allocation and native scheduler admission. Live comparisons establish selected steps and poses; complete timing and trajectory parity remain -open alongside reset, mounted and unknown-factory behavior. +open alongside mounted and unknown-factory behavior. Current native +packet handler 0x146bd39a0, teleport setter 0x140baed10 and reset method +0x1402c77b0 establish the new packet/reset rules. Matching native/direct +controls distinguish respawn pitch, previous position, retained head +state and queued movement after both resets. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../MixinRemotePlayerInterpolation.java | 22 ++ - ...xinRemotePlayerMovementPacketListener.java | 54 +++++ - .../input/BedrockRemotePlayerMovement.java | 81 ++++++++ + ...xinRemotePlayerMovementPacketListener.java | 54 +++ + .../input/BedrockRemotePlayerMovement.java | 81 +++++ .../input/RemotePlayerMovementPayload.java | 22 ++ - .../movement/RemotePlayerInterpolation.java | 133 ++++++++++++ - .../RemotePlayerInterpolationHandler.java | 165 +++++++++++++++ + .../movement/RemotePlayerInterpolation.java | 142 ++++++++ + .../RemotePlayerInterpolationHandler.java | 205 ++++++++++++ .../viafabricplus-bedrock.mixins.json | 2 + - ...ativeRemoteInterpolationReferenceTest.java | 74 +++++++ - .../RemotePlayerInterpolationHandlerTest.java | 190 ++++++++++++++++++ - .../RemotePlayerInterpolationTest.java | 114 +++++++++++ - 11 files changed, 859 insertions(+) + ...ativeRemoteInterpolationReferenceTest.java | 74 +++++ + .../RemotePlayerInterpolationHandlerTest.java | 311 ++++++++++++++++++ + .../RemotePlayerInterpolationTest.java | 133 ++++++++ + 11 files changed, 1048 insertions(+) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerInterpolation.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerMovementPacketListener.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockRemotePlayerMovement.java @@ -281,10 +300,10 @@ index 0000000..e07848c +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolation.java new file mode 100644 -index 0000000..07ca2fe +index 0000000..61a8f44 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolation.java -@@ -0,0 +1,133 @@ +@@ -0,0 +1,142 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -307,6 +326,11 @@ index 0000000..07ca2fe + private Queued queued; + + public void receive(final RemotePlayerMovement movement) { ++ if (movement.mode() == 1 || movement.mode() == 2) { ++ this.resetTargets(); ++ this.headTarget = movement.rotation().z(); ++ return; ++ } + if (movement.mode() == 3) { + this.headTarget = movement.rotation().z(); + this.headSteps = Math.max(this.positionSteps, this.rotationSteps); @@ -392,13 +416,18 @@ index 0000000..07ca2fe + } + + public boolean active() { -+ return this.positionSteps != 0 || this.rotationSteps != 0 || this.headSteps != 0; ++ return this.positionSteps != 0 || this.rotationSteps != 0 || this.headSteps != 0 || this.queued != null; + } + + public Position3f target() { return this.target; } + public Position3f rotation() { return this.rotation; } + + public void clear() { ++ this.resetTargets(); ++ this.queued = null; ++ } ++ ++ private void resetTargets() { + this.target = null; + this.rotation = null; + this.positionSteps = 0; @@ -406,7 +435,6 @@ index 0000000..07ca2fe + this.headSteps = 0; + this.headTarget = 0F; + this.mustComplete = false; -+ this.queued = null; + } + + static float wrap(final float value) { @@ -420,10 +448,10 @@ index 0000000..07ca2fe +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java new file mode 100644 -index 0000000..9961a14 +index 0000000..2eef0b4 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java -@@ -0,0 +1,165 @@ +@@ -0,0 +1,205 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -458,7 +486,9 @@ index 0000000..9961a14 + private UUID lifetime; + private float originOffset; + private Position3f eye; ++ private Position3f previousEye; + private Vec3 appliedPosition; ++ private Vec3 appliedPreviousPosition; + + public RemotePlayerInterpolationHandler(final RemotePlayer player, final InterpolationHandler host) { + this.player = player; @@ -466,7 +496,7 @@ index 0000000..9961a14 + } + + public boolean receive(final RemotePlayerMovement movement) { -+ if (movement == null || movement.mode() == 1 || movement.mode() == 2 || movement.riding() || this.player.isPassenger() ++ if (movement == null || movement.riding() || this.player.isPassenger() + || this.player.isLocalInstanceAuthoritative() || !BedrockInputPermissions.isBedrockConnection() + || !movement.uuid().equals(this.player.getUUID()) || movement.entityId() != this.player.getId() + || !movement.lifetime().equals(BedrockActorState.liveLifetime(this.player.getUUID()))) { @@ -475,7 +505,8 @@ index 0000000..9961a14 + } + final Minecraft client = Minecraft.getInstance(); + if (client.level == null || client.getConnection() == null || !client.getConnection().getConnection().isConnected() || this.player.level() != client.level -+ || client.level.getEntity(this.player.getId()) != this.player || !client.level.isTickingEntity(this.player) ++ || client.level.getEntity(this.player.getId()) != this.player ++ || ((movement.mode() == 0 || movement.mode() == RemotePlayerMovement.ACTOR) && !client.level.isTickingEntity(this.player)) + || client.player == null || this.player.hasIndirectPassenger(client.player) || this.player.isRemoved()) { + this.releaseNative(); + return false; @@ -488,23 +519,51 @@ index 0000000..9961a14 + this.lifetime = movement.lifetime(); + this.originOffset = movement.originOffset(); + if (!continuing || !this.player.position().equals(this.appliedPosition)) this.captureEye(); ++ final Vec3 oldPosition = this.player.oldPosition(); ++ if (!continuing || !oldPosition.equals(this.appliedPreviousPosition)) { ++ this.previousEye = oldPosition.equals(this.appliedPosition) ? this.eye : this.eyePosition(oldPosition); ++ this.appliedPreviousPosition = oldPosition; ++ } ++ if (movement.mode() != RemotePlayerMovement.ACTOR) { ++ // Native packet dispatch computes motion before the mode-specific helper, even for head-only input. ++ this.player.setDeltaMovement(new Vec3(movement.position().x() - this.previousEye.x(), ++ movement.position().y() - this.previousEye.y(), movement.position().z() - this.previousEye.z())); ++ } + this.nativeInterpolation.receive(movement); + // Every legacy head packet for these sources precedes its paired position packet. + this.player.lerpHeadTo(this.player.getYHeadRot(), 0); -+ if (movement.mode() == 3) { -+ final float rawPitch = movement.rotation().x(); -+ final float rawYaw = this.player.getYRot(); -+ final float pitch = RemotePlayerInterpolation.wrap(rawPitch); -+ final float yaw = RemotePlayerInterpolation.wrap(rawYaw); -+ this.player.setXRot(pitch); -+ this.player.setYRot(yaw); -+ this.player.xRotO = RemotePlayerInterpolation.wrap((pitch - rawPitch) + this.player.xRotO); -+ this.player.yRotO = RemotePlayerInterpolation.wrap((yaw - rawYaw) + this.player.yRotO); ++ if (movement.mode() == 1 || movement.mode() == 2 || movement.mode() == 3) { ++ this.applyRotation(movement.rotation().x(), movement.mode() == 3 ? this.player.getYRot() : movement.rotation().y()); ++ } ++ if (movement.mode() == 1 || movement.mode() == 2) { ++ final Position3f source = movement.position(); ++ // Teleport passes through native feet and eye conversions; respawn publishes the raw eye position. ++ this.eye = movement.mode() == 2 ? new Position3f(source.x() + 0F, ++ (source.y() - this.originOffset) + this.originOffset, source.z() + 0F) : source; ++ this.player.setPos(this.eye.x(), (double) this.eye.y() - this.originOffset, this.eye.z()); ++ this.appliedPosition = this.player.position(); ++ this.player.setDeltaMovement(Vec3.ZERO); ++ if (movement.mode() == 1) { ++ this.player.setXRot(0F); ++ } else { ++ this.player.setOldPosAndRot(this.appliedPosition, this.player.yRotO, this.player.xRotO); ++ this.previousEye = this.eye; ++ this.appliedPreviousPosition = this.player.oldPosition(); ++ } + } + this.player.onInterpolationStart(this); + return true; + } + ++ private void applyRotation(final float rawPitch, final float rawYaw) { ++ final float pitch = RemotePlayerInterpolation.wrap(rawPitch); ++ final float yaw = RemotePlayerInterpolation.wrap(rawYaw); ++ this.player.setXRot(pitch); ++ this.player.setYRot(yaw); ++ this.player.xRotO = RemotePlayerInterpolation.wrap((pitch - rawPitch) + this.player.xRotO); ++ this.player.yRotO = RemotePlayerInterpolation.wrap((yaw - rawYaw) + this.player.yRotO); ++ } ++ + private boolean validContext() { + final Minecraft client = Minecraft.getInstance(); + return this.lifetime != null && BedrockInputPermissions.isBedrockConnection() @@ -516,10 +575,14 @@ index 0000000..9961a14 + } + + private void captureEye() { -+ this.eye = new Position3f((float) this.player.getX(), (float) this.player.getY() + this.originOffset, (float) this.player.getZ()); ++ this.eye = this.eyePosition(this.player.position()); + this.appliedPosition = this.player.position(); + } + ++ private Position3f eyePosition(final Vec3 feet) { ++ return new Position3f((float) feet.x, (float) feet.y + this.originOffset, (float) feet.z); ++ } ++ + @Override + public void interpolate() { + if (PhysicalReplayContext.replaying()) return; @@ -531,8 +594,11 @@ index 0000000..9961a14 + this.cancel(); + return; + } -+ if (!this.nativeInterpolation.active()) return; + if (!this.player.position().equals(this.appliedPosition)) this.captureEye(); ++ // Entity.tick copies old position immediately before calling this handler, including while it is idle. ++ this.previousEye = this.eye; ++ this.appliedPreviousPosition = this.player.oldPosition(); ++ if (!this.nativeInterpolation.active()) return; + final var next = this.nativeInterpolation.tick(this.eye, this.player.getXRot(), this.player.getYRot(), this.player.xRotO, this.player.yRotO, this.player.getYHeadRot()); + if (next.clearMotion()) this.player.setDeltaMovement(Vec3.ZERO); + if (!next.position().equals(this.eye)) { @@ -586,7 +652,9 @@ index 0000000..9961a14 + this.connection = null; + this.level = null; + this.eye = null; ++ this.previousEye = null; + this.appliedPosition = null; ++ this.appliedPreviousPosition = null; + } +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json @@ -684,10 +752,10 @@ index 0000000..1a00bf6 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java new file mode 100644 -index 0000000..c88158b +index 0000000..cc3b1d1 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java -@@ -0,0 +1,190 @@ +@@ -0,0 +1,311 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -790,20 +858,119 @@ index 0000000..c88158b + } + + @Test -+ void resetModesAndMountedPlayersReturnToTheHostPath() { -+ for (int mode = 0; mode <= 2; mode++) { ++ void resetModesApplyImmediatelyAndPreserveTheLiveHeadAndDistinctPreviousPositions() { ++ for (int mode : new int[]{1, 2}) { + try (final var f = new Fixture()) { ++ f.player.setXRot(11.25F); ++ f.player.setYRot(90F); ++ f.player.setYHeadRot(90F); ++ f.player.xRotO = 11.25F; ++ f.player.yRotO = 90F; ++ final Vec3 previous = f.oldPosition.get(); + assertTrue(f.handler.receive(f.movement(0, false))); -+ assertFalse(f.handler.receive(f.movement(mode, mode == 0))); ++ final var reset = new RemotePlayerMovement(17, f.uuid, f.lifetime, mode, false, ++ new Position3f(.5F, 73.24002F, 4.5F), new Position3f(22.75F, 370.5F, -135.25F), 1.62001F, ++ (short) 0, 3, false, f.movement(mode, false).javaTarget()); ++ assertTrue(f.handler.receive(reset)); ++ assertEquals(new Vec3(.5D, (double) 73.24002F - 1.62001F, 4.5D), f.position.get()); ++ assertEquals(mode == 1 ? 0F : 22.75F, f.player.getXRot()); ++ assertEquals(10.5F, f.player.getYRot()); ++ assertEquals(11.25F, f.player.xRotO); ++ assertEquals(90F, f.player.yRotO); ++ assertEquals(90F, f.player.getYHeadRot()); ++ assertEquals(mode == 1 ? previous : f.position.get(), f.oldPosition.get()); + assertFalse(f.handler.hasActiveInterpolation()); + f.handler.interpolate(); -+ verify(f.host).interpolate(); -+ verify(f.player, never()).setPos(anyDouble(), anyDouble(), anyDouble()); ++ assertEquals(.5D, f.position.get().x); ++ verify(f.player).setDeltaMovement(Vec3.ZERO); ++ verify(f.host, never()).interpolate(); + } + } + } + + @Test ++ void headOnlyMotionUsesTheRawPacketPositionAndPreviousNativeTickPosition() { ++ try (final var f = new Fixture()) { ++ assertTrue(f.handler.receive(f.movement(0, false))); ++ f.tick(); ++ f.tick(); ++ assertEquals(1D, f.position.get().x); ++ final var head = new RemotePlayerMovement(17, f.uuid, f.lifetime, 3, false, ++ new Position3f(37, 123, 19), new Position3f(22.75F, 135, -135.25F), 1.62001F, ++ (short) 0, 0, false, new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, 0, 22.5F, false)); ++ assertTrue(f.handler.receive(head)); ++ verify(f.player).setDeltaMovement(new Vec3(36.5F, 123F - 24.413341522216797F, 13.5F)); ++ assertEquals(1D, f.position.get().x); ++ assertTrue(f.handler.hasActiveInterpolation()); ++ } ++ } ++ ++ @Test ++ void packetMotionRetainsPreviousEyePrecisionAcrossTheJavaFeetConversion() { ++ try (final var f = new Fixture()) { ++ final var source = f.movement(0, false); ++ assertTrue(f.handler.receive(new RemotePlayerMovement(17, f.uuid, f.lifetime, 0, false, ++ new Position3f(1.5F, .3F, 5.5F), Position3f.ZERO, source.originOffset(), (short) 0, 3, false, source.javaTarget()))); ++ f.tick(); ++ f.tick(); ++ final var head = new RemotePlayerMovement(17, f.uuid, f.lifetime, 3, false, ++ new Position3f(37, .125F, 19), Position3f.ZERO, source.originOffset(), ++ (short) 0, 0, false, new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, 0, 0, false)); ++ assertTrue(f.handler.receive(head)); ++ verify(f.player).setDeltaMovement(new Vec3(36.5F, .024999991059303284F, 13.5F)); ++ } ++ } ++ ++ @Test ++ void queuedActorMovementResumesAfterAnImmediatePlayerReset() { ++ for (int mode : new int[]{1, 2}) { ++ try (final var f = new Fixture()) { ++ final var source = f.movement(0, false); ++ for (boolean forced : new boolean[]{true, false}) { ++ assertTrue(f.handler.receive(new RemotePlayerMovement(17, f.uuid, f.lifetime, RemotePlayerMovement.ACTOR, false, ++ new Position3f(forced ? -2 : 2.5F, 73.24002F, forced ? 6.5F : 4.5F), new Position3f(11.25F, 45, 45), ++ source.originOffset(), (short) (forced ? 8 : 0), 6, false, source.javaTarget()))); ++ } ++ assertTrue(f.handler.receive(f.movement(mode, false))); ++ assertEquals(1.5D, f.position.get().x); ++ assertTrue(f.handler.hasActiveInterpolation()); ++ f.tick(); ++ assertEquals(1.5D, f.position.get().x); ++ for (int i = 0; i < 6; i++) f.tick(); ++ assertEquals(new Vec3(2.5D, (double) 73.24002F - 1.62001F, 4.5D), f.position.get()); ++ assertEquals(11.25F, f.player.getXRot()); ++ assertEquals(45F, f.player.getYRot()); ++ assertFalse(f.handler.hasActiveInterpolation()); ++ verify(f.host, never()).interpolate(); ++ } ++ } ++ } ++ ++ @Test ++ void resetsAndHeadOnlyUpdatesApplyWithoutNormalMovementTickAdmission() { ++ for (int mode : new int[]{1, 2, 3}) { ++ try (final var f = new Fixture()) { ++ when(f.level.isTickingEntity(f.player)).thenReturn(false); ++ assertFalse(f.handler.receive(f.movement(0, false))); ++ assertTrue(f.handler.receive(f.movement(mode, false))); ++ assertEquals(mode == 3 ? 0D : 1.5D, f.position.get().x); ++ verify(f.host, never()).interpolateTo(any(), anyFloat(), anyFloat(), anyBoolean()); ++ } ++ } ++ } ++ ++ @Test ++ void mountedPlayersRetainTheHostFallback() { ++ try (final var f = new Fixture()) { ++ assertTrue(f.handler.receive(f.movement(0, false))); ++ assertFalse(f.handler.receive(f.movement(0, true))); ++ f.handler.interpolate(); ++ verify(f.host).interpolate(); ++ verify(f.player, never()).setPos(anyDouble(), anyDouble(), anyDouble()); ++ } ++ } ++ ++ @Test + void retiresPendingMovementWhenItsWorldConnectionActorOrNativeLifetimeChanges() { + for (int change = 0; change < 7; change++) { + try (final var f = new Fixture()) { @@ -835,6 +1002,10 @@ index 0000000..c88158b + private final RemotePlayer player = mock(RemotePlayer.class); + private final InterpolationHandler host = mock(InterpolationHandler.class); + private final AtomicReference position = new AtomicReference<>(new Vec3(0, -(double) 1.62001F, 5.5)); ++ private final AtomicReference oldPosition = new AtomicReference<>(this.position.get()); ++ private final AtomicReference pitch = new AtomicReference<>(0F); ++ private final AtomicReference yaw = new AtomicReference<>(0F); ++ private final AtomicReference headYaw = new AtomicReference<>(0F); + private final RemotePlayerInterpolationHandler handler = new RemotePlayerInterpolationHandler(this.player, this.host); + private final MockedStatic game = mockStatic(Minecraft.class); + private final MockedStatic actors = mockStatic(BedrockActorState.class); @@ -855,6 +1026,19 @@ index 0000000..c88158b + when(this.level.getEntity(17)).thenReturn(this.player); + when(this.level.isTickingEntity(this.player)).thenReturn(true); + when(this.player.position()).thenAnswer(_ -> this.position.get()); ++ when(this.player.oldPosition()).thenAnswer(_ -> this.oldPosition.get()); ++ when(this.player.getXRot()).thenAnswer(_ -> this.pitch.get()); ++ when(this.player.getYRot()).thenAnswer(_ -> this.yaw.get()); ++ when(this.player.getYHeadRot()).thenAnswer(_ -> this.headYaw.get()); ++ doAnswer(call -> { this.pitch.set(call.getArgument(0)); return null; }).when(this.player).setXRot(anyFloat()); ++ doAnswer(call -> { this.yaw.set(call.getArgument(0)); return null; }).when(this.player).setYRot(anyFloat()); ++ doAnswer(call -> { this.headYaw.set(call.getArgument(0)); return null; }).when(this.player).setYHeadRot(anyFloat()); ++ doAnswer(call -> { ++ this.oldPosition.set(call.getArgument(0)); ++ this.player.yRotO = call.getArgument(1); ++ this.player.xRotO = call.getArgument(2); ++ return null; ++ }).when(this.player).setOldPosAndRot(any(Vec3.class), anyFloat(), anyFloat()); + when(this.player.getX()).thenAnswer(_ -> this.position.get().x); + when(this.player.getY()).thenAnswer(_ -> this.position.get().y); + when(this.player.getZ()).thenAnswer(_ -> this.position.get().z); @@ -864,6 +1048,11 @@ index 0000000..c88158b + }).when(this.player).setPos(anyDouble(), anyDouble(), anyDouble()); + } + ++ private void tick() { ++ this.oldPosition.set(this.position.get()); ++ this.handler.interpolate(); ++ } ++ + private RemotePlayerMovement movement(final int mode, final boolean riding) { + return new RemotePlayerMovement(17, this.uuid, this.lifetime, mode, riding, + new Position3f(1.5F, 73.24002F, 5.5F), Position3f.ZERO, 1.62001F, (short) 0, 3, false, @@ -880,10 +1069,10 @@ index 0000000..c88158b +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationTest.java new file mode 100644 -index 0000000..d3c0422 +index 0000000..32c2a6a --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationTest.java -@@ -0,0 +1,114 @@ +@@ -0,0 +1,133 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -946,6 +1135,25 @@ index 0000000..d3c0422 + } + + @Test ++ void queuedMovementSurvivesRespawnAndTeleportAfterTheirActiveCountersAreCleared() { ++ for (int mode : new int[]{1, 2}) { ++ final var interpolation = new RemotePlayerInterpolation(); ++ interpolation.receive(movement(RemotePlayerMovement.ACTOR, new Position3f(9, 0, 0), 8, 6)); ++ interpolation.receive(movement(RemotePlayerMovement.ACTOR, new Position3f(12, 0, 0), 0, 6)); ++ interpolation.receive(movement(mode, new Position3f(1, 0, 0), 0, 3)); ++ assertTrue(interpolation.active()); ++ Position3f position = new Position3f(1, 0, 0); ++ final var handoff = interpolation.tick(position, 0, 0, 0, 0, 90); ++ assertEquals(position, handoff.position()); ++ assertEquals(90F, handoff.headYaw()); ++ assertFalse(handoff.clearMotion()); ++ for (int i = 0; i < 6; i++) position = interpolation.tick(position, 0, 0, 0, 0, 90).position(); ++ assertEquals(new Position3f(12, 0, 0), position); ++ assertFalse(interpolation.active()); ++ } ++ } ++ ++ @Test + void takesTheShortRotationArcAndDoesNotMoveForANegativeNativeCounter() { + final var interpolation = new RemotePlayerInterpolation(); + final var source = movement(RemotePlayerMovement.ACTOR, new Position3f(9, 0, 0), 0, -1L); diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md index fde4ad6a..4006cfca 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md @@ -2,9 +2,9 @@ Negotiated receipts retain the original player mode, native inputs, and actor lifetime. The client pairs each receipt with its exact translated position or rotation packet. Admitted remote movement uses independent native counters for position, body rotation, and head rotation. -A decorator retains the real host handler. Unpaired packets, reset modes, known riding, local authority, and unticking entities use the host path. Ordinary Java movement keeps its interpolation history. Connection, world, entity, lifetime, and mount changes retire pending native movement. +A decorator retains the real host handler. Unpaired packets, known riding, and local authority use the host path. Normal and actor interpolation retain the existing ticking guard. Resets and head-only input apply without that normal-mode gate. Ordinary Java movement keeps its interpolation history. Connection, world, entity, lifetime, and mount changes retire pending native movement. -Head-only updates apply raw pitch immediately. They preserve live yaw, pending position and body targets, and forced queues. The head target uses the maximum remaining position/body counter. Explicit packet pairing prevents Java's rotation-only path from cancelling movement. Raw inputs avoid the legacy packet's byte-angle quantization. +Head-only updates apply raw pitch immediately. They preserve live yaw, pending position and body targets, and forced queues. The head target uses the maximum remaining position/body counter. Explicit packet pairing prevents Java's rotation-only path from cancelling movement. Raw inputs avoid the legacy packet's byte-angle quantization. The outer packet path derives motion from raw source position minus the retained previous native eye. Entity ticks retain that float history before interpolation, including while idle. ## Native evidence @@ -12,26 +12,28 @@ Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls use protocol 2225. Read-only S Original instructions from the verified current executable establish the component rules. Mode helper `0x1402d92c0` passes 720 head-only cases, including pending counters and absent-component fallback. Callback `0x143630d70` and angle helper `0x143631440` pass 512 position, rotation, and head-counter cases. Producer `0x1422c8d20` and writer `0x142c74350` pass 432 target cases, including unsigned duration conversion and forced completion. Queue kernel `0x143634ae0` passes 108 handoff and signed-counter cases. Synthetic component stores and explicit boundary callbacks exclude allocation, scheduling, and full factory admission. -A conditional Java/native test compares all 256 current callback cases with a head component. Default regressions cover observed position samples, overlapping head-only input, raw pitch, forced handoff, rotation wrapping, host fallback, and lifecycle replacement. +Original mode-helper and setter instructions also pass 54 teleport-origin cases, including low-Y rounding and signed zero. A conditional Java/native test compares all 256 current callback cases with a head component. Default regressions cover observed position samples, overlapping head-only input, raw pitch, forced handoff, rotation wrapping, host fallback, and lifecycle replacement. ## Installed comparisons -Earlier direct and ViaProxy controls retain the observed large- and small-move steps. Twelve settled phases per route compare movement origins and poses, including the previously occluded leg. The new overlapping-head controls use the final functional source and two fresh player identities. +Earlier direct and ViaProxy controls retain the observed large- and small-move steps. Twelve settled phases per route compare movement origins and poses, including the previously occluded leg. The overlapping-head regression also runs against the current rebuilt source with two fresh player identities. The new fixture sends normal movement followed immediately by a head-only packet. Both rebuilt routes reach the native movement endpoint and retain the head target. A settled update keeps the raw 22.75-degree pitch. Sparse movement after the overlap still uses its independent receive cache. Read-only audits pass all eight position, pitch, yaw, and head-yaw checks per route. -Strict journal decoders verify identical peer spawn, skin, normal, head-only, and sparse bytes against native. Camera instructions precede peer creation. Three settled images per phase compare six colored markers. All marker visibility checks agree. Median centroids stay within 0.34% of each viewport axis; boundaries stay within 0.66%. These projections do not establish frame timing or numeric native body angles. +Strict journal decoders verify identical peer spawn, skin, normal, head-only, and sparse bytes against native. Camera instructions precede peer creation. Three settled images per phase compare six colored markers. All marker visibility checks agree. Current direct/proxy journals contain 2,301 and 2,061 frames. Median centroids stay within 0.41% of each viewport axis; boundaries stay within 0.91%. These projections do not establish frame timing or numeric native body angles. ## Validation and limits -The full four-project build passes 3,352 tests, with 326 skipped and no failures. The conditional native comparison also passes separately. All 157 patches replay from fresh upstream clones. Bundle generation, ten artifact/manifest hashes, and all 1,651 embedded core-class comparisons pass. The installed JAR hashes match the compared builds. Later commit-message edits preserve both exact source trees. +The earlier normal/head comparisons use matching installed hashes. Their cleanup restores the backed-up native profile and owned server configuration. -All owned game, display, relay, and server processes are gone. Cleanup restores the backed-up native world, options, external server lists, and owned server configuration. +Current native reset controls establish immediate position changes and cleared motion/interpolation counters. Respawn resets pitch to zero and preserves previous position. Teleport retains wrapped pitch and resets previous position to its destination. Both retain the current head angle and queued movement. Teleport also retains the native float feet-to-eye roundtrip. -Current native reset controls expose remaining differences in the host fallback. Respawn applies position immediately, clears motion and interpolation counters, and resets pitch to zero. Teleport preserves pitch and resets previous position to its destination. Both retain the current head angle. A queued target survives either reset and resumes afterward. +A fourteen-phase native/direct baseline reproduces the old respawn-pitch and lost-queue defects. Rebuilt direct and ViaProxy routes now pass all fourteen position, pitch, yaw, and head checks. Forced queued movement resumes after either reset and reaches X=2.5. Complete journals contain 3,925 native, 2,933 direct, and 2,726 proxy frames. Strict decoders verify identical peer payloads. All marker visibility checks agree. -A fourteen-phase native/direct comparison verifies identical peer payloads and complete journals. Native resumes the queued target at X=2.5 after both resets. The current Java fallback instead stays at each reset destination and retains the supplied respawn pitch. These cases remain unfixed. The accepted native capture uses corrected teleport payloads and records seven reset-helper returns. An earlier malformed teleport trial and a later debugger pause are excluded. +Reset-pose projections still differ. Median marker centroids differ by up to 0.97% of a viewport axis; boundaries differ by up to 1.44%. The largest centroid difference occurs at an arm after respawn. Native body-angle and tick-order comparisons remain open. State checks do not establish full pose parity. The accepted native capture uses corrected teleport payloads; earlier malformed and debugger-paused trials are excluded. -The outer native player handler also assigns motion before mode dispatch, including head-only packets. It subtracts the previous StateVector position from the raw packet position. Current head-only receipts retain live coordinates instead of that raw position. Packet-level motion and reset parity require broader integration; helper arithmetic alone does not establish them. +Three targeted regressions fail before the correction and pass afterward. Further tests cover exact previous-eye precision, reset admission, retained live head state, distinct previous positions, and queued handoff through the real handler. The full build passes 3,358 tests, with 326 skipped and no failures. The conditional 256-case native comparison also passes separately. Fresh replay reproduces the exact built source trees. All 157 patches replay against pinned and current upstream tips. Bundle generation, ten artifact/manifest hashes, and 1,651 embedded core-class comparisons pass. Installed hashes match the compared builds. + +All owned Java/display, relay, and server processes are gone. The reset comparison restores owned server configuration and leaves the native profile untouched. Full factory, readiness, region, scheduler, and ground-state admission remain unverified. Mounted movement, broader head/body transitions, complete trajectories, and skin parity remain open. The original custom-skin report remains unresolved. From 831fb815aa938a66b389efb2b02c5d5ec8f2f05f Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:03:51 +0100 Subject: [PATCH 43/47] fix(players): retain displacement through native body control Copy the exact native previous-eye position after body control. Java's start-of-tick history copy erased respawn displacement and left the body at 40 degrees instead of the native 60.5 degrees. Read retained float motion for body control while preserving the existing turn formula and host fallback. Historical replay does not advance this live history. Head-only packet motion now uses the last completed native body tick position, including input received during active interpolation. Fold the correction and targeted regressions into the owning movement patch. Verify against current Bedrock 1.26.60.5 read-only traces, all 14 reset body states on both installed routes, and 8 overlapping-head controls per route. The full build passes 3,361 tests with 325 skipped; all 157 patches replay. Packet/tick scheduling and complete pose parity remain open. --- docs/bedrock-coverage.md | 2 +- ...polate-native-remote-player-movement.patch | 137 +++++++++++++++--- ...polate-native-remote-player-movement.pr.md | 22 ++- 3 files changed, 134 insertions(+), 27 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index 3a37b776..c95521f1 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -30,7 +30,7 @@ No complete area has verified Bedrock parity. | Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Short input edges during active Java use, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Remote reset state and queued movement match selected native controls, but reset-pose projections still differ. Readiness, ground state, exact timing, and mounted behavior remain unverified. 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. | +| Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Remote reset state, body angles, and queued movement match selected native controls. Head state and projections remain sensitive to packet/tick ordering. Readiness, ground state, exact timing, and mounted behavior remain unverified. 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. | | Fog and picking | Fog commands, inherited legacy biome bindings, pack-defined defaults, Overworld clock modulation, End dragon distance rules, native actor picking transport, and native float camera rays. Disabling the daylight rule freezes explicit Overworld clocks. | Both routes match selected native fog colors and distances, three non-cardinal rays, and paired armor-stand and block reach controls. Eye precision, other actor shapes, multipart target ordering, spatial blending, timed command transitions, directional sunrise tint, sky sampling, and other render modes remain open. | | Custom content | Resource-defined actors, materials, particles, sound, block geometry, lighting, and collision. | Unsupported material methods, missing assets, carrier capacity, and visual differences remain. Live-server admission does not establish parity. | | Platforms | Linux build, replay, and selected native comparisons. | Windows and macOS launcher/account/rendering paths need their own results. See [platform testing](platform-testing.md). | diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch index 78af16b1..2d24098c 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch @@ -1,4 +1,4 @@ -From c44453d7d2ed4c62bb9a53023de714b328927201 Mon Sep 17 00:00:00 2001 +From 408423877e8f84b10b58d94b23b59d924ab8e1d1 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:51:01 +0100 Subject: [PATCH] fix(players): interpolate negotiated remote movement as @@ -31,9 +31,12 @@ still runs after a reset leaves all counters at zero. Native packet dispatch derives motion from raw source position minus previous StateVector, including head-only packets. Retain exact native -eye history at the entity tick boundary instead of rounding it through -Java feet coordinates. Preserve source positions separately from their -exact translated Java targets. +eye history after native body control instead of rounding it through +Java feet coordinates. Body control reads current-minus-previous before +the StateVector copy. A respawn must retain that displacement through +the first body tick. Copy it afterward, including while interpolation is +idle. Historical physics replay must not advance this live history. +Preserve source positions separately from their exact Java targets. Return known riding, local authority and unpaired packets to the host path. Normal interpolation retains the existing Java ticking guard; @@ -52,19 +55,27 @@ packet handler 0x146bd39a0, teleport setter 0x140baed10 and reset method 0x1402c77b0 establish the new packet/reset rules. Matching native/direct controls distinguish respawn pitch, previous position, retained head state and queued movement after both resets. + +Read-only remote actor vtables identify current PlayerBodyControl::tick +0x1443419f0 through BodyControlComponent and callback 0x146947130. A +respawn moves the body from 40 to 62.5 degrees before the previous-position +copy, then settles at 60.5. A head-only packet received with one position +step left derives motion from the last completed body tick position. +These controls establish history ordering, not scheduler timing. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../MixinRemotePlayerInterpolation.java | 22 ++ ...xinRemotePlayerMovementPacketListener.java | 54 +++ - .../input/BedrockRemotePlayerMovement.java | 81 +++++ + .../render/MixinLivingEntityBodyRotation.java | 7 +- + .../input/BedrockRemotePlayerMovement.java | 81 ++++ .../input/RemotePlayerMovementPayload.java | 22 ++ - .../movement/RemotePlayerInterpolation.java | 142 ++++++++ - .../RemotePlayerInterpolationHandler.java | 205 ++++++++++++ + .../movement/RemotePlayerInterpolation.java | 142 +++++++ + .../RemotePlayerInterpolationHandler.java | 216 +++++++++++ .../viafabricplus-bedrock.mixins.json | 2 + - ...ativeRemoteInterpolationReferenceTest.java | 74 +++++ - .../RemotePlayerInterpolationHandlerTest.java | 311 ++++++++++++++++++ - .../RemotePlayerInterpolationTest.java | 133 ++++++++ - 11 files changed, 1048 insertions(+) + ...ativeRemoteInterpolationReferenceTest.java | 74 ++++ + .../RemotePlayerInterpolationHandlerTest.java | 356 ++++++++++++++++++ + .../RemotePlayerInterpolationTest.java | 133 +++++++ + 12 files changed, 1110 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerInterpolation.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerMovementPacketListener.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockRemotePlayerMovement.java @@ -183,6 +194,34 @@ index 0000000..c9330c6 + original.call(packet); + } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinLivingEntityBodyRotation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinLivingEntityBodyRotation.java +index 89c5db6..8fea24b 100644 +--- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinLivingEntityBodyRotation.java ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinLivingEntityBodyRotation.java +@@ -5,6 +5,7 @@ + */ + package com.viaversion.viafabricplus.bedrock.injection.mixin.features.render; + ++import com.viaversion.viafabricplus.bedrock.movement.RemotePlayerInterpolationHandler; + import com.viaversion.viafabricplus.bedrock.render.BedrockActorState; + import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerBodyRotation; + import net.minecraft.client.player.RemotePlayer; +@@ -27,10 +28,14 @@ public abstract class MixinLivingEntityBodyRotation { + final boolean nativeActor = BedrockActorState.state(player.getUUID()) != null; + final float body = this.nativeBodyRotation.beginTick(nativeActor, player.yBodyRot); + if (nativeActor) { ++ final var interpolation = player.getInterpolation() instanceof RemotePlayerInterpolationHandler handler ? handler : null; ++ final var displacement = interpolation == null ? null : interpolation.bodyDisplacement(); + if (body != player.yBodyRot) player.yBodyRotO = body; + player.yBodyRot = BedrockPlayerBodyRotation.tick(body, player.getYRot(), +- (float) player.getX() - (float) player.xo, (float) player.getZ() - (float) player.zo, ++ displacement == null ? (float) player.getX() - (float) player.xo : displacement.x(), ++ displacement == null ? (float) player.getZ() - (float) player.zo : displacement.z(), + player.getSwingAnimation(1F) > 0F, player.isPassenger()); ++ if (interpolation != null) interpolation.finishBodyTick(); + ci.cancel(); + } + } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockRemotePlayerMovement.java b/src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockRemotePlayerMovement.java new file mode 100644 index 0000000..5169027 @@ -448,10 +487,10 @@ index 0000000..61a8f44 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java new file mode 100644 -index 0000000..2eef0b4 +index 0000000..8cb6f5f --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java -@@ -0,0 +1,205 @@ +@@ -0,0 +1,216 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -583,6 +622,20 @@ index 0000000..2eef0b4 + return new Position3f((float) feet.x, (float) feet.y + this.originOffset, (float) feet.z); + } + ++ public Position3f bodyDisplacement() { ++ if (PhysicalReplayContext.replaying() || !this.validContext()) return null; ++ if (!this.player.position().equals(this.appliedPosition)) this.captureEye(); ++ return new Position3f(this.eye.x() - this.previousEye.x(), this.eye.y() - this.previousEye.y(), this.eye.z() - this.previousEye.z()); ++ } ++ ++ public void finishBodyTick() { ++ if (PhysicalReplayContext.replaying() || !this.validContext()) return; ++ if (!this.player.position().equals(this.appliedPosition)) this.captureEye(); ++ // Native body control reads displacement before the end-of-tick StateVector copy. ++ this.previousEye = this.eye; ++ this.appliedPreviousPosition = this.player.oldPosition(); ++ } ++ + @Override + public void interpolate() { + if (PhysicalReplayContext.replaying()) return; @@ -595,9 +648,6 @@ index 0000000..2eef0b4 + return; + } + if (!this.player.position().equals(this.appliedPosition)) this.captureEye(); -+ // Entity.tick copies old position immediately before calling this handler, including while it is idle. -+ this.previousEye = this.eye; -+ this.appliedPreviousPosition = this.player.oldPosition(); + if (!this.nativeInterpolation.active()) return; + final var next = this.nativeInterpolation.tick(this.eye, this.player.getXRot(), this.player.getYRot(), this.player.xRotO, this.player.yRotO, this.player.getYHeadRot()); + if (next.clearMotion()) this.player.setDeltaMovement(Vec3.ZERO); @@ -752,10 +802,10 @@ index 0000000..1a00bf6 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java new file mode 100644 -index 0000000..cc3b1d1 +index 0000000..563d95a --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java -@@ -0,0 +1,311 @@ +@@ -0,0 +1,356 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -764,7 +814,9 @@ index 0000000..cc3b1d1 +package com.viaversion.viafabricplus.bedrock.movement; + +import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; ++import com.viaversion.viafabricplus.bedrock.movement.replay.PhysicalReplayContext; +import com.viaversion.viafabricplus.bedrock.render.BedrockActorState; ++import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerBodyRotation; +import net.minecraft.SharedConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; @@ -782,6 +834,7 @@ index 0000000..cc3b1d1 +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + ++import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + @@ -889,7 +942,7 @@ index 0000000..cc3b1d1 + } + + @Test -+ void headOnlyMotionUsesTheRawPacketPositionAndPreviousNativeTickPosition() { ++ void headOnlyMotionUsesTheRawPacketPositionAndLastCompletedNativePosition() { + try (final var f = new Fixture()) { + assertTrue(f.handler.receive(f.movement(0, false))); + f.tick(); @@ -899,7 +952,7 @@ index 0000000..cc3b1d1 + new Position3f(37, 123, 19), new Position3f(22.75F, 135, -135.25F), 1.62001F, + (short) 0, 0, false, new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, 0, 22.5F, false)); + assertTrue(f.handler.receive(head)); -+ verify(f.player).setDeltaMovement(new Vec3(36.5F, 123F - 24.413341522216797F, 13.5F)); ++ verify(f.player).setDeltaMovement(new Vec3(36F, 123F - 48.826683044433594F, 13.5F)); + assertEquals(1D, f.position.get().x); + assertTrue(f.handler.hasActiveInterpolation()); + } @@ -917,7 +970,48 @@ index 0000000..cc3b1d1 + new Position3f(37, .125F, 19), Position3f.ZERO, source.originOffset(), + (short) 0, 0, false, new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, 0, 0, false)); + assertTrue(f.handler.receive(head)); -+ verify(f.player).setDeltaMovement(new Vec3(36.5F, .024999991059303284F, 13.5F)); ++ verify(f.player).setDeltaMovement(new Vec3(36F, -.07500001788139343F, 13.5F)); ++ } ++ } ++ ++ @Test ++ void bodyControlUsesTheRespawnDisplacementBeforeCopyingPreviousPosition() { ++ try (final var f = new Fixture()) { ++ assertTrue(f.handler.receive(f.movement(0, false))); ++ for (int i = 0; i < 3; i++) f.tick(); ++ final var reset = new RemotePlayerMovement(17, f.uuid, f.lifetime, 1, false, ++ new Position3f(.5F, 73.24002F, 4.5F), new Position3f(22.75F, 370.5F, -135.25F), 1.62001F, ++ (short) 0, 3, false, f.movement(1, false).javaTarget()); ++ assertTrue(f.handler.receive(reset)); ++ // Java copies its render history before interpolation; native body control still needs the pre-reset position. ++ f.oldPosition.set(f.position.get()); ++ f.handler.interpolate(); ++ final var displacement = f.handler.bodyDisplacement(); ++ assertEquals(new Position3f(-1, 0, -1), displacement); ++ float body = BedrockPlayerBodyRotation.tick(40F, f.player.getYRot(), displacement.x(), displacement.z(), false, false); ++ assertEquals(62.5F, body); ++ f.handler.finishBodyTick(); ++ assertEquals(Position3f.ZERO, f.handler.bodyDisplacement()); ++ for (int tick = 0; tick < 25; tick++) body = BedrockPlayerBodyRotation.tick(body, 10.5F, 0, 0, false, false); ++ assertEquals(60.5F, body); ++ } ++ } ++ ++ @Test ++ void historicalBodyTicksDoNotAdvanceTheLiveNativePositionHistory() { ++ try (final var f = new Fixture()) { ++ assertTrue(f.handler.receive(f.movement(0, false))); ++ f.oldPosition.set(f.position.get()); ++ f.handler.interpolate(); ++ final var displacement = f.handler.bodyDisplacement(); ++ assertEquals(.5F, displacement.x()); ++ try (final var ignored = PhysicalReplayContext.enter(f.level, f.connection, Set.of(f.player))) { ++ assertNull(f.handler.bodyDisplacement()); ++ f.handler.finishBodyTick(); ++ } ++ assertEquals(displacement, f.handler.bodyDisplacement()); ++ f.handler.finishBodyTick(); ++ assertEquals(Position3f.ZERO, f.handler.bodyDisplacement()); + } + } + @@ -1051,6 +1145,7 @@ index 0000000..cc3b1d1 + private void tick() { + this.oldPosition.set(this.position.get()); + this.handler.interpolate(); ++ this.handler.finishBodyTick(); + } + + private RemotePlayerMovement movement(final int mode, final boolean riding) { diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md index 4006cfca..cc51b38b 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md @@ -4,7 +4,9 @@ Negotiated receipts retain the original player mode, native inputs, and actor li A decorator retains the real host handler. Unpaired packets, known riding, and local authority use the host path. Normal and actor interpolation retain the existing ticking guard. Resets and head-only input apply without that normal-mode gate. Ordinary Java movement keeps its interpolation history. Connection, world, entity, lifetime, and mount changes retire pending native movement. -Head-only updates apply raw pitch immediately. They preserve live yaw, pending position and body targets, and forced queues. The head target uses the maximum remaining position/body counter. Explicit packet pairing prevents Java's rotation-only path from cancelling movement. Raw inputs avoid the legacy packet's byte-angle quantization. The outer packet path derives motion from raw source position minus the retained previous native eye. Entity ticks retain that float history before interpolation, including while idle. +Head-only updates apply raw pitch immediately. They preserve live yaw, pending position and body targets, and forced queues. The head target uses the maximum remaining position/body counter. Explicit packet pairing prevents Java's rotation-only path from cancelling movement. Raw inputs avoid the legacy packet's byte-angle quantization. + +The outer packet path derives motion from raw source position minus the retained previous native eye. Body control reads the exact native displacement before the previous-position copy. The client retains that float history after body control, including while interpolation is idle. Historical physics replay does not advance this live history. ## Native evidence @@ -12,6 +14,10 @@ Official Bedrock 1.26.60.5/BDS 1.26.60.6 controls use protocol 2225. Read-only S Original instructions from the verified current executable establish the component rules. Mode helper `0x1402d92c0` passes 720 head-only cases, including pending counters and absent-component fallback. Callback `0x143630d70` and angle helper `0x143631440` pass 512 position, rotation, and head-counter cases. Producer `0x1422c8d20` and writer `0x142c74350` pass 432 target cases, including unsigned duration conversion and forced completion. Queue kernel `0x143634ae0` passes 108 handoff and signed-counter cases. Synthetic component stores and explicit boundary callbacks exclude allocation, scheduling, and full factory admission. +Live remote actor vtables identify current player body control `0x1443419f0`, invoked through callback `0x146947130`. Read-only entry, return, and position-write samples establish the history order. After respawn, body yaw changes from 40 to 62.5 degrees before the previous-position copy, then settles at 60.5 degrees. The following return settles at 50 degrees. + +A head-only packet arrives with one interpolation step remaining and derives motion from the last completed body tick position. Debugger pauses alter wall-clock timing, so these controls establish state ordering only. The existing body-turn formula remains unchanged. + Original mode-helper and setter instructions also pass 54 teleport-origin cases, including low-Y rounding and signed zero. A conditional Java/native test compares all 256 current callback cases with a head component. Default regressions cover observed position samples, overlapping head-only input, raw pitch, forced handoff, rotation wrapping, host fallback, and lifecycle replacement. ## Installed comparisons @@ -20,7 +26,7 @@ Earlier direct and ViaProxy controls retain the observed large- and small-move s The new fixture sends normal movement followed immediately by a head-only packet. Both rebuilt routes reach the native movement endpoint and retain the head target. A settled update keeps the raw 22.75-degree pitch. Sparse movement after the overlap still uses its independent receive cache. Read-only audits pass all eight position, pitch, yaw, and head-yaw checks per route. -Strict journal decoders verify identical peer spawn, skin, normal, head-only, and sparse bytes against native. Camera instructions precede peer creation. Three settled images per phase compare six colored markers. All marker visibility checks agree. Current direct/proxy journals contain 2,301 and 2,061 frames. Median centroids stay within 0.41% of each viewport axis; boundaries stay within 0.91%. These projections do not establish frame timing or numeric native body angles. +Strict journal decoders verify identical peer spawn, skin, normal, head-only, and sparse bytes against native. Camera instructions precede peer creation. Three settled images per phase compare six colored markers. All marker visibility checks agree. Current direct/proxy journals contain 2,014 and 1,709 frames. Median centroids stay within 0.39% of each viewport axis; boundaries stay within 0.91%. These projections do not establish frame timing or numeric native body angles. ## Validation and limits @@ -28,11 +34,17 @@ The earlier normal/head comparisons use matching installed hashes. Their cleanup Current native reset controls establish immediate position changes and cleared motion/interpolation counters. Respawn resets pitch to zero and preserves previous position. Teleport retains wrapped pitch and resets previous position to its destination. Both retain the current head angle and queued movement. Teleport also retains the native float feet-to-eye roundtrip. -A fourteen-phase native/direct baseline reproduces the old respawn-pitch and lost-queue defects. Rebuilt direct and ViaProxy routes now pass all fourteen position, pitch, yaw, and head checks. Forced queued movement resumes after either reset and reaches X=2.5. Complete journals contain 3,925 native, 2,933 direct, and 2,726 proxy frames. Strict decoders verify identical peer payloads. All marker visibility checks agree. +Fourteen-phase installed direct and ViaProxy controls pass the declared position, pitch, yaw, head-yaw, and body-yaw expectations. Both queued-reset cases reach X=2.5. Respawn now settles at the native 60.5-degree body yaw, and the following return settles at 50 degrees. Strict decoders verify identical peer payloads for all fourteen phases. The current native journal contains 8,096 complete frames, including two auxiliary movement probes. Direct and proxy journals contain 2,927 and 2,735 complete frames. + +Main rotation and body yaw match current native medians in all twelve traced phases. Head yaw matches in ten phases. In the other two phases, native completes one interpolation tick between normal movement and teleport. Its retained head yaw is 30 degrees; both Java routes process those packets before a tick and retain zero. The existing reset rule preserves either live angle. These samples do not establish packet/tick scheduling parity. + +All marker visibility checks agree. Median marker centroids differ by up to 0.64% of a viewport axis, and boundaries differ by up to 1.44%. The largest remaining centroid difference occurs at the head after head-only input, with different retained head angles. These projections do not isolate a rendering defect or establish full pose parity. + +Targeted regressions cover respawn pitch, queued movement, and position-history ordering. Further tests cover exact previous-eye precision, reset admission, retained live head state, distinct previous positions, and queued handoff through the real handler. -Reset-pose projections still differ. Median marker centroids differ by up to 0.97% of a viewport axis; boundaries differ by up to 1.44%. The largest centroid difference occurs at an arm after respawn. Native body-angle and tick-order comparisons remain open. State checks do not establish full pose parity. The accepted native capture uses corrected teleport payloads; earlier malformed and debugger-paused trials are excluded. +The current full build passes 3,361 tests, with 325 skipped and no failures. This run includes the conditional 256-case native comparison. The respawn body-history regression fails with the old ordering and passes with the correction. A separate replay-scope regression protects the live position history. -Three targeted regressions fail before the correction and pass afterward. Further tests cover exact previous-eye precision, reset admission, retained live head state, distinct previous positions, and queued handoff through the real handler. The full build passes 3,358 tests, with 326 skipped and no failures. The conditional 256-case native comparison also passes separately. Fresh replay reproduces the exact built source trees. All 157 patches replay against pinned and current upstream tips. Bundle generation, ten artifact/manifest hashes, and 1,651 embedded core-class comparisons pass. Installed hashes match the compared builds. +Fresh replay reproduces the exact built source trees. All 157 patches replay against pinned and current upstream tips. Bundle generation, ten artifact/manifest hashes, and 1,651 embedded core-class comparisons pass. Installed hashes match the compared builds. All owned Java/display, relay, and server processes are gone. The reset comparison restores owned server configuration and leaves the native profile untouched. From 0c890e7b4b841e336f48a36c665672ef365b2513 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:08:18 +0100 Subject: [PATCH 44/47] fix(render): match native player head query limits Apply the native float wrap and 85-degree limits to world player target_y_rotation queries. Preserve small-angle rounding, signed zero, and the native lower bound for unordered input. Share the query between player animation and server controller selection, with existing GUI overrides retained. Fold the implementation and query-map regression into the owning appearance patch, then replay controller and GUI consumers. Current Bedrock 1.26.60.5 instructions supply 60 query controls. State-aligned native/direct/proxy comparisons distinguish rendering from packet/tick ordering and retain the matching first three post-teleport body ticks. The full build passes 3,362 tests with 325 skipped. Fresh 157-patch replays, bundle, ten artifact hashes and 1,651 embedded core-class comparisons pass. Four installed recordings retain matching peer payloads. Complete pose, frame timing and the original five-area compatibility goal remain open. --- docs/bedrock-coverage.md | 2 +- ...acter-creator-accounts-and-wardrobes.patch | 86 +++++++++++++------ ...acter-creator-accounts-and-wardrobes.pr.md | 8 ++ .../0016-render-server-player-costumes.patch | 22 ++--- .../0016-render-server-player-costumes.pr.md | 2 + ...0029-share-native-ui-scene-rendering.patch | 16 ++-- ...0029-share-native-ui-scene-rendering.pr.md | 2 + ...polate-native-remote-player-movement.pr.md | 10 +-- 8 files changed, 99 insertions(+), 49 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index c95521f1..f843c891 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -30,7 +30,7 @@ No complete area has verified Bedrock parity. | Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Short input edges during active Java use, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Remote reset state, body angles, and queued movement match selected native controls. Head state and projections remain sensitive to packet/tick ordering. Readiness, ground state, exact timing, and mounted behavior remain unverified. 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. | +| Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Selected reset, head, body, and queued-movement controls match native after packet/tick state alignment. Full trajectories and pose parity remain open. Readiness, ground state, exact timing, and mounted behavior remain unverified. 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. | | Fog and picking | Fog commands, inherited legacy biome bindings, pack-defined defaults, Overworld clock modulation, End dragon distance rules, native actor picking transport, and native float camera rays. Disabling the daylight rule freezes explicit Overworld clocks. | Both routes match selected native fog colors and distances, three non-cardinal rays, and paired armor-stand and block reach controls. Eye precision, other actor shapes, multipart target ordering, spatial blending, timed command transitions, directional sunrise tint, sky sampling, and other render modes remain open. | | Custom content | Resource-defined actors, materials, particles, sound, block geometry, lighting, and collision. | Unsupported material methods, missing assets, carrier capacity, and visual differences remain. Live-server admission does not establish parity. | | Platforms | Linux build, replay, and selected native comparisons. | Windows and macOS launcher/account/rendering paths need their own results. See [platform testing](platform-testing.md). | diff --git a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch index dd9dcb8d..2cfa2cd8 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch @@ -1,4 +1,4 @@ -From b0e1fc1eef7900a8c633db102001af2e8d2572f8 Mon Sep 17 00:00:00 2001 +From ec7196f3d606a8483edf4be16ad2bd1755598c04 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(appearance): manage Character Creator accounts and @@ -126,6 +126,14 @@ player graphs, including first-person and GUI actor perspectives. Retain head occupancy by value rather than retaining the mutable ItemStack. The existing avatar-lifetime regression covers equipped head state and an unmounted predicate while proving cached closures do not hold avatars. + +Apply the native query float wrap and 85-degree limits to the rendered +head-minus-body yaw. Preserve small-angle rounding and signed zero. Preserve native +MAXSS behavior for an unordered angle by selecting the lower bound. +Current 1.26.60.5 query callback 0x140684fd0 and helper 0x142c896e0 establish +this path through typed registration 0x142c898c0. Original instructions +pass 60 controls with explicit actor-type and virtual-getter boundaries. +A real player query-map regression fails before the correction. --- .../bedrock/ViaFabricPlusBedrock.java | 7 + .../appearance/BedrockActorQueries.java | 10 + @@ -162,7 +170,7 @@ an unmounted predicate while proving cached closures do not hold avatars. .../features/render/MixinOptionsEmote.java | 34 + .../viabedrock/ViaFabricPlusSkinProvider.java | 16 +- .../bedrock/render/BedrockPlayerRenderer.java | 7 +- - .../bedrock/render/BedrockPlayerSkins.java | 653 ++++++- + .../bedrock/render/BedrockPlayerSkins.java | 661 ++++++- .../screen/BedrockCharacterSlotsScreen.java | 1401 +++++++++++++++ .../screen/BedrockClassicSkinPackScreen.java | 83 +- .../screen/BedrockDressingRoomScreen.java | 88 +- @@ -200,9 +208,9 @@ an unmounted predicate while proving cached closures do not hold avatars. .../appearance/BedrockPersonaTintTest.java | 280 +++ .../BedrockPlayerAnimationsTest.java | 90 + .../appearance/NativeEmotePayloadTest.java | 103 ++ - .../render/BedrockActorQuerySnapshotTest.java | 98 ++ + .../render/BedrockActorQuerySnapshotTest.java | 120 ++ .../BedrockStandardPlayerGeometryTest.java | 28 + - 75 files changed, 14892 insertions(+), 118 deletions(-) + 75 files changed, 14922 insertions(+), 118 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockCharacterProfiles.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockClassicAssets.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockEmotePlayback.java @@ -6829,10 +6837,10 @@ index 39a1e1f..59657b0 100644 } catch (IOException e) { ViaFabricPlusBedrock.impl().logger().warn("Could not load the selected Bedrock appearance; using Steve", e); diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java -index a1e0f55..fd2eeb1 100644 +index a4e6f37..385eb7b 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java -@@ -75,10 +75,13 @@ final class BedrockPlayerRenderer extends AvatarRenderer { +@@ -87,10 +87,13 @@ final class BedrockPlayerRenderer extends AvatarRenderer { private void submitHand(final PoseStack poseStack, final SubmitNodeCollector collector, final int packedLight, final Identifier skinTexture, final boolean right, final boolean hasSleeve) { @@ -6849,7 +6857,7 @@ index a1e0f55..fd2eeb1 100644 } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java -index c482ba2..db50887 100644 +index c482ba2..c31a809 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java @@ -19,37 +19,62 @@ @@ -7017,7 +7025,7 @@ index c482ba2..db50887 100644 return null; } if (appearance.renderer == null || appearance.rendererEpoch != rendererEpoch) { -@@ -212,13 +272,447 @@ public final class BedrockPlayerSkins { +@@ -212,13 +272,455 @@ public final class BedrockPlayerSkins { BedrockGeometry.playerModel(surface.geometry().model(), appearance.slim, surface.geometry().meshes()), surface.texture())).toList(); appearance.renderer = BedrockPlayerLayerModels.construct(rendererContext, () -> new BedrockPlayerRenderer(rendererContext, @@ -7306,6 +7314,14 @@ index c482ba2..db50887 100644 + return geometry == null ? Map.of() : geometry.queries(); + } + ++ static float targetYaw(final float relativeYaw) { ++ float wrapped = (relativeYaw + 180F) % 360F; ++ if (wrapped < 0F) wrapped += 360F; ++ wrapped -= 180F; ++ // Native MAXSS selects the lower bound for an unordered wrapped angle. ++ return Float.isNaN(wrapped) ? -85F : Math.clamp(wrapped, -85F, 85F); ++ } ++ + static Map actorQueries(final Avatar entity, final AvatarRenderState state, final float partialTick) { + final Map queries = new HashMap<>(); + final UUID actorId = entity.getUUID(); @@ -7333,7 +7349,7 @@ index c482ba2..db50887 100644 + queries.put("blocking", BedrockActorState.isBlocking(entity.getUUID(), entity.isBlocking())); + queries.put("shield_blocking_bob", BedrockActorState.shieldBlockingBob(entity.getUUID(), partialTick)); + queries.put("target_x_rotation", state.xRot); -+ queries.put("target_y_rotation", state.yRot); ++ queries.put("target_y_rotation", targetYaw(state.yRot)); + queries.put("body_y_rotation", state.bodyRot); + queries.put("walk_distance", entity instanceof BedrockWalkingAnimation.Provider walking + ? walking.bedrockWalkingAnimation().sample(partialTick) : 0F); @@ -7466,7 +7482,7 @@ index c482ba2..db50887 100644 public static AvatarRenderer renderer(final PlayerSkin skin, final float ageInTicks) { if (skin == null || skin.body() == null) { return null; -@@ -238,10 +732,55 @@ public final class BedrockPlayerSkins { +@@ -238,10 +740,55 @@ public final class BedrockPlayerSkins { public static Model.Simple previewModel(final Identifier texture) { final UUID id = PLAYER_BY_TEXTURE.get(texture); final Appearance appearance = id == null ? null : APPEARANCES.get(id); @@ -7525,7 +7541,7 @@ index c482ba2..db50887 100644 } /** Supplies the additional animated surfaces in the same pose as a menu's primary skin. */ -@@ -251,14 +790,19 @@ public final class BedrockPlayerSkins { +@@ -251,14 +798,19 @@ public final class BedrockPlayerSkins { if (appearance == null || appearance.surfaces.isEmpty()) return List.of(); if (appearance.previewSurfaces == null) appearance.previewSurfaces = appearance.surfaces.stream().map(surface -> new PreviewSurface( @@ -7547,7 +7563,7 @@ index c482ba2..db50887 100644 public static void rendererContext(final EntityRendererProvider.Context context) { BedrockActorResources.clear(); -@@ -272,6 +816,7 @@ public final class BedrockPlayerSkins { +@@ -272,6 +824,7 @@ public final class BedrockPlayerSkins { APPEARANCES.values().forEach(BedrockPlayerSkins::release); APPEARANCES.clear(); PLAYER_BY_TEXTURE.clear(); @@ -7555,7 +7571,7 @@ index c482ba2..db50887 100644 SKIN_ASSEMBLER.clear(); skinConnection = null; } -@@ -289,7 +834,8 @@ public final class BedrockPlayerSkins { +@@ -289,7 +842,8 @@ public final class BedrockPlayerSkins { } private static Identifier textureId(final UUID playerUuid, final String kind) { @@ -7565,7 +7581,7 @@ index c482ba2..db50887 100644 } private static boolean supportedImage(final BufferedImage image) { -@@ -315,7 +861,10 @@ public final class BedrockPlayerSkins { +@@ -315,7 +869,10 @@ public final class BedrockPlayerSkins { static String geometryName(final String resourcePatch, final String binding) { try { @@ -7577,7 +7593,7 @@ index c482ba2..db50887 100644 final var name = geometry.get(binding); return name != null && name.isJsonPrimitive() && name.getAsJsonPrimitive().isString() ? name.getAsString() : ""; -@@ -345,7 +894,8 @@ public final class BedrockPlayerSkins { +@@ -345,7 +902,8 @@ public final class BedrockPlayerSkins { final List models = BedrockGeometryModel.fromJson(data); final BedrockGeometryModel model = models.stream().filter(value -> value.getIdentifier().equals(name)).findFirst() .orElseGet(() -> models.isEmpty() ? null : models.getFirst()); @@ -7587,7 +7603,7 @@ index c482ba2..db50887 100644 } catch (RuntimeException e) { LOGGER.warn("Failed to parse Bedrock player geometry", e); return null; -@@ -372,7 +922,8 @@ public final class BedrockPlayerSkins { +@@ -372,7 +930,8 @@ public final class BedrockPlayerSkins { final Map animatedModels = new HashMap<>(); for (final var entry : models) { final JsonObject model = entry.getAsJsonObject(); @@ -7597,7 +7613,7 @@ index c482ba2..db50887 100644 if (name.equals(identifier)) { if (body != null) { return null; -@@ -411,7 +962,8 @@ public final class BedrockPlayerSkins { +@@ -411,7 +970,8 @@ public final class BedrockPlayerSkins { final JsonObject model = animatedModels.get(type); final var animation = animationByType.get(type); if (model == null) { @@ -7607,7 +7623,7 @@ index c482ba2..db50887 100644 return null; } continue; -@@ -426,7 +978,8 @@ public final class BedrockPlayerSkins { +@@ -426,7 +986,8 @@ public final class BedrockPlayerSkins { BedrockGeometry.playerModel(parsed, false, meshes); surfaces.add(new Surface(parsed, Map.copyOf(meshes), animation)); } @@ -7617,7 +7633,7 @@ index c482ba2..db50887 100644 if (!coverage.getOrDefault(part, false)) { return null; } -@@ -437,7 +990,8 @@ public final class BedrockPlayerSkins { +@@ -437,7 +998,8 @@ public final class BedrockPlayerSkins { return null; } BedrockGeometry.playerModel(parsedBody, false, bodyMeshes); @@ -7627,7 +7643,7 @@ index c482ba2..db50887 100644 } catch (RuntimeException e) { LOGGER.warn("Failed to parse Bedrock persona geometry", e); return null; -@@ -474,12 +1028,21 @@ public final class BedrockPlayerSkins { +@@ -474,12 +1036,21 @@ public final class BedrockPlayerSkins { } } } @@ -7652,7 +7668,7 @@ index c482ba2..db50887 100644 } } return meshes; -@@ -501,7 +1064,13 @@ public final class BedrockPlayerSkins { +@@ -501,7 +1072,13 @@ public final class BedrockPlayerSkins { return vector; } @@ -7667,7 +7683,7 @@ index c482ba2..db50887 100644 Surface face() { return this.surfaces.stream().filter(surface -> surface.animation().type() == 1).findFirst().orElse(null); } -@@ -519,13 +1088,23 @@ public final class BedrockPlayerSkins { +@@ -519,13 +1096,23 @@ public final class BedrockPlayerSkins { private final Identifier headId; private final long started = System.nanoTime(); private final Geometry geometry; @@ -16331,10 +16347,10 @@ index 0000000..f8e433d +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorQuerySnapshotTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorQuerySnapshotTest.java new file mode 100644 -index 0000000..c3ff063 +index 0000000..5b88ad5 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorQuerySnapshotTest.java -@@ -0,0 +1,98 @@ +@@ -0,0 +1,120 @@ +package com.viaversion.viafabricplus.bedrock.render; + +import com.viaversion.viafabricplus.bedrock.appearance.BedrockActorAnimation; @@ -16416,6 +16432,28 @@ index 0000000..c3ff063 + assertNoAvatarReference(queries, Collections.newSetFromMap(new IdentityHashMap<>())); + } + ++ @Test ++ void wrapsAndBoundsWorldPlayerHeadTargetsBeforeAnimationEvaluation() { ++ final var avatar = mock(Avatar.class); ++ when(avatar.getUUID()).thenReturn(UUID.randomUUID()); ++ when(avatar.getMainArm()).thenReturn(HumanoidArm.RIGHT); ++ when(avatar.getMainHandItem()).thenReturn(ItemStack.EMPTY); ++ when(avatar.getUseItem()).thenReturn(ItemStack.EMPTY); ++ when(avatar.getItemBySlot(any(EquipmentSlot.class))).thenReturn(ItemStack.EMPTY); ++ when(avatar.getDeltaMovement()).thenReturn(Vec3.ZERO); ++ final var state = new AvatarRenderState(); ++ state.rightHandItemStack = ItemStack.EMPTY; ++ state.leftHandItemStack = ItemStack.EMPTY; ++ // Current native target_y_rotation, including the head/body angles from the reset fixture. ++ final float[][] controls = {{-119.5F, -85F}, {-89.5F, -85F}, {-85F, -85F}, {-84.75F, -84.75F}, ++ {-0F, 0F}, {.000001F, 0F}, {-.000001F, 0F}, {0F, 0F}, {84.75F, 84.75F}, {85F, 85F}, {119.5F, 85F}, {Float.NaN, -85F}, {Float.POSITIVE_INFINITY, -85F}}; ++ for (final float[] control : controls) { ++ state.yRot = control[0]; ++ final var queries = BedrockPlayerSkins.actorQueries(avatar, state, .5F); ++ assertEquals(control[1], queries.get("target_y_rotation")); ++ } ++ } ++ + /** Inspect captured values directly; collection through System.gc() would make this lifecycle test flaky. */ + private static void assertNoAvatarReference(final Object value, final Set visited) throws Exception { + if (value == null || !visited.add(value)) return; diff --git a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md index 65c1b819..445a8300 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md @@ -8,6 +8,8 @@ Keep character slots, persona recipe assembly, owned catalogs, purchases and emo Keep account entitlement, downloaded assets, character recipes, previews, and in-world playback under distinct owners. Assemble persona geometry and tints without leaking mutable resources across characters. +World-player `target_y_rotation` applies the native float wrap and the -85/+85-degree limits. Unordered wrapped values select the lower bound. Server player controller selection uses the same helper. Existing GUI camera overrides retain their previous values. + Prerequisites: - viafabricplus-bedrock/0008-share-native-actor-and-resource-runtime.patch: Land shared actor and resource playback before account appearance selection. @@ -18,6 +20,12 @@ Prerequisites: Regression coverage includes `BedrockAccountAppearanceTest`, `BedrockAppearanceStoreTest`, `BedrockBuiltinAssetsTest`, `BedrockCharacterColorsTest`. +Current Bedrock 1.26.60.5/protocol 2225 registration identifies target-y callback `0x140684fd0` and helper `0x142c896e0`. The observed remote-player vtable resolves its head and body getters to the matching native components. Original instructions execute 60 controls. Actor type, interpolated virtual getters, and CRT remainder are explicit fixture boundaries. The original wrapper, angle arithmetic, limit, and result publication execute. The player query-map regression covers limits, tiny angles, signed zero, and non-finite input. + +Fourteen-phase installed direct and ViaProxy controls use matching peer skin, spawn, and movement bytes against the native fixture. A private scheduling pause aligns the intermediate normal-then-teleport state and the first three body ticks after teleport. All four sampled angles match the twelve traced native phases. The [movement comparison](0041-interpolate-native-remote-player-movement.pr.md) describes the state controls and their limits. + +Three settled images per phase compare six colored markers. Median centroids differ by at most 0.26% of a viewport axis, and boundaries differ by at most 0.42%. The previously mismatched teleport head centroid now differs by less than 0.10% of either axis. All marker visibility checks agree. Solid-color heads do not establish every face orientation. Complete pose, skin, and frame-timing parity remain unverified. + Controlled account and emote references cover specific operations. Purchases, missing receipt keys, unavailable emotes, profile/LOD behavior, armor masking, and native multiplayer timing remain open. The [owning patch](0009-manage-character-creator-accounts-and-wardrobes.patch) retains implementation rationale and versioned evidence. diff --git a/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.patch b/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.patch index a019ff95..c3f95ebe 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.patch @@ -1,4 +1,4 @@ -From 60aca0aeb0126380e871033311e018366595e890 Mon Sep 17 00:00:00 2001 +From 523906b82cb74b20b5e07d8d4c240b6a82ca6076 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): render server player controllers and costumes @@ -604,10 +604,10 @@ index 0000000..93a9f3e + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java -index fd2eeb1..ae59316 100644 +index 385eb7b..d3a9eb6 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java -@@ -47,10 +47,12 @@ final class BedrockPlayerRenderer extends AvatarRenderer { +@@ -48,10 +48,12 @@ final class BedrockPlayerRenderer extends AvatarRenderer { this.personaLayers.add(layer); this.addLayer(layer); } @@ -621,7 +621,7 @@ index fd2eeb1..ae59316 100644 return this.nativeMaterial(this.getTextureLocation(state)); } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java -index b6713a5..74b22b4 100644 +index b4b7070..9f62bbb 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java @@ -34,9 +34,10 @@ import com.viaversion.viafabricplus.bedrock.appearance.BedrockPlayerAnimations; @@ -776,7 +776,7 @@ index b6713a5..74b22b4 100644 + queries.put("is_alive", true); + queries.put("target_x_rotation", state.xRot); + queries.put("head_x_rotation", BedrockPlayerQueries.headPitch(state.xRot)); -+ queries.put("target_y_rotation", state.yRot); ++ queries.put("target_y_rotation", targetYaw(state.yRot)); + queries.put("modified_move_speed", state.walkAnimationSpeed); + queries.put("modified_distance_moved", state.walkAnimationPos); + appearance.serverSelection = appearance.serverControllers.select(state.ageInTicks / 20F, @@ -1060,15 +1060,15 @@ index b6713a5..74b22b4 100644 } } finally { poses.popPose(); -@@ -585,6 +925,7 @@ public final class BedrockPlayerSkins { +@@ -593,6 +933,7 @@ public final class BedrockPlayerSkins { queries.put("blocking", BedrockActorState.isBlocking(entity.getUUID(), entity.isBlocking())); queries.put("shield_blocking_bob", BedrockActorState.shieldBlockingBob(entity.getUUID(), partialTick)); queries.put("target_x_rotation", state.xRot); + queries.put("head_x_rotation", BedrockPlayerQueries.headPitch(state.xRot)); - queries.put("target_y_rotation", state.yRot); + queries.put("target_y_rotation", targetYaw(state.yRot)); queries.put("body_y_rotation", state.bodyRot); queries.put("walk_distance", entity instanceof BedrockWalkingAnimation.Provider walking -@@ -821,6 +1162,8 @@ public final class BedrockPlayerSkins { +@@ -829,6 +1170,8 @@ public final class BedrockPlayerSkins { BedrockItemMetadata.clear(); SKIN_ASSEMBLER.clear(); skinConnection = null; @@ -1077,7 +1077,7 @@ index b6713a5..74b22b4 100644 } private static void release(final Appearance appearance) { -@@ -833,6 +1176,7 @@ public final class BedrockPlayerSkins { +@@ -841,6 +1184,7 @@ public final class BedrockPlayerSkins { for (final AnimatedSurface surface : appearance.surfaces) { textureManager.release(surface.texture()); } @@ -1085,7 +1085,7 @@ index b6713a5..74b22b4 100644 } private static Identifier textureId(final UUID playerUuid, final String kind) { -@@ -884,11 +1228,15 @@ public final class BedrockPlayerSkins { +@@ -892,11 +1236,15 @@ public final class BedrockPlayerSkins { 3, geometryName(skin.skinResourcePatch(), "animated_128x128")), skin.geometryData(), skin.skinData(), skin.animations()); } @@ -1103,7 +1103,7 @@ index b6713a5..74b22b4 100644 if (parsed.isJsonNull()) { return null; } -@@ -1109,6 +1457,31 @@ public final class BedrockPlayerSkins { +@@ -1117,6 +1465,31 @@ public final class BedrockPlayerSkins { private Map previewPose = Map.of(); private List previewSurfaces; private long rendererEpoch; diff --git a/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.pr.md b/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.pr.md index f5113461..e0f54dff 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0016-render-server-player-costumes.pr.md @@ -6,6 +6,8 @@ A server can replace minecraft:player with property-driven geometry, textures an ## Design constraints +World player controller selection uses the [appearance layer's native head query](0009-manage-character-creator-accounts-and-wardrobes.pr.md). + Evaluate accepted server player graphs against native actor snapshots. Keep wearer appearance, material bindings, UV animation, and effect lifetimes separate from unrelated players. ## Validation and remaining limits 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 fa7dd61c..07415d1c 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 ccfa20c4b552c7adf3338412b1e2198e4fa2ec24 Mon Sep 17 00:00:00 2001 +From a801354cf874458293a2d3f05096c4430a13b0b2 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 @@ -2434,7 +2434,7 @@ index 9c823f1..4accc01 100644 final List textures = new ArrayList<>(); for (int index = 0; index < material.textureCount(); index++) { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java -index f1a0f99..b85e966 100644 +index da31aeb..0b40385 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java @@ -373,7 +373,10 @@ public final class BedrockPlayerSkins { @@ -2453,8 +2453,8 @@ index f1a0f99..b85e966 100644 queries.put("is_alive", true); queries.put("target_x_rotation", state.xRot); queries.put("head_x_rotation", BedrockPlayerQueries.headPitch(state.xRot)); -- queries.put("target_y_rotation", state.yRot); -+ queries.put("target_y_rotation", gui == null ? state.yRot : gui.yaw()); +- queries.put("target_y_rotation", targetYaw(state.yRot)); ++ queries.put("target_y_rotation", gui == null ? targetYaw(state.yRot) : gui.yaw()); queries.put("modified_move_speed", state.walkAnimationSpeed); queries.put("modified_distance_moved", state.walkAnimationPos); - appearance.serverSelection = appearance.serverControllers.select(state.ageInTicks / 20F, @@ -2524,7 +2524,7 @@ index f1a0f99..b85e966 100644 variables.put("gliding_speed_value", Math.max(1, state.speedValue)); variables.put( "is_holding_right", -@@ -932,6 +961,7 @@ public final class BedrockPlayerSkins { +@@ -940,6 +969,7 @@ public final class BedrockPlayerSkins { queries.putAll(BedrockActorQueries.nativeState(actorState)); queries.putAll(BedrockActorQueries.equipmentAndVehicle(entity)); queries.put("is_emoting", BedrockActorQueries.isEmoting(actorId, actorState)); @@ -2532,12 +2532,12 @@ index f1a0f99..b85e966 100644 queries.put("life_time", state.ageInTicks / 20F); queries.put("frame_alpha", partialTick); queries.put("is_alive", entity.isAlive()); -@@ -953,7 +983,7 @@ public final class BedrockPlayerSkins { +@@ -961,7 +991,7 @@ public final class BedrockPlayerSkins { queries.put("shield_blocking_bob", BedrockActorState.shieldBlockingBob(entity.getUUID(), partialTick)); queries.put("target_x_rotation", state.xRot); queries.put("head_x_rotation", BedrockPlayerQueries.headPitch(state.xRot)); -- queries.put("target_y_rotation", state.yRot); -+ queries.put("target_y_rotation", BedrockGuiActorContext.current() == null ? state.yRot : BedrockGuiActorContext.current().yaw()); +- queries.put("target_y_rotation", targetYaw(state.yRot)); ++ queries.put("target_y_rotation", BedrockGuiActorContext.current() == null ? targetYaw(state.yRot) : BedrockGuiActorContext.current().yaw()); queries.put("body_y_rotation", state.bodyRot); queries.put("walk_distance", entity instanceof BedrockWalkingAnimation.Provider walking ? walking.bedrockWalkingAnimation().sample(partialTick) : 0F); 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 4141ffe9..53b0b2e6 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 @@ -6,6 +6,8 @@ Share bounded scene, layout, image, text and actor rendering primitives before r ## Design constraints +GUI camera overrides retain their existing yaw values. World player queries use the [appearance layer's native head limits](0009-manage-character-creator-accounts-and-wardrobes.pr.md). + Share authored scene, layout, image, text, actor, and focus primitives. Preserve same-pack bitmap metadata, legacy layouts, native atlas sampling, and GUI raster coverage. Prerequisites: diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md index cc51b38b..931a4d5d 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md @@ -26,7 +26,7 @@ Earlier direct and ViaProxy controls retain the observed large- and small-move s The new fixture sends normal movement followed immediately by a head-only packet. Both rebuilt routes reach the native movement endpoint and retain the head target. A settled update keeps the raw 22.75-degree pitch. Sparse movement after the overlap still uses its independent receive cache. Read-only audits pass all eight position, pitch, yaw, and head-yaw checks per route. -Strict journal decoders verify identical peer spawn, skin, normal, head-only, and sparse bytes against native. Camera instructions precede peer creation. Three settled images per phase compare six colored markers. All marker visibility checks agree. Current direct/proxy journals contain 2,014 and 1,709 frames. Median centroids stay within 0.39% of each viewport axis; boundaries stay within 0.91%. These projections do not establish frame timing or numeric native body angles. +Strict journal decoders verify identical peer spawn, skin, normal, head-only, and sparse bytes against native. Camera instructions precede peer creation. Three settled images per phase compare six colored markers. All marker visibility checks agree. Current direct/proxy journals contain 1,998 and 1,718 frames. Median centroids stay within 0.22% of each viewport axis; boundaries stay within 0.42%. These projections do not establish frame timing or numeric native body angles. ## Validation and limits @@ -34,15 +34,15 @@ The earlier normal/head comparisons use matching installed hashes. Their cleanup Current native reset controls establish immediate position changes and cleared motion/interpolation counters. Respawn resets pitch to zero and preserves previous position. Teleport retains wrapped pitch and resets previous position to its destination. Both retain the current head angle and queued movement. Teleport also retains the native float feet-to-eye roundtrip. -Fourteen-phase installed direct and ViaProxy controls pass the declared position, pitch, yaw, head-yaw, and body-yaw expectations. Both queued-reset cases reach X=2.5. Respawn now settles at the native 60.5-degree body yaw, and the following return settles at 50 degrees. Strict decoders verify identical peer payloads for all fourteen phases. The current native journal contains 8,096 complete frames, including two auxiliary movement probes. Direct and proxy journals contain 2,927 and 2,735 complete frames. +Fourteen-phase installed direct and ViaProxy controls pass the declared position, pitch, yaw, head-yaw, and body-yaw expectations. Both queued-reset cases reach X=2.5. Respawn settles at the native 60.5-degree body yaw, and the following return settles at 50 degrees. Strict decoders verify identical peer payloads for all fourteen phases. The native journal contains 8,096 complete frames, including two auxiliary movement probes. Current direct and proxy journals contain 2,514 and 2,228 complete frames. -Main rotation and body yaw match current native medians in all twelve traced phases. Head yaw matches in ten phases. In the other two phases, native completes one interpolation tick between normal movement and teleport. Its retained head yaw is 30 degrees; both Java routes process those packets before a tick and retain zero. The existing reset rule preserves either live angle. These samples do not establish packet/tick scheduling parity. +A private scheduling pause aligns the first interpolation tick before the normal-then-teleport pair completes. Both routes match native rotation, eye history, and counters at that boundary and through the first three post-teleport body ticks. Main rotation, head yaw, and body yaw then agree with native medians in all twelve traced phases. The remaining two phases use the previously verified zero body seed. Debugger and Java scheduling pauses exclude frame-rate and scheduler-timing claims. -All marker visibility checks agree. Median marker centroids differ by up to 0.64% of a viewport axis, and boundaries differ by up to 1.44%. The largest remaining centroid difference occurs at the head after head-only input, with different retained head angles. These projections do not isolate a rendering defect or establish full pose parity. +The [appearance query correction](0009-manage-character-creator-accounts-and-wardrobes.pr.md) covers the separate native head limit and current settled-pose projections. State checks and marker projections do not establish full pose parity. Targeted regressions cover respawn pitch, queued movement, and position-history ordering. Further tests cover exact previous-eye precision, reset admission, retained live head state, distinct previous positions, and queued handoff through the real handler. -The current full build passes 3,361 tests, with 325 skipped and no failures. This run includes the conditional 256-case native comparison. The respawn body-history regression fails with the old ordering and passes with the correction. A separate replay-scope regression protects the live position history. +The current full build passes 3,362 tests, with 325 skipped and no failures. This run includes the conditional 256-case native comparison. The respawn body-history regression fails with the old ordering and passes with the correction. A separate replay-scope regression protects the live position history. Fresh replay reproduces the exact built source trees. All 157 patches replay against pinned and current upstream tips. Bundle generation, ten artifact/manifest hashes, and 1,651 embedded core-class comparisons pass. Installed hashes match the compared builds. From 68061adee5964d87978f1b57aee30dc815863987 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:48:59 +0100 Subject: [PATCH 45/47] fix(players): preserve native pitch through movement and rendering Retain remote pitch beyond Java's look limits and sample its shortest angular delta before player animation and controller evaluation. Keep ordinary Java setters and explicit GUI camera pitch unchanged. Fold the changes into the owning interpolation and appearance patches. Native callback controls and installed direct/ViaProxy captures verify the selected boundaries; full rotation-history and pose parity remain open. --- docs/bedrock-coverage.md | 2 +- ...acter-creator-accounts-and-wardrobes.patch | 110 +++++++++++++----- ...acter-creator-accounts-and-wardrobes.pr.md | 6 + ...0029-share-native-ui-scene-rendering.patch | 14 +-- ...polate-native-remote-player-movement.patch | 98 ++++++++++++---- ...polate-native-remote-player-movement.pr.md | 6 + 6 files changed, 181 insertions(+), 55 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index f843c891..dc275aed 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -30,7 +30,7 @@ No complete area has verified Bedrock parity. | Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Short input edges during active Java use, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Selected reset, head, body, and queued-movement controls match native after packet/tick state alignment. Full trajectories and pose parity remain open. Readiness, ground state, exact timing, and mounted behavior remain unverified. 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. | +| Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps, pitch angles, and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Selected reset, head, body, and queued-movement controls match native after packet/tick state alignment. Full trajectories and pose parity remain open. Readiness, ground state, exact timing, and mounted behavior remain unverified. 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. | | Fog and picking | Fog commands, inherited legacy biome bindings, pack-defined defaults, Overworld clock modulation, End dragon distance rules, native actor picking transport, and native float camera rays. Disabling the daylight rule freezes explicit Overworld clocks. | Both routes match selected native fog colors and distances, three non-cardinal rays, and paired armor-stand and block reach controls. Eye precision, other actor shapes, multipart target ordering, spatial blending, timed command transitions, directional sunrise tint, sky sampling, and other render modes remain open. | | Custom content | Resource-defined actors, materials, particles, sound, block geometry, lighting, and collision. | Unsupported material methods, missing assets, carrier capacity, and visual differences remain. Live-server admission does not establish parity. | | Platforms | Linux build, replay, and selected native comparisons. | Windows and macOS launcher/account/rendering paths need their own results. See [platform testing](platform-testing.md). | diff --git a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch index 2cfa2cd8..4aa5d1b0 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch @@ -1,4 +1,4 @@ -From ec7196f3d606a8483edf4be16ad2bd1755598c04 Mon Sep 17 00:00:00 2001 +From 7866002ab986626f8d83c05f0a8c3a9338e82191 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(appearance): manage Character Creator accounts and @@ -134,6 +134,17 @@ Current 1.26.60.5 query callback 0x140684fd0 and helper 0x142c896e0 establish this path through typed registration 0x142c898c0. Original instructions pass 60 controls with explicit actor-type and virtual-getter boundaries. A real player query-map regression fails before the correction. + +Sample player render pitch with the native float angle wrap before pose +and controller extraction. Bedrock 1.26.60.5 target_x_rotation (140684ec0) +and head_x_rotation head zero (142cb53a0) interpolate the short angular +delta and leave the final result unwrapped. For previous179/current-179 +at half a tick, both original callbacks return180; Java linear sampling +returns0. Original instructions use executable SHA-256 +988f6a61403b4381aa41e6c66d64cce441b14e561a9b10f647c2758ef3f2672a, +with explicit actor-rotation storage and CRT fmodf boundaries. +Only actors with a native snapshot replace the host pitch sample. Later +GUI camera extraction keeps precedence over the world-player sample. --- .../bedrock/ViaFabricPlusBedrock.java | 7 + .../appearance/BedrockActorQueries.java | 10 + @@ -161,7 +172,7 @@ A real player query-map regression fails before the correction. .../appearance/NativeEmotePayload.java | 78 + .../appearance/NativeEmotePolicyPayload.java | 22 + .../ViaFabricPlusEmoteProvider.java | 40 + - .../render/MixinAvatarAnimationRenderer.java | 35 + + .../render/MixinAvatarAnimationRenderer.java | 36 + .../MixinBedrockFirstPersonBobbing.java | 29 + .../MixinBedrockFirstPersonProjection.java | 34 + .../MixinBedrockFirstPersonVisibility.java | 58 + @@ -170,7 +181,7 @@ A real player query-map regression fails before the correction. .../features/render/MixinOptionsEmote.java | 34 + .../viabedrock/ViaFabricPlusSkinProvider.java | 16 +- .../bedrock/render/BedrockPlayerRenderer.java | 7 +- - .../bedrock/render/BedrockPlayerSkins.java | 661 ++++++- + .../bedrock/render/BedrockPlayerSkins.java | 671 ++++++- .../screen/BedrockCharacterSlotsScreen.java | 1401 +++++++++++++++ .../screen/BedrockClassicSkinPackScreen.java | 83 +- .../screen/BedrockDressingRoomScreen.java | 88 +- @@ -208,9 +219,9 @@ A real player query-map regression fails before the correction. .../appearance/BedrockPersonaTintTest.java | 280 +++ .../BedrockPlayerAnimationsTest.java | 90 + .../appearance/NativeEmotePayloadTest.java | 103 ++ - .../render/BedrockActorQuerySnapshotTest.java | 120 ++ + .../render/BedrockActorQuerySnapshotTest.java | 154 ++ .../BedrockStandardPlayerGeometryTest.java | 28 + - 75 files changed, 14922 insertions(+), 118 deletions(-) + 75 files changed, 14967 insertions(+), 118 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockCharacterProfiles.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockClassicAssets.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockEmotePlayback.java @@ -6447,10 +6458,10 @@ index 0000000..40873b8 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java new file mode 100644 -index 0000000..5f01d8a +index 0000000..d591a84 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java -@@ -0,0 +1,35 @@ +@@ -0,0 +1,36 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -6481,6 +6492,7 @@ index 0000000..5f01d8a + final AvatarRenderState state, + final float partialTick, + final CallbackInfo ci) { ++ BedrockPlayerSkins.extractPitch(entity, state, partialTick); + ((BedrockPlayerAnimationState) state) + .bedrockAnimationPose(BedrockPlayerSkins.pose(entity, state, partialTick)); + ((BedrockPlayerAnimationState) state).bedrockBodyVisibility(BedrockPlayerSkins.bodyVisibility(entity.getUUID())); @@ -6857,7 +6869,7 @@ index a4e6f37..385eb7b 100644 } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java -index c482ba2..c31a809 100644 +index c482ba2..6321473 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java @@ -19,37 +19,62 @@ @@ -7025,7 +7037,7 @@ index c482ba2..c31a809 100644 return null; } if (appearance.renderer == null || appearance.rendererEpoch != rendererEpoch) { -@@ -212,13 +272,455 @@ public final class BedrockPlayerSkins { +@@ -212,13 +272,465 @@ public final class BedrockPlayerSkins { BedrockGeometry.playerModel(surface.geometry().model(), appearance.slim, surface.geometry().meshes()), surface.texture())).toList(); appearance.renderer = BedrockPlayerLayerModels.construct(rendererContext, () -> new BedrockPlayerRenderer(rendererContext, @@ -7314,10 +7326,20 @@ index c482ba2..c31a809 100644 + return geometry == null ? Map.of() : geometry.queries(); + } + -+ static float targetYaw(final float relativeYaw) { -+ float wrapped = (relativeYaw + 180F) % 360F; ++ public static void extractPitch(final Avatar entity, final AvatarRenderState state, final float partialTick) { ++ if (BedrockActorState.state(entity.getUUID()) == null) return; ++ // Native queries interpolate the shortest angular delta without wrapping the published result. ++ state.xRot = entity.xRotO + partialTick * wrapAngle(entity.getXRot() - entity.xRotO); ++ } ++ ++ private static float wrapAngle(final float angle) { ++ float wrapped = (angle + 180F) % 360F; + if (wrapped < 0F) wrapped += 360F; -+ wrapped -= 180F; ++ return wrapped - 180F; ++ } ++ ++ static float targetYaw(final float relativeYaw) { ++ final float wrapped = wrapAngle(relativeYaw); + // Native MAXSS selects the lower bound for an unordered wrapped angle. + return Float.isNaN(wrapped) ? -85F : Math.clamp(wrapped, -85F, 85F); + } @@ -7482,7 +7504,7 @@ index c482ba2..c31a809 100644 public static AvatarRenderer renderer(final PlayerSkin skin, final float ageInTicks) { if (skin == null || skin.body() == null) { return null; -@@ -238,10 +740,55 @@ public final class BedrockPlayerSkins { +@@ -238,10 +750,55 @@ public final class BedrockPlayerSkins { public static Model.Simple previewModel(final Identifier texture) { final UUID id = PLAYER_BY_TEXTURE.get(texture); final Appearance appearance = id == null ? null : APPEARANCES.get(id); @@ -7541,7 +7563,7 @@ index c482ba2..c31a809 100644 } /** Supplies the additional animated surfaces in the same pose as a menu's primary skin. */ -@@ -251,14 +798,19 @@ public final class BedrockPlayerSkins { +@@ -251,14 +808,19 @@ public final class BedrockPlayerSkins { if (appearance == null || appearance.surfaces.isEmpty()) return List.of(); if (appearance.previewSurfaces == null) appearance.previewSurfaces = appearance.surfaces.stream().map(surface -> new PreviewSurface( @@ -7563,7 +7585,7 @@ index c482ba2..c31a809 100644 public static void rendererContext(final EntityRendererProvider.Context context) { BedrockActorResources.clear(); -@@ -272,6 +824,7 @@ public final class BedrockPlayerSkins { +@@ -272,6 +834,7 @@ public final class BedrockPlayerSkins { APPEARANCES.values().forEach(BedrockPlayerSkins::release); APPEARANCES.clear(); PLAYER_BY_TEXTURE.clear(); @@ -7571,7 +7593,7 @@ index c482ba2..c31a809 100644 SKIN_ASSEMBLER.clear(); skinConnection = null; } -@@ -289,7 +842,8 @@ public final class BedrockPlayerSkins { +@@ -289,7 +852,8 @@ public final class BedrockPlayerSkins { } private static Identifier textureId(final UUID playerUuid, final String kind) { @@ -7581,7 +7603,7 @@ index c482ba2..c31a809 100644 } private static boolean supportedImage(final BufferedImage image) { -@@ -315,7 +869,10 @@ public final class BedrockPlayerSkins { +@@ -315,7 +879,10 @@ public final class BedrockPlayerSkins { static String geometryName(final String resourcePatch, final String binding) { try { @@ -7593,7 +7615,7 @@ index c482ba2..c31a809 100644 final var name = geometry.get(binding); return name != null && name.isJsonPrimitive() && name.getAsJsonPrimitive().isString() ? name.getAsString() : ""; -@@ -345,7 +902,8 @@ public final class BedrockPlayerSkins { +@@ -345,7 +912,8 @@ public final class BedrockPlayerSkins { final List models = BedrockGeometryModel.fromJson(data); final BedrockGeometryModel model = models.stream().filter(value -> value.getIdentifier().equals(name)).findFirst() .orElseGet(() -> models.isEmpty() ? null : models.getFirst()); @@ -7603,7 +7625,7 @@ index c482ba2..c31a809 100644 } catch (RuntimeException e) { LOGGER.warn("Failed to parse Bedrock player geometry", e); return null; -@@ -372,7 +930,8 @@ public final class BedrockPlayerSkins { +@@ -372,7 +940,8 @@ public final class BedrockPlayerSkins { final Map animatedModels = new HashMap<>(); for (final var entry : models) { final JsonObject model = entry.getAsJsonObject(); @@ -7613,7 +7635,7 @@ index c482ba2..c31a809 100644 if (name.equals(identifier)) { if (body != null) { return null; -@@ -411,7 +970,8 @@ public final class BedrockPlayerSkins { +@@ -411,7 +980,8 @@ public final class BedrockPlayerSkins { final JsonObject model = animatedModels.get(type); final var animation = animationByType.get(type); if (model == null) { @@ -7623,7 +7645,7 @@ index c482ba2..c31a809 100644 return null; } continue; -@@ -426,7 +986,8 @@ public final class BedrockPlayerSkins { +@@ -426,7 +996,8 @@ public final class BedrockPlayerSkins { BedrockGeometry.playerModel(parsed, false, meshes); surfaces.add(new Surface(parsed, Map.copyOf(meshes), animation)); } @@ -7633,7 +7655,7 @@ index c482ba2..c31a809 100644 if (!coverage.getOrDefault(part, false)) { return null; } -@@ -437,7 +998,8 @@ public final class BedrockPlayerSkins { +@@ -437,7 +1008,8 @@ public final class BedrockPlayerSkins { return null; } BedrockGeometry.playerModel(parsedBody, false, bodyMeshes); @@ -7643,7 +7665,7 @@ index c482ba2..c31a809 100644 } catch (RuntimeException e) { LOGGER.warn("Failed to parse Bedrock persona geometry", e); return null; -@@ -474,12 +1036,21 @@ public final class BedrockPlayerSkins { +@@ -474,12 +1046,21 @@ public final class BedrockPlayerSkins { } } } @@ -7668,7 +7690,7 @@ index c482ba2..c31a809 100644 } } return meshes; -@@ -501,7 +1072,13 @@ public final class BedrockPlayerSkins { +@@ -501,7 +1082,13 @@ public final class BedrockPlayerSkins { return vector; } @@ -7683,7 +7705,7 @@ index c482ba2..c31a809 100644 Surface face() { return this.surfaces.stream().filter(surface -> surface.animation().type() == 1).findFirst().orElse(null); } -@@ -519,13 +1096,23 @@ public final class BedrockPlayerSkins { +@@ -519,13 +1106,23 @@ public final class BedrockPlayerSkins { private final Identifier headId; private final long started = System.nanoTime(); private final Geometry geometry; @@ -16347,13 +16369,14 @@ index 0000000..f8e433d +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorQuerySnapshotTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorQuerySnapshotTest.java new file mode 100644 -index 0000000..5b88ad5 +index 0000000..2800806 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorQuerySnapshotTest.java -@@ -0,0 +1,120 @@ +@@ -0,0 +1,154 @@ +package com.viaversion.viafabricplus.bedrock.render; + +import com.viaversion.viafabricplus.bedrock.appearance.BedrockActorAnimation; ++import net.raphimc.viabedrock.api.model.NativeActorMessage; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.IdentityHashMap; @@ -16454,6 +16477,39 @@ index 0000000..5b88ad5 + } + } + ++ @Test ++ void samplesNativePitchAcrossTheWrapBeforePublishingAnimationQueries() { ++ final var avatar = mock(Avatar.class); ++ final UUID uuid = UUID.randomUUID(); ++ when(avatar.getUUID()).thenReturn(uuid); ++ when(avatar.getMainArm()).thenReturn(HumanoidArm.RIGHT); ++ when(avatar.getMainHandItem()).thenReturn(ItemStack.EMPTY); ++ when(avatar.getUseItem()).thenReturn(ItemStack.EMPTY); ++ when(avatar.getItemBySlot(any(EquipmentSlot.class))).thenReturn(ItemStack.EMPTY); ++ when(avatar.getDeltaMovement()).thenReturn(Vec3.ZERO); ++ final var state = new AvatarRenderState(); ++ state.rightHandItemStack = ItemStack.EMPTY; ++ state.leftHandItemStack = ItemStack.EMPTY; ++ // Original 1.26.60.5 target_x_rotation and head_x_rotation callbacks, including alpha one. ++ final float[][] controls = {{179F, -179F, .5F, 180F}, {-179F, 179F, .5F, -180F}, ++ {179F, -179F, 1F, 181F}, {-179F, 179F, .25F, -179.5F}, {11.25F, -22.75F, .5F, -5.75F}}; ++ for (final float[] control : controls) { ++ avatar.xRotO = control[0]; ++ when(avatar.getXRot()).thenReturn(control[1]); ++ state.xRot = 0F; ++ try (final var actors = mockStatic(BedrockActorState.class)) { ++ actors.when(() -> BedrockActorState.state(uuid)).thenReturn(mock(NativeActorMessage.Snapshot.class)); ++ BedrockPlayerSkins.extractPitch(avatar, state, control[2]); ++ } ++ assertEquals(control[3], BedrockPlayerSkins.actorQueries(avatar, state, control[2]).get("target_x_rotation")); ++ } ++ state.xRot = 44F; ++ try (final var actors = mockStatic(BedrockActorState.class)) { ++ BedrockPlayerSkins.extractPitch(avatar, state, .5F); ++ } ++ assertEquals(44F, state.xRot, "Ordinary Java avatars retain the host sample"); ++ } ++ + /** Inspect captured values directly; collection through System.gc() would make this lifecycle test flaky. */ + private static void assertNoAvatarReference(final Object value, final Set visited) throws Exception { + if (value == null || !visited.add(value)) return; diff --git a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md index 445a8300..4c1b5271 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md @@ -10,6 +10,8 @@ Keep account entitlement, downloaded assets, character recipes, previews, and in World-player `target_y_rotation` applies the native float wrap and the -85/+85-degree limits. Unordered wrapped values select the lower bound. Server player controller selection uses the same helper. Existing GUI camera overrides retain their previous values. +World-player pitch samples the shortest angular delta before animation and controller evaluation. The final sample remains unwrapped. Actors without a native snapshot retain Java sampling. GUI camera extraction takes precedence over the world sample. + Prerequisites: - viafabricplus-bedrock/0008-share-native-actor-and-resource-runtime.patch: Land shared actor and resource playback before account appearance selection. @@ -22,6 +24,10 @@ Regression coverage includes `BedrockAccountAppearanceTest`, `BedrockAppearanceS Current Bedrock 1.26.60.5/protocol 2225 registration identifies target-y callback `0x140684fd0` and helper `0x142c896e0`. The observed remote-player vtable resolves its head and body getters to the matching native components. Original instructions execute 60 controls. Actor type, interpolated virtual getters, and CRT remainder are explicit fixture boundaries. The original wrapper, angle arithmetic, limit, and result publication execute. The player query-map regression covers limits, tiny angles, signed zero, and non-finite input. +Original target-x callback `0x140684ec0` and head-x callback `0x142cb53a0` agree on 15 pitch controls. Head-x uses numeric index zero. Synthetic rotation storage and CRT remainder are explicit boundaries. The query-map regression covers both wrap directions, ordinary pitch, alpha one, and the Java fallback. Previous 179/current -179 yields 180 at half a tick and 181 at alpha one. + +Read-only observers retain 66 direct and 63 ViaProxy query samples from the rebuilt clients. Both original callbacks reproduce every sampled result bit for bit. Two direct head-only frames differ from Java's linear formula. One returns 180.52 degrees where linear sampling would return -93.08 degrees. These comparisons supply the recorded Java frame inputs. Native rotation-history, scheduler, and complete frame parity remain unverified. + Fourteen-phase installed direct and ViaProxy controls use matching peer skin, spawn, and movement bytes against the native fixture. A private scheduling pause aligns the intermediate normal-then-teleport state and the first three body ticks after teleport. All four sampled angles match the twelve traced native phases. The [movement comparison](0041-interpolate-native-remote-player-movement.pr.md) describes the state controls and their limits. Three settled images per phase compare six colored markers. Median centroids differ by at most 0.26% of a viewport axis, and boundaries differ by at most 0.42%. The previously mismatched teleport head centroid now differs by less than 0.10% of either axis. All marker visibility checks agree. Solid-color heads do not establish every face orientation. Complete pose, skin, and frame-timing parity remain 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 07415d1c..0b072338 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 a801354cf874458293a2d3f05096c4430a13b0b2 Mon Sep 17 00:00:00 2001 +From b0099742c74c4c44bf92e90ff04fe4f0ee88f054 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 @@ -1322,7 +1322,7 @@ index 0000000..58fa5ef + } +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java -index 36e46c4..1e4c765 100644 +index f0c921e..f5943e7 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinAvatarAnimationRenderer.java @@ -6,6 +6,7 @@ @@ -1333,10 +1333,10 @@ index 36e46c4..1e4c765 100644 import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerAnimationState; import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerAttachables; import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerSkins; -@@ -43,6 +44,7 @@ public abstract class MixinAvatarAnimationRenderer extends LivingEntityRenderer< - final AvatarRenderState state, +@@ -44,6 +45,7 @@ public abstract class MixinAvatarAnimationRenderer extends LivingEntityRenderer< final float partialTick, final CallbackInfo ci) { + BedrockPlayerSkins.extractPitch(entity, state, partialTick); + BedrockGuiActorContext.extracted(state); ((BedrockPlayerAnimationState) state) .bedrockAnimationPose(BedrockPlayerSkins.pose(entity, state, partialTick)); @@ -2434,7 +2434,7 @@ index 9c823f1..4accc01 100644 final List textures = new ArrayList<>(); for (int index = 0; index < material.textureCount(); index++) { diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java -index da31aeb..0b40385 100644 +index c07211c..1af8113 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java @@ -373,7 +373,10 @@ public final class BedrockPlayerSkins { @@ -2524,7 +2524,7 @@ index da31aeb..0b40385 100644 variables.put("gliding_speed_value", Math.max(1, state.speedValue)); variables.put( "is_holding_right", -@@ -940,6 +969,7 @@ public final class BedrockPlayerSkins { +@@ -950,6 +979,7 @@ public final class BedrockPlayerSkins { queries.putAll(BedrockActorQueries.nativeState(actorState)); queries.putAll(BedrockActorQueries.equipmentAndVehicle(entity)); queries.put("is_emoting", BedrockActorQueries.isEmoting(actorId, actorState)); @@ -2532,7 +2532,7 @@ index da31aeb..0b40385 100644 queries.put("life_time", state.ageInTicks / 20F); queries.put("frame_alpha", partialTick); queries.put("is_alive", entity.isAlive()); -@@ -961,7 +991,7 @@ public final class BedrockPlayerSkins { +@@ -971,7 +1001,7 @@ public final class BedrockPlayerSkins { queries.put("shield_blocking_bob", BedrockActorState.shieldBlockingBob(entity.getUUID(), partialTick)); queries.put("target_x_rotation", state.xRot); queries.put("head_x_rotation", BedrockPlayerQueries.headPitch(state.xRot)); diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch index 2d24098c..9a11ee5e 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch @@ -1,4 +1,4 @@ -From 408423877e8f84b10b58d94b23b59d924ab8e1d1 Mon Sep 17 00:00:00 2001 +From 36a84dea0418714bca8c27b09865ad0566670f72 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:51:01 +0100 Subject: [PATCH] fix(players): interpolate negotiated remote movement as @@ -62,22 +62,33 @@ respawn moves the body from 40 to 62.5 degrees before the previous-position copy, then settles at 60.5. A head-only packet received with one position step left derives motion from the last completed body tick position. These controls establish history ordering, not scheduler timing. + +Publish native remote pitch directly to the rotation field. Java 26.3 +Entity.setXRot clamps to +/-90 degrees, while the Bedrock 1.26.60.5 +original interpolation callback (143630d70, executable SHA-256 +988f6a61403b4381aa41e6c66d64cce441b14e561a9b10f647c2758ef3f2672a) +retains wrapped pitch beyond those bounds. Installed protocol-2225 controls +reproduced +179/-179 packets settling at +90/-90 before this correction. +Keep the direct field access inside the admitted native remote handler; +ordinary Java setters and local look input retain their host behavior. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + - .../MixinRemotePlayerInterpolation.java | 22 ++ + .../MixinRemotePlayerInterpolation.java | 22 + ...xinRemotePlayerMovementPacketListener.java | 54 +++ + .../movement/RemotePlayerRotationAccess.java | 17 + .../render/MixinLivingEntityBodyRotation.java | 7 +- .../input/BedrockRemotePlayerMovement.java | 81 ++++ - .../input/RemotePlayerMovementPayload.java | 22 ++ + .../input/RemotePlayerMovementPayload.java | 22 + .../movement/RemotePlayerInterpolation.java | 142 +++++++ - .../RemotePlayerInterpolationHandler.java | 216 +++++++++++ - .../viafabricplus-bedrock.mixins.json | 2 + + .../RemotePlayerInterpolationHandler.java | 217 ++++++++++ + .../viafabricplus-bedrock.mixins.json | 3 + ...ativeRemoteInterpolationReferenceTest.java | 74 ++++ - .../RemotePlayerInterpolationHandlerTest.java | 356 ++++++++++++++++++ - .../RemotePlayerInterpolationTest.java | 133 +++++++ - 12 files changed, 1110 insertions(+), 1 deletion(-) + .../RemotePlayerInterpolationHandlerTest.java | 378 ++++++++++++++++++ + .../RemotePlayerInterpolationTest.java | 133 ++++++ + 13 files changed, 1151 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerInterpolation.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerMovementPacketListener.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/RemotePlayerRotationAccess.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/BedrockRemotePlayerMovement.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/input/RemotePlayerMovementPayload.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolation.java @@ -194,6 +205,29 @@ index 0000000..c9330c6 + original.call(packet); + } +} +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/RemotePlayerRotationAccess.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/RemotePlayerRotationAccess.java +new file mode 100644 +index 0000000..d799bbb +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/RemotePlayerRotationAccess.java +@@ -0,0 +1,17 @@ ++/* ++ * 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.movement; ++ ++import net.minecraft.world.entity.Entity; ++import org.spongepowered.asm.mixin.Mixin; ++import org.spongepowered.asm.mixin.gen.Accessor; ++ ++/** The native interpolation handler publishes rotation without Java's player look bounds. */ ++@Mixin(Entity.class) ++public interface RemotePlayerRotationAccess { ++ @Accessor("xRot") ++ void bedrockRemotePitch(float pitch); ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinLivingEntityBodyRotation.java b/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinLivingEntityBodyRotation.java index 89c5db6..8fea24b 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinLivingEntityBodyRotation.java @@ -487,10 +521,10 @@ index 0000000..61a8f44 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java new file mode 100644 -index 0000000..8cb6f5f +index 0000000..175b799 --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java -@@ -0,0 +1,216 @@ +@@ -0,0 +1,217 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -499,6 +533,7 @@ index 0000000..8cb6f5f +package com.viaversion.viafabricplus.bedrock.movement; + +import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; ++import com.viaversion.viafabricplus.bedrock.injection.mixin.features.movement.RemotePlayerRotationAccess; +import com.viaversion.viafabricplus.bedrock.movement.replay.PhysicalReplayContext; +import com.viaversion.viafabricplus.bedrock.render.BedrockActorState; +import net.minecraft.client.Minecraft; @@ -583,7 +618,7 @@ index 0000000..8cb6f5f + this.appliedPosition = this.player.position(); + this.player.setDeltaMovement(Vec3.ZERO); + if (movement.mode() == 1) { -+ this.player.setXRot(0F); ++ ((RemotePlayerRotationAccess) this.player).bedrockRemotePitch(0F); + } else { + this.player.setOldPosAndRot(this.appliedPosition, this.player.yRotO, this.player.xRotO); + this.previousEye = this.eye; @@ -597,7 +632,7 @@ index 0000000..8cb6f5f + private void applyRotation(final float rawPitch, final float rawYaw) { + final float pitch = RemotePlayerInterpolation.wrap(rawPitch); + final float yaw = RemotePlayerInterpolation.wrap(rawYaw); -+ this.player.setXRot(pitch); ++ ((RemotePlayerRotationAccess) this.player).bedrockRemotePitch(pitch); + this.player.setYRot(yaw); + this.player.xRotO = RemotePlayerInterpolation.wrap((pitch - rawPitch) + this.player.xRotO); + this.player.yRotO = RemotePlayerInterpolation.wrap((yaw - rawYaw) + this.player.yRotO); @@ -656,7 +691,7 @@ index 0000000..8cb6f5f + this.player.setPos(this.eye.x(), (double) this.eye.y() - this.originOffset, this.eye.z()); + } + this.appliedPosition = this.player.position(); -+ this.player.setXRot(next.pitch()); ++ ((RemotePlayerRotationAccess) this.player).bedrockRemotePitch(next.pitch()); + this.player.setYRot(next.yaw()); + this.player.xRotO = next.previousPitch(); + this.player.yRotO = next.previousYaw(); @@ -708,14 +743,15 @@ index 0000000..8cb6f5f + } +} diff --git a/src/main/resources/viafabricplus-bedrock.mixins.json b/src/main/resources/viafabricplus-bedrock.mixins.json -index 590dad9..21b4eae 100644 +index 590dad9..65afd38 100644 --- a/src/main/resources/viafabricplus-bedrock.mixins.json +++ b/src/main/resources/viafabricplus-bedrock.mixins.json -@@ -209,6 +209,8 @@ +@@ -209,6 +209,9 @@ "features.movement.MixinFireworkRocketEntity", "features.movement.MixinLocalPlayer", "features.movement.MixinPlayerCorrectionPacketListener", + "features.movement.MixinRemotePlayerInterpolation", ++ "features.movement.RemotePlayerRotationAccess", + "features.movement.MixinRemotePlayerMovementPacketListener", "features.movement.MixinPlayer", "features.sound.MixinBedrockMiningAudio", @@ -802,10 +838,10 @@ index 0000000..1a00bf6 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java new file mode 100644 -index 0000000..563d95a +index 0000000..dace424 --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java -@@ -0,0 +1,356 @@ +@@ -0,0 +1,378 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -814,6 +850,7 @@ index 0000000..563d95a +package com.viaversion.viafabricplus.bedrock.movement; + +import com.viaversion.viafabricplus.bedrock.input.BedrockInputPermissions; ++import com.viaversion.viafabricplus.bedrock.injection.mixin.features.movement.RemotePlayerRotationAccess; +import com.viaversion.viafabricplus.bedrock.movement.replay.PhysicalReplayContext; +import com.viaversion.viafabricplus.bedrock.render.BedrockActorState; +import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerBodyRotation; @@ -896,7 +933,7 @@ index 0000000..563d95a + new Position3f(37, 123, 19), new Position3f(-22.75F, 135, -135.25F), 1.62001F, (short) 0, 0, false, + new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, 0, -22.5F, false)); + assertTrue(f.handler.receive(head)); -+ verify(f.player).setXRot(-22.75F); ++ assertEquals(-22.75F, f.player.getXRot()); + verify(f.player).setYRot(0F); + assertEquals(.5D, f.position.get().x); + assertTrue(f.handler.hasActiveInterpolation()); @@ -911,6 +948,25 @@ index 0000000..563d95a + } + + @Test ++ void retainsNativePitchBeyondJavaLookLimitsAcrossMovementAndHeadOnlyUpdates() { ++ try (final var f = new Fixture()) { ++ final var target = new RemotePlayerMovement(17, f.uuid, f.lifetime, 0, false, ++ new Position3f(.5F, 73.24002F, 4.5F), new Position3f(179F, 0F, 0F), 1.62001F, ++ (short) 0, 3, false, f.movement(0, false).javaTarget()); ++ assertTrue(f.handler.receive(target)); ++ f.tick(); ++ f.tick(); ++ f.tick(); ++ assertEquals(179F, f.player.getXRot()); ++ final var head = new RemotePlayerMovement(17, f.uuid, f.lifetime, 3, false, ++ target.position(), new Position3f(-179F, 0F, 0F), 1.62001F, ++ (short) 0, 0, false, new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, 0, 0F, false)); ++ assertTrue(f.handler.receive(head)); ++ assertEquals(-179F, f.player.getXRot()); ++ } ++ } ++ ++ @Test + void resetModesApplyImmediatelyAndPreserveTheLiveHeadAndDistinctPreviousPositions() { + for (int mode : new int[]{1, 2}) { + try (final var f = new Fixture()) { @@ -1093,7 +1149,7 @@ index 0000000..563d95a + private final ClientLevel level = mock(ClientLevel.class); + private final ClientPacketListener listener = mock(ClientPacketListener.class); + private final Connection connection = mock(Connection.class); -+ private final RemotePlayer player = mock(RemotePlayer.class); ++ private final RemotePlayer player = mock(RemotePlayer.class, withSettings().extraInterfaces(RemotePlayerRotationAccess.class)); + private final InterpolationHandler host = mock(InterpolationHandler.class); + private final AtomicReference position = new AtomicReference<>(new Vec3(0, -(double) 1.62001F, 5.5)); + private final AtomicReference oldPosition = new AtomicReference<>(this.position.get()); @@ -1124,7 +1180,9 @@ index 0000000..563d95a + when(this.player.getXRot()).thenAnswer(_ -> this.pitch.get()); + when(this.player.getYRot()).thenAnswer(_ -> this.yaw.get()); + when(this.player.getYHeadRot()).thenAnswer(_ -> this.headYaw.get()); -+ doAnswer(call -> { this.pitch.set(call.getArgument(0)); return null; }).when(this.player).setXRot(anyFloat()); ++ // Entity.setXRot in Java 26.3 bounds look pitch, unlike the native rotation component. ++ doAnswer(call -> { this.pitch.set(Math.clamp((float) call.getArgument(0) % 360F, -90F, 90F)); return null; }).when(this.player).setXRot(anyFloat()); ++ doAnswer(call -> { this.pitch.set(call.getArgument(0)); return null; }).when((RemotePlayerRotationAccess) this.player).bedrockRemotePitch(anyFloat()); + doAnswer(call -> { this.yaw.set(call.getArgument(0)); return null; }).when(this.player).setYRot(anyFloat()); + doAnswer(call -> { this.headYaw.set(call.getArgument(0)); return null; }).when(this.player).setYHeadRot(anyFloat()); + doAnswer(call -> { diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md index 931a4d5d..5ec514ca 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md @@ -6,6 +6,8 @@ A decorator retains the real host handler. Unpaired packets, known riding, and l Head-only updates apply raw pitch immediately. They preserve live yaw, pending position and body targets, and forced queues. The head target uses the maximum remaining position/body counter. Explicit packet pairing prevents Java's rotation-only path from cancelling movement. Raw inputs avoid the legacy packet's byte-angle quantization. +The admitted native handler publishes wrapped pitch directly to the rotation field. Java 26.3's setter clips pitch to -90/+90 degrees. Native remote rotation permits angles beyond those limits. Ordinary Java and local look input retain their existing setters. + The outer packet path derives motion from raw source position minus the retained previous native eye. Body control reads the exact native displacement before the previous-position copy. The client retains that float history after body control, including while interpolation is idle. Historical physics replay does not advance this live history. ## Native evidence @@ -20,8 +22,12 @@ A head-only packet arrives with one interpolation step remaining and derives mot Original mode-helper and setter instructions also pass 54 teleport-origin cases, including low-Y rounding and signed zero. A conditional Java/native test compares all 256 current callback cases with a head component. Default regressions cover observed position samples, overlapping head-only input, raw pitch, forced handoff, rotation wrapping, host fallback, and lifecycle replacement. +Three additional original-callback controls retain pitch beyond Java's look limits while moving from -179 toward 179 degrees. The installed baseline confirms that +179/-179 packets previously settled at +90/-90. The handler regression reproduces this clipping and covers normal movement followed by head-only input. + ## Installed comparisons +Six-phase direct and ViaProxy controls retain +179/-179-degree pitch through normal movement and immediate head-only input. Position, yaw, and head-yaw remain at their declared values. Complete journals contain 1,582 and 1,224 frames, with identical peer skin, spawn, and movement payloads. Installed add-on and proxy hashes match the verified build. The [appearance comparison](0009-manage-character-creator-accounts-and-wardrobes.pr.md) covers angular render queries and their remaining history limits. + Earlier direct and ViaProxy controls retain the observed large- and small-move steps. Twelve settled phases per route compare movement origins and poses, including the previously occluded leg. The overlapping-head regression also runs against the current rebuilt source with two fresh player identities. The new fixture sends normal movement followed immediately by a head-only packet. Both rebuilt routes reach the native movement endpoint and retain the head target. A settled update keeps the raw 22.75-degree pitch. Sparse movement after the overlap still uses its independent receive cache. Read-only audits pass all eight position, pitch, yaw, and head-yaw checks per route. From 09f0c935091771b389881506782c6134f8316cbf Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:53:15 +0100 Subject: [PATCH 46/47] fix(players): retain native pitch history for render queries Keep the native previous pitch before Java normalizes its public angle field. Use the retained value for animation queries and head-only updates, and retire it with the native interpolation context. A live native trace and matching peer packets reproduce the 360-degree error. Both rebuilt routes match the native history and query results across the selected transitions. Full timing and pose parity remain open. --- docs/bedrock-coverage.md | 2 +- patches/viafabricplus-bedrock/series.json | 5 ++ ...acter-creator-accounts-and-wardrobes.patch | 84 +++++++++++++----- ...acter-creator-accounts-and-wardrobes.pr.md | 6 +- ...polate-native-remote-player-movement.patch | 88 ++++++++++++++++--- ...polate-native-remote-player-movement.pr.md | 4 +- 6 files changed, 151 insertions(+), 38 deletions(-) diff --git a/docs/bedrock-coverage.md b/docs/bedrock-coverage.md index dc275aed..d01ccd84 100644 --- a/docs/bedrock-coverage.md +++ b/docs/bedrock-coverage.md @@ -30,7 +30,7 @@ No complete area has verified Bedrock parity. | Food use | Native components govern admission and Java food/consumable properties, with Creative and Peaceful exceptions. Local use retains native item identity across Java carriers. | Selected admission, input-deadline, timed-food, held-mode, release, and pause controls pass. Short input edges during active Java use, broader use cycles, and the original intermittent report remain open. See the [food-use evidence](../patches/viabedrock/deferred/0008-signal-sustained-bedrock-item-use.pr.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. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps, pitch angles, and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Selected reset, head, body, and queued-movement controls match native after packet/tick state alignment. Full trajectories and pose parity remain open. Readiness, ground state, exact timing, and mounted behavior remain unverified. 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. | +| Movement and effects | Completed physics frames, retained prediction history, correction replay, terrain queries, and authoritative impulses. [Remote-player live and sparse movement origins remain separate](../patches/viabedrock/upstreamable/0106-align-retained-remote-spawn-origins.pr.md). [Negotiated remote movement retains native float steps, pitch history, and head targets](../patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md). Overworld air blindness attenuates night-vision strength and fog color together. Both Java routes match native water-boundary fall controls. | Selected reset, head, body, and queued-movement controls match native after packet/tick state alignment. Full trajectories and pose parity remain open. Readiness, ground state, exact timing, and mounted behavior remain unverified. 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. | | Fog and picking | Fog commands, inherited legacy biome bindings, pack-defined defaults, Overworld clock modulation, End dragon distance rules, native actor picking transport, and native float camera rays. Disabling the daylight rule freezes explicit Overworld clocks. | Both routes match selected native fog colors and distances, three non-cardinal rays, and paired armor-stand and block reach controls. Eye precision, other actor shapes, multipart target ordering, spatial blending, timed command transitions, directional sunrise tint, sky sampling, and other render modes remain open. | | Custom content | Resource-defined actors, materials, particles, sound, block geometry, lighting, and collision. | Unsupported material methods, missing assets, carrier capacity, and visual differences remain. Live-server admission does not establish parity. | | Platforms | Linux build, replay, and selected native comparisons. | Windows and macOS launcher/account/rendering paths need their own results. See [platform testing](platform-testing.md). | diff --git a/patches/viafabricplus-bedrock/series.json b/patches/viafabricplus-bedrock/series.json index d377ed4d..2ab15985 100644 --- a/patches/viafabricplus-bedrock/series.json +++ b/patches/viafabricplus-bedrock/series.json @@ -386,6 +386,11 @@ "patch": "0008-share-native-actor-and-resource-runtime.patch", "reason": "Retain native actor lifetimes and body-control ownership before integrating remote interpolation." }, + { + "project": "viafabricplus-bedrock", + "patch": "0009-manage-character-creator-accounts-and-wardrobes.patch", + "reason": "Publish the player pitch-history contract consumed by animation and controller sampling." + }, { "project": "viafabricplus-bedrock", "patch": "0020-apply-server-movement-and-camera-permissions.patch", diff --git a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch index 4aa5d1b0..e9f5fe77 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.patch @@ -1,4 +1,4 @@ -From 7866002ab986626f8d83c05f0a8c3a9338e82191 Mon Sep 17 00:00:00 2001 +From 45bfe83f0b10b347602a3a5c4084ca8163402f3f 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(appearance): manage Character Creator accounts and @@ -145,6 +145,13 @@ returns0. Original instructions use executable SHA-256 with explicit actor-rotation storage and CRT fmodf boundaries. Only actors with a native snapshot replace the host pitch sample. Later GUI camera extraction keeps precedence over the world-player sample. + +Let interpolation owners supply the previous pitch before host render +normalization. The live 1.26.60.5 native rotation component retains +previous179.66669/current-179.66666 across a normal wrap. Java instead +shifts its public previous pitch to-180.33331. The original query keeps +its result unwrapped, so substituting the Java history changes it by360. +Keep this history contract independent of the remote movement adapter. --- .../bedrock/ViaFabricPlusBedrock.java | 7 + .../appearance/BedrockActorQueries.java | 10 + @@ -180,8 +187,9 @@ GUI camera extraction keeps precedence over the world-player sample. .../render/MixinMinecraftSkinLifecycle.java | 12 + .../features/render/MixinOptionsEmote.java | 34 + .../viabedrock/ViaFabricPlusSkinProvider.java | 16 +- + .../bedrock/render/BedrockPlayerPitch.java | 11 + .../bedrock/render/BedrockPlayerRenderer.java | 7 +- - .../bedrock/render/BedrockPlayerSkins.java | 671 ++++++- + .../bedrock/render/BedrockPlayerSkins.java | 672 ++++++- .../screen/BedrockCharacterSlotsScreen.java | 1401 +++++++++++++++ .../screen/BedrockClassicSkinPackScreen.java | 83 +- .../screen/BedrockDressingRoomScreen.java | 88 +- @@ -219,9 +227,9 @@ GUI camera extraction keeps precedence over the world-player sample. .../appearance/BedrockPersonaTintTest.java | 280 +++ .../BedrockPlayerAnimationsTest.java | 90 + .../appearance/NativeEmotePayloadTest.java | 103 ++ - .../render/BedrockActorQuerySnapshotTest.java | 154 ++ + .../render/BedrockActorQuerySnapshotTest.java | 167 ++ .../BedrockStandardPlayerGeometryTest.java | 28 + - 75 files changed, 14967 insertions(+), 118 deletions(-) + 76 files changed, 14992 insertions(+), 118 deletions(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockCharacterProfiles.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockClassicAssets.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/appearance/BedrockEmotePlayback.java @@ -249,6 +257,7 @@ GUI camera extraction keeps precedence over the world-player sample. create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockFirstPersonVisibility.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinBedrockHandItemLifecycle.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/render/MixinOptionsEmote.java + create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerPitch.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/BedrockCharacterSlotsScreen.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/BedrockEmotePreviewScreen.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/screen/BedrockEmoteWheelScreen.java @@ -6848,6 +6857,23 @@ index 39a1e1f..59657b0 100644 } } catch (IOException e) { ViaFabricPlusBedrock.impl().logger().warn("Could not load the selected Bedrock appearance; using Steve", e); +diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerPitch.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerPitch.java +new file mode 100644 +index 0000000..3dcee8a +--- /dev/null ++++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerPitch.java +@@ -0,0 +1,11 @@ ++/* ++ * 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.render; ++ ++/** Native interpolation history before the host adjusts its angles for linear rendering. */ ++public interface BedrockPlayerPitch { ++ float previousPitch(); ++} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java index a4e6f37..385eb7b 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerRenderer.java @@ -6869,7 +6895,7 @@ index a4e6f37..385eb7b 100644 } diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java -index c482ba2..6321473 100644 +index c482ba2..6d6692e 100644 --- a/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/render/BedrockPlayerSkins.java @@ -19,37 +19,62 @@ @@ -7037,7 +7063,7 @@ index c482ba2..6321473 100644 return null; } if (appearance.renderer == null || appearance.rendererEpoch != rendererEpoch) { -@@ -212,13 +272,465 @@ public final class BedrockPlayerSkins { +@@ -212,13 +272,466 @@ public final class BedrockPlayerSkins { BedrockGeometry.playerModel(surface.geometry().model(), appearance.slim, surface.geometry().meshes()), surface.texture())).toList(); appearance.renderer = BedrockPlayerLayerModels.construct(rendererContext, () -> new BedrockPlayerRenderer(rendererContext, @@ -7329,7 +7355,8 @@ index c482ba2..6321473 100644 + public static void extractPitch(final Avatar entity, final AvatarRenderState state, final float partialTick) { + if (BedrockActorState.state(entity.getUUID()) == null) return; + // Native queries interpolate the shortest angular delta without wrapping the published result. -+ state.xRot = entity.xRotO + partialTick * wrapAngle(entity.getXRot() - entity.xRotO); ++ final float previousPitch = entity.getInterpolation() instanceof BedrockPlayerPitch history ? history.previousPitch() : entity.xRotO; ++ state.xRot = previousPitch + partialTick * wrapAngle(entity.getXRot() - previousPitch); + } + + private static float wrapAngle(final float angle) { @@ -7504,7 +7531,7 @@ index c482ba2..6321473 100644 public static AvatarRenderer renderer(final PlayerSkin skin, final float ageInTicks) { if (skin == null || skin.body() == null) { return null; -@@ -238,10 +750,55 @@ public final class BedrockPlayerSkins { +@@ -238,10 +751,55 @@ public final class BedrockPlayerSkins { public static Model.Simple previewModel(final Identifier texture) { final UUID id = PLAYER_BY_TEXTURE.get(texture); final Appearance appearance = id == null ? null : APPEARANCES.get(id); @@ -7563,7 +7590,7 @@ index c482ba2..6321473 100644 } /** Supplies the additional animated surfaces in the same pose as a menu's primary skin. */ -@@ -251,14 +808,19 @@ public final class BedrockPlayerSkins { +@@ -251,14 +809,19 @@ public final class BedrockPlayerSkins { if (appearance == null || appearance.surfaces.isEmpty()) return List.of(); if (appearance.previewSurfaces == null) appearance.previewSurfaces = appearance.surfaces.stream().map(surface -> new PreviewSurface( @@ -7585,7 +7612,7 @@ index c482ba2..6321473 100644 public static void rendererContext(final EntityRendererProvider.Context context) { BedrockActorResources.clear(); -@@ -272,6 +834,7 @@ public final class BedrockPlayerSkins { +@@ -272,6 +835,7 @@ public final class BedrockPlayerSkins { APPEARANCES.values().forEach(BedrockPlayerSkins::release); APPEARANCES.clear(); PLAYER_BY_TEXTURE.clear(); @@ -7593,7 +7620,7 @@ index c482ba2..6321473 100644 SKIN_ASSEMBLER.clear(); skinConnection = null; } -@@ -289,7 +852,8 @@ public final class BedrockPlayerSkins { +@@ -289,7 +853,8 @@ public final class BedrockPlayerSkins { } private static Identifier textureId(final UUID playerUuid, final String kind) { @@ -7603,7 +7630,7 @@ index c482ba2..6321473 100644 } private static boolean supportedImage(final BufferedImage image) { -@@ -315,7 +879,10 @@ public final class BedrockPlayerSkins { +@@ -315,7 +880,10 @@ public final class BedrockPlayerSkins { static String geometryName(final String resourcePatch, final String binding) { try { @@ -7615,7 +7642,7 @@ index c482ba2..6321473 100644 final var name = geometry.get(binding); return name != null && name.isJsonPrimitive() && name.getAsJsonPrimitive().isString() ? name.getAsString() : ""; -@@ -345,7 +912,8 @@ public final class BedrockPlayerSkins { +@@ -345,7 +913,8 @@ public final class BedrockPlayerSkins { final List models = BedrockGeometryModel.fromJson(data); final BedrockGeometryModel model = models.stream().filter(value -> value.getIdentifier().equals(name)).findFirst() .orElseGet(() -> models.isEmpty() ? null : models.getFirst()); @@ -7625,7 +7652,7 @@ index c482ba2..6321473 100644 } catch (RuntimeException e) { LOGGER.warn("Failed to parse Bedrock player geometry", e); return null; -@@ -372,7 +940,8 @@ public final class BedrockPlayerSkins { +@@ -372,7 +941,8 @@ public final class BedrockPlayerSkins { final Map animatedModels = new HashMap<>(); for (final var entry : models) { final JsonObject model = entry.getAsJsonObject(); @@ -7635,7 +7662,7 @@ index c482ba2..6321473 100644 if (name.equals(identifier)) { if (body != null) { return null; -@@ -411,7 +980,8 @@ public final class BedrockPlayerSkins { +@@ -411,7 +981,8 @@ public final class BedrockPlayerSkins { final JsonObject model = animatedModels.get(type); final var animation = animationByType.get(type); if (model == null) { @@ -7645,7 +7672,7 @@ index c482ba2..6321473 100644 return null; } continue; -@@ -426,7 +996,8 @@ public final class BedrockPlayerSkins { +@@ -426,7 +997,8 @@ public final class BedrockPlayerSkins { BedrockGeometry.playerModel(parsed, false, meshes); surfaces.add(new Surface(parsed, Map.copyOf(meshes), animation)); } @@ -7655,7 +7682,7 @@ index c482ba2..6321473 100644 if (!coverage.getOrDefault(part, false)) { return null; } -@@ -437,7 +1008,8 @@ public final class BedrockPlayerSkins { +@@ -437,7 +1009,8 @@ public final class BedrockPlayerSkins { return null; } BedrockGeometry.playerModel(parsedBody, false, bodyMeshes); @@ -7665,7 +7692,7 @@ index c482ba2..6321473 100644 } catch (RuntimeException e) { LOGGER.warn("Failed to parse Bedrock persona geometry", e); return null; -@@ -474,12 +1046,21 @@ public final class BedrockPlayerSkins { +@@ -474,12 +1047,21 @@ public final class BedrockPlayerSkins { } } } @@ -7690,7 +7717,7 @@ index c482ba2..6321473 100644 } } return meshes; -@@ -501,7 +1082,13 @@ public final class BedrockPlayerSkins { +@@ -501,7 +1083,13 @@ public final class BedrockPlayerSkins { return vector; } @@ -7705,7 +7732,7 @@ index c482ba2..6321473 100644 Surface face() { return this.surfaces.stream().filter(surface -> surface.animation().type() == 1).findFirst().orElse(null); } -@@ -519,13 +1106,23 @@ public final class BedrockPlayerSkins { +@@ -519,13 +1107,23 @@ public final class BedrockPlayerSkins { private final Identifier headId; private final long started = System.nanoTime(); private final Geometry geometry; @@ -16369,10 +16396,10 @@ index 0000000..f8e433d +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorQuerySnapshotTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorQuerySnapshotTest.java new file mode 100644 -index 0000000..2800806 +index 0000000..502a52f --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/render/BedrockActorQuerySnapshotTest.java -@@ -0,0 +1,154 @@ +@@ -0,0 +1,167 @@ +package com.viaversion.viafabricplus.bedrock.render; + +import com.viaversion.viafabricplus.bedrock.appearance.BedrockActorAnimation; @@ -16391,6 +16418,7 @@ index 0000000..2800806 +import net.minecraft.world.entity.Avatar; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.HumanoidArm; ++import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.phys.Vec3; @@ -16508,6 +16536,18 @@ index 0000000..2800806 + BedrockPlayerSkins.extractPitch(avatar, state, .5F); + } + assertEquals(44F, state.xRot, "Ordinary Java avatars retain the host sample"); ++ ++ final var interpolation = mock(InterpolationHandler.class, withSettings().extraInterfaces(BedrockPlayerPitch.class)); ++ when(avatar.getInterpolation()).thenReturn(interpolation); ++ when(((BedrockPlayerPitch) interpolation).previousPitch()).thenReturn(179.66669F); ++ avatar.xRotO = -180.33331F; ++ when(avatar.getXRot()).thenReturn(-179.66666F); ++ try (final var actors = mockStatic(BedrockActorState.class)) { ++ actors.when(() -> BedrockActorState.state(uuid)).thenReturn(mock(NativeActorMessage.Snapshot.class)); ++ BedrockPlayerSkins.extractPitch(avatar, state, .5F); ++ } ++ assertEquals(180.00002F, BedrockPlayerSkins.actorQueries(avatar, state, .5F).get("target_x_rotation"), ++ "Java's adjusted previous angle must not replace native interpolation history"); + } + + /** Inspect captured values directly; collection through System.gc() would make this lifecycle test flaky. */ diff --git a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md index 4c1b5271..eef831db 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0009-manage-character-creator-accounts-and-wardrobes.pr.md @@ -10,7 +10,7 @@ Keep account entitlement, downloaded assets, character recipes, previews, and in World-player `target_y_rotation` applies the native float wrap and the -85/+85-degree limits. Unordered wrapped values select the lower bound. Server player controller selection uses the same helper. Existing GUI camera overrides retain their previous values. -World-player pitch samples the shortest angular delta before animation and controller evaluation. The final sample remains unwrapped. Actors without a native snapshot retain Java sampling. GUI camera extraction takes precedence over the world sample. +World-player pitch samples the shortest angular delta before animation and controller evaluation. The interpolation owner supplies native previous pitch before Java normalizes its public field. The final sample remains unwrapped. Actors without a native snapshot retain Java sampling. GUI camera extraction takes precedence over the world sample. Prerequisites: @@ -26,7 +26,9 @@ Current Bedrock 1.26.60.5/protocol 2225 registration identifies target-y callbac Original target-x callback `0x140684ec0` and head-x callback `0x142cb53a0` agree on 15 pitch controls. Head-x uses numeric index zero. Synthetic rotation storage and CRT remainder are explicit boundaries. The query-map regression covers both wrap directions, ordinary pitch, alpha one, and the Java fallback. Previous 179/current -179 yields 180 at half a tick and 181 at alpha one. -Read-only observers retain 66 direct and 63 ViaProxy query samples from the rebuilt clients. Both original callbacks reproduce every sampled result bit for bit. Two direct head-only frames differ from Java's linear formula. One returns 180.52 degrees where linear sampling would return -93.08 degrees. These comparisons supply the recorded Java frame inputs. Native rotation-history, scheduler, and complete frame parity remain unverified. +The live official client supplies twelve interpolation transitions and twelve rendered target-x results on matching peer packets. Original-code execution reproduces every native query result. At a normal wrap, native previous pitch remains near +179.67 degrees while Java shifts its public field near -180.33 degrees. Baseline comparisons find eleven direct and twelve ViaProxy samples whose query results differ by 360 degrees. + +The rebuilt direct and ViaProxy clients retain the matching native history in all 67 and 68 sampled frames. Both original query callbacks reproduce every result bit for bit at each recorded Java frame alpha. The query-map regression covers native history despite a different public Java field. The handler regression fails before history retention and covers a following head-only packet and context retirement. These comparisons establish selected state transitions, not wall-clock timing. Broader histories, scheduler order, and complete frame parity remain unverified. Fourteen-phase installed direct and ViaProxy controls use matching peer skin, spawn, and movement bytes against the native fixture. A private scheduling pause aligns the intermediate normal-then-teleport state and the first three body ticks after teleport. All four sampled angles match the twelve traced native phases. The [movement comparison](0041-interpolate-native-remote-player-movement.pr.md) describes the state controls and their limits. diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch index 9a11ee5e..b0f75f66 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.patch @@ -1,4 +1,4 @@ -From 36a84dea0418714bca8c27b09865ad0566670f72 Mon Sep 17 00:00:00 2001 +From 111f3c755fe09573e712afb47d9df25ba0e27fca Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:51:01 +0100 Subject: [PATCH] fix(players): interpolate negotiated remote movement as @@ -71,6 +71,20 @@ retains wrapped pitch beyond those bounds. Installed protocol-2225 controls reproduced +179/-179 packets settling at +90/-90 before this correction. Keep the direct field access inside the admitted native remote handler; ordinary Java setters and local look input retain their host behavior. + +Retain native previous pitch before LivingEntity normalizes its render +history by360 degrees. Capture the value after interpolation and before +body-tick completion, and use it for immediate head-only updates as well +as render queries. Retire this history with its native context; historical +physics replay cannot advance it. + +A read-only live Bedrock1.26.60.5 trace records12 interpolation transitions +and their rotation components on matching protocol2225 peer packets. +Java changed previous179.66669 to-180.33331 at a normal wrap, producing +query errors of360 degrees in11direct and12proxy samples. The native +query retains its unwrapped result. Native executable SHA-256 remains +988f6a61403b4381aa41e6c66d64cce441b14e561a9b10f647c2758ef3f2672a. +Debugger pauses exclude wall-clock timing from this comparison. --- .../bedrock/ViaFabricPlusBedrock.java | 2 + .../MixinRemotePlayerInterpolation.java | 22 + @@ -79,13 +93,13 @@ ordinary Java setters and local look input retain their host behavior. .../render/MixinLivingEntityBodyRotation.java | 7 +- .../input/BedrockRemotePlayerMovement.java | 81 ++++ .../input/RemotePlayerMovementPayload.java | 22 + - .../movement/RemotePlayerInterpolation.java | 142 +++++++ - .../RemotePlayerInterpolationHandler.java | 217 ++++++++++ + .../movement/RemotePlayerInterpolation.java | 142 ++++++ + .../RemotePlayerInterpolationHandler.java | 239 +++++++++++ .../viafabricplus-bedrock.mixins.json | 3 + ...ativeRemoteInterpolationReferenceTest.java | 74 ++++ - .../RemotePlayerInterpolationHandlerTest.java | 378 ++++++++++++++++++ + .../RemotePlayerInterpolationHandlerTest.java | 406 ++++++++++++++++++ .../RemotePlayerInterpolationTest.java | 133 ++++++ - 13 files changed, 1151 insertions(+), 1 deletion(-) + 13 files changed, 1201 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerInterpolation.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/MixinRemotePlayerMovementPacketListener.java create mode 100644 src/main/java/com/viaversion/viafabricplus/bedrock/injection/mixin/features/movement/RemotePlayerRotationAccess.java @@ -521,10 +535,10 @@ index 0000000..61a8f44 +} diff --git a/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java new file mode 100644 -index 0000000..175b799 +index 0000000..a958a6d --- /dev/null +++ b/src/main/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandler.java -@@ -0,0 +1,217 @@ +@@ -0,0 +1,239 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -536,6 +550,7 @@ index 0000000..175b799 +import com.viaversion.viafabricplus.bedrock.injection.mixin.features.movement.RemotePlayerRotationAccess; +import com.viaversion.viafabricplus.bedrock.movement.replay.PhysicalReplayContext; +import com.viaversion.viafabricplus.bedrock.render.BedrockActorState; ++import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerPitch; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientPacketListener; @@ -551,7 +566,7 @@ index 0000000..175b799 +import java.util.UUID; + +/** Decorates the real host handler so ordinary Java and unsupported modes retain their existing path. */ -+public final class RemotePlayerInterpolationHandler implements InterpolationHandler { ++public final class RemotePlayerInterpolationHandler implements InterpolationHandler, BedrockPlayerPitch { + private final RemotePlayer player; + private final InterpolationHandler host; + private final RemotePlayerInterpolation nativeInterpolation = new RemotePlayerInterpolation(); @@ -563,6 +578,8 @@ index 0000000..175b799 + private Position3f previousEye; + private Vec3 appliedPosition; + private Vec3 appliedPreviousPosition; ++ private float previousPitch; ++ private boolean hasPitchHistory; + + public RemotePlayerInterpolationHandler(final RemotePlayer player, final InterpolationHandler host) { + this.player = player; @@ -586,7 +603,10 @@ index 0000000..175b799 + return false; + } + final boolean continuing = this.validContext() && movement.lifetime().equals(this.lifetime); -+ if (!continuing) this.nativeInterpolation.clear(); ++ if (!continuing) { ++ this.nativeInterpolation.clear(); ++ this.hasPitchHistory = false; ++ } + this.host.cancel(); + this.connection = client.getConnection(); + this.level = client.level; @@ -630,12 +650,14 @@ index 0000000..175b799 + } + + private void applyRotation(final float rawPitch, final float rawYaw) { ++ final float previousPitch = this.previousPitch(); + final float pitch = RemotePlayerInterpolation.wrap(rawPitch); + final float yaw = RemotePlayerInterpolation.wrap(rawYaw); + ((RemotePlayerRotationAccess) this.player).bedrockRemotePitch(pitch); + this.player.setYRot(yaw); -+ this.player.xRotO = RemotePlayerInterpolation.wrap((pitch - rawPitch) + this.player.xRotO); ++ this.player.xRotO = RemotePlayerInterpolation.wrap((pitch - rawPitch) + previousPitch); + this.player.yRotO = RemotePlayerInterpolation.wrap((yaw - rawYaw) + this.player.yRotO); ++ this.capturePitchHistory(); + } + + private boolean validContext() { @@ -663,9 +685,21 @@ index 0000000..175b799 + return new Position3f(this.eye.x() - this.previousEye.x(), this.eye.y() - this.previousEye.y(), this.eye.z() - this.previousEye.z()); + } + ++ @Override ++ public float previousPitch() { ++ return this.hasPitchHistory && this.validContext() ? this.previousPitch : this.player.xRotO; ++ } ++ ++ private void capturePitchHistory() { ++ this.previousPitch = this.player.xRotO; ++ this.hasPitchHistory = true; ++ } ++ + public void finishBodyTick() { + if (PhysicalReplayContext.replaying() || !this.validContext()) return; + if (!this.player.position().equals(this.appliedPosition)) this.captureEye(); ++ // LivingEntity later shifts xRotO by 360 for its linear renderer; native queries retain this value. ++ this.capturePitchHistory(); + // Native body control reads displacement before the end-of-tick StateVector copy. + this.previousEye = this.eye; + this.appliedPreviousPosition = this.player.oldPosition(); @@ -696,6 +730,7 @@ index 0000000..175b799 + this.player.xRotO = next.previousPitch(); + this.player.yRotO = next.previousYaw(); + this.player.setYHeadRot(next.headYaw()); ++ this.capturePitchHistory(); + } + + @Override @@ -734,6 +769,7 @@ index 0000000..175b799 + private void releaseNative() { + this.nativeInterpolation.clear(); + this.lifetime = null; ++ this.hasPitchHistory = false; + this.connection = null; + this.level = null; + this.eye = null; @@ -838,10 +874,10 @@ index 0000000..1a00bf6 +} diff --git a/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java new file mode 100644 -index 0000000..dace424 +index 0000000..9fede7c --- /dev/null +++ b/src/test/java/com/viaversion/viafabricplus/bedrock/movement/RemotePlayerInterpolationHandlerTest.java -@@ -0,0 +1,378 @@ +@@ -0,0 +1,406 @@ +/* + * This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock + * Copyright (C) 2026 ViaVersion and contributors @@ -967,6 +1003,34 @@ index 0000000..dace424 + } + + @Test ++ void retainsNativePitchHistoryAcrossHostNormalizationAndHeadOnlyInput() { ++ try (final var f = new Fixture()) { ++ ((RemotePlayerRotationAccess) f.player).bedrockRemotePitch(179F); ++ f.player.xRotO = 179F; ++ final var target = new RemotePlayerMovement(17, f.uuid, f.lifetime, 0, false, ++ new Position3f(.5F, 73.24002F, 4.5F), new Position3f(-179F, 0F, 0F), 1.62001F, ++ (short) 0, 3, false, f.movement(0, false).javaTarget()); ++ assertTrue(f.handler.receive(target)); ++ f.tick(); ++ f.player.xRotO = f.player.getXRot(); ++ f.tick(); ++ // Live native component after the second interpolation step, before Java's tick normalization. ++ assertEquals(-179.66666F, f.player.getXRot()); ++ assertEquals(179.66669F, f.handler.previousPitch()); ++ f.player.xRotO = -180.33331F; ++ assertEquals(179.66669F, f.handler.previousPitch()); ++ final var head = new RemotePlayerMovement(17, f.uuid, f.lifetime, 3, false, ++ target.position(), new Position3f(-179F, 0F, 0F), 1.62001F, ++ (short) 0, 0, false, new RemotePlayerMovement.JavaTarget(false, 0, 0, 0, 0, 0F, false)); ++ assertTrue(f.handler.receive(head)); ++ assertEquals(179.66669F, f.handler.previousPitch()); ++ f.handler.cancel(); ++ f.player.xRotO = 22F; ++ assertEquals(22F, f.handler.previousPitch()); ++ } ++ } ++ ++ @Test + void resetModesApplyImmediatelyAndPreserveTheLiveHeadAndDistinctPreviousPositions() { + for (int mode : new int[]{1, 2}) { + try (final var f = new Fixture()) { diff --git a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md index 5ec514ca..a65eae5d 100644 --- a/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md +++ b/patches/viafabricplus-bedrock/upstreamable/0041-interpolate-native-remote-player-movement.pr.md @@ -8,6 +8,8 @@ Head-only updates apply raw pitch immediately. They preserve live yaw, pending p The admitted native handler publishes wrapped pitch directly to the rotation field. Java 26.3's setter clips pitch to -90/+90 degrees. Native remote rotation permits angles beyond those limits. Ordinary Java and local look input retain their existing setters. +The handler retains previous pitch before Java shifts its render history by 360 degrees. Immediate head-only updates use that retained value too. Completed body ticks refresh it, while historical physics replay cannot advance it. Context retirement restores the host history source. Java keeps its public render fields and normalizer. + The outer packet path derives motion from raw source position minus the retained previous native eye. Body control reads the exact native displacement before the previous-position copy. The client retains that float history after body control, including while interpolation is idle. Historical physics replay does not advance this live history. ## Native evidence @@ -26,7 +28,7 @@ Three additional original-callback controls retain pitch beyond Java's look limi ## Installed comparisons -Six-phase direct and ViaProxy controls retain +179/-179-degree pitch through normal movement and immediate head-only input. Position, yaw, and head-yaw remain at their declared values. Complete journals contain 1,582 and 1,224 frames, with identical peer skin, spawn, and movement payloads. Installed add-on and proxy hashes match the verified build. The [appearance comparison](0009-manage-character-creator-accounts-and-wardrobes.pr.md) covers angular render queries and their remaining history limits. +Six-phase direct and ViaProxy controls retain +179/-179-degree pitch through normal movement and immediate head-only input. Position, yaw, and head-yaw remain at their declared values. The native journal contains 2,696 complete frames. Fresh Java journals contain 1,579 and 1,287 frames after the upstream refresh. All three routes retain identical peer skin, spawn, and movement payloads. Installed add-on and proxy hashes match the verified build. The [appearance comparison](0009-manage-character-creator-accounts-and-wardrobes.pr.md) covers native pitch history, angular queries, and their remaining limits. Earlier direct and ViaProxy controls retain the observed large- and small-move steps. Twelve settled phases per route compare movement origins and poses, including the previously occluded leg. The overlapping-head regression also runs against the current rebuilt source with two fresh player identities. From 259a1e4af1d0c8949490cc4856fc406521ddcf6f Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:53:15 +0100 Subject: [PATCH 47/47] chore(deps): rebase ViaBedrock onto current upstream Advance the core pin to da6391a7 and replay all stacks. Keep the unchanged legacy codec API in setup while the pinned hosts and capture tooling still import it. Declare required transport compile dependencies in the owning patches and record their build prerequisite. The other upstream tips and the verified ViaFabricPlus build are current. The refreshed full build, capture self-tests, and installed routes pass. --- patches/viabedrock/series.json | 9 +- .../setup/0001-stackanvil-branding.patch | 201 +++++++++++++++++- ...ansport-initiated-raknet-disconnects.patch | 38 +++- ...ansport-initiated-raknet-disconnects.pr.md | 2 + ...accept-bds-http-capability-responses.patch | 22 +- ...accept-bds-http-capability-responses.pr.md | 2 +- targets.json | 2 +- 7 files changed, 257 insertions(+), 19 deletions(-) diff --git a/patches/viabedrock/series.json b/patches/viabedrock/series.json index e0989bdb..49c22fbc 100644 --- a/patches/viabedrock/series.json +++ b/patches/viabedrock/series.json @@ -367,7 +367,14 @@ }, { "file": "0090-accept-bds-http-capability-responses.patch", - "title": "fix(nethernet): accept BDS capability responses without metadata" + "title": "fix(nethernet): accept BDS capability responses without metadata", + "prerequisites": [ + { + "project": "viabedrock", + "patch": "0079-finish-transport-initiated-raknet-disconnects.patch", + "reason": "Provide the explicit Cloudburst repository used by the transport compile dependencies." + } + ] }, { "file": "0091-recover-unacknowledged-chunk-radius-requests.patch", diff --git a/patches/viabedrock/setup/0001-stackanvil-branding.patch b/patches/viabedrock/setup/0001-stackanvil-branding.patch index c5d43181..d0718d70 100644 --- a/patches/viabedrock/setup/0001-stackanvil-branding.patch +++ b/patches/viabedrock/setup/0001-stackanvil-branding.patch @@ -1,17 +1,27 @@ -From 3160cef569883a5f886e40ba7326d0a34fe88d74 Mon Sep 17 00:00:00 2001 +From d5ce0cd14449fe8de9477d35bd97e79e87f310f8 Mon Sep 17 00:00:00 2001 From: AlexProgrammerDE <40795980+AlexProgrammerDE@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:55:33 +0200 Subject: [PATCH] chore(build): configure StackAnvil identity and compatibility Keep branded artifacts and local dependency routing together with the 1 GiB Checkstyle worker allocation required by the enlarged local source tree. Exclude these build compatibility changes from upstream PRs. + +Retain the unchanged legacy RakNet codec API while the pinned add-on, +ViaProxy and capture tooling still import it. Core upstream removes these +classes in1d76de72. Keep this host compatibility bridge in setup, outside +all upstream PR branches. Preserve the original license and packet +implementation; transport feature patches declare the compile APIs. --- - build.gradle | 17 ++++++++++++++++- - .../main/groovy/viabedrock.checkstyle.gradle | 1 + - gradle.properties | 2 +- - 3 files changed, 18 insertions(+), 2 deletions(-) + build.gradle | 17 ++- + .../main/groovy/viabedrock.checkstyle.gradle | 1 + + gradle.properties | 2 +- + .../netty/raknet/AesEncryptionCodec.java | 100 ++++++++++++++++++ + .../viabedrock/netty/raknet/MessageCodec.java | 65 ++++++++++++ + 5 files changed, 183 insertions(+), 2 deletions(-) + create mode 100644 src/main/java/net/raphimc/viabedrock/netty/raknet/AesEncryptionCodec.java + create mode 100644 src/main/java/net/raphimc/viabedrock/netty/raknet/MessageCodec.java diff --git a/build.gradle b/build.gradle -index 8a16054d..bf1ef357 100644 +index 0c726ca3..bc49b9ed 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,10 @@ plugins { @@ -44,7 +54,7 @@ index 8a16054d..bf1ef357 100644 //mavenLocal() // Uncomment when updating maven { name = "ViaVersion" -@@ -91,7 +106,7 @@ dependencies { +@@ -80,7 +95,7 @@ dependencies { api "com.vdurmont:semver4j:3.1.0" api "com.mojang:brigadier:1.3.10" api "at.yawk.lz4:lz4-java:1.12.1" @@ -75,5 +85,182 @@ index bde64c7f..f781d5a7 100644 maven_name=ViaBedrock -maven_version=0.0.32-SNAPSHOT +maven_version=0.0.32-StackAnvil +diff --git a/src/main/java/net/raphimc/viabedrock/netty/raknet/AesEncryptionCodec.java b/src/main/java/net/raphimc/viabedrock/netty/raknet/AesEncryptionCodec.java +new file mode 100644 +index 00000000..2b486d54 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/netty/raknet/AesEncryptionCodec.java +@@ -0,0 +1,100 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors ++ * ++ * This program is free software: you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation, either version 3 of the License, or ++ * (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ */ ++package net.raphimc.viabedrock.netty.raknet; ++ ++import com.google.common.primitives.Longs; ++import io.netty.buffer.ByteBuf; ++import io.netty.buffer.ByteBufUtil; ++import io.netty.channel.ChannelHandlerContext; ++import io.netty.handler.codec.ByteToMessageCodec; ++import io.netty.handler.codec.CorruptedFrameException; ++ ++import javax.crypto.Cipher; ++import javax.crypto.NoSuchPaddingException; ++import javax.crypto.SecretKey; ++import javax.crypto.spec.IvParameterSpec; ++import java.nio.ByteBuffer; ++import java.security.InvalidAlgorithmParameterException; ++import java.security.InvalidKeyException; ++import java.security.MessageDigest; ++import java.security.NoSuchAlgorithmException; ++import java.util.Arrays; ++import java.util.List; ++ ++public class AesEncryptionCodec extends ByteToMessageCodec { ++ ++ private final SecretKey secretKey; ++ private final Cipher inCipher; ++ private final Cipher outCipher; ++ private final MessageDigest sha256; ++ private long sentPacketCounter; ++ private long receivedPacketCounter; ++ ++ public AesEncryptionCodec(final SecretKey secretKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { ++ final byte[] iv = new byte[16]; ++ System.arraycopy(secretKey.getEncoded(), 0, iv, 0, 12); ++ iv[15] = 2; ++ ++ this.secretKey = secretKey; ++ this.inCipher = Cipher.getInstance("AES/CTR/NoPadding"); ++ this.inCipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); ++ this.outCipher = Cipher.getInstance("AES/CTR/NoPadding"); ++ this.outCipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); ++ this.sha256 = MessageDigest.getInstance("SHA-256"); ++ } ++ ++ @Override ++ protected void encode(final ChannelHandlerContext ctx, final ByteBuf in, final ByteBuf out) throws Exception { ++ final byte[] hash = this.generateHash(in, this.sentPacketCounter++); ++ ++ final ByteBuffer inBuffer = in.nioBuffer(); ++ out.ensureWritable(in.readableBytes() + 8); ++ ++ this.outCipher.update(inBuffer, out.nioBuffer(0, in.readableBytes())); ++ this.outCipher.update(ByteBuffer.wrap(hash), out.nioBuffer(in.readableBytes(), 8)); ++ out.writerIndex(in.readableBytes() + 8); ++ } ++ ++ @Override ++ protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) throws Exception { ++ final ByteBuffer inBuffer = in.nioBuffer(); ++ final ByteBuffer outBuffer = inBuffer.duplicate(); ++ ++ this.inCipher.update(inBuffer, outBuffer); ++ final ByteBuf output = in.readRetainedSlice(in.readableBytes() - 8); ++ ++ final byte[] receivedHash = new byte[8]; ++ in.readBytes(receivedHash); ++ final byte[] expectedHash = this.generateHash(output, this.receivedPacketCounter++); ++ if (!MessageDigest.isEqual(receivedHash, expectedHash)) { ++ throw new CorruptedFrameException("Invalid encrypted packet"); ++ } ++ ++ out.add(output); ++ } ++ ++ private byte[] generateHash(final ByteBuf buf, final long packetCounter) { ++ this.sha256.update(Longs.toByteArray(Long.reverseBytes(packetCounter))); ++ this.sha256.update(ByteBufUtil.getBytes(buf)); ++ this.sha256.update(this.secretKey.getEncoded()); ++ final byte[] hash = this.sha256.digest(); ++ this.sha256.reset(); ++ return Arrays.copyOf(hash, 8); ++ } ++ ++} +diff --git a/src/main/java/net/raphimc/viabedrock/netty/raknet/MessageCodec.java b/src/main/java/net/raphimc/viabedrock/netty/raknet/MessageCodec.java +new file mode 100644 +index 00000000..28dfbf89 +--- /dev/null ++++ b/src/main/java/net/raphimc/viabedrock/netty/raknet/MessageCodec.java +@@ -0,0 +1,65 @@ ++/* ++ * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock ++ * Copyright (C) 2023-2026 RK_01/RaphiMC and contributors ++ * ++ * This program is free software: you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation, either version 3 of the License, or ++ * (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ */ ++package net.raphimc.viabedrock.netty.raknet; ++ ++import io.netty.buffer.ByteBuf; ++import io.netty.buffer.CompositeByteBuf; ++import io.netty.channel.ChannelHandlerContext; ++import io.netty.handler.codec.MessageToMessageCodec; ++import net.raphimc.viabedrock.ViaBedrock; ++import org.cloudburstmc.netty.channel.raknet.RakReliability; ++import org.cloudburstmc.netty.channel.raknet.packet.RakMessage; ++ ++import java.util.List; ++ ++public class MessageCodec extends MessageToMessageCodec { ++ ++ public static final String NAME = "viabedrock-raknet-message-codec"; ++ ++ private static final int MINECRAFT_MESSAGE_ID = 0xFE; ++ ++ @Override ++ protected void encode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) { ++ final CompositeByteBuf buf = ctx.alloc().compositeBuffer(2); ++ try { ++ buf.addComponent(true, ctx.alloc().ioBuffer(1).writeByte(MINECRAFT_MESSAGE_ID)); ++ buf.addComponent(true, in.retainedSlice()); ++ out.add(new RakMessage(buf.retain())); ++ } finally { ++ buf.release(); ++ } ++ } ++ ++ @Override ++ protected void decode(final ChannelHandlerContext ctx, final RakMessage in, final List out) { ++ if (in.channel() != 0 && in.reliability() != RakReliability.RELIABLE_ORDERED) { ++ return; ++ } ++ final ByteBuf buf = in.content(); ++ if (!buf.isReadable()) { ++ return; ++ } ++ final int messageId = buf.readUnsignedByte(); ++ if (messageId != MINECRAFT_MESSAGE_ID) { // Mojang client seems to ignore invalid messages ++ ViaBedrock.getPlatform().getLogger().warning("Received invalid RakNet message id: " + messageId); ++ return; ++ } ++ out.add(buf.readRetainedSlice(buf.readableBytes())); ++ } ++ ++} -- 2.56.0 diff --git a/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.patch b/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.patch index 42600536..afd75c48 100644 --- a/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.patch +++ b/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.patch @@ -1,4 +1,4 @@ -From 26d3765c22d6e1be511142d96b043642e9f27c1a Mon Sep 17 00:00:00 2001 +From 09deabe74c8034104bd32d97854cc9548dbc037c 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(network): finish transport-initiated RakNet disconnects @@ -16,25 +16,49 @@ disconnect, and transport events continue to downstream handlers. Embedded transport tests cover every RakNet reason and local close completion. The resolved Cloudburst and VFP runtime session codecs both emit the event before closing; no runtime-library reflection or client workaround is needed. + +Declare the Cloudburst repository and RakNet compile/test dependency +explicitly after upstream removes its unused transport dependencies. +This handler still consumes the transport disconnect reason enum. +The same repository supplies later independently maintained signaling +code; its build prerequisite is recorded in the patch series. --- - build.gradle | 1 + + build.gradle | 12 +++ .../viabedrock/netty/DisconnectHandler.java | 13 +++- .../netty/DisconnectHandlerTest.java | 74 +++++++++++++++++++ - 3 files changed, 86 insertions(+), 2 deletions(-) + 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/test/java/net/raphimc/viabedrock/netty/DisconnectHandlerTest.java diff --git a/build.gradle b/build.gradle -index 3643f0c8..e01266de 100644 +index 46fdf3f8..065fdf4f 100644 --- a/build.gradle +++ b/build.gradle -@@ -90,6 +90,7 @@ dependencies { +@@ -40,6 +40,16 @@ def cubeConverterDependency = stackAnvilMavenRepo.present + : "com.github.oryxel1:CubeConverter:7cf736c4a5" + + repositories { ++ if (findByName("OpenCollab Snapshots") == null) { ++ maven { ++ name = "OpenCollab Snapshots" ++ url = "https://repo.opencollab.dev/maven-snapshots" ++ content { ++ includeGroupByRegex "org\\.cloudburstmc\\..+" ++ includeGroup "dev.opencollab" ++ } ++ } ++ } + if (stackAnvilMavenRepo.present) { + maven { + name = "StackAnvilLocal" +@@ -80,6 +90,8 @@ dependencies { + compileOnly "com.google.guava:guava:33.7.2-jre" compileOnly "io.netty:netty-handler:4.2.19.Final" compileOnly "com.google.code.gson:gson:2.15.0" - compileOnly "org.cloudburstmc.netty:netty-transport-raknet:2.0.0.CR4-20260921.144528-1" ++ compileOnly "org.cloudburstmc.netty:netty-transport-raknet:2.0.0.CR4-20260921.144528-1" + testImplementation "org.cloudburstmc.netty:netty-transport-raknet:2.0.0.CR4-20260921.144528-1" - compileOnly "org.cloudburstmc.netty:netty-transport-nethernet:2.0.0.CR4-20260921.144528-1" api("io.netty:netty-codec-http:4.2.19.Final") { + transitive = false diff --git a/src/main/java/net/raphimc/viabedrock/netty/DisconnectHandler.java b/src/main/java/net/raphimc/viabedrock/netty/DisconnectHandler.java index 3302236b..36ba2656 100644 --- a/src/main/java/net/raphimc/viabedrock/netty/DisconnectHandler.java diff --git a/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.pr.md b/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.pr.md index 2769e863..39d30950 100644 --- a/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.pr.md +++ b/patches/viabedrock/upstreamable/0079-finish-transport-initiated-raknet-disconnects.pr.md @@ -17,6 +17,8 @@ The resolved Cloudburst session codec follows the same order. Runtime inspection establishes that the pinned VFP client loads NetworkCompatible 1.7.0. The implementation uses their shared public event type. +Declare the Cloudburst repository and RakNet compile/test dependencies explicitly. Current upstream removes its unused transport dependencies. The setup compatibility patch retains unchanged legacy codecs for the pinned hosts and capture tooling; it remains outside upstream PRs. + ## Evidence and verification The comparison uses Java 26.3 and Bedrock 1.26.51.1, build 51061372, protocol 2193. diff --git a/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.patch b/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.patch index 9b1de358..f56315c9 100644 --- a/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.patch +++ b/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.patch @@ -1,4 +1,4 @@ -From 9808c92318bda8a562e4de8e484798d789e8e239 Mon Sep 17 00:00:00 2001 +From 436ad0fd90ba7b35674c167180fa0f53dd98cfe7 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(nethernet): accept BDS capability responses without @@ -21,19 +21,37 @@ HTTP clients can sign the offer before LOGIN_START and preserve the same key through login. Exact BDS 1.26.51.1 protocol2193 rejects the unsigned offer with numeric37 and opens ICE/DTLS with the self-signed assertion. Token tests verify public-key binding and fresh reconnect keys. + +Declare the NetherNet compile dependency used by this signaling adapter. +Current upstream no longer declares its unused transport dependencies. +The transport build foundation supplies the Cloudburst repository. +Keep the established compile API version compatible with both host routes. --- + build.gradle | 1 + .../netty/nethernet/BedrockHttpSignaling.java | 188 ++++++++++++++++ .../protocol/packet/LoginPackets.java | 27 +-- .../viabedrock/protocol/storage/AuthData.java | 39 ++++ .../nethernet/BedrockHttpSignalingTest.java | 208 ++++++++++++++++++ .../protocol/packet/LoginIdentityTest.java | 90 ++++++++ .../protocol/storage/AuthDataTest.java | 44 ++++ - 6 files changed, 570 insertions(+), 26 deletions(-) + 7 files changed, 571 insertions(+), 26 deletions(-) create mode 100644 src/main/java/net/raphimc/viabedrock/netty/nethernet/BedrockHttpSignaling.java create mode 100644 src/test/java/net/raphimc/viabedrock/netty/nethernet/BedrockHttpSignalingTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/packet/LoginIdentityTest.java create mode 100644 src/test/java/net/raphimc/viabedrock/protocol/storage/AuthDataTest.java +diff --git a/build.gradle b/build.gradle +index 10525c72..bc575a6e 100644 +--- a/build.gradle ++++ b/build.gradle +@@ -104,6 +104,7 @@ dependencies { + compileOnly "io.netty:netty-handler:4.2.19.Final" + compileOnly "com.google.code.gson:gson:2.15.0" + compileOnly "org.cloudburstmc.netty:netty-transport-raknet:2.0.0.CR4-20260921.144528-1" ++ compileOnly "org.cloudburstmc.netty:netty-transport-nethernet:2.0.0.CR4-20260921.144528-1" + testImplementation "org.cloudburstmc.netty:netty-transport-raknet:2.0.0.CR4-20260921.144528-1" + + api("io.netty:netty-codec-http:4.2.19.Final") { diff --git a/src/main/java/net/raphimc/viabedrock/netty/nethernet/BedrockHttpSignaling.java b/src/main/java/net/raphimc/viabedrock/netty/nethernet/BedrockHttpSignaling.java new file mode 100644 index 00000000..d50777de diff --git a/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.pr.md b/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.pr.md index 126df609..87dc3302 100644 --- a/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.pr.md +++ b/patches/viabedrock/upstreamable/0090-accept-bds-http-capability-responses.pr.md @@ -16,7 +16,7 @@ Raw traffic, account identifiers, and screenshots remain private. ## Verification -- The patch applies to the pinned upstream base without setup. +- Apply the recorded [transport build prerequisite](0079-finish-transport-initiated-raknet-disconnects.pr.md) first. It supplies the Cloudburst repository. This patch declares its NetherNet compile dependency explicitly. - Loopback tests cover plaintext TLS rejection, empty capability responses, SDP delivery, unsupported endpoints, and invalid answers. - Core Checkstyle passes. - A native client joins strict BDS and retains the connection through walking and jumping. diff --git a/targets.json b/targets.json index 345d7326..78cb6c81 100644 --- a/targets.json +++ b/targets.json @@ -16,7 +16,7 @@ "fork": "StackAnvil/ViaBedrock", "prAssignees": ["RaphiMC", "Exterminate5573"], "baseBranch": "main", - "baseSha": "e54158276fcc9d4fff34258379259069ac4848fd", + "baseSha": "da6391a7b8714673331cee1d54b8dc9aae0460e5", "java": "25", "publication": "MavenJava", "buildTask": "build"