Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Book cloning fix #4784

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,13 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private Int2ObjectMap<GeyserStonecutterData> 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Konicai marked this conversation as resolved.
Show resolved Hide resolved
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()));
Expand All @@ -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());
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down