diff --git a/connector/pom.xml b/connector/pom.xml
index 1b6e9fc4e..131e6e48b 100644
--- a/connector/pom.xml
+++ b/connector/pom.xml
@@ -233,6 +233,12 @@
${adventure.version}
compile
+
+ net.kyori
+ adventure-text-serializer-plain
+ ${adventure.version}
+ compile
+
junit
diff --git a/connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java b/connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java
index cc9ebb2e8..b4f5a6d36 100644
--- a/connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java
+++ b/connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java
@@ -26,12 +26,48 @@
package org.geysermc.connector.inventory;
import com.github.steveice10.mc.protocol.data.game.inventory.ContainerType;
+import lombok.Getter;
+import lombok.Setter;
/**
- * Used to determine if rename packets should be sent.
+ * Used to determine if rename packets should be sent and stores
+ * the expected level cost for AnvilInventoryUpdater
*/
+@Getter @Setter
public class AnvilContainer extends Container {
+ /**
+ * Stores the level cost received as a window property from Java
+ */
+ private int javaLevelCost = 0;
+ /**
+ * A flag to specify whether javaLevelCost can be used as it can
+ * be outdated or not sent at all.
+ */
+ private boolean useJavaLevelCost = false;
+
+ /**
+ * The new name of the item as received from Bedrock
+ */
+ private String newName = null;
+
+ private GeyserItemStack lastInput = GeyserItemStack.EMPTY;
+ private GeyserItemStack lastMaterial = GeyserItemStack.EMPTY;
+
+ private int lastTargetSlot = -1;
+
public AnvilContainer(String title, int id, int size, ContainerType containerType, PlayerInventory playerInventory) {
super(title, id, size, containerType, playerInventory);
}
+
+ public GeyserItemStack getInput() {
+ return getItem(0);
+ }
+
+ public GeyserItemStack getMaterial() {
+ return getItem(1);
+ }
+
+ public GeyserItemStack getResult() {
+ return getItem(2);
+ }
}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java
index 8abff259c..23f2ba293 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java
@@ -32,6 +32,8 @@ import org.geysermc.connector.inventory.CartographyContainer;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
+import org.geysermc.connector.network.translators.chat.MessageTranslator;
+import org.geysermc.connector.utils.ItemUtils;
/**
* Used to send strings to the server and filter out unwanted words.
@@ -47,12 +49,31 @@ public class BedrockFilterTextTranslator extends PacketTranslator= MAX_LEVEL_COST) {
+ // Items can still be renamed when the level cost for renaming exceeds 40
+ totalCost = MAX_LEVEL_COST - 1;
+ }
+ }
+ return totalCost;
+ }
+
+ /**
+ * Calculate the levels needed to repair an item with its repair material
+ * E.g. iron_sword + iron_ingot
+ *
+ * @param session Geyser session
+ * @param input an item with durability
+ * @param material the item's respective repair material
+ * @return the number of levels needed or 0 if it is not possible to repair any further
+ */
+ private int calcRepairLevelCost(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
+ int newDamage = getDamage(input);
+ int unitRepair = Math.min(newDamage, input.getMapping(session).getMaxDamage() / 4);
+ if (unitRepair <= 0) {
+ // No damage to repair
+ return -1;
+ }
+ for (int i = 0; i < material.getAmount(); i++) {
+ newDamage -= unitRepair;
+ unitRepair = Math.min(newDamage, input.getMapping(session).getMaxDamage() / 4);
+ if (unitRepair <= 0) {
+ return i + 1;
+ }
+ }
+ return material.getAmount();
+ }
+
+ /**
+ * Calculate the levels cost for repairing items by combining two of the same item
+ *
+ * @param session Geyser session
+ * @param input an item with durability
+ * @param material a matching item
+ * @return the number of levels needed or 0 if it is not possible to repair any further
+ */
+ private int calcMergeRepairCost(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
+ // If the material item is damaged 112% or more, then the input item will not be repaired
+ if (getDamage(input) > 0 && getDamage(material) < (material.getMapping(session).getMaxDamage() * 112 / 100)) {
+ return 2;
+ }
+ return 0;
+ }
+
+ /**
+ * Calculate the levels needed for combining the enchantments of two items
+ *
+ * @param session Geyser session
+ * @param input an item with durability
+ * @param material a matching item
+ * @param bedrock True to count enchantments like Bedrock, False to count like Java
+ * @return the number of levels needed or -1 if no enchantments can be applied
+ */
+ private int calcMergeEnchantmentCost(GeyserSession session, GeyserItemStack input, GeyserItemStack material, boolean bedrock) {
+ boolean hasCompatible = false;
+ Object2IntMap combinedEnchantments = getEnchantments(session, input, bedrock);
+ int cost = 0;
+ for (Object2IntMap.Entry entry : getEnchantments(session, material, bedrock).object2IntEntrySet()) {
+ JavaEnchantment enchantment = entry.getKey();
+ EnchantmentData data = Registries.ENCHANTMENTS.get(enchantment);
+ if (data == null) {
+ GeyserConnector.getInstance().getLogger().debug("Java enchantment not in registry: " + enchantment);
+ continue;
+ }
+
+ boolean canApply = isEnchantedBook(session, input) || data.validItems().contains(input.getJavaId());
+ for (JavaEnchantment incompatible : data.incompatibleEnchantments()) {
+ if (combinedEnchantments.containsKey(incompatible)) {
+ canApply = false;
+ if (!bedrock) {
+ cost++;
+ }
+ }
+ }
+
+ if (canApply || (!bedrock && session.getGameMode() == GameMode.CREATIVE)) {
+ int currentLevel = combinedEnchantments.getOrDefault(enchantment, 0);
+ int newLevel = entry.getIntValue();
+ if (newLevel == currentLevel) {
+ newLevel++;
+ }
+ newLevel = Math.max(currentLevel, newLevel);
+ if (newLevel > data.maxLevel()) {
+ newLevel = data.maxLevel();
+ }
+ combinedEnchantments.put(enchantment, newLevel);
+
+ int rarityMultiplier = data.rarityMultiplier();
+ if (isEnchantedBook(session, material) && rarityMultiplier > 1) {
+ rarityMultiplier /= 2;
+ }
+ if (bedrock) {
+ if (newLevel > currentLevel) {
+ hasCompatible = true;
+ }
+ if (enchantment == JavaEnchantment.IMPALING) {
+ // Multiplier is halved on Bedrock for some reason
+ rarityMultiplier /= 2;
+ } else if (enchantment == JavaEnchantment.SWEEPING) {
+ // Doesn't exist on Bedrock
+ rarityMultiplier = 0;
+ }
+ cost += rarityMultiplier * (newLevel - currentLevel);
+ } else {
+ hasCompatible = true;
+ cost += rarityMultiplier * newLevel;
+ }
+ }
+ }
+
+ if (!hasCompatible) {
+ return -1;
+ }
+ return cost;
+ }
+
+ private Object2IntMap getEnchantments(GeyserSession session, GeyserItemStack itemStack, boolean bedrock) {
+ if (itemStack.getNbt() == null) {
+ return Object2IntMaps.emptyMap();
+ }
+ Object2IntMap enchantments = new Object2IntOpenHashMap<>();
+ Tag enchantmentTag;
+ if (isEnchantedBook(session, itemStack)) {
+ enchantmentTag = itemStack.getNbt().get("StoredEnchantments");
+ } else {
+ enchantmentTag = itemStack.getNbt().get("Enchantments");
+ }
+ if (enchantmentTag instanceof ListTag listTag) {
+ for (Tag tag : listTag.getValue()) {
+ if (tag instanceof CompoundTag enchantTag) {
+ if (enchantTag.get("id") instanceof StringTag javaEnchId) {
+ JavaEnchantment enchantment = JavaEnchantment.getByJavaIdentifier(javaEnchId.getValue());
+ if (enchantment == null) {
+ GeyserConnector.getInstance().getLogger().debug("Unknown java enchantment: " + javaEnchId.getValue());
+ continue;
+ }
+
+ Tag javaEnchLvl = enchantTag.get("lvl");
+ if (!(javaEnchLvl instanceof ShortTag || javaEnchLvl instanceof IntTag))
+ continue;
+
+ // Handle duplicate enchantments
+ if (bedrock) {
+ enchantments.putIfAbsent(enchantment, ((Number) javaEnchLvl.getValue()).intValue());
+ } else {
+ enchantments.mergeInt(enchantment, ((Number) javaEnchLvl.getValue()).intValue(), Math::max);
+ }
+ }
+ }
+ }
+ }
+ return enchantments;
+ }
+
+ private boolean isEnchantedBook(GeyserSession session, GeyserItemStack itemStack) {
+ return itemStack.getJavaId() == session.getItemMappings().getStoredItems().enchantedBook().getJavaId();
+ }
+
+ private boolean isCombining(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
+ return isEnchantedBook(session, material) || (input.getJavaId() == material.getJavaId() && hasDurability(session, input));
+ }
+
+ private boolean isRepairing(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
+ Set repairMaterials = input.getMapping(session).getRepairMaterials();
+ return repairMaterials != null && repairMaterials.contains(material.getMapping(session).getJavaIdentifier());
+ }
+
+ private boolean isRenaming(GeyserSession session, AnvilContainer anvilContainer, boolean bedrock) {
+ if (anvilContainer.getResult().isEmpty()) {
+ return false;
+ }
+ // This should really check the name field in all cases, but that requires the localized name
+ // of the item which can change depending on NBT and Minecraft Edition
+ String originalName = ItemUtils.getCustomName(anvilContainer.getInput().getNbt());
+ if (bedrock && originalName != null && anvilContainer.getNewName() != null) {
+ // Check text and formatting
+ String legacyOriginalName = MessageTranslator.convertMessageLenient(originalName, session.getLocale());
+ return !legacyOriginalName.equals(anvilContainer.getNewName());
+ }
+ return !Objects.equals(originalName, ItemUtils.getCustomName(anvilContainer.getResult().getNbt()));
+ }
+
+ private int getTagIntValueOr(GeyserItemStack itemStack, String tagName, int defaultValue) {
+ if (itemStack.getNbt() != null) {
+ Tag tag = itemStack.getNbt().get(tagName);
+ if (tag != null && tag.getValue() instanceof Number value) {
+ return value.intValue();
+ }
+ }
+ return defaultValue;
+ }
+
+ private int getRepairCost(GeyserItemStack itemStack) {
+ return getTagIntValueOr(itemStack, "RepairCost", 0);
+ }
+
+ private boolean hasDurability(GeyserSession session, GeyserItemStack itemStack) {
+ if (itemStack.getMapping(session).getMaxDamage() > 0) {
+ return getTagIntValueOr(itemStack, "Unbreakable", 0) == 0;
+ }
+ return false;
+ }
+
+ private int getDamage(GeyserItemStack itemStack) {
+ return getTagIntValueOr(itemStack, "Damage", 0);
+ }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java
index 14b918a4f..947f9e6ed 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java
@@ -147,6 +147,18 @@ public enum Enchantment {
*/
public static final String[] ALL_JAVA_IDENTIFIERS;
+ public static JavaEnchantment getByJavaIdentifier(String javaIdentifier) {
+ if (!javaIdentifier.startsWith("minecraft:")) {
+ javaIdentifier = "minecraft:" + javaIdentifier;
+ }
+ for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) {
+ if (ALL_JAVA_IDENTIFIERS[i].equalsIgnoreCase(javaIdentifier)) {
+ return VALUES[i];
+ }
+ }
+ return null;
+ }
+
static {
ALL_JAVA_IDENTIFIERS = new String[VALUES.length];
for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/StoredItemMappings.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/StoredItemMappings.java
index 7f8456d6a..6bbdb7421 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/item/StoredItemMappings.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/StoredItemMappings.java
@@ -43,6 +43,7 @@ public class StoredItemMappings {
private final ItemMapping barrier;
private final ItemMapping compass;
private final ItemMapping crossbow;
+ private final ItemMapping enchantedBook;
private final ItemMapping fishingRod;
private final ItemMapping lodestoneCompass;
private final ItemMapping milkBucket;
@@ -58,6 +59,7 @@ public class StoredItemMappings {
this.barrier = load(itemMappings, "barrier");
this.compass = load(itemMappings, "compass");
this.crossbow = load(itemMappings, "crossbow");
+ this.enchantedBook = load(itemMappings, "enchanted_book");
this.fishingRod = load(itemMappings, "fishing_rod");
this.lodestoneCompass = load(itemMappings, "lodestone_compass");
this.milkBucket = load(itemMappings, "milk_bucket");
diff --git a/connector/src/main/java/org/geysermc/connector/registry/Registries.java b/connector/src/main/java/org/geysermc/connector/registry/Registries.java
index 0d548c824..157d01ea3 100644
--- a/connector/src/main/java/org/geysermc/connector/registry/Registries.java
+++ b/connector/src/main/java/org/geysermc/connector/registry/Registries.java
@@ -39,12 +39,14 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
import org.geysermc.connector.network.translators.world.event.LevelEventTransformer;
+import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment;
import org.geysermc.connector.network.translators.sound.SoundHandler;
import org.geysermc.connector.network.translators.sound.SoundInteractionHandler;
import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
import org.geysermc.connector.registry.loader.*;
import org.geysermc.connector.registry.populator.ItemRegistryPopulator;
import org.geysermc.connector.registry.populator.RecipeRegistryPopulator;
+import org.geysermc.connector.registry.type.EnchantmentData;
import org.geysermc.connector.registry.type.ItemMappings;
import org.geysermc.connector.registry.type.ParticleMapping;
import org.geysermc.connector.registry.type.SoundMapping;
@@ -82,6 +84,11 @@ public class Registries {
*/
public static final VersionedRegistry