diff --git a/paper/src/main/java/com/denizenscript/denizen/paper/PaperModule.java b/paper/src/main/java/com/denizenscript/denizen/paper/PaperModule.java index 016e6b9694..e01fc60551 100644 --- a/paper/src/main/java/com/denizenscript/denizen/paper/PaperModule.java +++ b/paper/src/main/java/com/denizenscript/denizen/paper/PaperModule.java @@ -6,6 +6,7 @@ import com.denizenscript.denizen.nms.interfaces.packets.PacketOutChat; import com.denizenscript.denizen.objects.EntityTag; import com.denizenscript.denizen.objects.ItemTag; +import com.denizenscript.denizen.paper.datacomponents.ComponentAdaptersRegistry; import com.denizenscript.denizen.paper.events.*; import com.denizenscript.denizen.paper.properties.*; import com.denizenscript.denizen.paper.tags.PaperTagBase; @@ -130,6 +131,12 @@ public static void init() { PropertyParser.registerProperty(EntityWitherInvulnerable.class, EntityTag.class); PropertyParser.registerProperty(ItemArmorStand.class, ItemTag.class); + // Component adapters + if (NMSHandler.getVersion().isAtLeast(NMSVersion.v1_21)) { + PropertyParser.registerProperty(ItemRemovedComponents.class, ItemTag.class); + ComponentAdaptersRegistry.register(); + } + // Paper object extensions PaperElementExtensions.register(); PaperEntityExtensions.register(); diff --git a/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/ComponentAdaptersRegistry.java b/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/ComponentAdaptersRegistry.java new file mode 100644 index 0000000000..ad691abf2b --- /dev/null +++ b/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/ComponentAdaptersRegistry.java @@ -0,0 +1,9 @@ +package com.denizenscript.denizen.paper.datacomponents; + +public class ComponentAdaptersRegistry { + + public static void register() { + DataComponentAdapter.register(new FoodAdapter()); + DataComponentAdapter.register(new GliderAdapter()); + } +} diff --git a/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/DataComponentAdapter.java b/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/DataComponentAdapter.java new file mode 100644 index 0000000000..660e59463c --- /dev/null +++ b/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/DataComponentAdapter.java @@ -0,0 +1,227 @@ +package com.denizenscript.denizen.paper.datacomponents; + +import com.denizenscript.denizen.objects.ItemTag; +import com.denizenscript.denizen.objects.properties.item.ItemComponentsPatch; +import com.denizenscript.denizen.objects.properties.item.ItemProperty; +import com.denizenscript.denizen.utilities.Utilities; +import com.denizenscript.denizencore.objects.Mechanism; +import com.denizenscript.denizencore.objects.ObjectTag; +import com.denizenscript.denizencore.objects.core.ElementTag; +import com.denizenscript.denizencore.objects.core.MapTag; +import com.denizenscript.denizencore.objects.properties.PropertyParser; +import com.denizenscript.denizencore.utilities.CoreUtilities; +import io.papermc.paper.datacomponent.DataComponentType; +import org.bukkit.Registry; +import org.bukkit.inventory.ItemStack; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +public abstract class DataComponentAdapter
{ + + // <--[language] + // @name Item Components + // @group Minecraft Logic + // @description + // Minecraft item components (see <@link url https://minecraft.wiki/w/Data_component_format>) are managed as follows: + // Each item type has a default set of component values; a food item will have food components by default, a tool item will have tool components by default, etc. + // Different items can override their type's default components, either by setting values that weren't there previously (e.g. making an inedible item edible), or by removing values that are there by default (e.g. making a shield item that can't block). + // Items' overrides can later be reset, making them use their type's default values again. + // + // In Denizen, different item components are represented by item properties. + // These properties allow both setting a component override on an item, and clearing/resetting it by providing no input. + // Item properties' name will generally match their respective item component's name, but not always! + // Due to this, features that take item component names as input (such as <@link tag ItemTag.is_overridden>) accept both Minecraft component names and Denizen property names. + // + // Here is an example of applying all of this in a script: + // + // # We define a default apple item + // - define apple + // # We remove the apple's "food" component, making eating it restore no food points (it is still consumable due to the "consumable" component). + // - adjust def:apple removed:food + // # This check will pass, as the apple's "food" component is overridden to have no value. + // - if <[apple].is_overridden[food]>: + // - narrate "The apple has a modified food component! It will behave differently to a normal apple." + // # We reset the apple item's food component by adjusting with no value, making it a normal apple. + // - adjust def:apple food: + // + // --> + + public static final Map COMPONENTS_BY_PROPERTY = new HashMap<>(); + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + + public static DataComponentType getComponentType(String name) { + String nameLower = CoreUtilities.toLowerCase(name); + DataComponentType componentType = Registry.DATA_COMPONENT_TYPE.get(Utilities.parseNamespacedKey(nameLower)); + if (componentType == null) { + componentType = COMPONENTS_BY_PROPERTY.get(nameLower); + } + return componentType; + } + + public static void register(DataComponentAdapter adapter) { + DataComponentAdapter.Property.currentlyRegisteringComponentAdapter = adapter; + PropertyParser.registerPropertyGetter( + item -> !item.getItemStack().isEmpty() ? adapter.new Property(item) : null, + ItemTag.class, EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY, DataComponentAdapter.Property.class); + DataComponentAdapter.Property.currentlyRegisteringComponentAdapter = null; + String componentName = adapter.componentType.key().asMinimalString(); + ItemComponentsPatch.registerHandledComponent(componentName); + if (!adapter.name.equals(componentName)) { + COMPONENTS_BY_PROPERTY.put(adapter.name, adapter.componentType); + } + } + + static { + + // <--[tag] + // @attribute ]> + // @returns ElementTag(Boolean) + // @description + // Returns whether an item has a specific item component type overridden, see <@link language Item Components>. + // --> + ItemTag.tagProcessor.registerTag(ElementTag.class, ElementTag.class, "is_overridden", (attribute, object, param) -> { + DataComponentType componentType = getComponentType(param.asString()); + if (componentType == null) { + attribute.echoError("Invalid type specified, must be a valid item component type or property name."); + return null; + } + return new ElementTag(object.getItemStack().isDataOverridden(componentType)); + }); + } + + public final CT componentType; + public final Class
denizenType; + public final String name; + + public DataComponentAdapter(CT componentType, Class
denizenType, String name) { + this.componentType = componentType; + this.denizenType = denizenType; + this.name = name; + } + + public abstract DT getValue(ItemStack item); + + public abstract void setValue(ItemStack item, DT value, Mechanism mechanism); + + public boolean isDefaultValue(DT value) { + return false; + } + + public static abstract class NonValued extends DataComponentAdapter { + + public NonValued(DataComponentType.NonValued componentType, String name) { + super(componentType, ElementTag.class, name); + } + + @Override + public ElementTag getValue(ItemStack item) { + return new ElementTag(item.hasData(componentType)); + } + + @Override + public void setValue(ItemStack item, ElementTag value, Mechanism mechanism) { + if (!mechanism.requireBoolean()) { + return; + } + if (value.asBoolean()) { + item.setData(componentType); + } + else { + item.unsetData(componentType); + } + } + + // Overridden and false = removed, managed by ItemTag.removed + @Override + public boolean isDefaultValue(ElementTag value) { + return !value.asBoolean(); + } + } + + public static abstract class Valued extends DataComponentAdapter> { + + public static void setIfValid(Consumer setter, MapTag data, String key, String type, Predicate checker, Function converter, Mechanism mechanism) { + ElementTag value = data.getElement(key); + if (value == null) { + return; + } + T converted; + if (!checker.test(value) || (converted = converter.apply(value)) == null) { + mechanism.echoError("Invalid '" + key + "' specified: must be a " + type + '.'); + return; + } + setter.accept(converted); + } + + public Valued(Class denizenType, DataComponentType.Valued componentType, String name) { + super(componentType, denizenType, name); + } + + public abstract TD toDenizen(TP value); + + public abstract TP toPaper(TD value, Mechanism mechanism); + + @Override + public TD getValue(ItemStack item) { + TP data = item.getData(componentType); + return data != null ? toDenizen(data) : null; + } + + @Override + public void setValue(ItemStack item, TD value, Mechanism mechanism) { + TP converted = toPaper(value, mechanism); + if (converted != null) { + item.setData(componentType, converted); + } + } + } + + public class Property extends ItemProperty
{ + + private static DataComponentAdapter currentlyRegisteringComponentAdapter; + + public Property(ItemTag item) { + this.object = item; + } + + @Override + public DT getPropertyValue() { + return getValue(getItemStack()); + } + + @Override + public DT getPropertyValueNoDefault() { + if (!getItemStack().isDataOverridden(componentType)) { + return null; + } + return super.getPropertyValueNoDefault(); + } + + @Override + public boolean isDefaultValue(DT value) { + return DataComponentAdapter.this.isDefaultValue(value); + } + + @Override + public void setPropertyValue(DT value, Mechanism mechanism) { + if (value == null) { + getItemStack().resetData(componentType); + return; + } + setValue(getItemStack(), value, mechanism); + } + + @Override + public String getPropertyId() { + return name; + } + + public static void register() { + autoRegisterNullable(currentlyRegisteringComponentAdapter.name, DataComponentAdapter.Property.class, currentlyRegisteringComponentAdapter.denizenType, false); + } + } +} diff --git a/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/FoodAdapter.java b/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/FoodAdapter.java new file mode 100644 index 0000000000..078857314a --- /dev/null +++ b/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/FoodAdapter.java @@ -0,0 +1,46 @@ +package com.denizenscript.denizen.paper.datacomponents; + +import com.denizenscript.denizencore.objects.Mechanism; +import com.denizenscript.denizencore.objects.core.ElementTag; +import com.denizenscript.denizencore.objects.core.MapTag; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.FoodProperties; + +public class FoodAdapter extends DataComponentAdapter.Valued { + + // <--[property] + // @object ItemTag + // @name food + // @input MapTag + // @description + // Controls an item's food <@link language Item Components>. + // The map includes keys: + // - "nutrition", ElementTag(Number) representing the amount of food points restored by this item. + // - "saturation", ElementTag(Decimal) representing the amount of saturation points restored by this item. + // - "can_always_eat", ElementTag(Boolean) controlling whether the item can always be eaten, even if the player isn't hungry. + // @mechanism + // Provide no input to reset the item to its default value. + // --> + + public FoodAdapter() { + super(MapTag.class, DataComponentTypes.FOOD, "food"); + } + + @Override + public MapTag toDenizen(FoodProperties value) { + MapTag foodData = new MapTag(); + foodData.putObject("nutrition", new ElementTag(value.nutrition())); + foodData.putObject("saturation", new ElementTag(value.saturation())); + foodData.putObject("can_always_eat", new ElementTag(value.canAlwaysEat())); + return foodData; + } + + @Override + public FoodProperties toPaper(MapTag value, Mechanism mechanism) { + FoodProperties.Builder builder = FoodProperties.food(); + setIfValid(builder::nutrition, value, "nutrition", "number", ElementTag::isInt, ElementTag::asInt, mechanism); + setIfValid(builder::saturation, value, "saturation", "decimal number", ElementTag::isFloat, ElementTag::asFloat, mechanism); + setIfValid(builder::canAlwaysEat, value, "can_always_eat", "boolean", ElementTag::isBoolean, ElementTag::asBoolean, mechanism); + return builder.build(); + } +} diff --git a/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/GliderAdapter.java b/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/GliderAdapter.java new file mode 100644 index 0000000000..838f02ca72 --- /dev/null +++ b/paper/src/main/java/com/denizenscript/denizen/paper/datacomponents/GliderAdapter.java @@ -0,0 +1,20 @@ +package com.denizenscript.denizen.paper.datacomponents; + +import io.papermc.paper.datacomponent.DataComponentTypes; + +public class GliderAdapter extends DataComponentAdapter.NonValued { + + // <--[property] + // @object ItemTag + // @name glider + // @input ElementTag(Boolean) + // @description + // Controls whether an item can be used to glide when equipped (like elytras by default), see <@link language Item Components>. + // @mechanism + // Provide no input to reset the item to its default value. + // --> + + public GliderAdapter() { + super(DataComponentTypes.GLIDER, "glider"); + } +} diff --git a/paper/src/main/java/com/denizenscript/denizen/paper/properties/ItemRemovedComponents.java b/paper/src/main/java/com/denizenscript/denizen/paper/properties/ItemRemovedComponents.java new file mode 100644 index 0000000000..f4b6b17215 --- /dev/null +++ b/paper/src/main/java/com/denizenscript/denizen/paper/properties/ItemRemovedComponents.java @@ -0,0 +1,66 @@ +package com.denizenscript.denizen.paper.properties; + +import com.denizenscript.denizen.objects.ItemTag; +import com.denizenscript.denizen.objects.properties.item.ItemProperty; +import com.denizenscript.denizen.paper.datacomponents.DataComponentAdapter; +import com.denizenscript.denizencore.objects.Mechanism; +import com.denizenscript.denizencore.objects.core.ElementTag; +import com.denizenscript.denizencore.objects.core.ListTag; +import io.papermc.paper.datacomponent.DataComponentType; + +public class ItemRemovedComponents extends ItemProperty { + + // <--[property] + // @object ItemTag + // @name removed_components + // @input ListTag + // @description + // Controls the item components explicitly removed from an item. + // This can be used to remove item's default behavior, such as making consumable items non-consumable. + // See also <@link language Item Components>. + // --> + + public static boolean describes(ItemTag item) { + return !item.getItemStack().isEmpty(); + } + + public boolean isRemoved(DataComponentType componentType) { + return getItemStack().isDataOverridden(componentType) && !getItemStack().hasData(componentType); + } + + @Override + public ListTag getPropertyValue() { + return new ListTag(getMaterial().getDefaultDataTypes(), this::isRemoved, componentType -> new ElementTag(componentType.key().asMinimalString(), true)); + } + + @Override + public boolean isDefaultValue(ListTag value) { + return value.isEmpty(); + } + + @Override + public void setPropertyValue(ListTag value, Mechanism mechanism) { + for (DataComponentType componentType : getMaterial().getDefaultDataTypes()) { + if (isRemoved(componentType)) { + getItemStack().resetData(componentType); + } + } + for (String input : value) { + DataComponentType componentType = DataComponentAdapter.getComponentType(input); + if (componentType == null) { + mechanism.echoError("Invalid type to remove '" + input + "' specified: must be a valid property or item component name."); + continue; + } + getItemStack().unsetData(componentType); + } + } + + @Override + public String getPropertyId() { + return "removed_components"; + } + + public static void register() { + autoRegister("removed_components", ItemRemovedComponents.class, ListTag.class, false); + } +} diff --git a/v1_21/src/main/java/com/denizenscript/denizen/nms/v1_21/helpers/ItemHelperImpl.java b/v1_21/src/main/java/com/denizenscript/denizen/nms/v1_21/helpers/ItemHelperImpl.java index cd0972113a..50c9b133ef 100644 --- a/v1_21/src/main/java/com/denizenscript/denizen/nms/v1_21/helpers/ItemHelperImpl.java +++ b/v1_21/src/main/java/com/denizenscript/denizen/nms/v1_21/helpers/ItemHelperImpl.java @@ -1,5 +1,6 @@ package com.denizenscript.denizen.nms.v1_21.helpers; +import com.denizenscript.denizen.Denizen; import com.denizenscript.denizen.nms.interfaces.ItemHelper; import com.denizenscript.denizen.nms.util.PlayerProfile; import com.denizenscript.denizen.nms.v1_21.Handler; @@ -452,6 +453,12 @@ public MapTag getRawComponentsPatch(ItemStack item, boolean excludeHandled) { } RegistryOps registryOps = CraftRegistry.getMinecraftRegistry().createSerializationContext(NbtOps.INSTANCE); CompoundTag nmsPatch = (CompoundTag) DataComponentPatch.CODEC.encodeStart(registryOps, patch).getOrThrow(); + if (excludeHandled && Denizen.supportsPaper) { + nmsPatch.keySet().removeIf(s -> s.charAt(0) == '!'); + if (nmsPatch.isEmpty()) { + return new MapTag(); + } + } MapTag rawComponents = (MapTag) ItemRawNBT.nbtTagToObject(NBTAdapter.toAPI(nmsPatch)); rawComponents.putObject(ItemComponentsPatch.DATA_VERSION_KEY, new ElementTag(CraftMagicNumbers.INSTANCE.getDataVersion())); return rawComponents;