From f59e33d749c7e8fa3df69dc004e795a8ed4c7230 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 10 Oct 2022 15:40:07 -0400 Subject: [PATCH] Fix behavior of matching custom item predicates Huge thanks to Kastle for helping me disect this behavior. - The Unbreakable NBT tag is not the only source for determining if an item should be treated as unbreakable. The damage NBT is also taken into account. - Custom item options must be processed in an ascending order. - Multiple conditions may be necessary for an item to be selected. - Conditions do not have to be exact. See the comments in CustomItemTranslator for an explanation. - Added a test so we don't break this behavior in the future. --- .../CustomItemRegistryPopulator.java | 4 +- .../populator/ItemRegistryPopulator.java | 20 +- .../geyser/registry/type/ItemMapping.java | 15 +- .../geysermc/geyser/text/GeyserLocale.java | 22 +++ .../inventory/item/CustomItemTranslator.java | 105 +++++++++++ .../inventory/item/ItemTranslator.java | 171 +++--------------- .../inventory/item/CustomItemsTest.java | 116 ++++++++++++ 7 files changed, 293 insertions(+), 160 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/translator/inventory/item/CustomItemTranslator.java create mode 100644 core/src/test/java/org/geysermc/geyser/translator/inventory/item/CustomItemsTest.java diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java index 64543272e..017ede61e 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator.java @@ -30,7 +30,6 @@ import com.nukkitx.nbt.NbtMapBuilder; import com.nukkitx.nbt.NbtType; import com.nukkitx.protocol.bedrock.data.inventory.ComponentItemData; import com.nukkitx.protocol.bedrock.packet.StartGamePacket; -import it.unimi.dsi.fastutil.objects.Object2IntMaps; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.item.custom.CustomItemData; import org.geysermc.geyser.api.item.custom.CustomRenderOffsets; @@ -43,6 +42,7 @@ import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.NonVanillaItemRegistration; import javax.annotation.Nullable; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.OptionalInt; @@ -85,7 +85,7 @@ public class CustomItemRegistryPopulator { .maxDamage(customItemData.maxDamage()) .repairMaterials(customItemData.repairMaterials()) .hasSuspiciousStewEffect(false) - .customItemOptions(Object2IntMaps.emptyMap()) + .customItemOptions(Collections.emptyList()) .build(); NbtMapBuilder builder = createComponentNbt(customItemData, customItemData.identifier(), customItemId, diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java index 6c01ee9d2..f928361cc 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/ItemRegistryPopulator.java @@ -95,7 +95,10 @@ public class ItemRegistryPopulator { boolean customItemsAllowed = GeyserImpl.getInstance().getConfig().isAddNonBedrockItems(); - Multimap customItems = MultimapBuilder.hashKeys().hashSetValues().build(); + // List values here is important compared to HashSet - we need to preserve the order of what's given to us + // (as of 1.19.2 Java) to replicate some edge cases in Java predicate behavior where it checks from the bottom + // of the list first, then ascends. + Multimap customItems = MultimapBuilder.hashKeys().arrayListValues().build(); List nonVanillaCustomItems; MappingsConfigReader mappingsConfigReader = new MappingsConfigReader(); @@ -468,10 +471,10 @@ public class ItemRegistryPopulator { } // Add the custom item properties, if applicable - Object2IntMap customItemOptions; + List> customItemOptions; Collection customItemsToLoad = customItems.get(javaIdentifier); if (customItemsAllowed && !customItemsToLoad.isEmpty()) { - customItemOptions = new Object2IntOpenHashMap<>(customItemsToLoad.size()); + customItemOptions = new ObjectArrayList<>(customItemsToLoad.size()); for (CustomItemData customItem : customItemsToLoad) { int customProtocolId = nextFreeBedrockId++; @@ -491,12 +494,15 @@ public class ItemRegistryPopulator { entries.put(customMapping.stringId(), customMapping.startGamePacketItemEntry()); // ComponentItemData - used to register some custom properties componentItemData.add(customMapping.componentItemData()); - customItemOptions.put(customItem.customItemOptions(), customProtocolId); + customItemOptions.add(ObjectIntPair.of(customItem.customItemOptions(), customProtocolId)); customIdMappings.put(customMapping.integerId(), customMapping.stringId()); } + + // Important for later to find the best match and accurately replicate Java behavior + Collections.reverse(customItemOptions); } else { - customItemOptions = Object2IntMaps.emptyMap(); + customItemOptions = Collections.emptyList(); } mappingBuilder.customItemOptions(customItemOptions); @@ -550,7 +556,7 @@ public class ItemRegistryPopulator { .bedrockData(0) .bedrockBlockId(-1) .stackSize(1) - .customItemOptions(Object2IntMaps.emptyMap()) + .customItemOptions(Collections.emptyList()) .build(); if (customItemsAllowed) { @@ -567,7 +573,7 @@ public class ItemRegistryPopulator { .bedrockData(0) .bedrockBlockId(-1) .stackSize(1) - .customItemOptions(Object2IntMaps.emptyMap()) // TODO check for custom items with furnace minecart + .customItemOptions(Collections.emptyList()) // TODO check for custom items with furnace minecart .build()); creativeItems.add(ItemData.builder() diff --git a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java index 12ba7d208..e3d34b0ca 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java +++ b/core/src/main/java/org/geysermc/geyser/registry/type/ItemMapping.java @@ -25,15 +25,15 @@ package org.geysermc.geyser.registry.type; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import it.unimi.dsi.fastutil.objects.ObjectIntPair; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Value; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.item.custom.CustomItemOptions; -import org.geysermc.geyser.network.GameProtocol; -import org.geysermc.geyser.registry.BlockRegistries; +import java.util.Collections; +import java.util.List; import java.util.Set; @Value @@ -41,8 +41,8 @@ import java.util.Set; @EqualsAndHashCode public class ItemMapping { public static final ItemMapping AIR = new ItemMapping("minecraft:air", "minecraft:air", 0, 0, 0, - BlockRegistries.BLOCKS.forVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getBedrockAirId(), - 64, null, null, null, Object2IntMaps.emptyMap(), 0, null, false); + 0, // Air is never sent in full over the network for this to serialize. + 64, null, null, null, Collections.emptyList(), 0, null, false); String javaIdentifier; String bedrockIdentifier; @@ -62,7 +62,8 @@ public class ItemMapping { String translationString; - Object2IntMap customItemOptions; + @NonNull + List> customItemOptions; int maxDamage; diff --git a/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java b/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java index 86e015c0f..340674119 100644 --- a/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java +++ b/core/src/main/java/org/geysermc/geyser/text/GeyserLocale.java @@ -25,6 +25,7 @@ package org.geysermc.geyser.text; +import it.unimi.dsi.fastutil.objects.ObjectArrays; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; @@ -173,6 +174,16 @@ public class GeyserLocale { return localeProp.isEmpty() ? null : locale; } + /** + * Get a formatted language string with the default locale for Geyser + * + * @param key Language string to translate + * @return Translated string or the original message if it was not found in the given locale + */ + public static String getLocaleStringLog(String key) { + return getLocaleStringLog(key, ObjectArrays.EMPTY_ARRAY); + } + /** * Get a formatted language string with the default locale for Geyser * @@ -184,6 +195,17 @@ public class GeyserLocale { return getPlayerLocaleString(key, getDefaultLocale(), values); } + /** + * Get a formatted language string with the given locale for Geyser + * + * @param key Language string to translate + * @param locale Locale to translate to + * @return Translated string or the original message if it was not found in the given locale + */ + public static String getPlayerLocaleString(String key, String locale) { + return getPlayerLocaleString(key, locale, ObjectArrays.EMPTY_ARRAY); + } + /** * Get a formatted language string with the given locale for Geyser * diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/CustomItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/CustomItemTranslator.java new file mode 100644 index 000000000..29aa2f748 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/CustomItemTranslator.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019-2022 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.item; + +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import it.unimi.dsi.fastutil.objects.ObjectIntPair; +import org.geysermc.geyser.api.item.custom.CustomItemOptions; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.registry.type.ItemMapping; + +import java.util.List; +import java.util.OptionalInt; + +/** + * This is only a separate class for testing purposes so we don't have to load in GeyserImpl in ItemTranslator. + */ +final class CustomItemTranslator { + + static int getCustomItem(CompoundTag nbt, ItemMapping mapping) { + if (nbt == null) { + return -1; + } + List> customMappings = mapping.getCustomItemOptions(); + if (customMappings.isEmpty()) { + return -1; + } + + int customModelData = nbt.get("CustomModelData") instanceof IntTag customModelDataTag ? customModelDataTag.getValue() : 0; + int damage = nbt.get("Damage") instanceof IntTag damageTag ? damageTag.getValue() : 0; + boolean unbreakable = !isDamaged(mapping, nbt, damage); + + for (ObjectIntPair mappingTypes : customMappings) { + CustomItemOptions options = mappingTypes.key(); + + // Code note: there may be two or more conditions that a custom item must follow, hence the "continues" + // here with the return at the end. + + // Implementation details: Java's predicate system works exclusively on comparing float numbers. + // A value doesn't necessarily have to match 100%; it just has to be the first to meet all predicate conditions. + // This is also why the order of iteration is important as the first to match will be the chosen display item. + // For example, if CustomModelData is set to 2f as the requirement, then the NBT can be any number greater or equal (2, 3, 4...) + // The same behavior exists for Damage (in fraction form instead of whole numbers), + // and Damaged/Unbreakable handles no damage as 0f and damaged as 1f. + + if (unbreakable && options.unbreakable() != TriState.TRUE) { + continue; + } + + OptionalInt customModelDataOption = options.customModelData(); + if (customModelDataOption.isPresent() && customModelData < customModelDataOption.getAsInt()) { + continue; + } + + OptionalInt damagePredicate = options.damagePredicate(); + if (damagePredicate.isPresent() && damage < damagePredicate.getAsInt()) { + continue; + } + + return mappingTypes.valueInt(); + } + return -1; + } + + /* These two functions are based off their Mojmap equivalents from 1.19.2 */ + + private static boolean isDamaged(ItemMapping mapping, CompoundTag nbt, int damage) { + return isDamagableItem(mapping, nbt) && damage > 0; + } + + private static boolean isDamagableItem(ItemMapping mapping, CompoundTag nbt) { + if (mapping.getMaxDamage() > 0) { + Tag unbreakableTag = nbt.get("Unbreakable"); + return unbreakableTag != null && unbreakableTag.getValue() instanceof Number number && number.byteValue() == 0; + } + return false; + } + + private CustomItemTranslator() { + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java index b36833cb1..ab3feae5f 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/item/ItemTranslator.java @@ -34,12 +34,9 @@ import com.nukkitx.nbt.NbtType; import com.nukkitx.protocol.bedrock.data.inventory.ItemData; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2IntMap; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.item.custom.CustomItemOptions; -import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.registry.BlockRegistries; import org.geysermc.geyser.registry.type.ItemMapping; @@ -173,18 +170,18 @@ public abstract class ItemTranslator { builder.blockRuntimeId(bedrockItem.getBedrockBlockId()); } - translateCustomItem(nbt, builder, bedrockItem); - if (nbt != null) { // Translate the canDestroy and canPlaceOn Java NBT ListTag canDestroy = nbt.get("CanDestroy"); - String[] canBreak = new String[0]; ListTag canPlaceOn = nbt.get("CanPlaceOn"); - String[] canPlace = new String[0]; - canBreak = getCanModify(canDestroy, canBreak); - canPlace = getCanModify(canPlaceOn, canPlace); - builder.canBreak(canBreak); - builder.canPlace(canPlace); + String[] canBreak = getCanModify(canDestroy); + String[] canPlace = getCanModify(canPlaceOn); + if (canBreak != null) { + builder.canBreak(canBreak); + } + if (canPlace != null) { + builder.canPlace(canPlace); + } } return builder.build(); @@ -246,12 +243,11 @@ public abstract class ItemTranslator { * In Java, this is treated as normal NBT, but in Bedrock, these arguments are extra parts of the item data itself. * * @param canModifyJava the list of items in Java - * @param canModifyBedrock the empty list of items in Bedrock * @return the new list of items in Bedrock */ - private static String[] getCanModify(ListTag canModifyJava, String[] canModifyBedrock) { + private static String[] getCanModify(ListTag canModifyJava) { if (canModifyJava != null && canModifyJava.size() > 0) { - canModifyBedrock = new String[canModifyJava.size()]; + String[] canModifyBedrock = new String[canModifyJava.size()]; for (int i = 0; i < canModifyBedrock.length; i++) { // Get the Java identifier of the block that can be placed String block = ((StringTag) canModifyJava.get(i)).getValue(); @@ -261,8 +257,9 @@ public abstract class ItemTranslator { // This will unfortunately be limited - for example, beds and banners will be translated weirdly canModifyBedrock[i] = BlockRegistries.JAVA_TO_BEDROCK_IDENTIFIERS.getOrDefault(block, block).replace("minecraft:", ""); } + return canModifyBedrock; } - return canModifyBedrock; + return null; } /** @@ -276,7 +273,7 @@ public abstract class ItemTranslator { ItemMapping mapping = ITEM_STACK_TRANSLATORS.getOrDefault(javaId, DEFAULT_TRANSLATOR) .getItemMapping(javaId, itemStack.getNbt(), session.getItemMappings()); - int customItemId = getCustomItem(itemStack.getNbt(), mapping); + int customItemId = CustomItemTranslator.getCustomItem(itemStack.getNbt(), mapping); if (customItemId == -1) { // No custom item return mapping.getBedrockId(); @@ -329,66 +326,26 @@ public abstract class ItemTranslator { } protected NbtMap translateNbtToBedrock(CompoundTag tag) { - NbtMapBuilder builder = NbtMap.builder(); - if (tag.getValue() != null && !tag.getValue().isEmpty()) { - for (String str : tag.getValue().keySet()) { - Tag javaTag = tag.get(str); + if (!tag.getValue().isEmpty()) { + NbtMapBuilder builder = NbtMap.builder(); + for (Tag javaTag : tag.values()) { Object translatedTag = translateToBedrockNBT(javaTag); if (translatedTag == null) continue; builder.put(javaTag.getName(), translatedTag); } + return builder.build(); } - return builder.build(); + return NbtMap.EMPTY; } private Object translateToBedrockNBT(Tag tag) { - if (tag instanceof ByteArrayTag) { - return ((ByteArrayTag) tag).getValue(); - } - - if (tag instanceof ByteTag) { - return ((ByteTag) tag).getValue(); - } - - if (tag instanceof DoubleTag) { - return ((DoubleTag) tag).getValue(); - } - - if (tag instanceof FloatTag) { - return ((FloatTag) tag).getValue(); - } - - if (tag instanceof IntArrayTag) { - return ((IntArrayTag) tag).getValue(); - } - - if (tag instanceof IntTag) { - return ((IntTag) tag).getValue(); - } - - if (tag instanceof LongArrayTag) { - //Long array tag does not exist in BE - //LongArrayTag longArrayTag = (LongArrayTag) tag; - //return new com.nukkitx.nbt.tag.LongArrayTag(longArrayTag.getName(), longArrayTag.getValue()); - return null; - } - - if (tag instanceof LongTag) { - return ((LongTag) tag).getValue(); - } - - if (tag instanceof ShortTag) { - return ((ShortTag) tag).getValue(); - } - - if (tag instanceof StringTag) { - return ((StringTag) tag).getValue(); + if (tag instanceof CompoundTag compoundTag) { + return translateNbtToBedrock(compoundTag); } if (tag instanceof ListTag listTag) { - List tagList = new ArrayList<>(); for (Tag value : listTag) { tagList.add(translateToBedrockNBT(value)); @@ -400,11 +357,14 @@ public abstract class ItemTranslator { return new NbtList(type, tagList); } - if (tag instanceof CompoundTag compoundTag) { - return translateNbtToBedrock(compoundTag); + if (tag instanceof LongArrayTag) { + //Long array tag does not exist in BE + //LongArrayTag longArrayTag = (LongArrayTag) tag; + //return new com.nukkitx.nbt.tag.LongArrayTag(longArrayTag.getName(), longArrayTag.getValue()); + return null; } - return null; + return tag.getValue(); } private CompoundTag translateToJavaNBT(String name, NbtMap tag) { @@ -544,87 +504,10 @@ public abstract class ItemTranslator { * Translates the custom model data of an item */ private static void translateCustomItem(CompoundTag nbt, ItemData.Builder builder, ItemMapping mapping) { - int bedrockId = getCustomItem(nbt, mapping); + int bedrockId = CustomItemTranslator.getCustomItem(nbt, mapping); if (bedrockId != -1) { builder.id(bedrockId); } } - private static int getCustomItem(CompoundTag nbt, ItemMapping mapping) { - if (nbt == null) { - return -1; - } - Object2IntMap customMappings = mapping.getCustomItemOptions(); - if (customMappings.isEmpty()) { - return -1; - } - int customModelData = nbt.get("CustomModelData") instanceof IntTag customModelDataTag ? customModelDataTag.getValue() : 0; - TriState unbreakable = TriState.fromBoolean(nbt.get("Unbreakable") instanceof ByteTag unbreakableTag && unbreakableTag.getValue() == 1); - int damage = nbt.get("Damage") instanceof IntTag damageTag ? damageTag.getValue() : 0; - for (Object2IntMap.Entry mappingTypes : customMappings.object2IntEntrySet()) { - CustomItemOptions options = mappingTypes.getKey(); - - TriState unbreakableOption = options.unbreakable(); - if (unbreakableOption == unbreakable) { // Implementation note: if the option is NOT_SET then this comparison will always be false because of how the item unbreaking TriState is created - return mappingTypes.getIntValue(); - } - - OptionalInt customModelDataOption = options.customModelData(); - if (customModelDataOption.isPresent() && customModelDataOption.getAsInt() == customModelData) { - return mappingTypes.getIntValue(); - } - - OptionalInt damagePredicate = options.damagePredicate(); - if (damagePredicate.isPresent() && damagePredicate.getAsInt() == damage) { - return mappingTypes.getIntValue(); - } - } - return -1; - } - - /** - * Checks if an {@link ItemStack} is equal to another item stack - * - * @param itemStack the item stack to check - * @param equalsItemStack the item stack to check if equal to - * @param checkAmount if the amount should be taken into account - * @param trueIfAmountIsGreater if this should return true if the amount of the - * first item stack is greater than that of the second - * @param checkNbt if NBT data should be checked - * @return if an item stack is equal to another item stack - */ - public boolean equals(ItemStack itemStack, ItemStack equalsItemStack, boolean checkAmount, boolean trueIfAmountIsGreater, boolean checkNbt) { - if (itemStack.getId() != equalsItemStack.getId()) { - return false; - } - if (checkAmount) { - if (trueIfAmountIsGreater) { - if (itemStack.getAmount() < equalsItemStack.getAmount()) { - return false; - } - } else { - if (itemStack.getAmount() != equalsItemStack.getAmount()) { - return false; - } - } - } - - if (!checkNbt) { - return true; - } - if ((itemStack.getNbt() == null || itemStack.getNbt().isEmpty()) && (equalsItemStack.getNbt() != null && !equalsItemStack.getNbt().isEmpty())) { - return false; - } - - if ((itemStack.getNbt() != null && !itemStack.getNbt().isEmpty() && (equalsItemStack.getNbt() == null || !equalsItemStack.getNbt().isEmpty()))) { - return false; - } - - if (itemStack.getNbt() != null && equalsItemStack.getNbt() != null) { - return itemStack.getNbt().equals(equalsItemStack.getNbt()); - } - - return true; - } - } diff --git a/core/src/test/java/org/geysermc/geyser/translator/inventory/item/CustomItemsTest.java b/core/src/test/java/org/geysermc/geyser/translator/inventory/item/CustomItemsTest.java new file mode 100644 index 000000000..900feef72 --- /dev/null +++ b/core/src/test/java/org/geysermc/geyser/translator/inventory/item/CustomItemsTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2019-2022 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.item; + +import com.github.steveice10.opennbt.tag.builtin.ByteTag; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; +import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectIntPair; +import org.geysermc.geyser.api.item.custom.CustomItemOptions; +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.item.GeyserCustomItemOptions; +import org.geysermc.geyser.registry.type.ItemMapping; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.OptionalInt; + +public class CustomItemsTest { + private ItemMapping testMappingWithDamage; + private Object2IntMap tagToCustomItemWithDamage; + + @Before + public void setup() { + CustomItemOptions a = new GeyserCustomItemOptions(TriState.TRUE, OptionalInt.of(2), OptionalInt.empty()); + CustomItemOptions b = new GeyserCustomItemOptions(TriState.FALSE, OptionalInt.of(5), OptionalInt.empty()); + CustomItemOptions c = new GeyserCustomItemOptions(TriState.FALSE, OptionalInt.empty(), OptionalInt.of(3)); + CustomItemOptions d = new GeyserCustomItemOptions(TriState.TRUE, OptionalInt.empty(), OptionalInt.of(8)); + CustomItemOptions e = new GeyserCustomItemOptions(TriState.FALSE, OptionalInt.empty(), OptionalInt.of(12)); + CustomItemOptions f = new GeyserCustomItemOptions(TriState.FALSE, OptionalInt.of(8), OptionalInt.of(6)); + + Object2IntMap optionsToId = new Object2IntArrayMap<>(); + // Order here is important, hence why we're using an array map + optionsToId.put(f, 6); + optionsToId.put(e, 5); + optionsToId.put(d, 4); + optionsToId.put(c, 3); + optionsToId.put(b, 2); + optionsToId.put(a, 1); + + tagToCustomItemWithDamage = new Object2IntOpenHashMap<>(); + + CompoundTag tag = new CompoundTag(""); + tag.put(new IntTag("CustomModelData", 6)); + // Test item with no damage should be treated as unbreakable + tagToCustomItemWithDamage.put(tag, optionsToId.getInt(a)); + + tag = new CompoundTag(""); + tag.put(new IntTag("CustomModelData", 3)); + tag.put(new ByteTag("Unbreakable", (byte) 1)); + tagToCustomItemWithDamage.put(tag, optionsToId.getInt(a)); + + tag = new CompoundTag(""); + tag.put(new IntTag("Damage", 16)); + tag.put(new ByteTag("Unbreakable", (byte) 0)); + tagToCustomItemWithDamage.put(tag, optionsToId.getInt(e)); + + tag = new CompoundTag(""); + tag.put(new IntTag("CustomModelData", 7)); + tag.put(new IntTag("Damage", 6)); + tag.put(new ByteTag("Unbreakable", (byte) 0)); + tagToCustomItemWithDamage.put(tag, optionsToId.getInt(c)); + + tag = new CompoundTag(""); + tag.put(new IntTag("CustomModelData", 8)); + tag.put(new IntTag("Damage", 6)); + tag.put(new ByteTag("Unbreakable", (byte) 1)); + tagToCustomItemWithDamage.put(tag, optionsToId.getInt(a)); + + tag = new CompoundTag(""); + tag.put(new IntTag("CustomModelData", 9)); + tag.put(new IntTag("Damage", 6)); + tag.put(new ByteTag("Unbreakable", (byte) 0)); + tagToCustomItemWithDamage.put(tag, optionsToId.getInt(f)); + + testMappingWithDamage = ItemMapping.builder() + .customItemOptions(optionsToId.object2IntEntrySet().stream().map(entry -> ObjectIntPair.of(entry.getKey(), entry.getIntValue())).toList()) + .maxDamage(100) + .build(); + // Later, possibly add a condition with a mapping with no damage + } + + @Test + public void testCustomItems() { + for (Object2IntMap.Entry entry : this.tagToCustomItemWithDamage.object2IntEntrySet()) { + int id = CustomItemTranslator.getCustomItem(entry.getKey(), this.testMappingWithDamage); + Assert.assertEquals(entry.getKey() + " did not produce the correct custom item", entry.getIntValue(), id); + } + } +}