forked from GeyserMC/Geyser
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
This commit is contained in:
parent
d6290ccb66
commit
7fcfa7d54d
4 changed files with 238 additions and 7 deletions
|
@ -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
|
## What's Left to be Added/Fixed
|
||||||
- The Following Inventories
|
- The Following Inventories
|
||||||
- [ ] Enchantment Table
|
- [ ] Enchantment Table (as a proper GUI)
|
||||||
- [ ] Beacon
|
- [ ] Beacon
|
||||||
- [ ] Cartography Table
|
- [ ] Cartography Table
|
||||||
- [ ] Stonecutter
|
- [ ] Stonecutter
|
||||||
|
|
|
@ -70,6 +70,7 @@ import org.geysermc.connector.network.session.cache.*;
|
||||||
import org.geysermc.connector.network.translators.BiomeTranslator;
|
import org.geysermc.connector.network.translators.BiomeTranslator;
|
||||||
import org.geysermc.connector.network.translators.EntityIdentifierRegistry;
|
import org.geysermc.connector.network.translators.EntityIdentifierRegistry;
|
||||||
import org.geysermc.connector.network.translators.PacketTranslatorRegistry;
|
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.item.ItemRegistry;
|
||||||
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
|
import org.geysermc.connector.network.translators.world.block.BlockTranslator;
|
||||||
import org.geysermc.connector.utils.*;
|
import org.geysermc.connector.utils.*;
|
||||||
|
@ -177,6 +178,11 @@ public class GeyserSession implements CommandSender {
|
||||||
@Setter
|
@Setter
|
||||||
private long lastInteractedVillagerEid;
|
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.
|
* The current attack speed of the player. Used for sending proper cooldown timings.
|
||||||
*/
|
*/
|
||||||
|
@ -189,8 +195,6 @@ public class GeyserSession implements CommandSender {
|
||||||
@Setter
|
@Setter
|
||||||
private long lastHitTime;
|
private long lastHitTime;
|
||||||
|
|
||||||
private MinecraftProtocol protocol;
|
|
||||||
|
|
||||||
private boolean reducedDebugInfo = false;
|
private boolean reducedDebugInfo = false;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
|
@ -238,6 +242,8 @@ public class GeyserSession implements CommandSender {
|
||||||
@Setter
|
@Setter
|
||||||
private boolean thunder = false;
|
private boolean thunder = false;
|
||||||
|
|
||||||
|
private MinecraftProtocol protocol;
|
||||||
|
|
||||||
public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) {
|
public GeyserSession(GeyserConnector connector, BedrockServerSession bedrockServerSession) {
|
||||||
this.connector = connector;
|
this.connector = connector;
|
||||||
this.upstream = new UpstreamSession(bedrockServerSession);
|
this.upstream = new UpstreamSession(bedrockServerSession);
|
||||||
|
|
|
@ -25,18 +25,243 @@
|
||||||
|
|
||||||
package org.geysermc.connector.network.translators.inventory;
|
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.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.inventory.Inventory;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
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 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<InventoryActionData> 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
|
@Override
|
||||||
public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) {
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,6 @@ public abstract class InventoryTranslator {
|
||||||
put(WindowType.GRINDSTONE, new GrindstoneInventoryTranslator());
|
put(WindowType.GRINDSTONE, new GrindstoneInventoryTranslator());
|
||||||
put(WindowType.MERCHANT, new MerchantInventoryTranslator());
|
put(WindowType.MERCHANT, new MerchantInventoryTranslator());
|
||||||
put(WindowType.SMITHING, new SmithingInventoryTranslator());
|
put(WindowType.SMITHING, new SmithingInventoryTranslator());
|
||||||
//put(WindowType.ENCHANTMENT, new EnchantmentInventoryTranslator()); //TODO
|
|
||||||
|
|
||||||
InventoryTranslator furnace = new FurnaceInventoryTranslator();
|
InventoryTranslator furnace = new FurnaceInventoryTranslator();
|
||||||
put(WindowType.FURNACE, furnace);
|
put(WindowType.FURNACE, furnace);
|
||||||
|
@ -64,6 +63,7 @@ public abstract class InventoryTranslator {
|
||||||
put(WindowType.SMOKER, furnace);
|
put(WindowType.SMOKER, furnace);
|
||||||
|
|
||||||
InventoryUpdater containerUpdater = new ContainerInventoryUpdater();
|
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.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.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));
|
put(WindowType.SHULKER_BOX, new BlockInventoryTranslator(27, "minecraft:shulker_box[facing=north]", ContainerType.CONTAINER, containerUpdater));
|
||||||
|
|
Loading…
Reference in a new issue