From 8f20b616510bae4ae61d3dff699dc35a235d0b81 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Thu, 13 Nov 2025 23:25:53 +0100 Subject: [PATCH 1/2] Initial stab at "Bedrock client takes all items from crafting grid when closing the inventory" --- .../geyser/inventory/InventoryHolder.java | 2 +- .../geyser/session/GeyserSession.java | 9 +++ .../inventory/InventoryTranslator.java | 50 +++++++++++++++- .../BedrockContainerCloseTranslator.java | 3 +- .../geyser/util/InterruptibleFuture.java | 57 +++++++++++++++++++ 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/util/InterruptibleFuture.java diff --git a/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java index 6c811676e06..03e96377ebb 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java @@ -130,7 +130,7 @@ public boolean prepareInventory() { } public void translateRequests(List requests) { - this.translator.translateRequests(session, inventory, requests); + this.translator.translateRequests(session, inventory, requests, true); } public GeyserSession session() { diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index fdac156b6a7..d78910ee4a2 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -193,6 +193,7 @@ import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.geyser.util.EntityUtils; +import org.geysermc.geyser.util.InterruptibleFuture; import org.geysermc.geyser.util.InventoryUtils; import org.geysermc.geyser.util.LoginEncryptionUtils; import org.geysermc.geyser.util.MathUtils; @@ -376,6 +377,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private ScheduledFuture containerOutputFuture; + /** + * Used to delay specific inventory transactions until we know that the + * client isn't actually about to close their inventory + */ + private final InterruptibleFuture inventoryTransactionFuture; + /** * Stores session collision */ @@ -786,6 +793,8 @@ public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSessio this.collisionManager = new CollisionManager(this); this.blockBreakHandler = new BlockBreakHandler(this); + this.inventoryTransactionFuture = new InterruptibleFuture(this); + this.playerEntity = new SessionPlayerEntity(this); collisionManager.updatePlayerBoundingBox(this.playerEntity.getPosition()); diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java index 0c08c8df167..586878ebc65 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java @@ -47,6 +47,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.CraftResultsDeprecatedAction; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.DropAction; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.ItemStackRequestAction; +import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.PlaceAction; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.SwapAction; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.TransferItemStackRequestAction; import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.response.ItemStackResponse; @@ -88,6 +89,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.TimeUnit; import static org.geysermc.geyser.translator.inventory.BundleInventoryTranslator.isBundle; @@ -237,7 +239,7 @@ protected boolean shouldRejectItemPlace(GeyserSession session, Type inventory, C } /** - * Should be overrided if this request matches a certain criteria and shouldn't be treated normally. + * Should be overridden if this request matches a certain criteria and shouldn't be treated normally. * E.G. anvil renaming or enchanting */ protected boolean shouldHandleRequestFirst(ItemStackRequestAction action, Type inventory) { @@ -251,8 +253,51 @@ protected ItemStackResponse translateSpecialRequest(GeyserSession session, Type return rejectRequest(request); } - public final void translateRequests(GeyserSession session, Type inventory, List requests) { + public final void translateRequests(GeyserSession session, Type inventory, List requests, boolean firstTime) { boolean refresh = false; + + // If we get another request, we're not closing the inventory and should run the queued request asap + // to "not fall behind" on transactions + session.getInventoryTransactionFuture().runCurrentIfPresent(); + + // Fixes https://github.com/GeyserMC/Geyser/issues/5258 + // Bedrock edition has the quirk where it'll remove all items from the crafting inventory + // before closing it, which breaks some Java plugins... + boolean mustDelay = firstTime && requests.stream().allMatch(request -> { + if (request.getActions().length > 0) { + if (request.getActions()[0] instanceof PlaceAction action) { + return action.getSource().getContainerName().getContainer() == ContainerSlotType.CRAFTING_INPUT && + action.getDestination().getContainerName().getContainer() == ContainerSlotType.HOTBAR_AND_INVENTORY; + } + } + return false; + }); + + if (mustDelay) { + GeyserImpl.getInstance().getLogger().warning("Delaying action until we know if this is actually an inventory close! " + requests); + session.getInventoryTransactionFuture().schedule(() -> { + GeyserImpl.getInstance().getLogger().warning("DELAY FINISHED!"); + if (session.getPendingOrCurrentBedrockInventoryId() == inventory.getBedrockId()) { + translateRequests(session, inventory, requests, false); + } else { + ItemStackResponsePacket responsePacket = new ItemStackResponsePacket(); + requests.forEach(request -> responsePacket.getEntries().add(rejectRequest(request, false))); + session.sendUpstreamPacket(responsePacket); + + // Now update current inventory + if (session.getInventoryHolder() != null) { + session.getInventoryHolder().updateInventory(); + } else { + session.getPlayerInventoryHolder().updateInventory(); + } + } + }, + 800, + TimeUnit.MILLISECONDS + ); + return; + } + ItemStackResponsePacket responsePacket = new ItemStackResponsePacket(); for (ItemStackRequest request : requests) { ItemStackResponse response; @@ -987,6 +1032,7 @@ protected static ItemStackResponse rejectRequest(ItemStackRequest request, boole if (throwError && GeyserImpl.getInstance().getConfig().isDebugMode()) { new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace(); } + GeyserImpl.getInstance().getLogger().info("REJEEEEEEEEEEECTING"); return new ItemStackResponse(ItemStackResponseStatus.ERROR, request.getRequestId(), Collections.emptyList()); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java index b3fa987a5c1..e2591f4c4ab 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java @@ -45,7 +45,7 @@ public class BedrockContainerCloseTranslator extends PacketTranslator future = null; + private Runnable runnable = null; + + public InterruptibleFuture(GeyserSession session) { + this.session = session; + } + + public void runCurrentIfPresent() { + if (future != null && future.cancel(false)) { + runnable.run(); + } + + future = null; + runnable = null; + } + + public void schedule(Runnable runnable, long delay, TimeUnit unit) { + runCurrentIfPresent(); + this.future = session.scheduleInEventLoop(this.runnable = runnable, delay, unit); + } +} From 7a2ba154d9018499a400e5196b46b2278731e86b Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Tue, 25 Nov 2025 16:30:49 +0100 Subject: [PATCH 2/2] Code cleanup --- .../geysermc/geyser/inventory/InventoryHolder.java | 2 +- .../translator/inventory/InventoryTranslator.java | 14 +++++++++----- .../bedrock/BedrockContainerCloseTranslator.java | 2 +- .../geysermc/geyser/util/InterruptibleFuture.java | 7 ++++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java b/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java index 03e96377ebb..6c811676e06 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/InventoryHolder.java @@ -130,7 +130,7 @@ public boolean prepareInventory() { } public void translateRequests(List requests) { - this.translator.translateRequests(session, inventory, requests, true); + this.translator.translateRequests(session, inventory, requests); } public GeyserSession session() { diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java index 97116a3983a..3889f0c7426 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/InventoryTranslator.java @@ -253,7 +253,11 @@ protected ItemStackResponse translateSpecialRequest(GeyserSession session, Type return rejectRequest(request); } - public final void translateRequests(GeyserSession session, Type inventory, List requests, boolean firstTime) { + public final void translateRequests(GeyserSession session, Type inventory, List requests) { + this.translateRequests(session, inventory, requests, true); + } + + private void translateRequests(GeyserSession session, Type inventory, List requests, boolean firstTime) { boolean refresh = false; // If we get another request, we're not closing the inventory and should run the queued request asap @@ -274,9 +278,10 @@ public final void translateRequests(GeyserSession session, Type inventory, List< }); if (mustDelay) { - GeyserImpl.getInstance().getLogger().warning("Delaying action until we know if this is actually an inventory close! " + requests); + // We cannot clearly differentiate shift-clicking a single stack... + // So we try to guess and delay the processing (at least 10 ticks) until we either get a new inventory transaction, + // or until we know the inventory wasn't closed session.getInventoryTransactionFuture().schedule(() -> { - GeyserImpl.getInstance().getLogger().warning("DELAY FINISHED!"); if (session.getPendingOrCurrentBedrockInventoryId() == inventory.getBedrockId()) { translateRequests(session, inventory, requests, false); } else { @@ -292,7 +297,7 @@ public final void translateRequests(GeyserSession session, Type inventory, List< } } }, - 800, + 750, TimeUnit.MILLISECONDS ); return; @@ -1032,7 +1037,6 @@ protected static ItemStackResponse rejectRequest(ItemStackRequest request, boole if (throwError && GeyserImpl.getInstance().config().debugMode()) { new Throwable("DEBUGGING: ItemStackRequest rejected " + request.toString()).printStackTrace(); } - GeyserImpl.getInstance().getLogger().info("REJEEEEEEEEEEECTING"); return new ItemStackResponse(ItemStackResponseStatus.ERROR, request.getRequestId(), Collections.emptyList()); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java index e2591f4c4ab..84b9f8f2934 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java @@ -45,7 +45,7 @@ public class BedrockContainerCloseTranslator extends PacketTranslator future = null; - private Runnable runnable = null; + private @Nullable ScheduledFuture future = null; + private @Nullable Runnable runnable = null; public InterruptibleFuture(GeyserSession session) { this.session = session; } public void runCurrentIfPresent() { - if (future != null && future.cancel(false)) { + if (future != null && future.cancel(false) && runnable != null) { runnable.run(); }