diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java index 53b02ef8807..2955e69fcda 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.inventory.click; +import org.geysermc.geyser.item.Items; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerActionType; import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType; @@ -58,11 +59,20 @@ public final class ClickPlan { private final InventoryTranslator translator; private final Inventory inventory; private final int gridSize; + /** + * The recipe for cloning books requires special handling, this dictates whether that handling should be performed + */ + private final boolean handleBookCloneRecipe; public ClickPlan(GeyserSession session, InventoryTranslator translator, Inventory inventory) { + this(session, translator, inventory, false); + } + + public ClickPlan(GeyserSession session, InventoryTranslator translator, Inventory inventory, boolean handleBookCloneRecipe) { this.session = session; this.translator = translator; this.inventory = inventory; + this.handleBookCloneRecipe = handleBookCloneRecipe; this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize()); this.changedItems = null; @@ -376,7 +386,7 @@ private void reduceCraftingGrid(boolean makeAll) { for (int i = 0; i < gridSize; i++) { final int slot = i + 1; GeyserItemStack item = getItem(slot); - if (!item.isEmpty()) { + if (!item.isEmpty() && (!handleBookCloneRecipe || item.asItem() == Items.WRITTEN_BOOK)) { // These changes should be broadcasted to the server sub(slot, item, crafted); } 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 e228fc02fdc..60dafce8ccf 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -370,6 +370,13 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private Int2ObjectMap stonecutterRecipes; + /** + * Saves the ID for cloning books through the crafting table, as these need different handling + */ + @Setter + private int bookCloningID; + + /** * Whether to work around 1.13's different behavior in villager trading menus. */ 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 4c426b410df..d0c9e3d6b84 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 @@ -453,35 +453,46 @@ public ItemStackResponse translateCraftingRequest(GeyserSession session, Invento ClickPlan plan = new ClickPlan(session, this, inventory); // Track all the crafting table slots to report back the contents of the slots after crafting IntSet affectedSlots = new IntOpenHashSet(); + boolean reject = false; for (ItemStackRequestAction action : request.getActions()) { switch (action.getType()) { case CRAFT_RECIPE: { if (craftState != CraftState.START) { - return rejectRequest(request); + reject = true; + break; } craftState = CraftState.RECIPE_ID; + + if (((RecipeItemStackRequestAction) action).getRecipeNetworkId() == session.getBookCloningID()) { + // Book copying needs to be handled differently + // The original written book is leftover in the crafting grid + return translateBookCopyCraftingRequest(session, inventory, request); + } break; } case CRAFT_RESULTS_DEPRECATED: { CraftResultsDeprecatedAction deprecatedCraftAction = (CraftResultsDeprecatedAction) action; if (craftState != CraftState.RECIPE_ID) { - return rejectRequest(request); + reject = true; + break; } craftState = CraftState.DEPRECATED; if (deprecatedCraftAction.getResultItems().length != 1) { - return rejectRequest(request); + reject = true; + break; } resultSize = deprecatedCraftAction.getResultItems()[0].getCount(); timesCrafted = deprecatedCraftAction.getTimesCrafted(); if (resultSize <= 0 || timesCrafted <= 0) { - return rejectRequest(request); + reject = true; } break; } case CONSUME: { if (craftState != CraftState.DEPRECATED && craftState != CraftState.INGREDIENTS) { - return rejectRequest(request); + reject = true; + break; } craftState = CraftState.INGREDIENTS; affectedSlots.add(bedrockSlotToJava(((ConsumeAction) action).getSource())); @@ -491,15 +502,18 @@ public ItemStackResponse translateCraftingRequest(GeyserSession session, Invento case PLACE: { TransferItemStackRequestAction transferAction = (TransferItemStackRequestAction) action; if (craftState != CraftState.INGREDIENTS && craftState != CraftState.TRANSFER) { - return rejectRequest(request); + reject = true; + break; } craftState = CraftState.TRANSFER; if (transferAction.getSource().getContainer() != ContainerSlotType.CREATED_OUTPUT) { - return rejectRequest(request); + reject = true; + break; } if (transferAction.getCount() <= 0) { - return rejectRequest(request); + reject = true; + break; } int sourceSlot = bedrockSlotToJava(transferAction.getSource()); @@ -511,7 +525,8 @@ public ItemStackResponse translateCraftingRequest(GeyserSession session, Invento } else { if (leftover != 0) { if (transferAction.getCount() > leftover) { - return rejectRequest(request); + reject = true; + break; } if (transferAction.getCount() == leftover) { plan.add(Click.LEFT, destSlot); @@ -537,7 +552,8 @@ public ItemStackResponse translateCraftingRequest(GeyserSession session, Invento GeyserItemStack cursor = session.getPlayerInventory().getCursor(); int tempSlot = findTempSlot(inventory, cursor, true, sourceSlot, destSlot); if (tempSlot == -1) { - return rejectRequest(request); + reject = true; + break; } plan.add(Click.LEFT, tempSlot); //place cursor into temp slot @@ -559,14 +575,101 @@ public ItemStackResponse translateCraftingRequest(GeyserSession session, Invento break; } default: - return rejectRequest(request); + reject = true; } + + } + if (reject) { + return rejectRequest(request); } plan.execute(false); affectedSlots.addAll(plan.getAffectedSlots()); return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots)); } + /** + * Book copying is unique in that there is an item remaining in the crafting table when done. + */ + public ItemStackResponse translateBookCopyCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + CraftState craftState = CraftState.START; + boolean newBookHandled = false; + + ClickPlan plan = new ClickPlan(session, this, inventory, true); + for (ItemStackRequestAction action : request.getActions()) { + switch (action.getType()) { + case CRAFT_RECIPE -> { + if (craftState != CraftState.START) { + return rejectRequest(request); + } + craftState = CraftState.RECIPE_ID; + } + case CRAFT_RESULTS_DEPRECATED -> { + CraftResultsDeprecatedAction deprecatedCraftAction = (CraftResultsDeprecatedAction) action; + if (craftState != CraftState.RECIPE_ID) { + return rejectRequest(request); + } + craftState = CraftState.DEPRECATED; + + if (deprecatedCraftAction.getResultItems().length != 2) { + // Crafted item and old book + return rejectRequest(request); + } + int resultSize = deprecatedCraftAction.getResultItems()[0].getCount(); + int timesCrafted = deprecatedCraftAction.getTimesCrafted(); + if (resultSize != 1 || timesCrafted != 1) { + return rejectRequest(request); + } + } + case CONSUME -> { + // Ignore I guess + } + case CREATE -> { + // After the proper book is created this is called + } + case TAKE, PLACE -> { + TransferItemStackRequestAction transferAction = (TransferItemStackRequestAction) action; + if (craftState != CraftState.DEPRECATED) { + return rejectRequest(request); + } + + if (newBookHandled) { + // Don't let this execute for the old book and keep it in its old slot + // Bedrock wants to move it to the inventory; don't let it + continue; + } + + if (transferAction.getSource().getContainer() != ContainerSlotType.CREATED_OUTPUT) { + return rejectRequest(request); + } + if (transferAction.getCount() != 1) { + return rejectRequest(request); + } + + int sourceSlot = bedrockSlotToJava(transferAction.getSource()); + int destSlot = bedrockSlotToJava(transferAction.getDestination()); + + // Books are pretty simple in this regard - we'll yeet the written book when we execute + // the click plan, but otherwise a book isn't stackable so there aren't many options for it + if (isCursor(transferAction.getDestination())) { + plan.add(Click.LEFT, sourceSlot); + } else { + plan.add(Click.LEFT, sourceSlot); + plan.add(Click.LEFT, destSlot); + } + + newBookHandled = true; + } + default -> { + return rejectRequest(request); + } + } + } + + plan.execute(false); + return acceptRequest(request, makeContainerEntries(session, inventory, plan.getAffectedSlots())); + } + + public ItemStackResponse translateAutoCraftingRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { final int gridSize = getGridSize(); if (gridSize == -1) { diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java index f9b840dd9a2..4a3b77f0c2e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateRecipesTranslator.java @@ -180,7 +180,9 @@ public void translate(GeyserSession session, ClientboundUpdateRecipesPacket pack craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("685a742a-c42e-4a4e-88ea-5eb83fc98e5b"), context.getAndIncrementNetId())); } case CRAFTING_SPECIAL_BOOKCLONING -> { - craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("d1ca6b84-338e-4f2f-9c6b-76cc8b4bd98d"), context.getAndIncrementNetId())); + int bookCloningID = context.getAndIncrementNetId(); + session.setBookCloningID(bookCloningID); + craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("d1ca6b84-338e-4f2f-9c6b-76cc8b4bd98d"), bookCloningID)); } case CRAFTING_SPECIAL_REPAIRITEM -> { craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("00000000-0000-0000-0000-000000000001"), context.getAndIncrementNetId()));