diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java index 19f743f2b77..acf740e24ac 100644 --- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java @@ -41,29 +41,34 @@ /** * Called on Geyser's startup when looking for custom items. Custom items must be registered through this event. *

- * This event will not be called if the "add non-Bedrock items" setting is disabled in the Geyser config. + * This event will not be called if the "add-non-bedrock-items" setting is disabled in the Geyser config. */ public interface GeyserDefineCustomItemsEvent extends Event { /** - * A multimap of all the already registered (using the deprecated method) custom items indexed by the item's extended java item's identifier. + * A multimap of all the already registered custom items indexed by the item's extended java item's identifier. + * The map returned here will only contain items registered with the deprecated + * {@link GeyserDefineCustomItemsEvent#register(String, CustomItemData)} method. * - * @deprecated use {@link GeyserDefineCustomItemsEvent#customItemDefinitions()} + * @deprecated replaced with {@link GeyserDefineCustomItemsEvent#customItemDefinitions()} */ @Deprecated @NonNull Map> getExistingCustomItems(); /** - * A multimap of all the already registered custom item definitions indexed by the item's extended java item's identifier. + * A multimap of all the already registered custom item definitions + * indexed by the identifier of the Java item which the item is based on. */ @NonNull Map> customItemDefinitions(); /** - * A list of the already registered (using the deprecated method) non-vanilla custom items. + * A list of the already registered non-vanilla custom items. + * The map returned here will only contain items registered with the deprecated + * {@link GeyserDefineCustomItemsEvent#register(NonVanillaCustomItemData)} method. * - * @deprecated use {@link GeyserDefineCustomItemsEvent#nonVanillaCustomItemDefinitions()} + * @deprecated replaced with {@link GeyserDefineCustomItemsEvent#nonVanillaCustomItemDefinitions()} */ @Deprecated @NonNull diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java index 999f70a6df2..e8d84edab15 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java @@ -32,13 +32,13 @@ 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.NonVanillaCustomItemDefinition; -import org.geysermc.geyser.api.item.custom.v2.component.BlockPlacer; -import org.geysermc.geyser.api.item.custom.v2.component.Chargeable; -import org.geysermc.geyser.api.item.custom.v2.component.Consumable; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; -import org.geysermc.geyser.api.item.custom.v2.component.Equippable; -import org.geysermc.geyser.api.item.custom.v2.component.FoodProperties; -import org.geysermc.geyser.api.item.custom.v2.component.GeyserDataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.BlockPlacer; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.Chargeable; +import org.geysermc.geyser.api.item.custom.v2.component.java.Consumable; +import org.geysermc.geyser.api.item.custom.v2.component.java.Equippable; +import org.geysermc.geyser.api.item.custom.v2.component.java.FoodProperties; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.GeyserDataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; import org.geysermc.geyser.api.util.CreativeCategory; import org.geysermc.geyser.api.util.Identifier; @@ -203,39 +203,39 @@ default NonVanillaCustomItemDefinition.Builder toDefinition() { .tags(tags().stream().map(Identifier::of).collect(Collectors.toSet())) .protectionValue(protectionValue()) ) - .component(DataComponent.MAX_STACK_SIZE, stackSize()) - .component(DataComponent.MAX_DAMAGE, maxDamage()) + .component(ItemDataComponents.MAX_STACK_SIZE, stackSize()) + .component(ItemDataComponents.MAX_DAMAGE, maxDamage()) .component(GeyserDataComponent.ATTACK_DAMAGE, attackDamage()) .translationString(translationString()); if (isHat()) { - definition.component(DataComponent.EQUIPPABLE, new Equippable(Equippable.EquipmentSlot.HEAD)); + definition.component(ItemDataComponents.EQUIPPABLE, Equippable.builder().slot(Equippable.EquipmentSlot.HEAD).build()); } else if (armorType() != null) { switch (armorType()) { - case "helmet" -> definition.component(DataComponent.EQUIPPABLE, new Equippable(Equippable.EquipmentSlot.HEAD)); - case "chestplate" -> definition.component(DataComponent.EQUIPPABLE, new Equippable(Equippable.EquipmentSlot.CHEST)); - case "leggings" -> definition.component(DataComponent.EQUIPPABLE, new Equippable(Equippable.EquipmentSlot.LEGS)); - case "boots" -> definition.component(DataComponent.EQUIPPABLE, new Equippable(Equippable.EquipmentSlot.FEET)); + case "helmet" -> definition.component(ItemDataComponents.EQUIPPABLE, Equippable.builder().slot(Equippable.EquipmentSlot.HEAD)); + case "chestplate" -> definition.component(ItemDataComponents.EQUIPPABLE, Equippable.builder().slot(Equippable.EquipmentSlot.CHEST)); + case "leggings" -> definition.component(ItemDataComponents.EQUIPPABLE, Equippable.builder().slot(Equippable.EquipmentSlot.LEGS)); + case "boots" -> definition.component(ItemDataComponents.EQUIPPABLE, Equippable.of(Equippable.EquipmentSlot.FEET)); } } if (isEdible()) { - definition.component(DataComponent.CONSUMABLE, new Consumable(1.6F, Consumable.Animation.EAT)); // Default values + definition.component(ItemDataComponents.CONSUMABLE, Consumable.builder().consumeSeconds(1.6F).animation(Consumable.Animation.EAT)); // Default values if (canAlwaysEat()) { - definition.component(DataComponent.FOOD, new FoodProperties(0, 0, true)); + definition.component(ItemDataComponents.FOOD, FoodProperties.builder().canAlwaysEat(true)); } } if (isChargeable() && toolType() != null) { if (toolType().equals("bow")) { - definition.component(GeyserDataComponent.CHARGEABLE, new Chargeable(1.0F, true, Identifier.of("arrow"))); + definition.component(GeyserDataComponent.CHARGEABLE, Chargeable.builder().maxDrawDuration(1.0F).chargeOnDraw(true).ammunition(Identifier.of("arrow"))); } else { - definition.component(GeyserDataComponent.CHARGEABLE, new Chargeable(0.0F, false, Identifier.of("arrow"))); + definition.component(GeyserDataComponent.CHARGEABLE, Chargeable.builder().ammunition(Identifier.of("arrow"))); } } if (block() != null) { - definition.component(GeyserDataComponent.BLOCK_PLACER, new BlockPlacer(Identifier.of(block()), false)); + definition.component(GeyserDataComponent.BLOCK_PLACER, BlockPlacer.builder().block(Identifier.of(block()))); } return definition; diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/CustomItemDefinition.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/CustomItemDefinition.java index d65bfde5493..f2f44770850 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/CustomItemDefinition.java +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/CustomItemDefinition.java @@ -26,15 +26,19 @@ package org.geysermc.geyser.api.item.custom.v2; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; import org.geysermc.geyser.api.GeyserApi; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponentMap; import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.DataComponentMap; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; import org.geysermc.geyser.api.predicate.MinecraftPredicate; import org.geysermc.geyser.api.predicate.PredicateStrategy; import org.geysermc.geyser.api.predicate.context.item.ItemPredicateContext; +import org.geysermc.geyser.api.util.GenericBuilder; import org.geysermc.geyser.api.util.Identifier; import java.util.List; +import java.util.Objects; /** * This is used to define a custom item and its properties for a specific Java item and item model definition combination. @@ -59,11 +63,15 @@ public interface CustomItemDefinition { /** * The Bedrock identifier for this custom item. It cannot be in the {@code minecraft} namespace. + * + * @return the Bedrock item identifier */ @NonNull Identifier bedrockIdentifier(); /** * The display name of the item. If none is set, the display name is taken from the item's Bedrock identifier. + * + * @return the display name shown to Bedrock clients */ @NonNull String displayName(); @@ -71,6 +79,8 @@ public interface CustomItemDefinition { * The item model this definition is for. If the model is in the {@code minecraft} namespace, then the definition must have at least one predicate. * *

If multiple item definitions for a model are registered, then only one can have no predicate.

+ * + * @return the identifier of the Java item model */ @NonNull Identifier model(); @@ -82,6 +92,8 @@ public interface CustomItemDefinition { * *

{@code my_datapack:my_custom_item} => {@code my_datapack.my_custom_item}

*

{@code my_datapack:cool_items/cool_item_1} => {@code my_datapack.cool_items_cool_item_1}

+ * + * @return the icon shown to Bedrock players */ default @NonNull String icon() { String setIcon = bedrockOptions().icon(); @@ -103,7 +115,7 @@ public interface CustomItemDefinition { /** * The predicate strategy to be used. Determines if one of, or all of the predicates have to pass for this item definition to be used. Defaults to {@link PredicateStrategy#AND}. */ - PredicateStrategy predicateStrategy(); + @NonNull PredicateStrategy predicateStrategy(); /** * The priority of this definition. For all definitions for a single Java item model, definitions with a higher priority will be matched first. Defaults to 0. @@ -121,16 +133,16 @@ public interface CustomItemDefinition { *

Currently, the following components are (somewhat) supported:

* * * *

Note: some components, for example {@code minecraft:rarity} and {@code minecraft:attribute_modifiers}, are translated automatically, and do not have to be specified here.

@@ -148,33 +160,131 @@ public interface CustomItemDefinition { */ @NonNull List removedComponents(); - static Builder builder(Identifier bedrockIdentifier, Identifier itemModel) { + /** + * Creates a builder for the custom item definition. + * + * @param bedrockIdentifier the Bedrock item identifier + * @param itemModel the Java item model identifier + * @return a new builder + */ + static Builder builder(@NonNull Identifier bedrockIdentifier, @NonNull Identifier itemModel) { return GeyserApi.api().provider(Builder.class, bedrockIdentifier, itemModel); } - interface Builder { + /** + * The builder for the custom item definition. + */ + interface Builder extends GenericBuilder { + /** + * Sets the display name, as shown to the Bedrock client. + * When not set, the display name will be derived from the Bedrock item identifier. + * + * @param displayName the display name to show for Bedrock clients. + * @return this builder + */ + @This Builder displayName(@NonNull String displayName); + /** + * Sets the priority of this definition, used for definition matching. + * @see CustomItemDefinition#priority() + * + * @param priority the priority + * @return this builder + */ + @This Builder priority(int priority); + /** + * Sets the Bedrock item options for this definition. + * Those determine the icon seen on Bedrock edition, whether the item + * can be placed in the offhand slot, and other options. + * + * @see CustomItemBedrockOptions + * @param options the bedrock item options + * @return this builder + */ + @This Builder bedrockOptions(CustomItemBedrockOptions.@NonNull Builder options); + /** + * Adds a predicate that must match for Geyser to use this item definition. + * See {@link CustomItemDefinition#predicates()} for details. + * + * @param predicate a predicate that must match for this item to be used + * @return this builder + */ + @This Builder predicate(@NonNull MinecraftPredicate predicate); + /** + * Sets the predicate strategy that should be used for item definition matching. + * + * @param strategy the predicate strategy to use + * @return this builder + */ + @This Builder predicateStrategy(@NonNull PredicateStrategy strategy); + /** + * Sets data components that determine the item behavior. These are assumed to also be + * present server-side on the Java server. See {@link CustomItemDefinition#components()} + * for more information. + * + * @param component the type of the component - found in {@link ItemDataComponents} + * @param value the value of the component + * @return this builder + * @param the value held by the component + */ + @This Builder component(@NonNull DataComponent component, @NonNull T value); - Builder removeComponent(Identifier component); + /** + * Convenience method for {@link CustomItemDefinition.Builder#component(DataComponent, Object)} + * + * @param component the type of the component - found in {@link ItemDataComponents} + * @param builder the builder of the component + * @return this builder + * @param the value held by the component + */ + @This + default Builder component(@NonNull DataComponent component, @NonNull GenericBuilder builder) { + return component(component, builder.build()); + } + + /** + * Indicates a removed item component that will not be present on the custom item despite + * existing on the vanilla item. This must match server-side behavior, otherwise, issues + * will occur. See {@link CustomItemDefinition#removedComponents()} for more information. + * + * @param component the identifier of the vanilla base component to remove + * @return this builder + */ + @This + Builder removeComponent(@NonNull Identifier component); - default Builder removeComponent(DataComponent component) { + /** + * Convenience method for {@link CustomItemDefinition.Builder#removeComponent(Identifier)}. + * + * @param component the component type to remove + * @return this builder + */ + @This + default Builder removeComponent(@NonNull DataComponent component) { + Objects.requireNonNull(component); if (!component.vanilla()) { throw new IllegalArgumentException("Cannot remove non-vanilla component"); } return removeComponent(component.identifier()); } + /** + * Creates the custom item definition. + * + * @return the new custom item definition + */ + @Override CustomItemDefinition build(); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/NonVanillaCustomItemDefinition.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/NonVanillaCustomItemDefinition.java index 00d820a94e3..4e20f9e2c5f 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/NonVanillaCustomItemDefinition.java +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/NonVanillaCustomItemDefinition.java @@ -29,6 +29,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.GeyserDataComponent; import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; import org.geysermc.geyser.api.item.custom.v2.component.DataComponentMap; import org.geysermc.geyser.api.predicate.MinecraftPredicate; @@ -39,13 +40,13 @@ import java.util.List; /** - * Defines an entirely custom item, introduced by mods and therefore not based on a vanilla item, and its properties. + * Defines an entirely custom item, introduced by mods and therefore not based on a vanilla item and its properties. * *

A definition will be used when an item is received with the ID of the definition. Predicate matching, as is possible * right now with vanilla custom item definitions, is currently not implemented, so only one definition can be created for each * Java non-vanilla item.

* - *

Non-vanilla item definitions can be configured with additional components defined in {@link org.geysermc.geyser.api.item.custom.v2.component.GeyserDataComponent}.

+ *

Non-vanilla item definitions can be configured with additional components defined in {@link GeyserDataComponent}.

*/ public interface NonVanillaCustomItemDefinition extends CustomItemDefinition { @@ -80,6 +81,7 @@ public interface NonVanillaCustomItemDefinition extends CustomItemDefinition { *

Trying to use predicates will result in an error.

*/ @Override + @NonNull PredicateStrategy predicateStrategy(); /** @@ -91,23 +93,23 @@ public interface NonVanillaCustomItemDefinition extends CustomItemDefinition { int priority(); /** - * On top of vanilla Minecraft's item components, custom ones defined by Geyser in {@link org.geysermc.geyser.api.item.custom.v2.component.GeyserDataComponent} can + * On top of vanilla Minecraft's item components, custom ones defined by Geyser in {@link GeyserDataComponent} can * also be used. Like with vanilla data components, it is still expected that the item always has the behaviour defined by its components. * *

Default component removals are not supported for non-vanilla items, since here the data component map defines default components, instead of * a patch on top of a vanilla base item.

* * @see CustomItemDefinition#components() - * @see org.geysermc.geyser.api.item.custom.v2.component.GeyserDataComponent + * @see GeyserDataComponent */ @Override @NonNull DataComponentMap components(); - static Builder builder(Identifier javaIdentifier, int javaId) { + static Builder builder(@NonNull Identifier javaIdentifier, int javaId) { return builder(javaIdentifier, javaIdentifier, javaId); } - static Builder builder(Identifier javaIdentifier, Identifier bedrockIdentifier, int javaId) { + static Builder builder(@NonNull Identifier javaIdentifier, @NonNull Identifier bedrockIdentifier, int javaId) { return GeyserApi.api().provider(Builder.class, javaIdentifier, bedrockIdentifier, javaId); } diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Consumable.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Consumable.java deleted file mode 100644 index 561b2dc7c42..00000000000 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Consumable.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2025 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.api.item.custom.v2.component; - -import org.checkerframework.checker.index.qual.Positive; - -public record Consumable(@Positive float consumeSeconds, Animation animation) { - - public Consumable { - if (consumeSeconds <= 0.0F) { - throw new IllegalArgumentException("Consume seconds must be above 0"); - } - } - - /** - * Not all animations work perfectly on bedrock. Bedrock behaviour is noted per animation. The {@code toot_horn} animation doesn't exist on bedrock, and is therefore not listed here. - */ - public enum Animation { - /** - * Does nothing in 1st person, appears as eating in 3rd person. - */ - NONE, - /** - * Appears to look correctly. - */ - EAT, - /** - * Appears to look correctly. - */ - DRINK, - /** - * Does nothing in 1st person, eating in 3rd person. - */ - BLOCK, - /** - * Does nothing in 1st person, eating in 3rd person. - */ - BOW, - /** - * Does nothing in 1st person, but looks like spear in 3rd person. - */ - SPEAR, - /** - * Does nothing in 1st person, eating in 3rd person. - */ - CROSSBOW, - /** - * Does nothing in 1st person, but looks like spyglass in 3rd person. - */ - SPYGLASS, - /** - * Brush in 1st and 3rd person. Will look weird when not displayed handheld. - */ - BRUSH - } -} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/DataComponent.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/DataComponent.java index 91486d3bb8f..32ecc6b8b6e 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/DataComponent.java +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/DataComponent.java @@ -25,125 +25,44 @@ package org.geysermc.geyser.api.item.custom.v2.component; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.GeyserDataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; import org.geysermc.geyser.api.util.Identifier; import java.util.function.Predicate; /** - * Data components are used to indicate item behaviour of custom items. It is expected that any components set on a {@link CustomItemDefinition} are always present on the item server-side. + * Data components are used to indicate item behaviour of custom items. + * It is expected that any components set on a {@link CustomItemDefinition} are always present on the item server-side. * + * @see ItemDataComponents * @see GeyserDataComponent * @see CustomItemDefinition#components() */ -public final class DataComponent { - /** - * Marks the item as consumable. Of this component, only {@code consume_seconds} and {@code animation} properties are translated. Consume effects are done server side, - * and consume sounds and particles aren't possible. - * - *

Note that due to a bug on Bedrock, not all consume animations appear perfectly. See {@link Consumable.Animation}

- * - * @see Consumable - */ - public static final DataComponent CONSUMABLE = create("consumable"); - /** - * Marks the item as equippable. Of this component, only the {@code slot} property is translated. Other properties are done server-side, are done differently on Bedrock (e.g. {@code asset_id} is done via attachables), - * or are not possible on Bedrock at all (e.g. {@code camera_overlay}). - * - *

Note that on Bedrock, equippables can't have a stack size above 1.

- * - * @see Equippable - */ - public static final DataComponent EQUIPPABLE = create("equippable"); - /** - * Food properties of the item. All properties properly translate over to Bedrock. - * - * @see FoodProperties - */ - public static final DataComponent FOOD = create("food"); - /** - * Max damage value of the item. Must be at or above 0. Items with a max damage value above 0 can't have a stack size above 1. - */ - public static final DataComponent MAX_DAMAGE = create("max_damage", i -> i >= 0); - /** - * Max stack size of the item. Must be between 1 and 99. Items with a max stack size value above 1 can't have a max damage value above 0. - */ - public static final DataComponent MAX_STACK_SIZE = create("max_stack_size", i -> i >= 1 && i <= 99); // Reverse lambda - /** - * Marks the item to have a use cooldown. To properly function, the item must be able to be used: it must be consumable or have some other kind of use logic. - * - *

The cooldown group can be {@code null}, in this case the identifier of the vanilla item (in case of vanilla custom items), - * or the item itself (in case of non-vanilla custom items) will be used.

- * - * @see UseCooldown - */ - public static final DataComponent USE_COOLDOWN = create("use_cooldown"); +public interface DataComponent { + /** - * Marks the item to be enchantable. Must be at or above 0. + * The identifier of the data component. * - *

This component does not translate over perfectly, due to the way enchantments work on Bedrock. The component will be mapped to the {@code minecraft:enchantable} bedrock component with {@code slot=all}. - * This should, but does not guarantee, allow for compatibility with vanilla enchantments. Non-vanilla enchantments are unlikely to work.

+ * @return the identifier */ - public static final DataComponent ENCHANTABLE = create("enchantable", i -> i >= 0); + @NonNull + Identifier identifier(); + /** - * This component is only used for the {@link ToolProperties#canDestroyBlocksInCreative()} option. + * The predicate used to validate the component. * - *

Like other components, when not set this will fall back to the default value.

- * - * @see ToolProperties + * @return the validator */ - public static final DataComponent TOOL = create("tool"); + @NonNull + Predicate validator(); + /** - * Marks which items can be used to repair the item. + * Whether the component exists in vanilla Minecraft. * - * @see Repairable + * @return whether this component is vanilla */ - public static final DataComponent REPAIRABLE = create("repairable"); - /** - * Overrides the item's enchantment glint. - */ - public static final DataComponent ENCHANTMENT_GLINT_OVERRIDE = create("enchantment_glint_override"); - - private final Identifier identifier; - private final Predicate validator; - private final boolean vanilla; - - private DataComponent(Identifier identifier, Predicate validator, boolean vanilla) { - this.identifier = identifier; - this.validator = validator; - this.vanilla = vanilla; - } - - private static DataComponent create(String name) { - return new DataComponent<>(Identifier.of(name), t -> true, true); - } - - private static DataComponent create(String name, Predicate validator) { - return new DataComponent<>(Identifier.of(name), validator, true); - } - - static DataComponent createGeyser(String name) { - return new DataComponent<>(Identifier.of("geyser", name), t -> true, false); - } - - static DataComponent createGeyser(String name, Predicate validator) { - return new DataComponent<>(Identifier.of("geyser", name), validator, false); - } - - public Identifier identifier() { - return identifier; - } - - public boolean vanilla() { - return vanilla; - } - - public boolean validate(T value) { - return validator.test(value); - } - - @Override - public String toString() { - return "data component " + identifier.toString() + (vanilla ? "" : " (not vanilla)"); - } + boolean vanilla(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Equippable.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Equippable.java deleted file mode 100644 index b0589fd77ce..00000000000 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Equippable.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2025 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.api.item.custom.v2.component; - -public record Equippable(EquipmentSlot slot) { - - public enum EquipmentSlot { - HEAD, - CHEST, - LEGS, - FEET, - BODY, - SADDLE - } -} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/ToolProperties.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/ToolProperties.java deleted file mode 100644 index eb04e09daab..00000000000 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/ToolProperties.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2025 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.api.item.custom.v2.component; - -public record ToolProperties(boolean canDestroyBlocksInCreative) { -} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/geyser/BlockPlacer.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/geyser/BlockPlacer.java new file mode 100644 index 00000000000..3f0e2650828 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/geyser/BlockPlacer.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025 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.api.item.custom.v2.component.geyser; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.util.GenericBuilder; +import org.geysermc.geyser.api.util.Identifier; + +/** + * Allows modifying items so these can place blocks or take on the + * icon of the block they place. + */ +public interface BlockPlacer { + + /** + * The block placed by the item, used by the + * Bedrock client to predict block placing. + * This is a Bedrock edition block identifier. + * + * @return the identifier of the block to place + */ + @NonNull Identifier block(); + + /** + * Whether to use the block's rendering + * as the icon for the item. Defaults to {@code false}. + * + * @return whether to use the 3d block rendering for the + * item icon + */ + boolean useBlockIcon(); + + /** + * Creates a builder for the block placer component. + * + * @return a new builder + */ + static Builder builder() { + return GeyserApi.api().provider(BlockPlacer.Builder.class); + } + + /** + * Creates a block placer component. + * + * @param block the identifier of the block to place + * @param useBlockIcon whether to use the 3d block rendering for the item icon + * @return the block placer component + */ + static BlockPlacer of(Identifier block, boolean useBlockIcon) { + return BlockPlacer.builder().block(block).useBlockIcon(useBlockIcon).build(); + } + + /** + * Builder for the block placer component. + */ + interface Builder extends GenericBuilder { + + /** + * The identifier of the block to place. + * This should be the block identifier as it is + * known to the Bedrock client. + * + * @param block the identifier of the block + * @see BlockPlacer#block() + * @return this builder + */ + @This + Builder block(@NonNull Identifier block); + + /** + * Whether to use the block's icon over the item icon. + * Block items have a 3d-generated block icon. + * + * @param useBlockIcon whether to use the block icon + * @see BlockPlacer#useBlockIcon() + * @return this builder + */ + @This + Builder useBlockIcon(boolean useBlockIcon); + + /** + * Creates the block placer component. + * + * @return the new component + */ + @Override + BlockPlacer build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/geyser/Chargeable.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/geyser/Chargeable.java new file mode 100644 index 00000000000..a39d739f4d2 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/geyser/Chargeable.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2025 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.api.item.custom.v2.component.geyser; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.util.GenericBuilder; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.List; + +// TODO projectile component (?) + +/** + * The chargeable component allows creating crossbows + * or bows. This includes the draw duration, whether the item + * charges on being drawn, and the ammunition that can be + * used by the item. + */ +public interface Chargeable { + + /** + * The maximum draw duration determines how long the weapon + * can be drawn before releasing automatically. Defaults to {@code 0.0}. + * + * @return the maximum draw duration + */ + @NonNegative float maxDrawDuration(); + + /** + * Whether the item is being charged when being drawn, like a crossbow. Defaults to {@code false}. + * + * @return whether drawing the item charges it + */ + boolean chargeOnDraw(); + + /** + * The identifiers of the Bedrock items that can be + * used as ammunition by this bow. + * For example, this can contain {@code minecraft:arrow} to allow arrows to be shot. + * + * @return all valid ammunition items + */ + List<@NonNull Identifier> ammunition(); + + /** + * Creates a builder for the Chargeable component. + * + * @return a new builder + */ + static Builder builder() { + return GeyserApi.api().provider(Chargeable.Builder.class); + } + + /** + * Builder for the chargeable component. + */ + interface Builder extends GenericBuilder { + + /** + * Sets the maximum draw duration before the item is released. + * + * @param maxDrawDuration the non-negative maximum charging duration + * @see Chargeable#maxDrawDuration() + * @return this builder + */ + @This + Builder maxDrawDuration(@NonNegative float maxDrawDuration); + + /** + * Sets whether the item is charged when drawing. + * + * @param chargeOnDraw whether drawing charges the item + * @see Chargeable#chargeOnDraw() + * @return this builder + */ + @This + Builder chargeOnDraw(boolean chargeOnDraw); + + /** + * Adds an item that can be used as ammunition, such as {@code minecraft:arrow}. + * This will throw when trying to add an item that was already added. + * + * @param ammunition the Bedrock item identifier of possible ammunition + * @see Chargeable#ammunition() + * @return this builder + */ + @This + Builder ammunition(@NonNull Identifier ammunition); + + /** + * Creates the chargeable component. + * + * @return the new component + */ + @Override + Chargeable build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/GeyserDataComponent.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/geyser/GeyserDataComponent.java similarity index 70% rename from api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/GeyserDataComponent.java rename to api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/geyser/GeyserDataComponent.java index eae709e7ddf..1ca32b11c25 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/GeyserDataComponent.java +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/geyser/GeyserDataComponent.java @@ -23,9 +23,14 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.api.item.custom.v2.component; +package org.geysermc.geyser.api.item.custom.v2.component.geyser; +import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition; +import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.function.Predicate; /** * Geyser data components are data components used for non-vanilla items only. Like vanilla data components, they indicate behaviour of custom items, and like vanilla data components, it is expected @@ -36,24 +41,33 @@ * @see DataComponent * @see CustomItemDefinition#components() */ -public final class GeyserDataComponent { +public interface GeyserDataComponent { /** - * Marks this item as chargeable, meaning an item functions as a bow or a crossbow. A list of bedrock item identifiers can be given as ammunition. + * Marks this item as chargeable, meaning an item functions as a bow or a crossbow. + * A list of bedrock item identifiers can be given as ammunition. * * @see Chargeable */ - public static final DataComponent CHARGEABLE = DataComponent.createGeyser("chargeable"); + DataComponent CHARGEABLE = createGeyser("chargeable"); + /** * Places a visual indicator (=tooltip) of the item's attack damage. Must be at or above 0. * *

Attribute modifiers are automatically translated for custom vanilla items, but not for non-vanilla ones, which is why this component is here.

*/ - public static final DataComponent ATTACK_DAMAGE = DataComponent.createGeyser("attack_damage", i -> i >= 0); + DataComponent ATTACK_DAMAGE = createGeyser("attack_damage", i -> i >= 0); + /** * Indicates which block the item should place and whether it should replace the original item for that block. */ - public static final DataComponent BLOCK_PLACER = DataComponent.createGeyser("block_placer"); + DataComponent BLOCK_PLACER = createGeyser("block_placer"); + + private static DataComponent createGeyser(String id) { + return createGeyser(id, t -> true); + } - private GeyserDataComponent() {} + private static DataComponent createGeyser(String id, Predicate predicate) { + return GeyserApi.api().provider(DataComponent.class, Identifier.of("geysermc", id), predicate, false); + } } diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/Consumable.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/Consumable.java new file mode 100644 index 00000000000..102c3000806 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/Consumable.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2025 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.api.item.custom.v2.component.java; + +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.util.GenericBuilder; + +/** + * The consumable component is used to mark + * an item as consumable. Further, it allows specifying + * the consume duration and animation to play when consuming. + */ +public interface Consumable { + + /** + * The seconds it takes to consume the item. + * This it the amount of time the animation will play for. Defaults to {@code 1.6}. + * + * @return the consume duration, in seconds + */ + @Positive float consumeSeconds(); + + /** + * The animation that should play when consuming the item. Defaults to {@link Animation#EAT}. + * + * @return the animation to play + */ + @NonNull Animation animation(); + + /** + * Creates a builder for the consumable component. + * + * @return a new builder + */ + static Builder builder() { + return GeyserApi.api().provider(Consumable.Builder.class); + } + + /** + * Creates a consumable component. + * + * @param consumeSeconds the consume duration, in seconds + * @param animation the animation to play when consuming + * @return the consumable component + */ + static Consumable of(float consumeSeconds, Animation animation) { + return Consumable.builder().consumeSeconds(consumeSeconds).animation(animation).build(); + } + + /** + * Not all animations work perfectly on bedrock. Bedrock behaviour is noted per animation. The {@code toot_horn} animation doesn't exist on bedrock, and is therefore not listed here. + */ + enum Animation { + /** + * Does nothing in 1st person, appears as eating in 3rd person. + */ + NONE, + /** + * Appears to look correctly. + */ + EAT, + /** + * Appears to look correctly. + */ + DRINK, + /** + * Does nothing in 1st person, eating in 3rd person. + */ + BLOCK, + /** + * Does nothing in 1st person, eating in 3rd person. + */ + BOW, + /** + * Does nothing in 1st person, but looks like spear in 3rd person. + */ + SPEAR, + /** + * Does nothing in 1st person, eating in 3rd person. + */ + CROSSBOW, + /** + * Does nothing in 1st person, but looks like spyglass in 3rd person. + */ + SPYGLASS, + /** + * Brush in 1st and 3rd person. Will look weird when not displayed handheld. + */ + BRUSH; + } + + /** + * Builder for the consumable component. + */ + interface Builder extends GenericBuilder { + /** + * Sets the time in seconds that consumption takes. This also + * determines the animation length. + * + * @param consumeSeconds the seconds it takes to consume the item + * @see Consumable#consumeSeconds() + * @return this builder + */ + @This + Builder consumeSeconds(@Positive float consumeSeconds); + + /** + * Sets the animation to play when consuming the item. + * See {@link Animation} for more details - some animations + * do not work correctly. + * + * @param animation the animation to play + * @see Consumable#animation() + * @return this builder + */ + @This + Builder animation(@NonNull Animation animation); + + /** + * Creates the consumable component. + * + * @return the new component + */ + @Override + Consumable build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/Equippable.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/Equippable.java new file mode 100644 index 00000000000..d28ab19f6fd --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/Equippable.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025 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.api.item.custom.v2.component.java; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.util.GenericBuilder; + +/** + * The equippable component is used to mark an item as equippable. + * Bedrock allows specifying the slot where an item can be worn. + */ +public interface Equippable { + + /** + * The equipment slot where this item + * can be worn. + * + * @return the equipment slot + */ + @NonNull EquipmentSlot slot(); + + /** + * Creates a builder for the equippable component. + * + * @return a new builder + */ + static Builder builder() { + return GeyserApi.api().provider(Equippable.Builder.class); + } + + /** + * Creates an equippable component for an equipment slot. + * + * @param slot the slot in which the item can be equipped + * @return the Equippable component + */ + static Equippable of(EquipmentSlot slot) { + return builder().slot(slot).build(); + } + + /** + * Builder for the equippable component + */ + interface Builder extends GenericBuilder { + + /** + * The equipment slot where the item can be equipped + * + * @param slot the equipment slot + * @see Equippable#slot() + * @return this builder + */ + @This + Builder slot(@NonNull EquipmentSlot slot); + + /** + * Creates the equippable component. + * + * @return the new component + */ + @Override + Equippable build(); + } + + /** + * The slot in which the equipment can be worn. + */ + enum EquipmentSlot { + HEAD, + CHEST, + LEGS, + FEET, + BODY, + SADDLE + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/FoodProperties.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/FoodProperties.java new file mode 100644 index 00000000000..97fe528769a --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/FoodProperties.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025 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.api.item.custom.v2.component.java; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.util.GenericBuilder; + +/** + * The food properties component can be used to define properties + * for consumable items. This includes setting the nutrition and + * saturation values, and whether the item can always be eaten. + */ +public interface FoodProperties { + + /** + * The nutrition of the item. Defaults to {@code 0}. + * + * @return the nutrition + */ + @NonNegative int nutrition(); + + /** + * The saturation of the item. Defaults to {@code 0.0}. + * + * @return the saturation + */ + @NonNegative float saturation(); + + /** + * Whether this item can always be eaten, + * even when not hungry. In vanilla, this would + * include items such as golden apples. Defaults to {@code false}. + * + * @return whether the item can always be eaten + */ + boolean canAlwaysEat(); + + /** + * Creates a builder for the food properties component. + * + * @return a new builder + */ + static Builder builder() { + return GeyserApi.api().provider(FoodProperties.Builder.class); + } + + /** + * Creates a food properties component. + * + * @param nutrition the nutrition of the item + * @param saturation the saturation of the item + * @param canAlwaysEat whether the item can always be eaten + * @return the food properties component + */ + static FoodProperties of(int nutrition, float saturation, boolean canAlwaysEat) { + return FoodProperties.builder().nutrition(nutrition).saturation(saturation).canAlwaysEat(canAlwaysEat).build(); + } + + /** + * Builder for the food properties component. + */ + interface Builder extends GenericBuilder { + + /** + * Sets the nutrition of the item which is added to the hunger bar. + * + * @param nutrition the nutrition of the item. + * @see FoodProperties#nutrition() + * @return this builder + */ + @This + Builder nutrition(@NonNegative int nutrition); + + /** + * Sets the saturation of the item. + * + * @param saturation the saturation of the item + * @see FoodProperties#saturation() + * @return this builder + */ + @This + Builder saturation(@NonNegative float saturation); + + /** + * Sets whether this item can always be eaten, + * even when the hunger bar is full. + * + * @param canAlwaysEat whether the item can always be eaten + * @see FoodProperties#canAlwaysEat() + * @return this builder + */ + @This + Builder canAlwaysEat(boolean canAlwaysEat); + + /** + * Creates the food properties component. + * + * @return the new component + */ + @Override + FoodProperties build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/ItemDataComponents.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/ItemDataComponents.java new file mode 100644 index 00000000000..c612b15cea0 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/ItemDataComponents.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2025 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.api.item.custom.v2.component.java; + +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition; +import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.GeyserDataComponent; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.function.Predicate; + +/** + * Represents various Java item data components to the extent + * that these can be translated to custom items for Bedrock edition players. + * These can be set in {@link CustomItemDefinition.Builder#component(DataComponent, Object)} + * to specify the item's behavior. It's expected that these components would also + * be present on the item server-side to avoid de-syncs. + * + * @see CustomItemDefinition#components() + * @see GeyserDataComponent + */ +public interface ItemDataComponents { + + /** + * Marks the item as consumable. Of this component, only {@code consume_seconds} and {@code animation} properties are translated. Consume effects are done server side, + * and consume sounds and disabling consume particles aren't possible. + * + *

Note that due to a bug on Bedrock, not all consume animations appear perfectly. See {@link Consumable.Animation}.

+ * + * @see Consumable + */ + DataComponent CONSUMABLE = create("consumable"); + + /** + * Marks the item as equippable. Of this component, only the {@code slot} property is translated. Other properties are done server-side, are done differently on Bedrock (e.g. {@code asset_id} is done via attachables), + * or are not possible on Bedrock at all (e.g. {@code camera_overlay}). + * + *

Note that on Bedrock, equippables can't have a stack size above 1.

+ * + * @see Equippable + */ + DataComponent EQUIPPABLE = create("equippable"); + + /** + * Food properties of the item. All properties properly translate over to Bedrock. + * + * @see FoodProperties + */ + DataComponent FOOD = create("food"); + + /** + * Max damage value of the item. Must be at or above 0. Items with a max damage value above 0 cannot have a stack size above 1. + */ + DataComponent MAX_DAMAGE = create("max_damage", i -> i >= 0); + + /** + * Max stack size of the item. Must be between 1 and 99. Items with a max stack size value above 1 cannot have a max damage value above 0. + */ + DataComponent MAX_STACK_SIZE = create("max_stack_size", i -> i >= 1 && i <= 99); // Reverse lambda + + /** + * Marks the item to have a use cooldown. To properly function, the item must be able to be used: it must be consumable or have some other kind of use logic. + * + *

The cooldown group can be {@code null}, in this case the identifier of the vanilla item (in case of vanilla custom items), + * or the item itself (in case of non-vanilla custom items) will be used.

+ * + * @see UseCooldown + */ + DataComponent USE_COOLDOWN = create("use_cooldown"); + + /** + * Marks the item to be enchantable. Must be at or above 0. + * + *

This component does not translate over perfectly, due to the way enchantments work on Bedrock. The component will be mapped to the {@code minecraft:enchantable} bedrock component with {@code slot=all}. + * This should, but does not guarantee, allow for compatibility with vanilla enchantments. Non-vanilla enchantments are unlikely to work.

+ */ + DataComponent ENCHANTABLE = create("enchantable", i -> i >= 0); + + /** + * This component is only used for the {@link ToolProperties#canDestroyBlocksInCreative()} option. + * + * + * @see ToolProperties + */ + DataComponent TOOL = create("tool"); + + /** + * Marks which items can be used to repair the item. + * + * @see Repairable + */ + DataComponent REPAIRABLE = create("repairable"); + + /** + * Overrides the item's enchantment glint. + */ + DataComponent ENCHANTMENT_GLINT_OVERRIDE = create("enchantment_glint_override"); + + private static DataComponent create(String id) { + return create(id, t -> true); + } + + private static DataComponent create(String id, Predicate consumer) { + return GeyserApi.api().provider(DataComponent.class, Identifier.of(id), consumer, true); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/Repairable.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/Repairable.java new file mode 100644 index 00000000000..579784997b2 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/Repairable.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 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.api.item.custom.v2.component.java; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.util.GenericBuilder; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.Arrays; +import java.util.List; + +/** + * The repairable component determines which other items can be used + * to repair the item. + */ +public interface Repairable { + + /** + * The Bedrock identifiers of the items + * that can be used to repair this item. + * + * @return the identifiers + */ + List<@NonNull Identifier> items(); + + /** + * Creates a builder for the repairable component. + * + * @return a new builder + */ + static Builder builder() { + return GeyserApi.api().provider(Repairable.Builder.class); + } + + /** + * Creates a repairable component. + * + * @param items the identifiers of the items that + * can repair the item + * @return the repairable component + */ + static Repairable of(Identifier... items) { + Repairable.Builder builder = builder(); + Arrays.stream(items).forEach(builder::item); + return builder.build(); + } + + /** + * Builder for the repairable component. + */ + interface Builder extends GenericBuilder { + + /** + * Adds an item that can be used to repair the item. + * This will throw when trying to add an item that was already added. + * + * @param item the Bedrock item identifier that can be used to repair the item + * @see Repairable#items() + * @return this builder + */ + @This + Builder item(@NonNull Identifier item); + + /** + * Creates the repairable component. + * + * @return the new component + */ + @Override + Repairable build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/ToolProperties.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/ToolProperties.java new file mode 100644 index 00000000000..caebfd2e137 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/ToolProperties.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 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.api.item.custom.v2.component.java; + +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.util.GenericBuilder; + +/** + * The tool properties component can be used to mark + * if the item can destroy blocks when used in creative mode. + */ +public interface ToolProperties { + + /** + * Whether this item can destroy blocks when trying to break them in + * creative mode. Defaults to {@code true}. + * + * @return whether this item can destroy blocks in creative mode + */ + boolean canDestroyBlocksInCreative(); + + /** + * Creates a builder for the tool properties component. + * + * @return a new builder + */ + static Builder builder() { + return GeyserApi.api().provider(ToolProperties.Builder.class); + } + + /** + * Creates a tool properties component. + * + * @param canDestroyBlocksInCreative determines if the item will break blocks in creative mode + * @return a tool properties component + */ + static ToolProperties of(boolean canDestroyBlocksInCreative) { + return builder().canDestroyBlocksInCreative(canDestroyBlocksInCreative).build(); + } + + /** + * Builder for the tool properties component. + */ + interface Builder extends GenericBuilder { + + /** + * Sets whether this item can destroy blocks when trying to break them in + * creative mode. + * + * @param canDestroyBlocksInCreative determines if the item will break blocks in creative mode + * @see ToolProperties#canDestroyBlocksInCreative() + * @return this builder + */ + @This + Builder canDestroyBlocksInCreative(boolean canDestroyBlocksInCreative); + + /** + * Creates the tool properties component. + * + * @return the new component + */ + @Override + ToolProperties build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/UseCooldown.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/UseCooldown.java new file mode 100644 index 00000000000..b876f7da8be --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/java/UseCooldown.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2025 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.api.item.custom.v2.component.java; + +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.util.GenericBuilder; +import org.geysermc.geyser.api.util.Identifier; + +/** + * The use cooldown component is used to add an item use cooldown to items. + */ +public interface UseCooldown { + + /** + * The duration of time in seconds items with a matching category will + * spend cooling down before being usable again. + * + * @return the cooldown duration + */ + @Positive float seconds(); + + /** + * The cooldown type of the item. Other items in this group + * will also have the cooldown applied when any item of this + * group is used. {@code null} will result in the item identifier + * being used. + * + * @return the cooldown identifier + */ + @Nullable Identifier cooldownGroup(); + + /** + * Creates a builder for the use cooldown component. + * + * @return a new builder + */ + static Builder builder() { + return GeyserApi.api().provider(UseCooldown.Builder.class); + } + + /** + * Builder for the use cooldown component. + */ + interface Builder extends GenericBuilder { + + /** + * Sets the duration in seconds in which the item + * cannot be used again. + * + * @param seconds the cooldown time + * @see UseCooldown#seconds() + * @return this builder + */ + @This + Builder seconds(@Positive float seconds); + + /** + * Sets the cooldown group that this cooldown belongs to. + * When any item in this group is used, all items in the group + * are not usable for the amount of time specified in {@link Builder#seconds()} + * {@code null} results in the item identifier being specified instead. + * + * @param cooldownGroup the cooldown group identifier + * @see UseCooldown#cooldownGroup() + * @return this builder + */ + @This + Builder cooldownGroup(@Nullable Identifier cooldownGroup); + + /** + * Creates the use cooldown component. + * + * @return the new component + */ + @Override + UseCooldown build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/predicate/MinecraftPredicate.java b/api/src/main/java/org/geysermc/geyser/api/predicate/MinecraftPredicate.java index 1bd93d7e6a0..b9258b0d708 100644 --- a/api/src/main/java/org/geysermc/geyser/api/predicate/MinecraftPredicate.java +++ b/api/src/main/java/org/geysermc/geyser/api/predicate/MinecraftPredicate.java @@ -35,7 +35,7 @@ /** * A predicate for a {@link MinecraftPredicateContext}. * - *

Right now this is used to determine if a {@link CustomItemDefinition} should be used. While this does + *

This is used to determine if a {@link CustomItemDefinition} should be used. While this does * allow you to define your own predicates with custom checks, it is recommended to use commonly defined predicates and predicate creators when possible, * since these support conflict detection among other benefits. See {@link CustomItemDefinition#predicates()}.

* diff --git a/api/src/main/java/org/geysermc/geyser/api/predicate/PredicateStrategy.java b/api/src/main/java/org/geysermc/geyser/api/predicate/PredicateStrategy.java index 02b1d747b54..0315daa1578 100644 --- a/api/src/main/java/org/geysermc/geyser/api/predicate/PredicateStrategy.java +++ b/api/src/main/java/org/geysermc/geyser/api/predicate/PredicateStrategy.java @@ -25,6 +25,9 @@ package org.geysermc.geyser.api.predicate; +/** + * Represents all possible strategies for evaluating multiple predicates. + */ public enum PredicateStrategy { /** * Require all predicates to pass diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/BlockPlacer.java b/api/src/main/java/org/geysermc/geyser/api/util/GenericBuilder.java similarity index 82% rename from api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/BlockPlacer.java rename to api/src/main/java/org/geysermc/geyser/api/util/GenericBuilder.java index 3aed4918a39..39066726cac 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/BlockPlacer.java +++ b/api/src/main/java/org/geysermc/geyser/api/util/GenericBuilder.java @@ -23,9 +23,18 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.api.item.custom.v2.component; +package org.geysermc.geyser.api.util; -import org.geysermc.geyser.api.util.Identifier; +/** + * An interface for builders. + * + * @param the type of the object built + */ +public interface GenericBuilder { -public record BlockPlacer(Identifier block, boolean useBlockIcon) { + /** + * Builds the object from the builder + * @return the object + */ + T build(); } diff --git a/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java b/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java index 679d2f9798f..1e5c1fd6b52 100644 --- a/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java +++ b/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java @@ -25,15 +25,54 @@ package org.geysermc.geyser.api.util; +import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.GeyserApi; +/** + * An identifying object for representing unique objects. + * This identifier consists of two parts: + *
    + *
  • + * a namespace, which is usually a name identifying your work + *
  • + *
  • + * a path, which holds a value. + *
  • + *
+ * + * Examples of identifiers: + *
    + *
  • {@code minecraft:fox}
  • + *
  • {@code geysermc:one_fun_example}
  • + *
+ * + * If this identifier is referencing anything not in the + * vanilla Minecraft game, the namespace cannot be "minecraft". + * Further, paths cannot contain colons ({@code :}). + */ public interface Identifier { + + /** + * The namespace for Minecraft. + */ String DEFAULT_NAMESPACE = "minecraft"; - static Identifier of(String namespace, String path) { + /** + * Attempts to create a new identifier from a namespace and path. + * + * @return the identifier for this namespace and path + * @throws IllegalArgumentException if either namespace or path are invalid. + */ + static Identifier of(@NonNull String namespace, @NonNull String path) { return GeyserApi.api().provider(Identifier.class, namespace, path); } + /** + * Attempts to create a new identifier from a string representation. + * + * @return the identifier for this namespace and path + * @throws IllegalArgumentException if either the namespace or path are invalid + */ static Identifier of(String identifier) { String[] split = identifier.split(":"); String namespace; @@ -50,10 +89,19 @@ static Identifier of(String identifier) { return of(namespace, path); } + /** + * Returns the namespace of this identifier. + */ String namespace(); + /** + * Returns the path of this identifier. + */ String path(); - + + /** + * Checks whether this identifier is using the "minecraft" namespace. + */ default boolean vanilla() { return namespace().equals(DEFAULT_NAMESPACE); } diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCustomItemsEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCustomItemsEventImpl.java index b5bb07ed054..a0350ab0a72 100644 --- a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCustomItemsEventImpl.java +++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCustomItemsEventImpl.java @@ -90,7 +90,7 @@ public boolean register(@NonNull String identifier, @NonNull CustomItemData cust } @Override - public boolean register(NonVanillaCustomItemData customItemData) { + public boolean register(@NonNull NonVanillaCustomItemData customItemData) { try { register(customItemData.toDefinition().build()); deprecatedNonVanillaCustomItems.add(customItemData); diff --git a/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java b/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java index 169c8fb523b..6980c9fbf9f 100644 --- a/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java +++ b/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java @@ -27,9 +27,22 @@ import net.kyori.adventure.key.Key; import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.util.MinecraftKey; + +import java.util.Objects; public record IdentifierImpl(Key identifier) implements Identifier { + public static IdentifierImpl of(String namespace, String value) throws IllegalArgumentException { + Objects.requireNonNull(namespace, "namespace cannot be null!"); + Objects.requireNonNull(value, "value cannot be null!"); + try { + return new IdentifierImpl(MinecraftKey.key(namespace, value)); + } catch (Throwable e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + @Override public String namespace() { return identifier.namespace(); diff --git a/core/src/main/java/org/geysermc/geyser/item/custom/ComponentConverters.java b/core/src/main/java/org/geysermc/geyser/item/custom/ComponentConverters.java index 582173e1cc1..072b6f6be47 100644 --- a/core/src/main/java/org/geysermc/geyser/item/custom/ComponentConverters.java +++ b/core/src/main/java/org/geysermc/geyser/item/custom/ComponentConverters.java @@ -28,8 +28,10 @@ import org.cloudburstmc.nbt.NbtMapBuilder; import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; import org.geysermc.geyser.api.item.custom.v2.component.DataComponentMap; -import org.geysermc.geyser.api.item.custom.v2.component.Repairable; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; +import org.geysermc.geyser.api.item.custom.v2.component.java.Repairable; import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.registry.populator.CustomItemRegistryPopulator; import org.geysermc.geyser.util.MinecraftKey; import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.item.component.Consumable; @@ -60,18 +62,18 @@ * *

Most components convert over nicely, and it is very much preferred to have every API component have a converter in here. However, this is not always possible. At the moment, there are 2 exceptions: *

    - *
  • The MCPL counterpart of the {@link DataComponent#REPAIRABLE} component is just an ID holder set, which can't be used in the custom item registry populator. + *
  • The MCPL counterpart of the {@link ItemDataComponents#REPAIRABLE} component is just an ID holder set, which can't be used in the custom item registry populator. * Also see {@link org.geysermc.geyser.registry.populator.CustomItemRegistryPopulator#computeRepairableProperties(Repairable, NbtMapBuilder)}.
  • - *
  • Non-vanilla data components (from {@link org.geysermc.geyser.api.item.custom.v2.component.GeyserDataComponent}) don't have converters registered, for obvious reasons. + *
  • Non-vanilla data components (from {@link org.geysermc.geyser.api.item.custom.v2.component.geyser.GeyserDataComponent}) don't have converters registered, for obvious reasons. * They're used directly in the custom item registry populator. Eventually, some may have converters introduced as Mojang introduces such components in Java.
  • *
- * For both of these cases proper accommodations have been made in the {@link org.geysermc.geyser.registry.populator.CustomItemRegistryPopulator}. + * For both of these cases proper accommodations have been made in the {@link CustomItemRegistryPopulator}. */ public class ComponentConverters { private static final Map, ComponentConverter> converters = new HashMap<>(); static { - registerConverter(DataComponent.CONSUMABLE, (itemMap, value) -> { + registerConverter(ItemDataComponents.CONSUMABLE, (itemMap, value) -> { Consumable.ItemUseAnimation convertedAnimation = switch (value.animation()) { case NONE -> Consumable.ItemUseAnimation.NONE; case EAT -> Consumable.ItemUseAnimation.EAT; @@ -87,7 +89,7 @@ public class ComponentConverters { true, List.of())); }); - registerConverter(DataComponent.EQUIPPABLE, (itemMap, value) -> { + registerConverter(ItemDataComponents.EQUIPPABLE, (itemMap, value) -> { EquipmentSlot convertedSlot = switch (value.slot()) { case HEAD -> EquipmentSlot.HELMET; case CHEST -> EquipmentSlot.CHESTPLATE; @@ -100,21 +102,21 @@ public class ComponentConverters { null, null, null, false, false, false, false, false, null)); }); - registerConverter(DataComponent.FOOD, (itemMap, value) -> itemMap.put(DataComponentTypes.FOOD, + registerConverter(ItemDataComponents.FOOD, (itemMap, value) -> itemMap.put(DataComponentTypes.FOOD, new FoodProperties(value.nutrition(), value.saturation(), value.canAlwaysEat()))); - registerConverter(DataComponent.MAX_DAMAGE, (itemMap, value) -> itemMap.put(DataComponentTypes.MAX_DAMAGE, value)); - registerConverter(DataComponent.MAX_STACK_SIZE, (itemMap, value) -> itemMap.put(DataComponentTypes.MAX_STACK_SIZE, value)); + registerConverter(ItemDataComponents.MAX_DAMAGE, (itemMap, value) -> itemMap.put(DataComponentTypes.MAX_DAMAGE, value)); + registerConverter(ItemDataComponents.MAX_STACK_SIZE, (itemMap, value) -> itemMap.put(DataComponentTypes.MAX_STACK_SIZE, value)); - registerConverter(DataComponent.USE_COOLDOWN, (itemMap, value) -> itemMap.put(DataComponentTypes.USE_COOLDOWN, + registerConverter(ItemDataComponents.USE_COOLDOWN, (itemMap, value) -> itemMap.put(DataComponentTypes.USE_COOLDOWN, new UseCooldown(value.seconds(), MinecraftKey.identifierToKey(value.cooldownGroup())))); - registerConverter(DataComponent.ENCHANTABLE, (itemMap, value) -> itemMap.put(DataComponentTypes.ENCHANTABLE, value)); + registerConverter(ItemDataComponents.ENCHANTABLE, (itemMap, value) -> itemMap.put(DataComponentTypes.ENCHANTABLE, value)); - registerConverter(DataComponent.TOOL, (itemMap, value) -> itemMap.put(DataComponentTypes.TOOL, + registerConverter(ItemDataComponents.TOOL, (itemMap, value) -> itemMap.put(DataComponentTypes.TOOL, new ToolData(List.of(), 1.0F, 1, value.canDestroyBlocksInCreative()))); - registerConverter(DataComponent.ENCHANTMENT_GLINT_OVERRIDE, (itemMap, value) -> itemMap.put(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, value)); + registerConverter(ItemDataComponents.ENCHANTMENT_GLINT_OVERRIDE, (itemMap, value) -> itemMap.put(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, value)); } private static void registerConverter(DataComponent component, ComponentConverter converter) { diff --git a/core/src/main/java/org/geysermc/geyser/item/custom/GeyserCustomItemDefinition.java b/core/src/main/java/org/geysermc/geyser/item/custom/GeyserCustomItemDefinition.java index 6fc4d77ec35..9c44028a9e6 100644 --- a/core/src/main/java/org/geysermc/geyser/item/custom/GeyserCustomItemDefinition.java +++ b/core/src/main/java/org/geysermc/geyser/item/custom/GeyserCustomItemDefinition.java @@ -32,15 +32,17 @@ import org.checkerframework.checker.nullness.qual.NonNull; 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.component.DataComponentMap; import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.DataComponentMap; import org.geysermc.geyser.api.predicate.MinecraftPredicate; import org.geysermc.geyser.api.predicate.PredicateStrategy; import org.geysermc.geyser.api.predicate.context.item.ItemPredicateContext; import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.item.custom.impl.DataComponentImpl; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Set; @EqualsAndHashCode @@ -89,6 +91,7 @@ public GeyserCustomItemDefinition(Builder builder) { } @Override + @NonNull public PredicateStrategy predicateStrategy() { return predicateStrategy; } @@ -125,7 +128,9 @@ public static class Builder implements CustomItemDefinition.Builder { private CustomItemBedrockOptions bedrockOptions = CustomItemBedrockOptions.builder().build(); private PredicateStrategy predicateStrategy = PredicateStrategy.AND; - public Builder(Identifier bedrockIdentifier, Identifier model) { + public Builder(@NonNull Identifier bedrockIdentifier, @NonNull Identifier model) { + Objects.requireNonNull(bedrockIdentifier, "bedrockIdentifier cannot be null"); + Objects.requireNonNull(model, "model cannot be null"); this.bedrockIdentifier = bedrockIdentifier; this.displayName = bedrockIdentifier.toString(); this.model = model; @@ -133,6 +138,7 @@ public Builder(Identifier bedrockIdentifier, Identifier model) { @Override public CustomItemDefinition.Builder displayName(@NonNull String displayName) { + Objects.requireNonNull(displayName, "displayName cannot be null"); this.displayName = displayName; return this; } @@ -145,27 +151,36 @@ public CustomItemDefinition.Builder priority(int priority) { @Override public CustomItemDefinition.Builder bedrockOptions(CustomItemBedrockOptions.@NonNull Builder options) { + Objects.requireNonNull(options, "options cannot be null"); this.bedrockOptions = options.build(); return this; } @Override public CustomItemDefinition.Builder predicate(@NonNull MinecraftPredicate predicate) { + Objects.requireNonNull(predicate, "predicate cannot be null"); predicates.add(predicate); return this; } @Override public CustomItemDefinition.Builder predicateStrategy(@NonNull PredicateStrategy strategy) { + Objects.requireNonNull(strategy, "strategy cannot be null"); predicateStrategy = strategy; return this; } @Override public CustomItemDefinition.Builder component(@NonNull DataComponent component, @NonNull T value) { + Objects.requireNonNull(component, "component cannot be null"); + Objects.requireNonNull(value, "value cannot be null"); + if (!(component instanceof DataComponentImpl dataComponent)) { + throw new IllegalArgumentException("Cannot use custom implementations of the DataComponent interface! Found: " + component.getClass().getSimpleName()); + } + if (!component.vanilla() && !(this instanceof GeyserNonVanillaCustomItemDefinition.Builder)) { throw new IllegalArgumentException("That component cannot be used for vanilla items"); - } else if (!component.validate(value)) { + } else if (!dataComponent.validate(value)) { throw new IllegalArgumentException("Value " + value + " is invalid for " + component); } components.put(component, value); @@ -173,7 +188,8 @@ public CustomItemDefinition.Builder component(@NonNull DataComponent comp } @Override - public CustomItemDefinition.Builder removeComponent(Identifier component) { + public CustomItemDefinition.Builder removeComponent(@NonNull Identifier component) { + Objects.requireNonNull(component, "component cannot be null"); removedComponents.add(component); return this; } diff --git a/core/src/main/java/org/geysermc/geyser/item/custom/GeyserNonVanillaCustomItemDefinition.java b/core/src/main/java/org/geysermc/geyser/item/custom/GeyserNonVanillaCustomItemDefinition.java index 8cf82a3ec8b..fc02d4b1643 100644 --- a/core/src/main/java/org/geysermc/geyser/item/custom/GeyserNonVanillaCustomItemDefinition.java +++ b/core/src/main/java/org/geysermc/geyser/item/custom/GeyserNonVanillaCustomItemDefinition.java @@ -112,7 +112,7 @@ public NonVanillaCustomItemDefinition.Builder component(@NonNull DataCompone } @Override - public CustomItemDefinition.Builder removeComponent(Identifier component) { + public CustomItemDefinition.Builder removeComponent(@NonNull Identifier component) { throw new UnsupportedOperationException("Removing default item components is not supported for non-vanilla items"); } diff --git a/core/src/main/java/org/geysermc/geyser/item/custom/impl/BlockPlacerImpl.java b/core/src/main/java/org/geysermc/geyser/item/custom/impl/BlockPlacerImpl.java new file mode 100644 index 00000000000..2e161e6eded --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/custom/impl/BlockPlacerImpl.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 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.item.custom.impl; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.BlockPlacer; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.Objects; + +public record BlockPlacerImpl( + Identifier block, + boolean useBlockIcon +) implements BlockPlacer { + + public static class Builder implements BlockPlacer.Builder { + private Identifier block; + private boolean useBlockIcon; + + @Override + public Builder block(@NonNull Identifier block) { + Objects.requireNonNull(block, "block cannot be null"); + this.block = block; + return this; + } + + @Override + public Builder useBlockIcon(boolean useBlockIcon) { + this.useBlockIcon = useBlockIcon; + return this; + } + + @Override + public BlockPlacer build() { + Objects.requireNonNull(block, "block cannot be null"); + return new BlockPlacerImpl(block, useBlockIcon); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/custom/impl/ChargeableImpl.java b/core/src/main/java/org/geysermc/geyser/item/custom/impl/ChargeableImpl.java new file mode 100644 index 00000000000..c19276c0754 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/custom/impl/ChargeableImpl.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025 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.item.custom.impl; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.Chargeable; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public record ChargeableImpl( + @NonNegative float maxDrawDuration, + boolean chargeOnDraw, + @NonNull List<@NonNull Identifier> ammunition +) implements Chargeable { + + public static class Builder implements Chargeable.Builder { + private float maxDrawDuration; + private boolean chargeOnDraw; + private final List ammunition = new ArrayList<>(); + + @Override + public Chargeable.Builder maxDrawDuration(@NonNegative float maxDrawDuration) { + if (maxDrawDuration < 0) { + throw new IllegalArgumentException("maxDrawDuration must be positive"); + } + this.maxDrawDuration = maxDrawDuration; + return this; + } + + @Override + public Chargeable.Builder chargeOnDraw(boolean chargeOnDraw) { + this.chargeOnDraw = chargeOnDraw; + return this; + } + + @Override + public Chargeable.Builder ammunition(@NonNull Identifier ammunition) { + Objects.requireNonNull(ammunition, "ammunition cannot be null"); + if (this.ammunition.contains(ammunition)) { + throw new IllegalArgumentException("duplicate ammunition " + ammunition); + } + this.ammunition.add(ammunition); + return this; + } + + @Override + public Chargeable build() { + Objects.requireNonNull(ammunition, "ammunition cannot be null"); + return new ChargeableImpl(maxDrawDuration, chargeOnDraw, ammunition); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/item/custom/impl/ConsumableImpl.java b/core/src/main/java/org/geysermc/geyser/item/custom/impl/ConsumableImpl.java new file mode 100644 index 00000000000..ca2de75d058 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/custom/impl/ConsumableImpl.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 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.item.custom.impl; + +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.item.custom.v2.component.java.Consumable; + +import java.util.Objects; + +public record ConsumableImpl( + float consumeSeconds, + @NonNull Animation animation +) implements Consumable { + + public static class Builder implements Consumable.Builder { + private float consumeSeconds = 1.6F; + private Animation animation = Animation.EAT; + + @Override + public Builder consumeSeconds(@Positive float consumeSeconds) { + if (consumeSeconds <= 0.0F) { + throw new IllegalArgumentException("consume seconds must be above 0"); + } + this.consumeSeconds = consumeSeconds; + return this; + } + + @Override + public Builder animation(@NonNull Animation animation) { + Objects.requireNonNull(animation, "animation cannot be null"); + this.animation = animation; + return this; + } + + @Override + public Consumable build() { + return new ConsumableImpl(consumeSeconds, animation); + } + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Repairable.java b/core/src/main/java/org/geysermc/geyser/item/custom/impl/DataComponentImpl.java similarity index 76% rename from api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Repairable.java rename to core/src/main/java/org/geysermc/geyser/item/custom/impl/DataComponentImpl.java index 9a7c01d0696..d735b4a68eb 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Repairable.java +++ b/core/src/main/java/org/geysermc/geyser/item/custom/impl/DataComponentImpl.java @@ -23,9 +23,22 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.api.item.custom.v2.component; +package org.geysermc.geyser.item.custom.impl; +import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; import org.geysermc.geyser.api.util.Identifier; -public record Repairable(Identifier... items) { +import java.util.function.Predicate; + +public record DataComponentImpl( + Identifier identifier, + Predicate validator, + boolean vanilla +) implements DataComponent { + + public boolean validate(T value) { + return this.validator.test(value); + } + } + diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/FoodProperties.java b/core/src/main/java/org/geysermc/geyser/item/custom/impl/EquippableImpl.java similarity index 61% rename from api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/FoodProperties.java rename to core/src/main/java/org/geysermc/geyser/item/custom/impl/EquippableImpl.java index aa1229fa230..f8883a27df3 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/FoodProperties.java +++ b/core/src/main/java/org/geysermc/geyser/item/custom/impl/EquippableImpl.java @@ -23,15 +23,31 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.api.item.custom.v2.component; +package org.geysermc.geyser.item.custom.impl; -import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.item.custom.v2.component.java.Equippable; -public record FoodProperties(@NonNegative int nutrition, @NonNegative float saturation, boolean canAlwaysEat) { +import java.util.Objects; - public FoodProperties { - if (nutrition < 0 || saturation < 0.0F) { - throw new IllegalArgumentException("Nutrition and saturation must be at or above 0"); +public record EquippableImpl( + EquipmentSlot slot +) implements Equippable { + + public static class Builder implements Equippable.Builder { + private EquipmentSlot slot; + + @Override + public Equippable.Builder slot(@NonNull EquipmentSlot slot) { + Objects.requireNonNull(slot, "slot cannot be null"); + this.slot = slot; + return this; + } + + @Override + public Equippable build() { + Objects.requireNonNull(slot, "slot cannot be null"); + return new EquippableImpl(slot); } } } diff --git a/core/src/main/java/org/geysermc/geyser/item/custom/impl/FoodPropertiesImpl.java b/core/src/main/java/org/geysermc/geyser/item/custom/impl/FoodPropertiesImpl.java new file mode 100644 index 00000000000..f18307d926e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/custom/impl/FoodPropertiesImpl.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 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.item.custom.impl; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.geysermc.geyser.api.item.custom.v2.component.java.FoodProperties; + +public record FoodPropertiesImpl( + int nutrition, + float saturation, + boolean canAlwaysEat +) implements FoodProperties { + + @SuppressWarnings("ConstantValue") // must enforce api + public static class Builder implements FoodProperties.Builder { + private int nutrition; + private float saturation; + private boolean canAlwaysEat; + + @Override + public FoodProperties.Builder nutrition(@NonNegative int nutrition) { + if (nutrition < 0) throw new IllegalArgumentException("nutrition cannot be negative"); + this.nutrition = nutrition; + return this; + } + + @Override + public FoodProperties.Builder saturation(@NonNegative float saturation) { + if (saturation < 0) throw new IllegalArgumentException("saturation cannot be negative"); + this.saturation = saturation; + return this; + } + + @Override + public FoodProperties.Builder canAlwaysEat(boolean canAlwaysEat) { + this.canAlwaysEat = canAlwaysEat; + return this; + } + + @Override + public FoodProperties build() { + return new FoodPropertiesImpl(nutrition, saturation, canAlwaysEat); + } + } + +} diff --git a/core/src/main/java/org/geysermc/geyser/item/custom/impl/RepairableImpl.java b/core/src/main/java/org/geysermc/geyser/item/custom/impl/RepairableImpl.java new file mode 100644 index 00000000000..a58b2eb1993 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/item/custom/impl/RepairableImpl.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 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.item.custom.impl; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.item.custom.v2.component.java.Repairable; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public record RepairableImpl(@NonNull List<@NonNull Identifier> items) implements Repairable { + + public static class Builder implements Repairable.Builder { + private final List items = new ArrayList<>(); + + @Override + public Builder item(@NonNull Identifier item) { + Objects.requireNonNull(items, "item cannot be null"); + if (this.items.contains(item)) { + throw new IllegalArgumentException("duplicate repairable item: " + item); + } + this.items.add(item); + return this; + } + + @Override + public Repairable build() { + return new RepairableImpl(items); + } + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Chargeable.java b/core/src/main/java/org/geysermc/geyser/item/custom/impl/ToolPropertiesImpl.java similarity index 63% rename from api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Chargeable.java rename to core/src/main/java/org/geysermc/geyser/item/custom/impl/ToolPropertiesImpl.java index dedcd1b0fb5..749ee1f181b 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/Chargeable.java +++ b/core/src/main/java/org/geysermc/geyser/item/custom/impl/ToolPropertiesImpl.java @@ -23,17 +23,26 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.api.item.custom.v2.component; +package org.geysermc.geyser.item.custom.impl; -import org.checkerframework.checker.index.qual.NonNegative; -import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.api.item.custom.v2.component.java.ToolProperties; -// TODO projectile component -public record Chargeable(@NonNegative float maxDrawDuration, boolean chargeOnDraw, Identifier... ammunition) { +public record ToolPropertiesImpl( + boolean canDestroyBlocksInCreative +) implements ToolProperties { - public Chargeable { - if (maxDrawDuration < 0.0F) { - throw new IllegalArgumentException("Max draw duration must be at or above 0"); + public static class Builder implements ToolProperties.Builder { + private boolean destroyBlocksInCreative = true; + + @Override + public ToolProperties.Builder canDestroyBlocksInCreative(boolean destroyBlocksInCreative) { + this.destroyBlocksInCreative = destroyBlocksInCreative; + return this; + } + + @Override + public ToolProperties build() { + return new ToolPropertiesImpl(destroyBlocksInCreative); } } } diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/UseCooldown.java b/core/src/main/java/org/geysermc/geyser/item/custom/impl/UseCooldownImpl.java similarity index 62% rename from api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/UseCooldown.java rename to core/src/main/java/org/geysermc/geyser/item/custom/impl/UseCooldownImpl.java index 4bfd6dfb036..de01dc42206 100644 --- a/api/src/main/java/org/geysermc/geyser/api/item/custom/v2/component/UseCooldown.java +++ b/core/src/main/java/org/geysermc/geyser/item/custom/impl/UseCooldownImpl.java @@ -23,17 +23,36 @@ * @link https://github.com/GeyserMC/Geyser */ -package org.geysermc.geyser.api.item.custom.v2.component; +package org.geysermc.geyser.item.custom.impl; -import org.checkerframework.checker.index.qual.Positive; import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.item.custom.v2.component.java.UseCooldown; import org.geysermc.geyser.api.util.Identifier; -public record UseCooldown(@Positive float seconds, @Nullable Identifier cooldownGroup) { - - public UseCooldown { - if (seconds <= 0.0F) { - throw new IllegalArgumentException("Cooldown seconds must be above 0"); +public record UseCooldownImpl( + float seconds, + @Nullable Identifier cooldownGroup +) implements UseCooldown { + + public static class Builder implements UseCooldown.Builder { + private Identifier cooldownGroup; + private float seconds; + + @Override + public Builder cooldownGroup(@Nullable Identifier cooldownGroup) { + this.cooldownGroup = cooldownGroup; + return this; + } + + @Override + public Builder seconds(float seconds) { + this.seconds = seconds; + return this; + } + + @Override + public UseCooldown build() { + return new UseCooldownImpl(seconds, cooldownGroup); } } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index eabb46bd62b..4153d8a1888 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.registry.loader; -import net.kyori.adventure.key.Key; import org.geysermc.geyser.api.bedrock.camera.CameraFade; import org.geysermc.geyser.api.bedrock.camera.CameraPosition; import org.geysermc.geyser.api.block.custom.CustomBlockData; @@ -43,23 +42,41 @@ 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.NonVanillaCustomItemDefinition; +import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.BlockPlacer; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.Chargeable; +import org.geysermc.geyser.api.item.custom.v2.component.java.Consumable; +import org.geysermc.geyser.api.item.custom.v2.component.java.Equippable; +import org.geysermc.geyser.api.item.custom.v2.component.java.FoodProperties; +import org.geysermc.geyser.api.item.custom.v2.component.java.Repairable; +import org.geysermc.geyser.api.item.custom.v2.component.java.ToolProperties; +import org.geysermc.geyser.api.item.custom.v2.component.java.UseCooldown; import org.geysermc.geyser.api.pack.PathPackCodec; import org.geysermc.geyser.api.pack.UrlPackCodec; import org.geysermc.geyser.api.pack.option.PriorityOption; import org.geysermc.geyser.api.pack.option.SubpackOption; import org.geysermc.geyser.api.pack.option.UrlFallbackOption; import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.event.GeyserEventRegistrar; +import org.geysermc.geyser.extension.command.GeyserExtensionCommand; import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.impl.camera.GeyserCameraFade; import org.geysermc.geyser.impl.camera.GeyserCameraPosition; -import org.geysermc.geyser.event.GeyserEventRegistrar; -import org.geysermc.geyser.extension.command.GeyserExtensionCommand; import org.geysermc.geyser.item.GeyserCustomItemData; import org.geysermc.geyser.item.GeyserCustomItemOptions; import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData; import org.geysermc.geyser.item.custom.GeyserCustomItemBedrockOptions; import org.geysermc.geyser.item.custom.GeyserCustomItemDefinition; import org.geysermc.geyser.item.custom.GeyserNonVanillaCustomItemDefinition; +import org.geysermc.geyser.item.custom.impl.BlockPlacerImpl; +import org.geysermc.geyser.item.custom.impl.ChargeableImpl; +import org.geysermc.geyser.item.custom.impl.ConsumableImpl; +import org.geysermc.geyser.item.custom.impl.DataComponentImpl; +import org.geysermc.geyser.item.custom.impl.EquippableImpl; +import org.geysermc.geyser.item.custom.impl.FoodPropertiesImpl; +import org.geysermc.geyser.item.custom.impl.RepairableImpl; +import org.geysermc.geyser.item.custom.impl.ToolPropertiesImpl; +import org.geysermc.geyser.item.custom.impl.UseCooldownImpl; import org.geysermc.geyser.level.block.GeyserCustomBlockComponents; import org.geysermc.geyser.level.block.GeyserCustomBlockData; import org.geysermc.geyser.level.block.GeyserGeometryComponent; @@ -75,6 +92,7 @@ import java.nio.file.Path; import java.util.Map; +import java.util.function.Predicate; /** * Registers the provider data from the provider. @@ -84,7 +102,7 @@ public class ProviderRegistryLoader implements RegistryLoader, Prov @Override public Map, ProviderSupplier> load(Map, ProviderSupplier> providers) { // misc - providers.put(Identifier.class, args -> new IdentifierImpl(Key.key((String) args[0], (String) args[1]))); + providers.put(Identifier.class, args -> IdentifierImpl.of((String) args[0], (String) args[1])); // commands providers.put(Command.Builder.class, args -> new GeyserExtensionCommand.Builder<>((Extension) args[0])); @@ -116,10 +134,28 @@ public Map, ProviderSupplier> load(Map, ProviderSupplier> prov providers.put(NonVanillaCustomItemDefinition.Builder.class, args -> new GeyserNonVanillaCustomItemDefinition.Builder((Identifier) args[0], (Identifier) args[1], (int) args[2])); providers.put(CustomItemBedrockOptions.Builder.class, args -> new GeyserCustomItemBedrockOptions.Builder()); + providers.put(DataComponent.class, args -> dataComponentProvider((Identifier) args[1], (Predicate) args[2], (Boolean) args[3])); + + // item components + providers.put(Consumable.Builder.class, args -> new ConsumableImpl.Builder()); + providers.put(Equippable.Builder.class, args -> new EquippableImpl.Builder()); + providers.put(FoodProperties.Builder.class, args -> new FoodPropertiesImpl.Builder()); + providers.put(Repairable.Builder.class, args -> new RepairableImpl.Builder()); + providers.put(ToolProperties.Builder.class, args -> new ToolPropertiesImpl.Builder()); + providers.put(UseCooldown.Builder.class, args -> new UseCooldownImpl.Builder()); + + // geyser components + providers.put(Chargeable.Builder.class, args -> new ChargeableImpl.Builder()); + providers.put(BlockPlacer.Builder.class, args -> new BlockPlacerImpl.Builder()); + // cameras providers.put(CameraFade.Builder.class, args -> new GeyserCameraFade.Builder()); providers.put(CameraPosition.Builder.class, args -> new GeyserCameraPosition.Builder()); return providers; } + + public DataComponentImpl dataComponentProvider(Identifier identifier, Predicate predicate, boolean vanilla) { + return new DataComponentImpl<>(identifier, predicate, vanilla); + } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/DataComponentReaders.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/DataComponentReaders.java index d225828eb5c..d7dde8ae490 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/DataComponentReaders.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/DataComponentReaders.java @@ -29,7 +29,7 @@ import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; import org.geysermc.geyser.api.util.Identifier; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; import org.geysermc.geyser.registry.mappings.components.readers.BooleanComponentReader; @@ -67,12 +67,12 @@ public static void readDataComponent(CustomItemDefinition.Builder builder, Strin READERS.put(MinecraftKey.key("consumable"), new ConsumableReader()); READERS.put(MinecraftKey.key("equippable"), new EquippableReader()); READERS.put(MinecraftKey.key("food"), new FoodPropertiesReader()); - READERS.put(MinecraftKey.key("max_damage"), new IntComponentReader(DataComponent.MAX_DAMAGE, 0)); - READERS.put(MinecraftKey.key("max_stack_size"), new IntComponentReader(DataComponent.MAX_STACK_SIZE, 1, 99)); + READERS.put(MinecraftKey.key("max_damage"), new IntComponentReader(ItemDataComponents.MAX_DAMAGE, 0)); + READERS.put(MinecraftKey.key("max_stack_size"), new IntComponentReader(ItemDataComponents.MAX_STACK_SIZE, 1, 99)); READERS.put(MinecraftKey.key("use_cooldown"), new UseCooldownReader()); READERS.put(MinecraftKey.key("enchantable"), new EnchantableReader()); READERS.put(MinecraftKey.key("tool"), new ToolPropertiesReader()); READERS.put(MinecraftKey.key("repairable"), new RepairableReader()); - READERS.put(MinecraftKey.key("enchantment_glint_override"), new BooleanComponentReader(DataComponent.ENCHANTMENT_GLINT_OVERRIDE)); + READERS.put(MinecraftKey.key("enchantment_glint_override"), new BooleanComponentReader(ItemDataComponents.ENCHANTMENT_GLINT_OVERRIDE)); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/ConsumableReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/ConsumableReader.java index 077094d7e23..a47b1809f2d 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/ConsumableReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/ConsumableReader.java @@ -27,8 +27,9 @@ import com.google.gson.JsonElement; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.api.item.custom.v2.component.Consumable; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.java.Consumable; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; +import org.geysermc.geyser.item.custom.impl.ConsumableImpl; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; import org.geysermc.geyser.registry.mappings.components.DataComponentReader; import org.geysermc.geyser.registry.mappings.util.MappingsUtil; @@ -37,7 +38,7 @@ public class ConsumableReader extends DataComponentReader { public ConsumableReader() { - super(DataComponent.CONSUMABLE); + super(ItemDataComponents.CONSUMABLE); } @Override @@ -45,6 +46,6 @@ protected Consumable readDataComponent(@NonNull JsonElement element, String... c float consumeSeconds = MappingsUtil.readOrDefault(element, "consume_seconds", NodeReader.POSITIVE_DOUBLE.andThen(Double::floatValue), 1.6F, context); Consumable.Animation animation = MappingsUtil.readOrDefault(element, "animation", NodeReader.CONSUMABLE_ANIMATION, Consumable.Animation.EAT, context); - return new Consumable(consumeSeconds, animation); + return new ConsumableImpl(consumeSeconds, animation); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/EnchantableReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/EnchantableReader.java index ed6b05108fd..ef0e9bfd803 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/EnchantableReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/EnchantableReader.java @@ -28,7 +28,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; import org.geysermc.geyser.registry.mappings.components.DataComponentReader; import org.geysermc.geyser.registry.mappings.util.MappingsUtil; @@ -62,7 +62,7 @@ public class EnchantableReader extends DataComponentReader { public EnchantableReader() { - super(DataComponent.ENCHANTABLE); + super(ItemDataComponents.ENCHANTABLE); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/EquippableReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/EquippableReader.java index cfdf2cac029..06287d9e712 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/EquippableReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/EquippableReader.java @@ -27,8 +27,9 @@ import com.google.gson.JsonElement; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; -import org.geysermc.geyser.api.item.custom.v2.component.Equippable; +import org.geysermc.geyser.api.item.custom.v2.component.java.Equippable; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; +import org.geysermc.geyser.item.custom.impl.EquippableImpl; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; import org.geysermc.geyser.registry.mappings.components.DataComponentReader; import org.geysermc.geyser.registry.mappings.util.MappingsUtil; @@ -37,12 +38,12 @@ public class EquippableReader extends DataComponentReader { public EquippableReader() { - super(DataComponent.EQUIPPABLE); + super(ItemDataComponents.EQUIPPABLE); } @Override protected Equippable readDataComponent(@NonNull JsonElement element, String... context) throws InvalidCustomMappingsFileException { Equippable.EquipmentSlot slot = MappingsUtil.readOrThrow(element, "slot", NodeReader.EQUIPMENT_SLOT, context); - return new Equippable(slot); + return new EquippableImpl(slot); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/FoodPropertiesReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/FoodPropertiesReader.java index e07c41a43ec..b5f59b8fce3 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/FoodPropertiesReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/FoodPropertiesReader.java @@ -27,8 +27,9 @@ import com.google.gson.JsonElement; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; -import org.geysermc.geyser.api.item.custom.v2.component.FoodProperties; +import org.geysermc.geyser.api.item.custom.v2.component.java.FoodProperties; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; +import org.geysermc.geyser.item.custom.impl.FoodPropertiesImpl; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; import org.geysermc.geyser.registry.mappings.components.DataComponentReader; import org.geysermc.geyser.registry.mappings.util.MappingsUtil; @@ -37,7 +38,7 @@ public class FoodPropertiesReader extends DataComponentReader { public FoodPropertiesReader() { - super(DataComponent.FOOD); + super(ItemDataComponents.FOOD); } @Override @@ -46,6 +47,6 @@ protected FoodProperties readDataComponent(@NonNull JsonElement element, String. float saturation = MappingsUtil.readOrDefault(element, "saturation", NodeReader.NON_NEGATIVE_DOUBLE.andThen(Double::floatValue), 0.0F, context); boolean canAlwaysEat = MappingsUtil.readOrDefault(element, "can_always_eat", NodeReader.BOOLEAN, false, context); - return new FoodProperties(nutrition, saturation, canAlwaysEat); + return new FoodPropertiesImpl(nutrition, saturation, canAlwaysEat); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/RepairableReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/RepairableReader.java index 79ee41d64d0..a500e0b218e 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/RepairableReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/RepairableReader.java @@ -27,9 +27,10 @@ import com.google.gson.JsonElement; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; -import org.geysermc.geyser.api.item.custom.v2.component.Repairable; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; +import org.geysermc.geyser.api.item.custom.v2.component.java.Repairable; import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.item.custom.impl.RepairableImpl; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; import org.geysermc.geyser.registry.mappings.components.DataComponentReader; import org.geysermc.geyser.registry.mappings.util.MappingsUtil; @@ -40,17 +41,17 @@ public class RepairableReader extends DataComponentReader { public RepairableReader() { - super(DataComponent.REPAIRABLE); + super(ItemDataComponents.REPAIRABLE); } @Override protected Repairable readDataComponent(@NonNull JsonElement node, String... context) throws InvalidCustomMappingsFileException { try { Identifier item = MappingsUtil.readOrThrow(node, "items", NodeReader.IDENTIFIER, context); - return new Repairable(item); + return Repairable.of(item); } catch (InvalidCustomMappingsFileException exception) { List items = MappingsUtil.readArrayOrThrow(node, "items", NodeReader.IDENTIFIER, context); - return new Repairable(items.toArray(Identifier[]::new)); + return new RepairableImpl(items); } } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/ToolPropertiesReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/ToolPropertiesReader.java index 3c8a08f8707..815a949d43e 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/ToolPropertiesReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/ToolPropertiesReader.java @@ -27,8 +27,9 @@ import com.google.gson.JsonElement; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; -import org.geysermc.geyser.api.item.custom.v2.component.ToolProperties; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; +import org.geysermc.geyser.api.item.custom.v2.component.java.ToolProperties; +import org.geysermc.geyser.item.custom.impl.ToolPropertiesImpl; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; import org.geysermc.geyser.registry.mappings.components.DataComponentReader; import org.geysermc.geyser.registry.mappings.util.MappingsUtil; @@ -37,11 +38,11 @@ public class ToolPropertiesReader extends DataComponentReader { public ToolPropertiesReader() { - super(DataComponent.TOOL); + super(ItemDataComponents.TOOL); } @Override protected ToolProperties readDataComponent(@NonNull JsonElement element, String... context) throws InvalidCustomMappingsFileException { - return new ToolProperties(MappingsUtil.readOrDefault(element, "can_destroy_blocks_in_creative", NodeReader.BOOLEAN, true, context)); + return new ToolPropertiesImpl(MappingsUtil.readOrDefault(element, "can_destroy_blocks_in_creative", NodeReader.BOOLEAN, true, context)); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/UseCooldownReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/UseCooldownReader.java index 69baeb03936..f4c9d64ddec 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/UseCooldownReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/components/readers/UseCooldownReader.java @@ -27,9 +27,10 @@ import com.google.gson.JsonElement; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; -import org.geysermc.geyser.api.item.custom.v2.component.UseCooldown; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; +import org.geysermc.geyser.api.item.custom.v2.component.java.UseCooldown; import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.item.custom.impl.UseCooldownImpl; import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException; import org.geysermc.geyser.registry.mappings.components.DataComponentReader; import org.geysermc.geyser.registry.mappings.util.MappingsUtil; @@ -38,7 +39,7 @@ public class UseCooldownReader extends DataComponentReader { public UseCooldownReader() { - super(DataComponent.USE_COOLDOWN); + super(ItemDataComponents.USE_COOLDOWN); } @Override @@ -46,6 +47,6 @@ protected UseCooldown readDataComponent(@NonNull JsonElement element, String... float seconds = MappingsUtil.readOrThrow(element, "seconds", NodeReader.POSITIVE_DOUBLE.andThen(Double::floatValue), context); Identifier cooldownGroup = MappingsUtil.readOrDefault(element, "cooldown_group", NodeReader.IDENTIFIER, null, context); - return new UseCooldown(seconds, cooldownGroup); + return new UseCooldownImpl(seconds, cooldownGroup); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/NodeReader.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/NodeReader.java index 8c07dfcf23d..5adc61b744f 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/util/NodeReader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/util/NodeReader.java @@ -26,8 +26,9 @@ package org.geysermc.geyser.registry.mappings.util; import com.google.gson.JsonPrimitive; -import org.geysermc.geyser.api.item.custom.v2.component.Consumable; -import org.geysermc.geyser.api.item.custom.v2.component.Equippable; +import org.geysermc.geyser.Constants; +import org.geysermc.geyser.api.item.custom.v2.component.java.Consumable; +import org.geysermc.geyser.api.item.custom.v2.component.java.Equippable; import org.geysermc.geyser.api.predicate.PredicateStrategy; import org.geysermc.geyser.api.predicate.context.item.ChargedProjectile; import org.geysermc.geyser.api.util.CreativeCategory; @@ -90,6 +91,15 @@ public interface NodeReader { NodeReader IDENTIFIER = NON_EMPTY_STRING.andThen(Identifier::of); + NodeReader GEYSER_IDENTIFIER = NON_EMPTY_STRING.validate(s -> !s.startsWith("minecraft:"), "namespace cannot be minecraft") + .andThen(Identifier::of) + .andThen(identifier -> { + if (identifier.namespace().equals(Identifier.DEFAULT_NAMESPACE)) { + return Identifier.of(Constants.GEYSER_CUSTOM_NAMESPACE, identifier.path()); + } + return identifier; + }); + NodeReader CREATIVE_CATEGORY = NON_EMPTY_STRING.andThen(CreativeCategory::fromName).validate(Objects::nonNull, "unknown creative category"); NodeReader PREDICATE_STRATEGY = ofEnum(PredicateStrategy.class); diff --git a/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v2.java b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v2.java index 0b426d16606..ba9c4c4701b 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v2.java +++ b/core/src/main/java/org/geysermc/geyser/registry/mappings/versions/MappingsReader_v2.java @@ -28,9 +28,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import net.kyori.adventure.key.Key; import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.item.custom.v2.CustomItemBedrockOptions; import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition; @@ -45,7 +43,6 @@ import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping; import org.geysermc.geyser.registry.mappings.util.MappingsUtil; import org.geysermc.geyser.registry.mappings.util.NodeReader; -import org.geysermc.geyser.util.MinecraftKey; import java.nio.file.Path; import java.util.HashSet; @@ -119,7 +116,7 @@ private void readItemDefinitionEntry(JsonElement data, Identifier itemIdentifier @Override public CustomItemDefinition readItemMappingEntry(Identifier parentModel, JsonElement element) throws InvalidCustomMappingsFileException { - Identifier bedrockIdentifier = MappingsUtil.readOrThrow(element, "bedrock_identifier", NodeReader.IDENTIFIER, "item definition"); + Identifier bedrockIdentifier = MappingsUtil.readOrThrow(element, "bedrock_identifier", NodeReader.GEYSER_IDENTIFIER, "item definition"); // We now know the Bedrock identifier, make a base context so that the error can be easily located in the JSON file String context = "item definition (bedrock identifier=" + bedrockIdentifier + ")"; @@ -129,9 +126,6 @@ public CustomItemDefinition readItemMappingEntry(Identifier parentModel, JsonEle throw new InvalidCustomMappingsFileException("reading item model", "no model present", context); } - if (bedrockIdentifier.namespace().equals(Key.MINECRAFT_NAMESPACE)) { - bedrockIdentifier = Identifier.of(Constants.GEYSER_CUSTOM_NAMESPACE, bedrockIdentifier.path()); // Use geyser_custom namespace when no namespace or the minecraft namespace was given - } CustomItemDefinition.Builder builder = CustomItemDefinition.builder(bedrockIdentifier, model); MappingsUtil.readIfPresent(element, "display_name", builder::displayName, NodeReader.NON_EMPTY_STRING, context); 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 b42515ca465..d4abf2bc099 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 @@ -40,11 +40,11 @@ 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.NonVanillaCustomItemDefinition; -import org.geysermc.geyser.api.item.custom.v2.component.BlockPlacer; -import org.geysermc.geyser.api.item.custom.v2.component.Chargeable; -import org.geysermc.geyser.api.item.custom.v2.component.DataComponent; -import org.geysermc.geyser.api.item.custom.v2.component.GeyserDataComponent; -import org.geysermc.geyser.api.item.custom.v2.component.Repairable; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.BlockPlacer; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.Chargeable; +import org.geysermc.geyser.api.item.custom.v2.component.geyser.GeyserDataComponent; +import org.geysermc.geyser.api.item.custom.v2.component.java.ItemDataComponents; +import org.geysermc.geyser.api.item.custom.v2.component.java.Repairable; import org.geysermc.geyser.api.predicate.MinecraftPredicate; import org.geysermc.geyser.api.predicate.context.item.ItemPredicateContext; import org.geysermc.geyser.api.predicate.item.ItemConditionPredicate; @@ -71,10 +71,8 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.UseCooldown; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -253,7 +251,7 @@ private static DataComponents checkComponents(CustomItemDefinition definition, I throw new InvalidItemComponentsException("Stack size must be 1 when max damage is above 0"); } - Repairable repairable = definition.components().get(DataComponent.REPAIRABLE); + Repairable repairable = definition.components().get(ItemDataComponents.REPAIRABLE); if (repairable != null) { for (Identifier item : repairable.items()) { if (Registries.JAVA_ITEM_IDENTIFIERS.get(item.toString()) == null) { @@ -290,7 +288,7 @@ private static NbtMapBuilder createComponentNbt(Key itemIdentifier, CustomItemDe computeCreativeDestroyProperties(canDestroyInCreative, itemProperties, componentBuilder); // Using API component here because MCPL one is just an ID holder set - Repairable repairable = customItemDefinition.components().get(DataComponent.REPAIRABLE); + Repairable repairable = customItemDefinition.components().get(ItemDataComponents.REPAIRABLE); if (repairable != null) { computeRepairableProperties(repairable, componentBuilder); } @@ -327,9 +325,9 @@ private static NbtMapBuilder createComponentNbt(Key itemIdentifier, CustomItemDe BlockPlacer blockPlacer = vanillaMapping.map(mapping -> { String bedrockIdentifier = mapping.getBedrockIdentifier(); if (bedrockIdentifier.equals("minecraft:fire_charge") || bedrockIdentifier.equals("minecraft:flint_and_steel")) { - return new BlockPlacer(Identifier.of("fire"), false); + return BlockPlacer.builder().block(Identifier.of("fire")).build(); } else if (mapping.getFirstBlockRuntimeId() != null) { - return new BlockPlacer(Identifier.of(mapping.getBedrockIdentifier()), false); + return BlockPlacer.builder().block(Identifier.of(mapping.getBedrockIdentifier())).build(); } return null; }).orElse(customItemDefinition.components().get(GeyserDataComponent.BLOCK_PLACER)); @@ -339,8 +337,8 @@ private static NbtMapBuilder createComponentNbt(Key itemIdentifier, CustomItemDe } Chargeable chargeable = vanillaMapping.map(GeyserMappingItem::getBedrockIdentifier).map(identifier -> switch (identifier) { - case "minecraft:bow" -> new Chargeable(1.0F, false, Identifier.of("arrow")); - case "minecraft:crossbow" -> new Chargeable(0.0F, true, Identifier.of("arrow")); + case "minecraft:bow" -> Chargeable.builder().maxDrawDuration(1.0F).ammunition(Identifier.of("arrow")).build(); + case "minecraft:crossbow" -> Chargeable.builder().chargeOnDraw(true).ammunition(Identifier.of("arrow")).build(); default -> null; }).orElse(customItemDefinition.components().get(GeyserDataComponent.CHARGEABLE)); @@ -458,7 +456,7 @@ private static void computeCreativeDestroyProperties(boolean canDestroyInCreativ *

This method passes the Java identifiers straight to bedrock - which isn't perfect.

*/ private static void computeRepairableProperties(Repairable repairable, NbtMapBuilder componentBuilder) { - List items = Arrays.stream(repairable.items()) + List items = repairable.items().stream() .map(identifier -> NbtMap.builder() .putString("name", identifier.toString()) .build()).toList(); @@ -532,7 +530,7 @@ private static void computeChargeableProperties(NbtMapBuilder itemProperties, Nb } componentBuilder.putCompound("minecraft:shooter", NbtMap.builder() - .putList("ammunition", NbtType.COMPOUND, Arrays.stream(chargeable.ammunition()) + .putList("ammunition", NbtType.COMPOUND, chargeable.ammunition().stream() .map(ammunition -> NbtMap.builder() .putCompound("item", NbtMap.builder() 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 6064f576a0d..69f215690d8 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 @@ -166,7 +166,7 @@ public static void populate() { throw new AssertionError("Unable to load Bedrock item components", e); } - boolean customItemsAllowed = GeyserImpl.getInstance().getConfig().isAddNonBedrockItems(); + boolean customItemsAllowed = bootstrap.getGeyserConfig().isAddNonBedrockItems(); Multimap customItems = MultimapBuilder.hashKeys().arrayListValues().build(); Multimap nonVanillaCustomItems = MultimapBuilder.hashKeys().arrayListValues().build(); @@ -609,7 +609,6 @@ public static void populate() { try { NonVanillaItemRegistration registration = CustomItemRegistryPopulator.registerCustomItem(customItem, customItemId, palette.protocolVersion); - ItemMapping mapping = registration.mapping(); Item javaItem = registration.javaItem(); while (javaItem.javaId() >= mappings.size()) { diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index 05ea066d82c..2932dfffe02 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -1393,8 +1393,8 @@ public boolean hasFishingRodCast() { */ public void setFishingRodCast(boolean cast) { this.hasFishingRodCast = cast; - InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(this, playerInventoryHolder.inventory(), - playerInventoryHolder.inventory().getOffsetForHotbar(playerInventoryHolder.inventory().getHeldItemSlot())); + int slot = getPlayerInventory().getOffsetForHotbar(getPlayerInventory().getHeldItemSlot()); + this.playerInventoryHolder.updateSlot(slot); } /** diff --git a/core/src/main/java/org/geysermc/geyser/util/MinecraftKey.java b/core/src/main/java/org/geysermc/geyser/util/MinecraftKey.java index bca979e82df..891c3ed8211 100644 --- a/core/src/main/java/org/geysermc/geyser/util/MinecraftKey.java +++ b/core/src/main/java/org/geysermc/geyser/util/MinecraftKey.java @@ -40,14 +40,21 @@ public static Key key(@Subst("empty") String s) { return Key.key(s); } - public static Key identifierToKey(@Nullable Identifier identifier) { + /** + * To prevent constant warnings from invalid regex. + */ + public static Key key(@Subst("empty") String namespace, @Subst("empty") String value) { + return Key.key(namespace, value); + } + + public static @Nullable Key identifierToKey(@Nullable Identifier identifier) { if (identifier == null) { return null; } - return identifier instanceof IdentifierImpl impl ? impl.identifier() : Key.key(identifier.namespace(), identifier.path()); + return identifier instanceof IdentifierImpl impl ? impl.identifier() : key(identifier.namespace(), identifier.path()); } - public static Identifier keyToIdentifier(@Nullable Key key) { + public static @Nullable Identifier keyToIdentifier(@Nullable Key key) { if (key == null) { return null; }