diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java index 42b9ae1a0..16fd7cde6 100644 --- a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java @@ -45,11 +45,12 @@ public class StoredItemMappings { private final ItemMapping barrier; private final ItemMapping compass; private final ItemMapping crossbow; + private final ItemMapping egg; private final ItemMapping glassBottle; private final ItemMapping milkBucket; private final ItemMapping powderSnowBucket; - private final ItemMapping egg; private final ItemMapping shield; + private final ItemMapping upgradeTemplate; private final ItemMapping wheat; private final ItemMapping writableBook; @@ -59,11 +60,12 @@ public class StoredItemMappings { this.barrier = load(itemMappings, Items.BARRIER); this.compass = load(itemMappings, Items.COMPASS); this.crossbow = load(itemMappings, Items.CROSSBOW); + this.egg = load(itemMappings, Items.EGG); this.glassBottle = load(itemMappings, Items.GLASS_BOTTLE); this.milkBucket = load(itemMappings, Items.MILK_BUCKET); this.powderSnowBucket = load(itemMappings, Items.POWDER_SNOW_BUCKET); - this.egg = load(itemMappings, Items.EGG); this.shield = load(itemMappings, Items.SHIELD); + this.upgradeTemplate = load(itemMappings, Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE); this.wheat = load(itemMappings, Items.WHEAT); this.writableBook = load(itemMappings, Items.WRITABLE_BOOK); } diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java index 0b45d881a..65cc28420 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMappings.java @@ -29,6 +29,8 @@ import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import lombok.Builder; import lombok.Value; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; @@ -39,7 +41,6 @@ import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.item.type.PotionItem; -import javax.annotation.Nonnull; import java.util.List; import java.util.Map; import java.util.Set; @@ -77,7 +78,7 @@ public class ItemMappings implements DefinitionRegistry { * @param itemStack the itemstack * @return an item entry from the given java edition identifier */ - @Nonnull + @NonNull public ItemMapping getMapping(ItemStack itemStack) { return this.getMapping(itemStack.getId()); } @@ -89,11 +90,12 @@ public class ItemMappings implements DefinitionRegistry { * @param javaId the id * @return an item entry from the given java edition identifier */ - @Nonnull + @NonNull public ItemMapping getMapping(int javaId) { return javaId >= 0 && javaId < this.items.length ? this.items[javaId] : ItemMapping.AIR; } + @Nullable public ItemMapping getMapping(Item javaItem) { return getMapping(javaItem.javaIdentifier()); } @@ -105,6 +107,7 @@ public class ItemMappings implements DefinitionRegistry { * @param javaIdentifier the block state identifier * @return an item entry from the given java edition identifier */ + @Nullable public ItemMapping getMapping(String javaIdentifier) { return this.cachedJavaMappings.computeIfAbsent(javaIdentifier, key -> { for (ItemMapping mapping : this.items) { @@ -122,6 +125,7 @@ public class ItemMappings implements DefinitionRegistry { * @param data the item data * @return an item entry from the given item data */ + @NonNull public ItemMapping getMapping(ItemData data) { ItemDefinition definition = data.getDefinition(); if (ItemDefinition.AIR.equals(definition)) { @@ -158,11 +162,22 @@ public class ItemMappings implements DefinitionRegistry { return ItemMapping.AIR; } + @Nullable @Override public ItemDefinition getDefinition(int bedrockId) { return this.itemDefinitions.get(bedrockId); } + @Nullable + public ItemDefinition getDefinition(String bedrockIdentifier) { + for (ItemDefinition itemDefinition : this.itemDefinitions.values()) { + if (itemDefinition.getIdentifier().equals(bedrockIdentifier)) { + return itemDefinition; + } + } + return null; + } + @Override public boolean isRegistered(ItemDefinition definition) { return getDefinition(definition.getRuntimeId()) == definition; 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 721fb0dfe..e09cff9e4 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -425,6 +425,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private boolean emulatePost1_18Logic = true; + /** + * Whether to emulate pre-1.20 smithing table behavior. + * Adapts ViaVersion's furnace UI to one Bedrock can use. + * See {@link org.geysermc.geyser.translator.inventory.OldSmithingTableTranslator}. + */ + @Setter + private boolean oldSmithingTable = false; + /** * The current attack speed of the player. Used for sending proper cooldown timings. * Setting a default fixes cooldowns not showing up on a fresh world. diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/OldSmithingTableTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/OldSmithingTableTranslator.java new file mode 100644 index 000000000..5f08f1b8a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/OldSmithingTableTranslator.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2019-2023 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.translator.inventory; + +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType; +import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType; +import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; +import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest; +import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData; +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.TakeAction; +import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.response.ItemStackResponse; +import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket; +import org.geysermc.geyser.inventory.BedrockContainerSlot; +import org.geysermc.geyser.inventory.Inventory; +import org.geysermc.geyser.inventory.updater.UIInventoryUpdater; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.util.InventoryUtils; + +import java.util.function.IntFunction; + +/** + * Translator for smithing tables for pre-1.20 servers. + * This adapts ViaVersion's furnace ui to the 1.20+ smithing table; with the addition of a fake smithing template so Bedrock clients can use it. + */ +public class OldSmithingTableTranslator extends AbstractBlockInventoryTranslator { + + public static final OldSmithingTableTranslator INSTANCE = new OldSmithingTableTranslator(); + + private static final IntFunction UPGRADE_TEMPLATE = InventoryUtils.getUpgradeTemplate(); + + private OldSmithingTableTranslator() { + super(3, "minecraft:smithing_table", ContainerType.SMITHING_TABLE, UIInventoryUpdater.INSTANCE); + } + + @Override + public int bedrockSlotToJava(ItemStackRequestSlotData slotInfoData) { + return switch (slotInfoData.getContainer()) { + case SMITHING_TABLE_INPUT -> 0; + case SMITHING_TABLE_MATERIAL -> 1; + case SMITHING_TABLE_RESULT, CREATED_OUTPUT -> 2; + default -> super.bedrockSlotToJava(slotInfoData); + }; + } + + @Override + public BedrockContainerSlot javaSlotToBedrockContainer(int slot) { + return switch (slot) { + case 0 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_INPUT, 51); + case 1 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_MATERIAL, 52); + case 2 -> new BedrockContainerSlot(ContainerSlotType.SMITHING_TABLE_RESULT, 50); + default -> super.javaSlotToBedrockContainer(slot); + }; + } + + @Override + public int javaSlotToBedrock(int slot) { + return switch (slot) { + case 0 -> 51; + case 1 -> 52; + case 2 -> 50; + default -> super.javaSlotToBedrock(slot); + }; + } + + @Override + public boolean shouldHandleRequestFirst(ItemStackRequestAction action, Inventory inventory) { + return true; + } + + @Override + protected ItemStackResponse translateSpecialRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) { + for (var action: request.getActions()) { + switch (action.getType()) { + case DROP -> { + if (isInvalidAction(((DropAction) action).getSource())) { + return rejectRequest(request, false); + } + } + case TAKE -> { + if (isInvalidAction(((TakeAction) action).getSource()) || + isInvalidAction(((TakeAction) action).getDestination())) { + return rejectRequest(request, false); + } + } + case SWAP -> { + if (isInvalidAction(((SwapAction) action).getSource()) || + isInvalidAction(((SwapAction) action).getDestination())) { + return rejectRequest(request, false); + } + } + case PLACE -> { + if (isInvalidAction(((PlaceAction) action).getSource()) || + isInvalidAction(((PlaceAction) action).getDestination())) { + return rejectRequest(request, false); + } + } + } + } + // Allow everything else that doesn't involve the fake template + return super.translateRequest(session, inventory, request); + } + + private boolean isInvalidAction(ItemStackRequestSlotData slotData) { + return slotData.getContainer().equals(ContainerSlotType.SMITHING_TABLE_TEMPLATE); + } + + @Override + public void openInventory(GeyserSession session, Inventory inventory) { + super.openInventory(session, inventory); + + // pre-1.20 server has no concept of templates, but we are working with a 1.20 client + // put a fake netherite upgrade template in the template slot otherwise the client doesn't recognize a valid recipe + InventorySlotPacket slotPacket = new InventorySlotPacket(); + slotPacket.setContainerId(ContainerId.UI); + slotPacket.setSlot(53); + slotPacket.setItem(UPGRADE_TEMPLATE.apply(session.getUpstream().getProtocolVersion())); + session.sendUpstreamPacket(slotPacket); + } +} 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 bef6b9884..1af0ff814 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 @@ -46,6 +46,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.DefaultDescri import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount; import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket; import org.cloudburstmc.protocol.bedrock.packet.TrimDataPacket; +import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.inventory.recipe.GeyserRecipe; import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe; import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe; @@ -81,11 +82,24 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator NETHERITE_UPGRADES = List.of( + "minecraft:netherite_sword", + "minecraft:netherite_shovel", + "minecraft:netherite_pickaxe", + "minecraft:netherite_axe", + "minecraft:netherite_hoe", + "minecraft:netherite_helmet", + "minecraft:netherite_chestplate", + "minecraft:netherite_leggings", + "minecraft:netherite_boots" + ); + @Override public void translate(GeyserSession session, ClientboundUpdateRecipesPacket packet) { Map> recipeTypes = Registries.CRAFTING_DATA.forVersion(session.getUpstream().getProtocolVersion()); // Get the last known network ID (first used for the pregenerated recipes) and increment from there. int netId = InventoryUtils.LAST_RECIPE_NET_ID + 1; + boolean sendTrimRecipes = false; Int2ObjectMap recipeMap = new Int2ObjectOpenHashMap<>(Registries.RECIPES.forVersion(session.getUpstream().getProtocolVersion())); Int2ObjectMap> unsortedStonecutterData = new Int2ObjectOpenHashMap<>(); @@ -168,6 +182,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator { + sendTrimRecipes = true; // ignored currently - see below } default -> { @@ -214,23 +229,28 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator getSmithingTransformRecipes(GeyserSession session) { + List recipes = new ArrayList<>(); + ItemMapping template = session.getItemMappings().getStoredItems().upgradeTemplate(); + + for (String identifier : NETHERITE_UPGRADES) { + recipes.add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData.of(identifier + "_smithing", + getDescriptorFromId(session, template.getBedrockIdentifier()), + getDescriptorFromId(session, identifier.replace("netherite", "diamond")), + getDescriptorFromId(session, "minecraft:netherite_ingot"), + ItemData.builder().definition(Objects.requireNonNull(session.getItemMappings().getDefinition(identifier))).count(1).build(), + "smithing_table", + session.getLastRecipeNetId().getAndIncrement())); + } + return recipes; + } + + private ItemDescriptorWithCount getDescriptorFromId(GeyserSession session, String bedrockId) { + ItemDefinition bedrockDefinition = session.getItemMappings().getDefinition(bedrockId); + if (bedrockDefinition != null) { + return ItemDescriptorWithCount.fromItem(ItemData.builder().definition(bedrockDefinition).count(1).build()); + } + GeyserImpl.getInstance().getLogger().debug("Unable to find item with identifier " + bedrockId); + return ItemDescriptorWithCount.EMPTY; + } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java index 8730d5ac1..fd733773e 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java @@ -25,11 +25,14 @@ package org.geysermc.geyser.translator.protocol.java.inventory; +import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.inventory.ClientboundOpenScreenPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClosePacket; +import net.kyori.adventure.text.Component; import org.geysermc.geyser.inventory.Inventory; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.InventoryTranslator; +import org.geysermc.geyser.translator.inventory.OldSmithingTableTranslator; import org.geysermc.geyser.translator.protocol.PacketTranslator; import org.geysermc.geyser.translator.protocol.Translator; import org.geysermc.geyser.translator.text.MessageTranslator; @@ -38,6 +41,8 @@ import org.geysermc.geyser.util.InventoryUtils; @Translator(packet = ClientboundOpenScreenPacket.class) public class JavaOpenScreenTranslator extends PacketTranslator { + private static final Component SMITHING_TABLE_COMPONENT = Component.translatable("container.upgrade"); + @Override public void translate(GeyserSession session, ClientboundOpenScreenPacket packet) { if (packet.getContainerId() == 0) { @@ -46,6 +51,12 @@ public class JavaOpenScreenTranslator extends PacketTranslator getUpgradeTemplate() { + return protocolVersion -> ItemData.builder() + .definition(Registries.ITEMS.forVersion(protocolVersion).getStoredItems().upgradeTemplate().getBedrockDefinition()) + .count(1).build(); + } + /** * See {@link #findOrCreateItem(GeyserSession, String)}. This is for finding a specified {@link ItemStack}. *