From fd09a05aaf3780ea3f14f0a302b676907f17399a Mon Sep 17 00:00:00 2001 From: Eclipse Date: Fri, 6 Dec 2024 13:02:00 +0000 Subject: [PATCH] Update custom item translator for 1.21.4, implement custom model data --- .../CustomItemRegistryPopulator_v2.java | 71 +----- .../translator/item/CustomItemTranslator.java | 206 ++++++++++++------ core/src/main/resources/mappings | 2 +- 3 files changed, 147 insertions(+), 132 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator_v2.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator_v2.java index 94d1a5bbe..4e8754073 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator_v2.java +++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomItemRegistryPopulator_v2.java @@ -42,23 +42,18 @@ import org.geysermc.geyser.api.item.custom.v2.CustomItemBedrockOptions; import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition; import org.geysermc.geyser.api.item.custom.v2.predicate.CustomItemPredicate; import org.geysermc.geyser.item.GeyserCustomMappingData; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.components.WearableSlot; -import org.geysermc.geyser.item.type.ArmorItem; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.registry.mappings.MappingsConfigReader; import org.geysermc.geyser.registry.type.GeyserMappingItem; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Consumable; -import org.geysermc.mcprotocollib.protocol.data.game.item.component.ConsumeEffect; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable; import org.geysermc.mcprotocollib.protocol.data.game.item.component.FoodProperties; import org.geysermc.mcprotocollib.protocol.data.game.item.component.UseCooldown; -import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -181,7 +176,7 @@ public class CustomItemRegistryPopulator_v2 { boolean canDestroyInCreative = true; if (vanillaMapping.getToolType() != null) { // This is not using the isTool boolean because it is not just a render type here. - canDestroyInCreative = computeToolProperties(vanillaMapping.getToolType(), itemProperties, componentBuilder, vanillaJavaItem.attackDamage()); + canDestroyInCreative = computeToolProperties(vanillaMapping.getToolType(), itemProperties, componentBuilder, vanillaJavaItem.defaultAttackDamage()); } itemProperties.putBoolean("can_destroy_in_creative", canDestroyInCreative); @@ -498,60 +493,9 @@ public class CustomItemRegistryPopulator_v2 { return NbtMap.builder().putList("scale", NbtType.FLOAT, List.of(x, y, z)).build(); } - // TODO this needs to be a simpler method once we just load default vanilla components from mappings or something + // TODO is this right? private static DataComponents patchDataComponents(Item javaItem, CustomItemDefinition definition) { - DataComponents components = new DataComponents(new HashMap<>()); // TODO faster map ? - - components.put(DataComponentType.MAX_STACK_SIZE, javaItem.maxStackSize()); - components.put(DataComponentType.MAX_DAMAGE, javaItem.maxDamage()); - - Consumable consumable = getItemConsumable(javaItem); - if (consumable != null) { - components.put(DataComponentType.CONSUMABLE, consumable); - } - - if (canAlwaysEat(javaItem)) { - components.put(DataComponentType.FOOD, new FoodProperties(0, 0, true)); - } - - if (javaItem.glint()) { - components.put(DataComponentType.ENCHANTMENT_GLINT_OVERRIDE, true); - } - - if (javaItem instanceof ArmorItem armor) { // TODO equippable - } - - components.put(DataComponentType.RARITY, javaItem.rarity().ordinal()); - - components.getDataComponents().putAll(definition.components().getDataComponents()); - return components; - } - - private static Consumable getItemConsumable(Item item) { - if (item == Items.APPLE || item == Items.BAKED_POTATO || item == Items.BEETROOT || item == Items.BEETROOT_SOUP || item == Items.BREAD - || item == Items.CARROT || item == Items.CHORUS_FRUIT || item == Items.COOKED_CHICKEN || item == Items.COOKED_COD - || item == Items.COOKED_MUTTON || item == Items.COOKED_PORKCHOP || item == Items.COOKED_RABBIT || item == Items.COOKED_SALMON - || item == Items.COOKIE || item == Items.ENCHANTED_GOLDEN_APPLE || item == Items.GOLDEN_APPLE || item == Items.GLOW_BERRIES - || item == Items.GOLDEN_CARROT || item == Items.MELON_SLICE || item == Items.MUSHROOM_STEW || item == Items.POISONOUS_POTATO - || item == Items.POTATO || item == Items.PUFFERFISH || item == Items.PUMPKIN_PIE || item == Items.RABBIT_STEW - || item == Items.BEEF || item == Items.CHICKEN || item == Items.COD || item == Items.MUTTON || item == Items.PORKCHOP - || item == Items.RABBIT || item == Items.ROTTEN_FLESH || item == Items.SPIDER_EYE || item == Items.COOKED_BEEF - || item == Items.SUSPICIOUS_STEW || item == Items.SWEET_BERRIES || item == Items.TROPICAL_FISH) { - return Consumables.DEFAULT_FOOD; - } else if (item == Items.POTION) { - return Consumables.DEFAULT_DRINK; - } else if (item == Items.HONEY_BOTTLE) { - return Consumables.HONEY_BOTTLE; - } else if (item == Items.OMINOUS_BOTTLE) { - return Consumables.OMINOUS_BOTTLE; - } else if (item == Items.DRIED_KELP) { - return Consumables.DRIED_KELP; - } - return null; - } - - private static boolean canAlwaysEat(Item item) { - return item == Items.CHORUS_FRUIT || item == Items.ENCHANTED_GOLDEN_APPLE || item == Items.GOLDEN_APPLE || item == Items.HONEY_BOTTLE || item == Items.SUSPICIOUS_STEW; + return javaItem.gatherComponents(definition.components()); } @SuppressWarnings("unchecked") @@ -568,13 +512,4 @@ public class CustomItemRegistryPopulator_v2 { } } } - - private static final class Consumables { - private static final Consumable DEFAULT_FOOD = new Consumable(1.6F, Consumable.ItemUseAnimation.EAT, BuiltinSound.ENTITY_GENERIC_EAT, true, List.of()); - private static final Consumable DEFAULT_DRINK = new Consumable(1.6F, Consumable.ItemUseAnimation.DRINK, BuiltinSound.ENTITY_GENERIC_DRINK, false, List.of()); - private static final Consumable HONEY_BOTTLE = new Consumable(2.0F, Consumable.ItemUseAnimation.DRINK, BuiltinSound.ITEM_HONEY_BOTTLE_DRINK, false, List.of()); - private static final Consumable OMINOUS_BOTTLE = new Consumable(2.0F, Consumable.ItemUseAnimation.DRINK, BuiltinSound.ITEM_HONEY_BOTTLE_DRINK, - false, List.of(new ConsumeEffect.PlaySound(BuiltinSound.ITEM_OMINOUS_BOTTLE_DISPOSE))); - private static final Consumable DRIED_KELP = new Consumable(0.8F, Consumable.ItemUseAnimation.EAT, BuiltinSound.ENTITY_GENERIC_EAT, false, List.of()); - } } diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/CustomItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/CustomItemTranslator.java index fdc90c215..539044c87 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/CustomItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/CustomItemTranslator.java @@ -25,103 +25,183 @@ package org.geysermc.geyser.translator.item; +import com.google.common.collect.Multimap; +import lombok.extern.slf4j.Slf4j; +import net.kyori.adventure.key.Key; +import org.cloudburstmc.protocol.bedrock.data.TrimMaterial; +import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition; +import org.geysermc.geyser.api.item.custom.v2.predicate.CustomItemPredicate; +import org.geysermc.geyser.api.item.custom.v2.predicate.ConditionPredicate; +import org.geysermc.geyser.api.item.custom.v2.predicate.RangeDispatchPredicate; +import org.geysermc.geyser.api.item.custom.v2.predicate.match.ChargeType; +import org.geysermc.geyser.api.item.custom.v2.predicate.MatchPredicate; +import org.geysermc.geyser.api.item.custom.v2.predicate.match.CustomModelDataString; +import org.geysermc.geyser.api.item.custom.v2.predicate.match.MatchPredicateProperty; +import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.level.JavaDimension; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.registry.RegistryEntryData; +import org.geysermc.geyser.util.MinecraftKey; +import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.ArmorTrim; import org.geysermc.mcprotocollib.protocol.data.game.item.component.CustomModelData; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType; import it.unimi.dsi.fastutil.Pair; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition; -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.Collection; import java.util.List; -import java.util.OptionalInt; +import java.util.function.Function; /** * This is only a separate class for testing purposes so we don't have to load in GeyserImpl in ItemTranslator. */ +@Slf4j public final class CustomItemTranslator { @Nullable - public static ItemDefinition getCustomItem(DataComponents components, ItemMapping mapping) { + public static ItemDefinition getCustomItem(GeyserSession session, int stackSize, DataComponents components, ItemMapping mapping) { if (components == null) { return null; } - List> customMappings = mapping.getCustomItemOptions(); - if (customMappings.isEmpty()) { + + Multimap> allCustomItems = mapping.getCustomItemDefinitions(); + if (allCustomItems == null) { return null; } - // TODO 1.21.4 - float customModelDataInt = 0; - CustomModelData customModelData = components.get(DataComponentType.CUSTOM_MODEL_DATA); - if (customModelData != null) { - if (!customModelData.floats().isEmpty()) { - customModelDataInt = customModelData.floats().get(0); - } + Key itemModel = components.getOrDefault(DataComponentType.ITEM_MODEL, MinecraftKey.key("air")); + System.out.println(itemModel + " is the model!"); + Collection> customItems = allCustomItems.get(itemModel); + if (customItems.isEmpty()) { + return null; } - boolean checkDamage = mapping.getJavaItem().defaultMaxDamage() > 0; - int damage = !checkDamage ? 0 : components.getOrDefault(DataComponentType.DAMAGE, 0); - boolean unbreakable = checkDamage && !isDamaged(components, damage); - - for (Pair 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 (checkDamage) { - if (unbreakable && options.unbreakable() == TriState.FALSE) { - continue; + for (Pair customModel : customItems) { + if (customModel.first().model().equals(itemModel)) { + boolean allMatch = true; + for (CustomItemPredicate predicate : customModel.first().predicates()) { + if (!predicateMatches(session, predicate, stackSize, components)) { + allMatch = false; + break; + } } - - OptionalInt damagePredicate = options.damagePredicate(); - if (damagePredicate.isPresent() && damage < damagePredicate.getAsInt()) { - continue; - } - } else { - if (options.unbreakable() != TriState.NOT_SET || options.damagePredicate().isPresent()) { - // These will never match on this item. 1.19.2 behavior - // Maybe move this to CustomItemRegistryPopulator since it'll be the same for every item? If so, add a test. - continue; + if (allMatch) { + return customModel.second(); } } - - OptionalInt customModelDataOption = options.customModelData(); - if (customModelDataOption.isPresent() && customModelDataInt < customModelDataOption.getAsInt()) { - continue; - } - - if (options.defaultItem()) { - return null; - } - - return mappingTypes.value(); } - return null; } - /* These two functions are based off their Mojmap equivalents from 1.19.2 */ + private static boolean predicateMatches(GeyserSession session, CustomItemPredicate predicate, int stackSize, DataComponents components) { + if (predicate instanceof ConditionPredicate condition) { + return switch (condition.property()) { + case BROKEN -> nextDamageWillBreak(components); + case DAMAGED -> isDamaged(components); + case CUSTOM_MODEL_DATA -> getCustomBoolean(components, condition.index()); + } == condition.expected(); + } else if (predicate instanceof MatchPredicate match) { // TODO not much of a fan of the casts here, find a solution for the types? + if (match.property() == MatchPredicateProperty.CHARGE_TYPE) { + ChargeType expected = (ChargeType) match.data(); + List charged = components.get(DataComponentType.CHARGED_PROJECTILES); + if (charged == null) { + return expected == ChargeType.NONE; + } else if (expected == ChargeType.ROCKET) { + for (ItemStack projectile : charged) { + if (projectile.getId() == Items.FIREWORK_ROCKET.javaId()) { + return true; + } + } + return false; + } + return true; + } else if (match.property() == MatchPredicateProperty.TRIM_MATERIAL) { + Key material = (Key) match.data(); + ArmorTrim trim = components.get(DataComponentType.TRIM); + if (trim == null || trim.material().isCustom()) { + return false; + } + RegistryEntryData registered = session.getRegistryCache().trimMaterials().entryById(trim.material().id()); + return registered != null && registered.key().equals(material); + } else if (match.property() == MatchPredicateProperty.CONTEXT_DIMENSION) { + Key dimension = (Key) match.data(); + RegistryEntryData registered = session.getRegistryCache().dimensions().entryByValue(session.getDimensionType()); + return registered != null && dimension.equals(registered.key()); + } else if (match.property() == MatchPredicateProperty.CUSTOM_MODEL_DATA) { + CustomModelDataString expected = (CustomModelDataString) match.data(); + return expected.value().equals(getSafeCustomModelData(components, CustomModelData::strings, expected.index())); + } + } else if (predicate instanceof RangeDispatchPredicate rangeDispatch) { + double propertyValue = switch (rangeDispatch.property()) { + case BUNDLE_FULLNESS -> { + List stacks = components.get(DataComponentType.BUNDLE_CONTENTS); + if (stacks == null) { + yield 0; + } + int bundleWeight = 0; + for (ItemStack stack : stacks) { + bundleWeight += stack.getAmount(); + } + yield bundleWeight; + } + case DAMAGE -> tryNormalize(rangeDispatch, components.get(DataComponentType.DAMAGE), components.get(DataComponentType.MAX_DAMAGE)); + case COUNT -> tryNormalize(rangeDispatch, stackSize, components.get(DataComponentType.MAX_STACK_SIZE)); + case CUSTOM_MODEL_DATA -> getCustomFloat(components, rangeDispatch.index()); + } * rangeDispatch.scale(); + return propertyValue >= rangeDispatch.threshold(); + } - private static boolean isDamaged(DataComponents components, int damage) { - return isDamagableItem(components) && damage > 0; + throw new IllegalStateException("Unimplemented predicate type"); } - private static boolean isDamagableItem(DataComponents components) { - // mapping.getMaxDamage > 0 should also be checked (return false if not true) but we already check prior to this function - Boolean unbreakable = components.get(DataComponentType.UNBREAKABLE); - // Tag must either not be present or be set to false - return unbreakable == null || !unbreakable; + private static boolean getCustomBoolean(DataComponents components, int index) { + Boolean b = getSafeCustomModelData(components, CustomModelData::flags, index); + return b != null && b; + } + + private static float getCustomFloat(DataComponents components, int index) { + Float f = getSafeCustomModelData(components, CustomModelData::floats, index); + return f == null ? 0.0F : f; + } + + private static T getSafeCustomModelData(DataComponents components, Function> listGetter, int index) { + CustomModelData modelData = components.get(DataComponentType.CUSTOM_MODEL_DATA); + if (modelData == null || index < 0) { + return null; + } + List list = listGetter.apply(modelData); + if (index < list.size()) { + return list.get(index); + } + return null; + } + + private static double tryNormalize(RangeDispatchPredicate predicate, @Nullable Integer value, @Nullable Integer max) { + if (value == null) { + return 0.0; + } else if (max == null) { + return value; + } else if (!predicate.normalizeIfPossible()) { + return Math.min(value, max); + } + return Math.max(0.0, Math.min(1.0, (double) value / max)); + } + + /* These three functions are based off their Mojmap equivalents from 1.21.3 */ + private static boolean nextDamageWillBreak(DataComponents components) { + return isDamageableItem(components) && components.getOrDefault(DataComponentType.DAMAGE, 0) >= components.getOrDefault(DataComponentType.MAX_DAMAGE, 0) - 1; + } + + private static boolean isDamaged(DataComponents components) { + return isDamageableItem(components) && components.getOrDefault(DataComponentType.DAMAGE, 0) > 0; + } + + private static boolean isDamageableItem(DataComponents components) { + return components.getOrDefault(DataComponentType.UNBREAKABLE, false) && components.getOrDefault(DataComponentType.MAX_DAMAGE, 0) > 0; } private CustomItemTranslator() { diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings index 8707dd144..452312f88 160000 --- a/core/src/main/resources/mappings +++ b/core/src/main/resources/mappings @@ -1 +1 @@ -Subproject commit 8707dd144b20632f4a2f4b5497d8e5fb211e6c93 +Subproject commit 452312f88317cce019b8f336f485ffa7b2c19557