Update custom item translator for 1.21.4, implement custom model data

This commit is contained in:
Eclipse 2024-12-06 13:02:00 +00:00
parent f6f6423d25
commit fd09a05aaf
No known key found for this signature in database
GPG key ID: 95E6998F82EC938A
3 changed files with 147 additions and 132 deletions

View file

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

View file

@ -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<Pair<CustomItemOptions, ItemDefinition>> customMappings = mapping.getCustomItemOptions();
if (customMappings.isEmpty()) {
Multimap<Key, Pair<CustomItemDefinition, ItemDefinition>> 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<Pair<CustomItemDefinition, ItemDefinition>> 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<CustomItemOptions, ItemDefinition> 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<CustomItemDefinition, ItemDefinition> 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<ItemStack> 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<TrimMaterial> 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<JavaDimension> 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<ItemStack> 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> T getSafeCustomModelData(DataComponents components, Function<CustomModelData, List<T>> listGetter, int index) {
CustomModelData modelData = components.get(DataComponentType.CUSTOM_MODEL_DATA);
if (modelData == null || index < 0) {
return null;
}
List<T> 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() {

@ -1 +1 @@
Subproject commit 8707dd144b20632f4a2f4b5497d8e5fb211e6c93
Subproject commit 452312f88317cce019b8f336f485ffa7b2c19557