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>> CRAFTING_DATA = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new)); + /** + * A registry holding data of all the known enchantments. + */ + public static final SimpleMappedRegistry ENCHANTMENTS; + /** * A registry holding a CompoundTag of the known entity identifiers. */ @@ -140,5 +147,6 @@ public class Registries { // Create registries that require other registries to load first POTION_MIXES = SimpleRegistry.create(PotionMixRegistryLoader::new); + ENCHANTMENTS = SimpleMappedRegistry.create("mappings/enchantments.json", EnchantmentRegistryLoader::new); } } \ No newline at end of file diff --git a/connector/src/main/java/org/geysermc/connector/registry/loader/EnchantmentRegistryLoader.java b/connector/src/main/java/org/geysermc/connector/registry/loader/EnchantmentRegistryLoader.java new file mode 100644 index 000000000..a1a95fe1a --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/registry/loader/EnchantmentRegistryLoader.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019-2021 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.connector.registry.loader; + +import com.fasterxml.jackson.databind.JsonNode; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.MinecraftProtocol; +import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment; +import org.geysermc.connector.registry.Registries; +import org.geysermc.connector.registry.type.EnchantmentData; +import org.geysermc.connector.registry.type.ItemMapping; +import org.geysermc.connector.utils.FileUtils; + +import java.io.InputStream; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.Map; + +public class EnchantmentRegistryLoader implements RegistryLoader> { + @Override + public Map load(String input) { + InputStream enchantmentsStream = FileUtils.getResource(input); + JsonNode enchantmentsNode; + try { + enchantmentsNode = GeyserConnector.JSON_MAPPER.readTree(enchantmentsStream); + } catch (Exception e) { + throw new AssertionError("Unable to load enchantment data", e); + } + + Map enchantments = new EnumMap<>(JavaEnchantment.class); + Iterator> it = enchantmentsNode.fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + JavaEnchantment key = JavaEnchantment.getByJavaIdentifier(entry.getKey()); + JsonNode node = entry.getValue(); + int rarityMultiplier = switch (node.get("rarity").textValue()) { + case "common" -> 1; + case "uncommon" -> 2; + case "rare" -> 4; + case "very_rare" -> 8; + default -> throw new IllegalStateException("Unexpected value: " + node.get("rarity").textValue()); + }; + int maxLevel = node.get("max_level").asInt(); + + EnumSet incompatibleEnchantments = EnumSet.noneOf(JavaEnchantment.class); + JsonNode incompatibleEnchantmentsNode = node.get("incompatible_enchantments"); + if (incompatibleEnchantmentsNode != null) { + for (JsonNode incompatibleNode : incompatibleEnchantmentsNode) { + incompatibleEnchantments.add(JavaEnchantment.getByJavaIdentifier(incompatibleNode.textValue())); + } + } + + IntSet validItems = new IntOpenHashSet(); + for (JsonNode itemNode : node.get("valid_items")) { + String javaIdentifier = itemNode.textValue(); + ItemMapping itemMapping = Registries.ITEMS.forVersion(MinecraftProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getMapping(javaIdentifier); + if (itemMapping != null) { + validItems.add(itemMapping.getJavaId()); + } else { + throw new NullPointerException("No item entry exists for java identifier: " + javaIdentifier); + } + } + + EnchantmentData enchantmentData = new EnchantmentData(rarityMultiplier, maxLevel, incompatibleEnchantments, validItems); + enchantments.put(key, enchantmentData); + } + return enchantments; + } +} diff --git a/connector/src/main/java/org/geysermc/connector/registry/populator/ItemRegistryPopulator.java b/connector/src/main/java/org/geysermc/connector/registry/populator/ItemRegistryPopulator.java index b413b3a80..8f3e87e53 100644 --- a/connector/src/main/java/org/geysermc/connector/registry/populator/ItemRegistryPopulator.java +++ b/connector/src/main/java/org/geysermc/connector/registry/populator/ItemRegistryPopulator.java @@ -37,10 +37,7 @@ import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import com.nukkitx.protocol.bedrock.packet.StartGamePacket; import com.nukkitx.protocol.bedrock.v465.Bedrock_v465; import com.nukkitx.protocol.bedrock.v471.Bedrock_v471; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.objects.*; import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.network.translators.item.StoredItemMappings; @@ -365,7 +362,12 @@ public class ItemRegistryPopulator { .bedrockId(bedrockId) .bedrockData(mappingItem.getBedrockData()) .bedrockBlockId(bedrockBlockId) - .stackSize(stackSize); + .stackSize(stackSize) + .maxDamage(mappingItem.getMaxDamage()); + + if (mappingItem.getRepairMaterials() != null) { + mappingBuilder = mappingBuilder.repairMaterials(new ObjectOpenHashSet<>(mappingItem.getRepairMaterials())); + } if (mappingItem.getToolType() != null) { if (mappingItem.getToolTier() != null) { diff --git a/connector/src/main/java/org/geysermc/connector/registry/type/EnchantmentData.java b/connector/src/main/java/org/geysermc/connector/registry/type/EnchantmentData.java new file mode 100644 index 000000000..cd16a093a --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/registry/type/EnchantmentData.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019-2021 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.connector.registry.type; + +import it.unimi.dsi.fastutil.ints.IntSet; +import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment; + +import java.util.Set; + +public record EnchantmentData(int rarityMultiplier, int maxLevel, Set incompatibleEnchantments, + IntSet validItems) { +} diff --git a/connector/src/main/java/org/geysermc/connector/registry/type/GeyserMappingItem.java b/connector/src/main/java/org/geysermc/connector/registry/type/GeyserMappingItem.java index da91c412e..12e5544b7 100644 --- a/connector/src/main/java/org/geysermc/connector/registry/type/GeyserMappingItem.java +++ b/connector/src/main/java/org/geysermc/connector/registry/type/GeyserMappingItem.java @@ -28,6 +28,8 @@ package org.geysermc.connector.registry.type; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import java.util.List; + /** * Represents Geyser's own serialized item information before being processed per-version */ @@ -40,4 +42,6 @@ public class GeyserMappingItem { @JsonProperty("stack_size") int stackSize = 64; @JsonProperty("tool_type") String toolType; @JsonProperty("tool_tier") String toolTier; + @JsonProperty("max_damage") int maxDamage = 0; + @JsonProperty("repair_materials") List repairMaterials; } diff --git a/connector/src/main/java/org/geysermc/connector/registry/type/ItemMapping.java b/connector/src/main/java/org/geysermc/connector/registry/type/ItemMapping.java index 4da25893f..1a7714968 100644 --- a/connector/src/main/java/org/geysermc/connector/registry/type/ItemMapping.java +++ b/connector/src/main/java/org/geysermc/connector/registry/type/ItemMapping.java @@ -31,13 +31,15 @@ import lombok.Value; import org.geysermc.connector.network.MinecraftProtocol; import org.geysermc.connector.registry.BlockRegistries; +import java.util.Set; + @Value @Builder @EqualsAndHashCode public class ItemMapping { public static final ItemMapping AIR = new ItemMapping("minecraft:air", "minecraft:air", 0, 0, 0, BlockRegistries.BLOCKS.forVersion(MinecraftProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getBedrockAirId(), - 64, null, null, null); + 64, null, null, null, 0, null); String javaIdentifier; String bedrockIdentifier; @@ -57,6 +59,10 @@ public class ItemMapping { String translationString; + int maxDamage; + + Set repairMaterials; + /** * Gets if this item is a block. * diff --git a/connector/src/main/java/org/geysermc/connector/utils/ItemUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ItemUtils.java index db4e9e2e1..8bd2edfd1 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ItemUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ItemUtils.java @@ -58,4 +58,19 @@ public class ItemUtils { } return original; } + + /** + * @param itemTag the NBT tag of the item + * @return the custom name of the item + */ + public static String getCustomName(CompoundTag itemTag) { + if (itemTag != null) { + if (itemTag.get("display") instanceof CompoundTag displayTag) { + if (displayTag.get("Name") instanceof StringTag nameTag) { + return nameTag.getValue(); + } + } + } + return null; + } } diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings index 7ff1b6567..5b6239f0a 160000 --- a/connector/src/main/resources/mappings +++ b/connector/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 7ff1b6567b56c7b0b8e28786b9bbc30abfaededf +Subproject commit 5b6239f0a43ec9a38d65ed53b8d1bfaf564c1c3b diff --git a/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java b/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java index 649e96425..cfb2bbc97 100644 --- a/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java +++ b/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java @@ -81,6 +81,16 @@ public class MessageTranslatorTest { Assert.assertEquals("Unimplemented formatting chars not stripped", "Bold Underline", MessageTranslator.convertMessageLenient("§m§nBold Underline")); } + @Test + public void convertToPlainText() { + Assert.assertEquals("JSON message is not handled properly", "Many colors here", MessageTranslator.convertToPlainText("{\"extra\":[{\"color\":\"red\",\"text\":\"M\"},{\"color\":\"gold\",\"text\":\"a\"},{\"color\":\"yellow\",\"text\":\"n\"},{\"color\":\"green\",\"text\":\"y \"},{\"color\":\"aqua\",\"text\":\"c\"},{\"color\":\"dark_purple\",\"text\":\"o\"},{\"color\":\"red\",\"text\":\"l\"},{\"color\":\"gold\",\"text\":\"o\"},{\"color\":\"yellow\",\"text\":\"r\"},{\"color\":\"green\",\"text\":\"s \"},{\"color\":\"aqua\",\"text\":\"h\"},{\"color\":\"dark_purple\",\"text\":\"e\"},{\"color\":\"red\",\"text\":\"r\"},{\"color\":\"gold\",\"text\":\"e\"}],\"text\":\"\"}", "en_US")); + Assert.assertEquals("Legacy formatted message is not handled properly (Colors)", "Many colors here", MessageTranslator.convertToPlainText("§cM§6a§en§ay §bc§5o§cl§6o§er§as §bh§5e§cr§6e", "en_US")); + Assert.assertEquals("Legacy formatted message is not handled properly (Style)", "Obf Bold Strikethrough Underline Italic Reset", MessageTranslator.convertToPlainText("§kObf §lBold §mStrikethrough §nUnderline §oItalic §rReset", "en_US")); + Assert.assertEquals("Valid lenient JSON is not handled properly", "Strange", MessageTranslator.convertToPlainText("§rStrange", "en_US")); + Assert.assertEquals("Empty message is not handled properly", "", MessageTranslator.convertToPlainText("", "en_US")); + Assert.assertEquals("Whitespace is not preserved", " ", MessageTranslator.convertToPlainText(" ", "en_US")); + } + @Test public void testNullTextPacket() { DefaultComponentSerializer.get().deserialize("null");