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.
This commit is contained in:
Camotoy 2022-10-10 15:40:07 -04:00
parent f4b810534b
commit f59e33d749
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
7 changed files with 293 additions and 160 deletions

View file

@ -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,

View file

@ -95,7 +95,10 @@ public class ItemRegistryPopulator {
boolean customItemsAllowed = GeyserImpl.getInstance().getConfig().isAddNonBedrockItems();
Multimap<String, CustomItemData> 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<String, CustomItemData> customItems = MultimapBuilder.hashKeys().arrayListValues().build();
List<NonVanillaCustomItemData> nonVanillaCustomItems;
MappingsConfigReader mappingsConfigReader = new MappingsConfigReader();
@ -468,10 +471,10 @@ public class ItemRegistryPopulator {
}
// Add the custom item properties, if applicable
Object2IntMap<CustomItemOptions> customItemOptions;
List<ObjectIntPair<CustomItemOptions>> customItemOptions;
Collection<CustomItemData> 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()

View file

@ -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> customItemOptions;
@NonNull
List<ObjectIntPair<CustomItemOptions>> customItemOptions;
int maxDamage;

View file

@ -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
*

View file

@ -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<ObjectIntPair<CustomItemOptions>> 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<CustomItemOptions> 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() {
}
}

View file

@ -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<Object> 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<CustomItemOptions> 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<CustomItemOptions> 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;
}
}

View file

@ -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<CompoundTag> 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<CustomItemOptions> 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<CompoundTag> 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);
}
}
}