From 52679f9f8139a512eebdcf0ef177e29072833f7e Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Tue, 29 Oct 2024 01:33:57 -0400 Subject: [PATCH] On-the-fly smithing transform recipes --- .../geyser/inventory/GeyserItemStack.java | 7 ++ .../recipe/GeyserSmithingRecipe.java | 43 +++++++++ .../geyser/session/GeyserSession.java | 8 +- .../SmithingInventoryTranslator.java | 29 +++--- .../java/JavaRecipeBookAddTranslator.java | 3 + .../JavaContainerSetSlotTranslator.java | 89 +++++++++++++++++-- 6 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserSmithingRecipe.java diff --git a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java index b4809edf264..a2797e6dd41 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/GeyserItemStack.java @@ -182,6 +182,13 @@ public ItemMapping getMapping(GeyserSession session) { return session.getItemMappings().getMapping(this.javaId); } + public SlotDisplay asSlotDisplay() { + if (isEmpty()) { + return new EmptySlotDisplay(); + } + return new ItemStackSlotDisplay(this.getItemStack()); + } + public Item asItem() { if (isEmpty()) { return Items.AIR; diff --git a/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserSmithingRecipe.java b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserSmithingRecipe.java new file mode 100644 index 00000000000..7e4131a4cee --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/inventory/recipe/GeyserSmithingRecipe.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.inventory.recipe; + +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.SmithingRecipeDisplay; +import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay; + +public record GeyserSmithingRecipe(SlotDisplay template, + SlotDisplay base, + SlotDisplay addition, + SlotDisplay result) implements GeyserRecipe { + public GeyserSmithingRecipe(SmithingRecipeDisplay display) { + this(display.template(), display.base(), display.addition(), display.result()); + } + + @Override + public boolean isShaped() { + return false; + } +} 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 7e55261f456..ef8ad17ef80 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -29,6 +29,7 @@ import com.google.gson.JsonObject; import io.netty.channel.Channel; import io.netty.channel.EventLoop; +import it.unimi.dsi.fastutil.Pair; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; @@ -77,6 +78,7 @@ import org.cloudburstmc.protocol.bedrock.data.definitions.DimensionDefinition; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; +import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.CraftingRecipeData; import org.cloudburstmc.protocol.bedrock.packet.AvailableEntityIdentifiersPacket; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.cloudburstmc.protocol.bedrock.packet.BiomeDefinitionListPacket; @@ -141,6 +143,7 @@ import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.PlayerInventory; import org.geysermc.geyser.inventory.recipe.GeyserRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserSmithingRecipe; import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.BlockItem; @@ -311,7 +314,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private final AtomicInteger itemNetId = new AtomicInteger(2); @Setter - private ScheduledFuture craftingGridFuture; + private ScheduledFuture containerOutputFuture; /** * Stores session collision @@ -447,6 +450,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private final Int2ObjectMap> javaToBedrockRecipeIds; private final Int2ObjectMap craftingRecipes; + @Setter + private Pair lastCreatedRecipe = null; // TODO try to prevent sending duplicate recipes private final AtomicInteger lastRecipeNetId; /** @@ -455,6 +460,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { */ @Setter private Int2ObjectMap stonecutterRecipes; + private final List smithingRecipes = new ArrayList<>(); /** * 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/SmithingInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/SmithingInventoryTranslator.java index c68347fd3ea..dbe24230a52 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/SmithingInventoryTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/SmithingInventoryTranslator.java @@ -33,6 +33,11 @@ import org.geysermc.geyser.level.block.Blocks; public class SmithingInventoryTranslator extends AbstractBlockInventoryTranslator { + public static final int TEMPLATE = 0; + public static final int INPUT = 1; + public static final int MATERIAL = 2; + public static final int OUTPUT = 3; + public SmithingInventoryTranslator() { super(4, Blocks.SMITHING_TABLE, ContainerType.SMITHING_TABLE, UIInventoryUpdater.INSTANCE); } @@ -40,10 +45,10 @@ public SmithingInventoryTranslator() { @Override public int bedrockSlotToJava(ItemStackRequestSlotData slotInfoData) { return switch (slotInfoData.getContainer()) { - case SMITHING_TABLE_TEMPLATE -> 0; - case SMITHING_TABLE_INPUT -> 1; - case SMITHING_TABLE_MATERIAL -> 2; - case SMITHING_TABLE_RESULT, CREATED_OUTPUT -> 3; + case SMITHING_TABLE_TEMPLATE -> TEMPLATE; + case SMITHING_TABLE_INPUT -> INPUT; + case SMITHING_TABLE_MATERIAL -> MATERIAL; + case SMITHING_TABLE_RESULT, CREATED_OUTPUT -> OUTPUT; default -> super.bedrockSlotToJava(slotInfoData); }; } @@ -51,10 +56,10 @@ public int bedrockSlotToJava(ItemStackRequestSlotData slotInfoData) { @Override public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { return switch (slot) { - case 0 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_TEMPLATE, 53); - case 1 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_INPUT, 51); - case 2 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_MATERIAL, 52); - case 3 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_RESULT, 50); + case TEMPLATE -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_TEMPLATE, 53); + case INPUT -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_INPUT, 51); + case MATERIAL -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_MATERIAL, 52); + case OUTPUT -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_RESULT, 50); default -> super.javaSlotToBedrockContainer(slot); }; } @@ -62,10 +67,10 @@ public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { @Override public int javaSlotToBedrock(int slot) { return switch (slot) { - case 0 -> 53; - case 1 -> 51; - case 2 -> 52; - case 3 -> 50; + case TEMPLATE -> 53; + case INPUT -> 51; + case MATERIAL -> 52; + case OUTPUT -> 50; default -> super.javaSlotToBedrock(slot); }; } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java index da8fd01d3b5..f23ebebcb79 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java @@ -42,6 +42,7 @@ import org.geysermc.geyser.inventory.recipe.GeyserRecipe; import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserSmithingRecipe; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.BedrockRequiresTagItem; import org.geysermc.geyser.item.type.Item; @@ -176,6 +177,8 @@ public void translate(GeyserSession session, ClientboundRecipeBookAddPacket pack } } javaToBedrockRecipeIds.put(contents.id(), bedrockRecipeIds); + session.getSmithingRecipes().add(new GeyserSmithingRecipe(smithingRecipe)); + System.out.println(new GeyserSmithingRecipe(smithingRecipe)); } } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java index c98ab1ff6db..8f5fab1f9d7 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaContainerSetSlotTranslator.java @@ -29,6 +29,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.RecipeUnlockingRequirement; import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData; +import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData; import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount; import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket; import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket; @@ -36,15 +37,17 @@ import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; +import org.geysermc.geyser.inventory.recipe.GeyserSmithingRecipe; +import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.PlayerInventoryTranslator; +import org.geysermc.geyser.translator.inventory.SmithingInventoryTranslator; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.util.InventoryUtils; import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; -import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.EmptySlotDisplay; import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.ItemStackSlotDisplay; import org.geysermc.mcprotocollib.protocol.data.game.recipe.display.slot.SlotDisplay; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.inventory.ClientboundContainerSetSlotPacket; @@ -83,7 +86,11 @@ public void translate(GeyserSession session, ClientboundContainerSetSlotPacket p return; } - updateCraftingGrid(session, slot, packet.getItem(), inventory, translator); + if (translator instanceof SmithingInventoryTranslator) { + updateSmithingTableOutput(session, slot, packet.getItem(), inventory); + } else { + updateCraftingGrid(session, slot, packet.getItem(), inventory, translator); + } GeyserItemStack newItem = GeyserItemStack.from(packet.getItem()); if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) { @@ -119,15 +126,15 @@ private static void updateCraftingGrid(GeyserSession session, int slot, ItemStac } // Only process the most recent crafting grid result, and cancel the previous one. - if (session.getCraftingGridFuture() != null) { - session.getCraftingGridFuture().cancel(false); + if (session.getContainerOutputFuture() != null) { + session.getContainerOutputFuture().cancel(false); } if (InventoryUtils.isEmpty(item)) { return; } - session.setCraftingGridFuture(session.scheduleInEventLoop(() -> { + session.setContainerOutputFuture(session.scheduleInEventLoop(() -> { int offset = gridSize == 4 ? 28 : 32; int gridDimensions = gridSize == 4 ? 2 : 3; int firstRow = -1, height = -1; @@ -172,7 +179,7 @@ private static void updateCraftingGrid(GeyserSession session, int slot, ItemStac for (int col = firstCol; col < width + firstCol; col++) { GeyserItemStack geyserItemStack = inventory.getItem(col + (row * gridDimensions) + 1); ingredients[index] = geyserItemStack.getItemData(session); - javaIngredients.add(geyserItemStack.isEmpty() ? new EmptySlotDisplay() : new ItemStackSlotDisplay(geyserItemStack.getItemStack())); + javaIngredients.add(geyserItemStack.asSlotDisplay()); InventorySlotPacket slotPacket = new InventorySlotPacket(); slotPacket.setContainerId(ContainerId.UI); @@ -216,4 +223,74 @@ private static void updateCraftingGrid(GeyserSession session, int slot, ItemStac } }, 150, TimeUnit.MILLISECONDS)); } + + private static void updateSmithingTableOutput(GeyserSession session, int slot, ItemStack output, Inventory inventory) { + if (slot != SmithingInventoryTranslator.OUTPUT) { + return; + } + + // Only process the most recent output result, and cancel the previous one. + if (session.getContainerOutputFuture() != null) { + session.getContainerOutputFuture().cancel(false); + } + + if (InventoryUtils.isEmpty(output)) { + return; + } + + session.setContainerOutputFuture(session.scheduleInEventLoop(() -> { + GeyserItemStack template = inventory.getItem(SmithingInventoryTranslator.TEMPLATE); + if (template.asItem() != Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE) { + // Technically we should probably also do this for custom items, but last I checked Bedrock doesn't even support that. + return; + } + + GeyserItemStack input = inventory.getItem(SmithingInventoryTranslator.INPUT); + GeyserItemStack material = inventory.getItem(SmithingInventoryTranslator.MATERIAL); + GeyserItemStack geyserOutput = GeyserItemStack.from(output); + + for (GeyserSmithingRecipe recipe : session.getSmithingRecipes()) { + if (InventoryUtils.acceptsAsInput(session, recipe.result(), geyserOutput) + && InventoryUtils.acceptsAsInput(session, recipe.base(), input) + && InventoryUtils.acceptsAsInput(session, recipe.addition(), material) + && InventoryUtils.acceptsAsInput(session, recipe.template(), template)) { + // The client already recognizes this item. + return; + } + } + + session.getSmithingRecipes().add(new GeyserSmithingRecipe( + template.asSlotDisplay(), + input.asSlotDisplay(), + material.asSlotDisplay(), + new ItemStackSlotDisplay(output) + )); + + UUID uuid = UUID.randomUUID(); + + ItemData bedrockAddition = ItemTranslator.translateToBedrock(session, material.getItemStack()); + + CraftingDataPacket craftPacket = new CraftingDataPacket(); + craftPacket.getCraftingData().add(SmithingTransformRecipeData.of( + uuid.toString(), + ItemDescriptorWithCount.fromItem(ItemTranslator.translateToBedrock(session, template.getItemStack())), + ItemDescriptorWithCount.fromItem(ItemTranslator.translateToBedrock(session, input.getItemStack())), + ItemDescriptorWithCount.fromItem(bedrockAddition), + ItemTranslator.translateToBedrock(session, output), + "smithing_table", + session.getLastRecipeNetId().incrementAndGet() + )); + craftPacket.setCleanRecipes(false); + session.sendUpstreamPacket(craftPacket); + + // Just set one of the slots to air, then right back to its proper item. + InventorySlotPacket slotPacket = new InventorySlotPacket(); + slotPacket.setContainerId(ContainerId.UI); + slotPacket.setSlot(session.getInventoryTranslator().javaSlotToBedrock(SmithingInventoryTranslator.MATERIAL)); + slotPacket.setItem(ItemData.AIR); + session.sendUpstreamPacket(slotPacket); + + session.getInventoryTranslator().updateSlot(session, inventory, SmithingInventoryTranslator.MATERIAL); + }, 150, TimeUnit.MILLISECONDS)); + } }