From 7fcfa7d54d4fc3b0e6d4416047b0394eb48e962a Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+DoctorMacc@users.noreply.github.com> Date: Thu, 20 Aug 2020 20:53:47 -0400 Subject: [PATCH] Implement an enchantment table GUI (#1177) Until 1.16, enchantment tables were impossible to implement properly in Geyser. When a user selects an enchantment in Bedrock, the client creates the book on its end and assumes the server is OK with it. Java requires a button to be pressed to select the enchantment. With 1.16, server authoritative inventories remove that on Bedrock. However, until our inventory rewrite is finished we are still stuck without enchantment table support. This commit serves as an alternative as we wait. Enchantment table GUI support is still impossible since we are using the pre-1.16 inventory system. To solve this, this commit replaces the enchantment table GUI with a hopper GUI. The first slot serves as the spot you place the weapon. The second slot acts as the lapis slot - Geyser prevents any item from going in there that is not lapis. The final three slots act as the buttons; an enchanted book acts as each button, with the ability to show the translated text of each enchantment. https://cdn.discordapp.com/attachments/613194828359925800/746164042359504927/unknown.png --- README.md | 2 +- .../network/session/GeyserSession.java | 10 +- .../EnchantmentInventoryTranslator.java | 231 +++++++++++++++++- .../inventory/InventoryTranslator.java | 2 +- 4 files changed, 238 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 64ff2252..92462e7e 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Take a look [here](https://github.com/GeyserMC/Geyser/wiki#Setup) for how to set ## What's Left to be Added/Fixed - The Following Inventories - - [ ] Enchantment Table + - [ ] Enchantment Table (as a proper GUI) - [ ] Beacon - [ ] Cartography Table - [ ] Stonecutter diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index cea17fdf..a385c21c 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -70,6 +70,7 @@ import org.geysermc.connector.network.session.cache.*; import org.geysermc.connector.network.translators.BiomeTranslator; import org.geysermc.connector.network.translators.EntityIdentifierRegistry; import org.geysermc.connector.network.translators.PacketTranslatorRegistry; +import org.geysermc.connector.network.translators.inventory.EnchantmentInventoryTranslator; import org.geysermc.connector.network.translators.item.ItemRegistry; import org.geysermc.connector.network.translators.world.block.BlockTranslator; import org.geysermc.connector.utils.*; @@ -177,6 +178,11 @@ public class GeyserSession implements CommandSender { @Setter private long lastInteractedVillagerEid; + /** + * Stores the enchantment information the client has received if they are in an enchantment table GUI + */ + private final EnchantmentInventoryTranslator.EnchantmentSlotData[] enchantmentSlotData = new EnchantmentInventoryTranslator.EnchantmentSlotData[3]; + /** * The current attack speed of the player. Used for sending proper cooldown timings. */ @@ -189,8 +195,6 @@ public class GeyserSession implements CommandSender { @Setter private long lastHitTime; - private MinecraftProtocol protocol; - private boolean reducedDebugInfo = false; @Setter @@ -238,6 +242,8 @@ public class GeyserSession implements CommandSender { @Setter private boolean thunder = false; + private MinecraftProtocol protocol; + public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) { this.connector = connector; this.upstream = new UpstreamSession(bedrockServerSession); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java index c8e9ed18..cbcdce10 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/EnchantmentInventoryTranslator.java @@ -25,18 +25,243 @@ package org.geysermc.connector.network.translators.inventory; +import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientClickWindowButtonPacket; +import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtMapBuilder; +import com.nukkitx.nbt.NbtType; import com.nukkitx.protocol.bedrock.data.inventory.ContainerType; +import com.nukkitx.protocol.bedrock.data.inventory.InventoryActionData; +import com.nukkitx.protocol.bedrock.data.inventory.ItemData; +import com.nukkitx.protocol.bedrock.packet.InventoryContentPacket; +import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.geysermc.connector.common.ChatColor; import org.geysermc.connector.inventory.Inventory; import org.geysermc.connector.network.session.GeyserSession; -import org.geysermc.connector.network.translators.inventory.updater.ContainerInventoryUpdater; +import org.geysermc.connector.network.translators.inventory.updater.InventoryUpdater; +import org.geysermc.connector.network.translators.item.ItemTranslator; +import org.geysermc.connector.utils.InventoryUtils; +import org.geysermc.connector.utils.LocaleUtils; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * A temporary reconstruction of the enchantment table UI until our inventory rewrite is complete. + * The enchantment table on Bedrock without server authoritative inventories doesn't tell us which button is pressed + * when selecting an enchantment. + */ public class EnchantmentInventoryTranslator extends BlockInventoryTranslator { - public EnchantmentInventoryTranslator() { - super(2, "minecraft:enchanting_table", ContainerType.ENCHANTMENT, new ContainerInventoryUpdater()); + + private static final int DYE_ID = 351; + private static final short LAPIS_DAMAGE = 4; + private static final int ENCHANTED_BOOK_ID = 403; + + public EnchantmentInventoryTranslator(InventoryUpdater updater) { + super(2, "minecraft:hopper[enabled=false,facing=down]", ContainerType.HOPPER, updater); + } + + @Override + public void translateActions(GeyserSession session, Inventory inventory, List actions) { + for (InventoryActionData action : actions) { + if (action.getSource().getContainerId() == inventory.getId()) { + // This is the hopper UI + switch (action.getSlot()) { + case 1: + // Don't allow the slot to be put through if the item isn't lapis + if ((action.getToItem().getId() != DYE_ID + && action.getToItem().getDamage() != LAPIS_DAMAGE) && action.getToItem() != ItemData.AIR) { + updateInventory(session, inventory); + InventoryUtils.updateCursor(session); + return; + } + break; + case 2: + case 3: + case 4: + // The books here act as buttons + ClientClickWindowButtonPacket packet = new ClientClickWindowButtonPacket(inventory.getId(), action.getSlot() - 2); + session.sendDownstreamPacket(packet); + updateInventory(session, inventory); + InventoryUtils.updateCursor(session); + return; + default: + break; + } + } + } + + super.translateActions(session, inventory, actions); + } + + @Override + public void updateInventory(GeyserSession session, Inventory inventory) { + super.updateInventory(session, inventory); + ItemData[] items = new ItemData[5]; + items[0] = ItemTranslator.translateToBedrock(session, inventory.getItem(0)); + items[1] = ItemTranslator.translateToBedrock(session, inventory.getItem(1)); + for (int i = 0; i < 3; i++) { + items[i + 2] = session.getEnchantmentSlotData()[i].getItem() != null ? session.getEnchantmentSlotData()[i].getItem() : createEnchantmentBook(); + } + + InventoryContentPacket contentPacket = new InventoryContentPacket(); + contentPacket.setContainerId(inventory.getId()); + contentPacket.setContents(items); + session.sendUpstreamPacket(contentPacket); } @Override public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) { + int bookSlotToUpdate; + switch (key) { + case 0: + case 1: + case 2: + // Experience required + bookSlotToUpdate = key; + session.getEnchantmentSlotData()[bookSlotToUpdate].setExperienceRequired(value); + break; + case 4: + case 5: + case 6: + // Enchantment name + bookSlotToUpdate = key - 4; + if (value != -1) { + session.getEnchantmentSlotData()[bookSlotToUpdate].setEnchantmentType(EnchantmentTableEnchantments.values()[value - 1]); + } else { + // -1 means no enchantment specified + session.getEnchantmentSlotData()[bookSlotToUpdate].setEnchantmentType(null); + } + break; + case 7: + case 8: + case 9: + // Enchantment level + bookSlotToUpdate = key - 7; + session.getEnchantmentSlotData()[bookSlotToUpdate].setEnchantmentLevel(value); + break; + default: + return; + } + updateEnchantmentBook(session, inventory, bookSlotToUpdate); + } + @Override + public void openInventory(GeyserSession session, Inventory inventory) { + super.openInventory(session, inventory); + for (int i = 0; i < session.getEnchantmentSlotData().length; i++) { + session.getEnchantmentSlotData()[i] = new EnchantmentSlotData(); + } + } + + @Override + public void closeInventory(GeyserSession session, Inventory inventory) { + super.closeInventory(session, inventory); + Arrays.fill(session.getEnchantmentSlotData(), null); + } + + private ItemData createEnchantmentBook() { + NbtMapBuilder root = NbtMap.builder(); + NbtMapBuilder display = NbtMap.builder(); + + display.putString("Name", ChatColor.RESET + "No Enchantment"); + + root.put("display", display.build()); + return ItemData.of(ENCHANTED_BOOK_ID, (short) 0, 1, root.build()); + } + + private void updateEnchantmentBook(GeyserSession session, Inventory inventory, int slot) { + NbtMapBuilder root = NbtMap.builder(); + NbtMapBuilder display = NbtMap.builder(); + EnchantmentSlotData data = session.getEnchantmentSlotData()[slot]; + if (data.getEnchantmentType() != null) { + display.putString("Name", ChatColor.ITALIC + data.getEnchantmentType().toString(session) + + (data.getEnchantmentLevel() != -1 ? " " + toRomanNumeral(session, data.getEnchantmentLevel()) : "") + "?"); + } else { + display.putString("Name", ChatColor.RESET + "No Enchantment"); + } + + display.putList("Lore", NbtType.STRING, Collections.singletonList(ChatColor.DARK_GRAY + data.getExperienceRequired() + "xp")); + root.put("display", display.build()); + ItemData book = ItemData.of(ENCHANTED_BOOK_ID, (short) 0, 1, root.build()); + + InventorySlotPacket slotPacket = new InventorySlotPacket(); + slotPacket.setContainerId(inventory.getId()); + slotPacket.setSlot(slot + 2); + slotPacket.setItem(book); + session.sendUpstreamPacket(slotPacket); + data.setItem(book); + } + + private String toRomanNumeral(GeyserSession session, int level) { + return LocaleUtils.getLocaleString("enchantment.level." + level, + session.getClientData().getLanguageCode()); + } + + /** + * Stores the data of each slot in an enchantment table + */ + @NoArgsConstructor + @Getter + @Setter + @ToString + public static class EnchantmentSlotData { + private EnchantmentTableEnchantments enchantmentType = null; + private int enchantmentLevel = 0; + private int experienceRequired = 0; + private ItemData item; + } + + /** + * Classifies enchantments by Java order + */ + public enum EnchantmentTableEnchantments { + PROTECTION, + FIRE_PROTECTION, + FEATHER_FALLING, + BLAST_PROTECTION, + PROJECTILE_PROTECTION, + RESPIRATION, + AQUA_AFFINITY, + THORNS, + DEPTH_STRIDER, + FROST_WALKER, + BINDING_CURSE, + SHARPNESS, + SMITE, + BANE_OF_ARTHROPODS, + KNOCKBACK, + FIRE_ASPECT, + LOOTING, + SWEEPING, + EFFICIENCY, + SILK_TOUCH, + UNBREAKING, + FORTUNE, + POWER, + PUNCH, + FLAME, + INFINITY, + LUCK_OF_THE_SEA, + LURE, + LOYALTY, + IMPALING, + RIPTIDE, + CHANNELING, + MENDING, + VANISHING_CURSE, // After this is not documented + MULTISHOT, + PIERCING, + QUICK_CHARGE, + SOUL_SPEED; + + public String toString(GeyserSession session) { + return LocaleUtils.getLocaleString("enchantment.minecraft." + this.toString().toLowerCase(), + session.getClientData().getLanguageCode()); + } } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java index 7d06aed1..e44e4bd0 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/InventoryTranslator.java @@ -56,7 +56,6 @@ public abstract class InventoryTranslator { put(WindowType.GRINDSTONE, new GrindstoneInventoryTranslator()); put(WindowType.MERCHANT, new MerchantInventoryTranslator()); put(WindowType.SMITHING, new SmithingInventoryTranslator()); - //put(WindowType.ENCHANTMENT, new EnchantmentInventoryTranslator()); //TODO InventoryTranslator furnace = new FurnaceInventoryTranslator(); put(WindowType.FURNACE, furnace); @@ -64,6 +63,7 @@ public abstract class InventoryTranslator { put(WindowType.SMOKER, furnace); InventoryUpdater containerUpdater = new ContainerInventoryUpdater(); + put(WindowType.ENCHANTMENT, new EnchantmentInventoryTranslator(containerUpdater)); //TODO put(WindowType.GENERIC_3X3, new BlockInventoryTranslator(9, "minecraft:dispenser[facing=north,triggered=false]", ContainerType.DISPENSER, containerUpdater)); put(WindowType.HOPPER, new BlockInventoryTranslator(5, "minecraft:hopper[enabled=false,facing=down]", ContainerType.HOPPER, containerUpdater)); put(WindowType.SHULKER_BOX, new BlockInventoryTranslator(27, "minecraft:shulker_box[facing=north]", ContainerType.CONTAINER, containerUpdater));