From d869e745e0aea56ddc1cadfdda48235fb181db27 Mon Sep 17 00:00:00 2001 From: Zigy <105124180+ZigyTheBird@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:09:34 +0330 Subject: [PATCH] Custom entity properties API (#5788) * Custom entity properties API * Fix build fail * Resolve comments * Entity property registration improvements * oops * Add boolean and enum property sync API * default value + no packet if value unchanged * Don't send packet if no properties were updated * small refactor * the refactor, part two * Move updateProperties to GeyserEntity * Don't inherit properties from parent * temp * type-safe property updating * Address review * call the GeyserDefineEntityPropertiesEvent once, require specifying entity identifier instead of calling the event once for every entity type * Migrate to identifiers (from custom items v2, thanks eclipse), remove duplicate logic * fix test * Merge 1.21.9, update copper golem entity property usage * fixup javadocs --------- Co-authored-by: onebeastchris --- .../entity/property/BatchPropertyUpdater.java | 69 +++++ .../entity/property/GeyserEntityProperty.java | 65 +++++ .../type/GeyserBooleanEntityProperty.java | 35 +++ .../type/GeyserEnumEntityProperty.java | 42 +++ .../type/GeyserFloatEntityProperty.java | 52 ++++ .../type/GeyserIntEntityProperty.java | 57 +++++ .../type/GeyserStringEnumProperty.java | 49 ++++ .../geyser/api/entity/type/GeyserEntity.java | 30 ++- .../GeyserDefineEntityPropertiesEvent.java | 242 ++++++++++++++++++ .../geysermc/geyser/api/util/Identifier.java | 116 +++++++++ .../geyser/entity/EntityDefinition.java | 19 +- .../geyser/entity/EntityDefinitions.java | 145 +++++++++-- .../geyser/entity/GeyserEntityData.java | 1 - .../properties/GeyserEntityProperties.java | 140 ++++------ .../GeyserEntityPropertyManager.java | 61 ++--- .../properties/VanillaEntityProperties.java | 94 ------- .../properties/type/AbstractEnumProperty.java | 88 +++++++ .../properties/type/BooleanProperty.java | 30 ++- .../entity/properties/type/EnumProperty.java | 53 ++-- .../entity/properties/type/FloatProperty.java | 43 +++- .../entity/properties/type/IntProperty.java | 48 +++- .../entity/properties/type/PropertyType.java | 16 +- .../properties/type/StringEnumProperty.java | 66 +++++ .../geysermc/geyser/entity/type/Entity.java | 49 ++++ .../entity/type/ThrowableEggEntity.java | 17 +- .../entity/type/living/CopperGolemEntity.java | 69 +++-- .../type/living/animal/ArmadilloEntity.java | 29 ++- .../entity/type/living/animal/BeeEntity.java | 9 +- .../type/living/animal/HappyGhastEntity.java | 11 +- .../animal/farm/TemperatureVariantAnimal.java | 27 +- .../living/animal/tameable/WolfEntity.java | 25 +- .../type/living/monster/CreakingEntity.java | 49 ++-- .../geysermc/geyser/impl/IdentifierImpl.java | 65 +++++ .../loader/ProviderRegistryLoader.java | 5 + .../geyser/session/GeyserSession.java | 4 + .../geysermc/geyser/util/MinecraftKey.java | 24 ++ 36 files changed, 1555 insertions(+), 389 deletions(-) create mode 100644 api/src/main/java/org/geysermc/geyser/api/entity/property/BatchPropertyUpdater.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/entity/property/GeyserEntityProperty.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserBooleanEntityProperty.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserEnumEntityProperty.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserFloatEntityProperty.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserIntEntityProperty.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserStringEnumProperty.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java create mode 100644 api/src/main/java/org/geysermc/geyser/api/util/Identifier.java delete mode 100644 core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java create mode 100644 core/src/main/java/org/geysermc/geyser/entity/properties/type/AbstractEnumProperty.java create mode 100644 core/src/main/java/org/geysermc/geyser/entity/properties/type/StringEnumProperty.java create mode 100644 core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/BatchPropertyUpdater.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/BatchPropertyUpdater.java new file mode 100644 index 000000000..308d23cd1 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/BatchPropertyUpdater.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.api.entity.property; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; + +/** + * Collects property changes to be applied as a single, batched update to an entity. + *

+ * Notes: + *

+ * + *
{@code
+ * entity.updatePropertiesBatched(updater -> {
+ *     updater.update(SOME_FLOAT_PROPERTY, 0.15f);
+ *     updater.update(SOME_BOOLEAN_PROPERTY, true);
+ *     updater.update(SOME_INT_PROPERTY, null); // reset to default
+ * });
+ * }
+ * + * @since 2.9.0 + */ +@FunctionalInterface +public interface BatchPropertyUpdater { + + /** + * Queues an update for the given property within the current batch. + *

+ * If {@code value} is {@code null}, the property will be reset to its default value + * as declared when the property was registered during the {@link GeyserDefineEntityPropertiesEvent}. + * + * @param property a {@link GeyserEntityProperty} registered for the target entity type + * @param value the new value, or {@code null} to reset to the default + * @param the property's value type + * + * @since 2.9.0 + */ + void update(@NonNull GeyserEntityProperty property, @Nullable T value); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/GeyserEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/GeyserEntityProperty.java new file mode 100644 index 000000000..9489a9946 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/GeyserEntityProperty.java @@ -0,0 +1,65 @@ +/* + * 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.entity.property; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.util.Identifier; + +/** + * Represents a property that can be attached to an entity. + *

+ * Entity properties are used to describe metadata about an entity, such as + * integers, floats, booleans, or enums. + * @see + * Official documentation for info + * + * @param the type of value stored by this property + * + * @since 2.9.0 + */ +public interface GeyserEntityProperty { + + /** + * Gets the unique name of this property. + * Custom properties cannot use the vanilla namespace + * to avoid collisions with vanilla entity properties. + * + * @return the property identifier + * @since 2.9.0 + */ + @NonNull + Identifier identifier(); + + /** + * Gets the default value of this property which + * is set upon spawning entities. + * + * @return the default value of this property + * @since 2.9.0 + */ + @NonNull + T defaultValue(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserBooleanEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserBooleanEntityProperty.java new file mode 100644 index 000000000..1c988d5d7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserBooleanEntityProperty.java @@ -0,0 +1,35 @@ +/* + * 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.entity.property.type; + +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; + +/** + * Represents a boolean entity property. + * @since 2.9.0 + */ +public interface GeyserBooleanEntityProperty extends GeyserEntityProperty { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserEnumEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserEnumEntityProperty.java new file mode 100644 index 000000000..283111c7f --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserEnumEntityProperty.java @@ -0,0 +1,42 @@ +/* + * 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.entity.property.type; + +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; + +/** + * Represents a Java enum-backed enum property. + * There are a few key limitations: + *

+ * + * @param the enum type + * @since 2.9.0 + */ +public interface GeyserEnumEntityProperty> extends GeyserEntityProperty { +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserFloatEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserFloatEntityProperty.java new file mode 100644 index 000000000..8337e09fa --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserFloatEntityProperty.java @@ -0,0 +1,52 @@ +/* + * 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.entity.property.type; + +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; +import org.geysermc.geyser.api.util.Identifier; + +/** + * Represents a float-backed entity property with inclusive bounds. + * Values associated with this property must be always within the {@code [min(), max()]} bounds. + * + * @see GeyserDefineEntityPropertiesEvent#registerFloatProperty(Identifier, Identifier, float, float, Float) + * @since 2.9.0 + */ +public interface GeyserFloatEntityProperty extends GeyserEntityProperty { + + /** + * @return the inclusive lower bound for this property + * @since 2.9.0 + */ + float min(); + + /** + * @return the inclusive upper bound for this property + * @since 2.9.0 + */ + float max(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserIntEntityProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserIntEntityProperty.java new file mode 100644 index 000000000..36c4996bd --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserIntEntityProperty.java @@ -0,0 +1,57 @@ +/* + * 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.entity.property.type; + +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; +import org.geysermc.geyser.api.util.Identifier; + +/** + * Represents an int-backed entity property with inclusive bounds. + * There are a few key limitations: + *
    + *
  • Values must be always within the {@code [min(), max()]} bounds
  • + *
  • Molang evaluation uses floats under the hood; very large integers can lose precision. + * Prefer keeping values in a practical range to avoid rounding issues.
  • + *
+ * + * @see GeyserDefineEntityPropertiesEvent#registerIntegerProperty(Identifier, Identifier, int, int, Integer) + * @since 2.9.0 + */ +public interface GeyserIntEntityProperty extends GeyserEntityProperty { + + /** + * @return the inclusive lower bound for this property + * @since 2.9.0 + */ + int min(); + + /** + * @return the inclusive upper bound for this property + * @since 2.9.0 + */ + int max(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserStringEnumProperty.java b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserStringEnumProperty.java new file mode 100644 index 000000000..ebd541ef7 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/property/type/GeyserStringEnumProperty.java @@ -0,0 +1,49 @@ +/* + * 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.entity.property.type; + +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; + +import java.util.List; + +/** + * Represents a string-backed enum property. + * There are a few key limitations: + *
    + *
  • There cannot be more than 16 values
  • + *
  • The values' names cannot be longer than 32 chars, must start with a letter, and may contain numbers and underscores
  • + *
+ * + * @since 2.9.0 + */ +public interface GeyserStringEnumProperty extends GeyserEntityProperty { + + /** + * @return an unmodifiable list of all registered values + * @since 2.9.0 + */ + List values(); +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java b/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java index 02acb4e21..1f8698518 100644 --- a/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java +++ b/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java @@ -26,9 +26,17 @@ package org.geysermc.geyser.api.entity.type; import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.connection.GeyserConnection; +import org.geysermc.geyser.api.entity.property.BatchPropertyUpdater; +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; + +import java.util.function.Consumer; /** - * Represents a unique instance of an entity. Each {@link org.geysermc.geyser.api.connection.GeyserConnection} + * Represents a unique instance of an entity. Each {@link GeyserConnection} * have their own sets of entities - no two instances will share the same GeyserEntity instance. */ public interface GeyserEntity { @@ -37,4 +45,24 @@ public interface GeyserEntity { */ @NonNegative int javaId(); + + /** + * Updates an entity property with a new value. + * If the new value is null, the property is reset to the default value. + * + * @param property a {@link GeyserEntityProperty} registered for this type in the {@link GeyserDefineEntityPropertiesEvent} + * @param value the new property value + * @param the type of the value + * @since 2.9.0 + */ + default void updateProperty(@NonNull GeyserEntityProperty property, @Nullable T value) { + this.updatePropertiesBatched(consumer -> consumer.update(property, value)); + } + + /** + * Updates multiple properties with just one update packet. + * @see BatchPropertyUpdater + * @since 2.9.0 + */ + void updatePropertiesBatched(Consumer consumer); } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java new file mode 100644 index 000000000..ef8380041 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java @@ -0,0 +1,242 @@ +/* + * 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.event.lifecycle; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.event.Event; +import org.geysermc.geyser.api.entity.EntityData; +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserBooleanEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserEnumEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserFloatEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserIntEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserStringEnumProperty; +import org.geysermc.geyser.api.entity.type.GeyserEntity; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +/** + * Lifecycle event fired during Geyser's startup to allow custom entity properties + * to be registered for a specific entity type. + *

+ * Listeners can add new properties for any entity by passing the target entity's + * identifier (e.g., {@code Identifier.of("player")}) to the registration methods. + * The returned {@link GeyserEntityProperty} is used to identify the properties and to + * update the value of a specific entity instance. + * + *

Example usage

+ *
{@code
+ * public void onDefine(GeyserDefineEntityPropertiesEvent event) {
+ *     Identifier player = Identifier.of("player");
+ *     GeyserFloatEntityProperty ANIMATION_SPEED =
+ *         event.registerFloatProperty(player, Identifier.of("my_group:animation_speed"), 0.0f, 1.0f, 0.1f);
+ *     GeyserBooleanEntityProperty SHOW_SHORTS =
+ *         event.registerBooleanProperty(player, Identifier.of("my_group:show_shorts"), false);
+ * }
+ * }
+ * + * Retrieving entity instances is possible with the {@link EntityData#entityByJavaId(int)} method, or + * {@link EntityData#playerEntity()} for the connection player entity. + * To update the value of a property on a specific entity, use {@link GeyserEntity#updateProperty(GeyserEntityProperty, Object)}, + * or {@link GeyserEntity#updatePropertiesBatched(Consumer)} to update multiple properties efficiently at once. + * + *

Notes: + *

    + *
  • Default values must fall within the provided bounds.
  • + *
  • There cannot be more than 32 properties registered per entity type in total
  • + *
  • {@link #properties(Identifier)} returns properties registered for the given entity + * (including those added earlier in the same callback), including vanilla properties.
  • + *
+ * + * @since 2.9.0 + */ +public interface GeyserDefineEntityPropertiesEvent extends Event { + + /** + * Returns an unmodifiable view of all properties that have been registered + * so far for the given entity type. This includes entity properties used for vanilla gameplay, + * such as those used for creaking animations. + * + * @param entityType the Java edition entity type identifier + * @return an unmodifiable collection of registered properties + * + * @since 2.9.0 + */ + Collection> properties(@NonNull Identifier entityType); + + /** + * Registers a {@code float}-backed entity property. + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param min the minimum allowed value (inclusive) + * @param max the maximum allowed value (inclusive) + * @param defaultValue the default value assigned initially on entity spawn - if null, it will be the minimum value + * @return the created float property + * + * @since 2.9.0 + */ + GeyserFloatEntityProperty registerFloatProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, float min, float max, @Nullable Float defaultValue); + + /** + * Registers a {@code float}-backed entity property with a default value set to the minimum value. + * @see #registerFloatProperty(Identifier, Identifier, float, float, Float) + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param min the minimum allowed value (inclusive) + * @param max the maximum allowed value (inclusive) + * @return the created float property + * + * @since 2.9.0 + */ + default GeyserFloatEntityProperty registerFloatProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, float min, float max) { + return registerFloatProperty(entityType, propertyIdentifier, min, max, null); + } + + /** + * Registers an {@code int}-backed entity property. + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param min the minimum allowed value (inclusive) + * @param max the maximum allowed value (inclusive) + * @param defaultValue the default value assigned initially on entity spawn - if null, it will be the minimum value + * @return the created int property + * + * @since 2.9.0 + */ + GeyserIntEntityProperty registerIntegerProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, int min, int max, @Nullable Integer defaultValue); + + /** + * Registers an {@code int}-backed entity property with a default value set to the minimum value. + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param min the minimum allowed value (inclusive) + * @param max the maximum allowed value (inclusive) + * @return the created int property + * + * @since 2.9.0 + */ + default GeyserIntEntityProperty registerIntegerProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, int min, int max) { + return registerIntegerProperty(entityType, propertyIdentifier, min, max, null); + } + + /** + * Registers a {@code boolean}-backed entity property. + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param defaultValue the default boolean value + * @return the created boolean property handle + * + * @since 2.9.0 + */ + GeyserBooleanEntityProperty registerBooleanProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, boolean defaultValue); + + /** + * Registers a {@code boolean}-backed entity property with a default of {@code false}. + * @see #registerBooleanProperty(Identifier, Identifier, boolean) + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @return the created boolean property + * @since 2.9.0 + */ + default GeyserBooleanEntityProperty registerBooleanProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier) { + return registerBooleanProperty(entityType, propertyIdentifier, false); + } + + /** + * Registers a typed {@linkplain Enum enum}-backed entity property. + *

+ * The enum constants define the allowed values. If {@code defaultValue} is {@code null}, + * the first enum value is set as the default. + * @see GeyserEnumEntityProperty for further limitations + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param enumClass the enum class that defines allowed values + * @param defaultValue the default enum value, or {@code null} for the first enum value to be the default + * @param the enum type + * @return the created enum property + * + * @since 2.9.0 + */ + > GeyserEnumEntityProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull Class enumClass, @Nullable E defaultValue); + + /** + * Registers a typed {@linkplain Enum enum}-backed entity property with the first value set as the default. + * @see #registerEnumProperty(Identifier, Identifier, Class, Enum) + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param enumClass the enum class that defines allowed values + * @param the enum type + * @return the created enum property + * + * @since 2.9.0 + */ + default > GeyserEnumEntityProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull Class enumClass) { + return registerEnumProperty(entityType, propertyIdentifier, enumClass, null); + } + + /** + * Registers a string-backed "enum-like" entity property where the set of allowed values + * is defined by the provided list. If {@code defaultValue} is {@code null}, the first value is used as the default + * on entity spawn. The default must be one of the values in {@code values}. + * @see GeyserStringEnumProperty + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param values the allowed string values + * @param defaultValue the default string value, or {@code null} for the first value to be used + * @return the created string-enum property + * + * @since 2.9.0 + */ + GeyserStringEnumProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull List values, @Nullable String defaultValue); + + /** + * Registers a string-backed "enum-like" entity property with the first value as the default. + * @see #registerEnumProperty(Identifier, Identifier, List, String) + * + * @param entityType the Java edition entity type identifier + * @param propertyIdentifier the unique property identifier + * @param values the allowed string values + * @return the created string-enum property handle + * + * @since 2.9.0 + */ + default GeyserStringEnumProperty registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull List values) { + return registerEnumProperty(entityType, propertyIdentifier, values, null); + } +} 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 new file mode 100644 index 000000000..e82a695d1 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/util/Identifier.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024-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.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 :}). + * + * @since 2.9.0 + */ +public interface Identifier { + + /** + * The namespace for Minecraft. + * @since 2.9.0 + */ + String DEFAULT_NAMESPACE = "minecraft"; + + /** + * 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. + * @since 2.9.0 + */ + 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 + * @since 2.9.0 + */ + static Identifier of(String identifier) { + String[] split = identifier.split(":"); + String namespace; + String path; + if (split.length == 1) { + namespace = DEFAULT_NAMESPACE; + path = split[0]; + } else if (split.length == 2) { + namespace = split[0]; + path = split[1]; + } else { + throw new IllegalArgumentException("':' in identifier path: " + identifier); + } + return of(namespace, path); + } + + /** + * @return the namespace of this identifier. + * @since 2.9.0 + */ + String namespace(); + + /** + * @return the path of this identifier. + * @since 2.9.0 + */ + String path(); + + /** + * Checks whether this identifier is using the "minecraft" namespace. + * @since 2.9.0 + */ + default boolean vanilla() { + return namespace().equals(DEFAULT_NAMESPACE); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java index d26a25d2c..1cc529a07 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java @@ -31,6 +31,7 @@ import lombok.experimental.Accessors; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.entity.factory.EntityFactory; import org.geysermc.geyser.entity.properties.GeyserEntityProperties; +import org.geysermc.geyser.entity.properties.type.PropertyType; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.translator.entity.EntityMetadataTranslator; @@ -54,7 +55,7 @@ public record EntityDefinition(EntityFactory factory, Entit float width, float height, float offset, GeyserEntityProperties registeredProperties, List> translators) { public static Builder inherited(EntityFactory factory, EntityDefinition parent) { - return new Builder<>(factory, parent.entityType, parent.identifier, parent.width, parent.height, parent.offset, parent.registeredProperties, new ObjectArrayList<>(parent.translators)); + return new Builder<>(factory, parent.entityType, parent.identifier, parent.width, parent.height, parent.offset, new ObjectArrayList<>(parent.translators)); } public static Builder builder(EntityFactory factory) { @@ -89,7 +90,7 @@ public record EntityDefinition(EntityFactory factory, Entit private float width; private float height; private float offset = 0.00001f; - private GeyserEntityProperties registeredProperties; + private GeyserEntityProperties.Builder propertiesBuilder; private final List> translators; private Builder(EntityFactory factory) { @@ -97,14 +98,13 @@ public record EntityDefinition(EntityFactory factory, Entit translators = new ObjectArrayList<>(); } - public Builder(EntityFactory factory, EntityType type, String identifier, float width, float height, float offset, GeyserEntityProperties registeredProperties, List> translators) { + public Builder(EntityFactory factory, EntityType type, String identifier, float width, float height, float offset, List> translators) { this.factory = factory; this.type = type; this.identifier = identifier; this.width = width; this.height = height; this.offset = offset; - this.registeredProperties = registeredProperties; this.translators = translators; } @@ -131,8 +131,11 @@ public record EntityDefinition(EntityFactory factory, Entit return this; } - public Builder properties(GeyserEntityProperties registeredProperties) { - this.registeredProperties = registeredProperties; + public Builder property(PropertyType propertyType) { + if (this.propertiesBuilder == null) { + this.propertiesBuilder = new GeyserEntityProperties.Builder(this.identifier); + } + propertiesBuilder.add(propertyType); return this; } @@ -158,13 +161,11 @@ public record EntityDefinition(EntityFactory factory, Entit if (identifier == null && type != null) { identifier = "minecraft:" + type.name().toLowerCase(Locale.ROOT); } + GeyserEntityProperties registeredProperties = propertiesBuilder == null ? null : propertiesBuilder.build(); EntityDefinition definition = new EntityDefinition<>(factory, type, identifier, width, height, offset, registeredProperties, translators); if (register && definition.entityType() != null) { Registries.ENTITY_DEFINITIONS.get().putIfAbsent(definition.entityType(), definition); Registries.JAVA_ENTITY_IDENTIFIERS.get().putIfAbsent("minecraft:" + type.name().toLowerCase(Locale.ROOT), definition); - if (definition.registeredProperties() != null) { - Registries.BEDROCK_ENTITY_PROPERTIES.get().add(definition.registeredProperties().toNbtMap(identifier)); - } } return definition; } diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index 893d0cbdd..fbe8b7883 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -25,10 +25,23 @@ package org.geysermc.geyser.entity; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserFloatEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserStringEnumProperty; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; +import org.geysermc.geyser.api.util.Identifier; import org.geysermc.geyser.entity.factory.EntityFactory; -import org.geysermc.geyser.entity.properties.VanillaEntityProperties; +import org.geysermc.geyser.entity.properties.type.BooleanProperty; +import org.geysermc.geyser.entity.properties.type.EnumProperty; +import org.geysermc.geyser.entity.properties.type.FloatProperty; +import org.geysermc.geyser.entity.properties.type.IntProperty; +import org.geysermc.geyser.entity.properties.type.PropertyType; +import org.geysermc.geyser.entity.properties.type.StringEnumProperty; import org.geysermc.geyser.entity.type.AbstractArrowEntity; import org.geysermc.geyser.entity.type.AbstractWindChargeEntity; import org.geysermc.geyser.entity.type.AreaEffectCloudEntity; @@ -37,8 +50,6 @@ import org.geysermc.geyser.entity.type.BoatEntity; import org.geysermc.geyser.entity.type.ChestBoatEntity; import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity; import org.geysermc.geyser.entity.type.DisplayBaseEntity; -import org.geysermc.geyser.entity.type.HangingEntity; -import org.geysermc.geyser.entity.type.ThrowableEggEntity; import org.geysermc.geyser.entity.type.EnderCrystalEntity; import org.geysermc.geyser.entity.type.EnderEyeEntity; import org.geysermc.geyser.entity.type.Entity; @@ -49,6 +60,7 @@ import org.geysermc.geyser.entity.type.FireballEntity; import org.geysermc.geyser.entity.type.FireworkEntity; import org.geysermc.geyser.entity.type.FishingHookEntity; import org.geysermc.geyser.entity.type.FurnaceMinecartEntity; +import org.geysermc.geyser.entity.type.HangingEntity; import org.geysermc.geyser.entity.type.InteractionEntity; import org.geysermc.geyser.entity.type.ItemEntity; import org.geysermc.geyser.entity.type.ItemFrameEntity; @@ -60,6 +72,7 @@ import org.geysermc.geyser.entity.type.PaintingEntity; import org.geysermc.geyser.entity.type.SpawnerMinecartEntity; import org.geysermc.geyser.entity.type.TNTEntity; import org.geysermc.geyser.entity.type.TextDisplayEntity; +import org.geysermc.geyser.entity.type.ThrowableEggEntity; import org.geysermc.geyser.entity.type.ThrowableEntity; import org.geysermc.geyser.entity.type.ThrowableItemEntity; import org.geysermc.geyser.entity.type.ThrownPotionEntity; @@ -83,17 +96,14 @@ import org.geysermc.geyser.entity.type.living.TadpoleEntity; import org.geysermc.geyser.entity.type.living.animal.ArmadilloEntity; import org.geysermc.geyser.entity.type.living.animal.AxolotlEntity; import org.geysermc.geyser.entity.type.living.animal.BeeEntity; -import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity; -import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity; -import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; import org.geysermc.geyser.entity.type.living.animal.FoxEntity; import org.geysermc.geyser.entity.type.living.animal.FrogEntity; import org.geysermc.geyser.entity.type.living.animal.GoatEntity; +import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity; import org.geysermc.geyser.entity.type.living.animal.HoglinEntity; import org.geysermc.geyser.entity.type.living.animal.MooshroomEntity; import org.geysermc.geyser.entity.type.living.animal.OcelotEntity; import org.geysermc.geyser.entity.type.living.animal.PandaEntity; -import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity; import org.geysermc.geyser.entity.type.living.animal.PolarBearEntity; import org.geysermc.geyser.entity.type.living.animal.PufferFishEntity; import org.geysermc.geyser.entity.type.living.animal.RabbitEntity; @@ -102,6 +112,10 @@ import org.geysermc.geyser.entity.type.living.animal.SnifferEntity; import org.geysermc.geyser.entity.type.living.animal.StriderEntity; import org.geysermc.geyser.entity.type.living.animal.TropicalFishEntity; import org.geysermc.geyser.entity.type.living.animal.TurtleEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.PigEntity; +import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity; import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity; import org.geysermc.geyser.entity.type.living.animal.horse.ChestedHorseEntity; @@ -158,6 +172,10 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.Boolea import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + public final class EntityDefinitions { public static final EntityDefinition ACACIA_BOAT; public static final EntityDefinition ACACIA_CHEST_BOAT; @@ -469,7 +487,7 @@ public final class EntityDefinitions { EGG = EntityDefinition.inherited(ThrowableEggEntity::new, throwableItemBase) .type(EntityType.EGG) .heightAndWidth(0.25f) - .properties(VanillaEntityProperties.CLIMATE_VARIANT) + .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY) .build(); ENDER_PEARL = EntityDefinition.inherited(ThrowableItemEntity::new, throwableItemBase) .type(EntityType.ENDER_PEARL) @@ -715,7 +733,9 @@ public final class EntityDefinitions { .height(0.49f).width(0.98f) .addTranslator(MetadataTypes.WEATHERING_COPPER_STATE, CopperGolemEntity::setWeatheringState) .addTranslator(MetadataTypes.COPPER_GOLEM_STATE, CopperGolemEntity::setGolemState) - .properties(VanillaEntityProperties.COPPER_GOLEM) + .property(CopperGolemEntity.CHEST_INTERACTION_PROPERTY) + .property(CopperGolemEntity.HAS_FLOWER_PROPERTY) + .property(CopperGolemEntity.OXIDATION_LEVEL_STATE_ENUM_PROPERTY) .build(); CREAKING = EntityDefinition.inherited(CreakingEntity::new, mobEntityBase) .type(EntityType.CREAKING) @@ -724,7 +744,8 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setActive) .addTranslator(MetadataTypes.BOOLEAN, CreakingEntity::setIsTearingDown) .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, CreakingEntity::setHomePos) - .properties(VanillaEntityProperties.CREAKING) + .property(CreakingEntity.STATE_PROPERTY) + .property(CreakingEntity.SWAYING_TICKS_PROPERTY) .build(); CREEPER = EntityDefinition.inherited(CreeperEntity::new, mobEntityBase) .type(EntityType.CREEPER) @@ -977,7 +998,7 @@ public final class EntityDefinitions { ARMADILLO = EntityDefinition.inherited(ArmadilloEntity::new, ageableEntityBase) .type(EntityType.ARMADILLO) .height(0.65f).width(0.7f) - .properties(VanillaEntityProperties.ARMADILLO) + .property(ArmadilloEntity.STATE_PROPERTY) .addTranslator(MetadataTypes.ARMADILLO_STATE, ArmadilloEntity::setArmadilloState) .build(); AXOLOTL = EntityDefinition.inherited(AxolotlEntity::new, ageableEntityBase) @@ -990,20 +1011,20 @@ public final class EntityDefinitions { BEE = EntityDefinition.inherited(BeeEntity::new, ageableEntityBase) .type(EntityType.BEE) .heightAndWidth(0.6f) - .properties(VanillaEntityProperties.BEE) + .property(BeeEntity.NECTAR_PROPERTY) .addTranslator(MetadataTypes.BYTE, BeeEntity::setBeeFlags) .addTranslator(MetadataTypes.INT, BeeEntity::setAngerTime) .build(); CHICKEN = EntityDefinition.inherited(ChickenEntity::new, ageableEntityBase) .type(EntityType.CHICKEN) .height(0.7f).width(0.4f) - .properties(VanillaEntityProperties.CLIMATE_VARIANT) + .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY) .addTranslator(MetadataTypes.CHICKEN_VARIANT, ChickenEntity::setVariant) .build(); COW = EntityDefinition.inherited(CowEntity::new, ageableEntityBase) .type(EntityType.COW) .height(1.4f).width(0.9f) - .properties(VanillaEntityProperties.CLIMATE_VARIANT) + .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY) .addTranslator(MetadataTypes.COW_VARIANT, CowEntity::setVariant) .build(); FOX = EntityDefinition.inherited(FoxEntity::new, ageableEntityBase) @@ -1023,7 +1044,7 @@ public final class EntityDefinitions { HAPPY_GHAST = EntityDefinition.inherited(HappyGhastEntity::new, ageableEntityBase) .type(EntityType.HAPPY_GHAST) .heightAndWidth(4f) - .properties(VanillaEntityProperties.HAPPY_GHAST) + .property(HappyGhastEntity.CAN_MOVE_PROPERTY) .addTranslator(null) // Is leash holder .addTranslator(MetadataTypes.BOOLEAN, HappyGhastEntity::setStaysStill) .build(); @@ -1062,7 +1083,7 @@ public final class EntityDefinitions { PIG = EntityDefinition.inherited(PigEntity::new, ageableEntityBase) .type(EntityType.PIG) .heightAndWidth(0.9f) - .properties(VanillaEntityProperties.CLIMATE_VARIANT) + .property(TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY) .addTranslator(MetadataTypes.INT, PigEntity::setBoost) .addTranslator(MetadataTypes.PIG_VARIANT, PigEntity::setVariant) .build(); @@ -1208,7 +1229,7 @@ public final class EntityDefinitions { WOLF = EntityDefinition.inherited(WolfEntity::new, tameableEntityBase) .type(EntityType.WOLF) .height(0.85f).width(0.6f) - .properties(VanillaEntityProperties.WOLF_SOUND_VARIANT) + .property(WolfEntity.SOUND_VARIANT) // "Begging" on wiki.vg, "Interested" in Nukkit - the tilt of the head .addTranslator(MetadataTypes.BOOLEAN, (wolfEntity, entityMetadata) -> wolfEntity.setFlag(EntityFlag.INTERESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .addTranslator(MetadataTypes.INT, WolfEntity::setCollarColor) @@ -1242,7 +1263,95 @@ public final class EntityDefinitions { } public static void init() { - // no-op + // entities would be initialized before this event is called + GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineEntityPropertiesEvent() { + @Override + public GeyserFloatEntityProperty registerFloatProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, float min, float max, @Nullable Float defaultValue) { + Objects.requireNonNull(identifier); + Objects.requireNonNull(propertyId); + if (propertyId.vanilla()) { + throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId); + } + FloatProperty property = new FloatProperty(propertyId, min, max, defaultValue); + registerProperty(identifier, property); + return property; + } + + @Override + public IntProperty registerIntegerProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, int min, int max, @Nullable Integer defaultValue) { + Objects.requireNonNull(identifier); + Objects.requireNonNull(propertyId); + if (propertyId.vanilla()) { + throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId); + } + IntProperty property = new IntProperty(propertyId, min, max, defaultValue); + registerProperty(identifier, property); + return property; + } + + @Override + public BooleanProperty registerBooleanProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, boolean defaultValue) { + Objects.requireNonNull(identifier); + Objects.requireNonNull(propertyId); + if (propertyId.vanilla()) { + throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId); + } + BooleanProperty property = new BooleanProperty(propertyId, defaultValue); + registerProperty(identifier, property); + return property; + } + + @Override + public > EnumProperty registerEnumProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, @NonNull Class enumClass, @Nullable E defaultValue) { + Objects.requireNonNull(identifier); + Objects.requireNonNull(propertyId); + Objects.requireNonNull(enumClass); + if (propertyId.vanilla()) { + throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId); + } + EnumProperty property = new EnumProperty<>(propertyId, enumClass, defaultValue == null ? enumClass.getEnumConstants()[0] : defaultValue); + registerProperty(identifier, property); + return property; + } + + @Override + public GeyserStringEnumProperty registerEnumProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, @NonNull List values, @Nullable String defaultValue) { + Objects.requireNonNull(identifier); + Objects.requireNonNull(propertyId); + Objects.requireNonNull(values); + if (propertyId.vanilla()) { + throw new IllegalArgumentException("Cannot register custom property in vanilla namespace! " + propertyId); + } + StringEnumProperty property = new StringEnumProperty(propertyId, values, defaultValue); + registerProperty(identifier, property); + return property; + } + + @Override + public Collection> properties(@NonNull Identifier identifier) { + Objects.requireNonNull(identifier); + var definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(identifier.toString()); + if (definition == null) { + throw new IllegalArgumentException("Unknown entity type: " + identifier); + } + return List.copyOf(definition.registeredProperties().getProperties()); + } + }); + + for (var definition : Registries.ENTITY_DEFINITIONS.get().values()) { + if (definition.registeredProperties() != null) { + Registries.BEDROCK_ENTITY_PROPERTIES.get().add(definition.registeredProperties().toNbtMap(definition.identifier())); + } + } + } + + private static void registerProperty(Identifier entityType, PropertyType property) { + var definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(entityType.toString()); + if (definition == null) { + throw new IllegalArgumentException("Unknown entity type: " + entityType); + } + + definition.registeredProperties().add(entityType.toString(), property); } private EntityDefinitions() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java index 6f8f2525f..e0267ff13 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java +++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java @@ -44,7 +44,6 @@ import java.util.concurrent.CompletableFuture; public class GeyserEntityData implements EntityData { private final GeyserSession session; - private final Set movementLockOwners = new HashSet<>(); public GeyserEntityData(GeyserSession session) { diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java index eaa7b7448..fa9439998 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityProperties.java @@ -31,36 +31,38 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList; import lombok.EqualsAndHashCode; import lombok.ToString; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.cloudburstmc.nbt.NbtType; -import org.geysermc.geyser.entity.properties.type.BooleanProperty; -import org.geysermc.geyser.entity.properties.type.EnumProperty; -import org.geysermc.geyser.entity.properties.type.FloatProperty; -import org.geysermc.geyser.entity.properties.type.IntProperty; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty; import org.geysermc.geyser.entity.properties.type.PropertyType; +import org.geysermc.geyser.registry.Registries; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; @EqualsAndHashCode @ToString public class GeyserEntityProperties { - private final ObjectArrayList properties; + + private final static Pattern ENTITY_PROPERTY_PATTERN = Pattern.compile("^[a-z0-9_.:-]*:[a-z0-9_.:-]*$"); + + private final ObjectArrayList> properties; private final Object2IntMap propertyIndices; - private GeyserEntityProperties(ObjectArrayList properties, - Object2IntMap propertyIndices) { - this.properties = properties; - this.propertyIndices = propertyIndices; + private GeyserEntityProperties() { + this.properties = new ObjectArrayList<>(); + this.propertyIndices = new Object2IntOpenHashMap<>(); } public NbtMap toNbtMap(String entityType) { NbtMapBuilder mapBuilder = NbtMap.builder(); List nbtProperties = new ArrayList<>(); - for (PropertyType property : properties) { + for (PropertyType property : properties) { nbtProperties.add(property.nbtMap()); } mapBuilder.putList("properties", NbtType.COMPOUND, nbtProperties); @@ -68,7 +70,30 @@ public class GeyserEntityProperties { return mapBuilder.putString("type", entityType).build(); } - public @NonNull List getProperties() { + public void add(String entityType, @NonNull PropertyType property) { + if (!Registries.BEDROCK_ENTITY_PROPERTIES.get().isEmpty()) { + throw new IllegalStateException("Cannot add properties outside the GeyserDefineEntityProperties event!"); + } + + if (this.properties.size() > 32) { + throw new IllegalArgumentException("Cannot register more than 32 properties for entity type " + entityType); + } + + Objects.requireNonNull(property, "property cannot be null!"); + String name = property.identifier().toString(); + if (propertyIndices.containsKey(name)) { + throw new IllegalArgumentException( + "Property with name " + name + " already exists on builder!"); + } else if (!ENTITY_PROPERTY_PATTERN.matcher(name).matches()) { + throw new IllegalArgumentException( + "Cannot register property with name " + name + " because property name is invalid! Must match: " + ENTITY_PROPERTY_PATTERN.pattern() + ); + } + this.properties.add(property); + propertyIndices.put(name, properties.size() - 1); + } + + public @NonNull List> getProperties() { return properties; } @@ -77,89 +102,24 @@ public class GeyserEntityProperties { } public static class Builder { - private final ObjectArrayList properties = new ObjectArrayList<>(); - private final Object2IntMap propertyIndices = new Object2IntOpenHashMap<>(); + private GeyserEntityProperties properties; + private final String identifier; - public Builder addInt(@NonNull String name, int min, int max) { - if (propertyIndices.containsKey(name)) { - throw new IllegalArgumentException( - "Property with name " + name + " already exists on builder!"); + public Builder(String identifier) { + this.identifier = identifier; + } + + public Builder add(@NonNull PropertyType property) { + Objects.requireNonNull(property, "property cannot be null!"); + if (properties == null) { + properties = new GeyserEntityProperties(); } - PropertyType property = new IntProperty(name, min, max); - this.properties.add(property); - propertyIndices.put(name, properties.size() - 1); + properties.add(identifier, property); return this; } - public Builder addInt(@NonNull String name) { - if (propertyIndices.containsKey(name)) { - throw new IllegalArgumentException( - "Property with name " + name + " already exists on builder!"); - } - PropertyType property = new IntProperty(name, Integer.MIN_VALUE, Integer.MAX_VALUE); - this.properties.add(property); - propertyIndices.put(name, properties.size() - 1); - return this; - } - - public Builder addFloat(@NonNull String name, float min, float max) { - if (propertyIndices.containsKey(name)) { - throw new IllegalArgumentException( - "Property with name " + name + " already exists on builder!"); - } - PropertyType property = new FloatProperty(name, min, max); - this.properties.add(property); - propertyIndices.put(name, properties.size() - 1); - return this; - } - - public Builder addFloat(@NonNull String name) { - if (propertyIndices.containsKey(name)) { - throw new IllegalArgumentException( - "Property with name " + name + " already exists on builder!"); - } - PropertyType property = new FloatProperty(name, Float.MIN_NORMAL, Float.MAX_VALUE); - this.properties.add(property); - propertyIndices.put(name, properties.size() - 1); - return this; - } - - public Builder addBoolean(@NonNull String name) { - if (propertyIndices.containsKey(name)) { - throw new IllegalArgumentException( - "Property with name " + name + " already exists on builder!"); - } - PropertyType property = new BooleanProperty(name); - this.properties.add(property); - propertyIndices.put(name, properties.size() - 1); - return this; - } - - public Builder addEnum(@NonNull String name, List values) { - if (propertyIndices.containsKey(name)) { - throw new IllegalArgumentException( - "Property with name " + name + " already exists on builder!"); - } - PropertyType property = new EnumProperty(name, values); - this.properties.add(property); - propertyIndices.put(name, properties.size() - 1); - return this; - } - - public Builder addEnum(@NonNull String name, String... values) { - if (propertyIndices.containsKey(name)) { - throw new IllegalArgumentException( - "Property with name " + name + " already exists on builder!"); - } - List valuesList = Arrays.asList(values); // Convert array to list - PropertyType property = new EnumProperty(name, valuesList); - this.properties.add(property); - propertyIndices.put(name, properties.size() - 1); - return this; - } - - public GeyserEntityProperties build() { - return new GeyserEntityProperties(properties, propertyIndices); + public @Nullable GeyserEntityProperties build() { + return properties; } } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java index 29026b172..a5f189cc7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/GeyserEntityPropertyManager.java @@ -25,10 +25,12 @@ package org.geysermc.geyser.entity.properties; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty; import org.cloudburstmc.protocol.bedrock.data.entity.FloatEntityProperty; import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty; -import org.geysermc.geyser.entity.properties.type.EnumProperty; import org.geysermc.geyser.entity.properties.type.PropertyType; import java.util.List; @@ -36,34 +38,29 @@ import java.util.List; public class GeyserEntityPropertyManager { private final GeyserEntityProperties properties; - - private final ObjectArrayList intEntityProperties = new ObjectArrayList<>(); - private final ObjectArrayList floatEntityProperties = new ObjectArrayList<>(); + private final Object2ObjectMap intEntityProperties = new Object2ObjectArrayMap<>(); + private final Object2ObjectMap floatEntityProperties = new Object2ObjectArrayMap<>(); public GeyserEntityPropertyManager(GeyserEntityProperties properties) { this.properties = properties; + for (PropertyType property : properties.getProperties()) { + String name = property.identifier().toString(); + int index = properties.getPropertyIndex(name); + addProperty(name, property.defaultValue(index)); + } } - public void add(String propertyName, int value) { - int index = properties.getPropertyIndex(propertyName); - intEntityProperties.add(new IntEntityProperty(index, value)); + public void addProperty(PropertyType propertyType, T value) { + int index = properties.getPropertyIndex(propertyType.identifier().toString()); + this.addProperty(propertyType.identifier().toString(), propertyType.createValue(index, value)); } - public void add(String propertyName, boolean value) { - int index = properties.getPropertyIndex(propertyName); - intEntityProperties.add(new IntEntityProperty(index, value ? 1 : 0)); - } - - public void add(String propertyName, String value) { - int index = properties.getPropertyIndex(propertyName); - PropertyType property = properties.getProperties().get(index); - int enumIndex = ((EnumProperty) property).getIndex(value); - intEntityProperties.add(new IntEntityProperty(index, enumIndex)); - } - - public void add(String propertyName, float value) { - int index = properties.getPropertyIndex(propertyName); - floatEntityProperties.add(new FloatEntityProperty(index, value)); + private void addProperty(String propertyName, EntityProperty entityProperty) { + if (entityProperty instanceof FloatEntityProperty floatEntityProperty) { + floatEntityProperties.put(propertyName, floatEntityProperty); + } else if (entityProperty instanceof IntEntityProperty intEntityProperty) { + intEntityProperties.put(propertyName, intEntityProperty); + } } public boolean hasFloatProperties() { @@ -78,21 +75,17 @@ public class GeyserEntityPropertyManager { return hasFloatProperties() || hasIntProperties(); } - public ObjectArrayList intProperties() { - return this.intEntityProperties; - } - public void applyIntProperties(List properties) { - properties.addAll(intEntityProperties); + properties.addAll(intEntityProperties.values()); intEntityProperties.clear(); } - public ObjectArrayList floatProperties() { - return this.floatEntityProperties; - } - public void applyFloatProperties(List properties) { - properties.addAll(floatEntityProperties); + properties.addAll(floatEntityProperties.values()); floatEntityProperties.clear(); } -} \ No newline at end of file + + public NbtMap toNbtMap(String entityType) { + return this.properties.toNbtMap(entityType); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java b/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java deleted file mode 100644 index f8a2d49eb..000000000 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java +++ /dev/null @@ -1,94 +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.entity.properties; - -import org.geysermc.geyser.entity.type.living.CopperGolemEntity; -import org.geysermc.geyser.entity.type.living.monster.CreakingEntity; - -public class VanillaEntityProperties { - - public static final String CLIMATE_VARIANT_ID = "minecraft:climate_variant"; - - public static final GeyserEntityProperties ARMADILLO = new GeyserEntityProperties.Builder() - .addEnum("minecraft:armadillo_state", - "unrolled", - "rolled_up", - "rolled_up_peeking", - "rolled_up_relaxing", - "rolled_up_unrolling") - .build(); - - public static final GeyserEntityProperties BEE = new GeyserEntityProperties.Builder() - .addBoolean("minecraft:has_nectar") - .build(); - - public static final GeyserEntityProperties CLIMATE_VARIANT = new GeyserEntityProperties.Builder() - .addEnum(CLIMATE_VARIANT_ID, - "temperate", - "warm", - "cold") - .build(); - - public static final GeyserEntityProperties CREAKING = new GeyserEntityProperties.Builder() - .addEnum(CreakingEntity.CREAKING_STATE, - "neutral", - "hostile_observed", - "hostile_unobserved", - "twitching", - "crumbling") - .addInt(CreakingEntity.CREAKING_SWAYING_TICKS, 0, 6) - .build(); - - public static final GeyserEntityProperties HAPPY_GHAST = new GeyserEntityProperties.Builder() - .addBoolean("minecraft:can_move") - .build(); - - public static final GeyserEntityProperties WOLF_SOUND_VARIANT = new GeyserEntityProperties.Builder() - .addEnum("minecraft:sound_variant", - "default", - "big", - "cute", - "grumpy", - "mad", - "puglin", - "sad") - .build(); - - public static final GeyserEntityProperties COPPER_GOLEM = new GeyserEntityProperties.Builder() - .addEnum(CopperGolemEntity.CHEST_INTERACTION, - "none", - "take", - "take_fail", - "put", - "put_fail") - .addBoolean(CopperGolemEntity.HAS_FLOWER) - .addEnum(CopperGolemEntity.OXIDIZATION_LEVEL, - "unoxidized", - "exposed", - "weathered", - "oxidized") - .build(); -} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/AbstractEnumProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/AbstractEnumProperty.java new file mode 100644 index 000000000..0c921b54e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/AbstractEnumProperty.java @@ -0,0 +1,88 @@ +/* + * 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.entity.properties.type; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtType; +import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.List; +import java.util.regex.Pattern; + +public interface AbstractEnumProperty extends PropertyType { + + Pattern VALUE_VALIDATION_REGEX = Pattern.compile("^[A-Za-z][A-Za-z0-9_]{0,31}$"); + + @Override + default NbtMap nbtMap() { + return NbtMap.builder() + .putString("name", identifier().toString()) + .putList("enum", NbtType.STRING, allBedrockValues()) + .putInt("type", 3) + .build(); + } + + default void validateAllValues(Identifier name, List values) { + if (values.size() > 16) { + throw new IllegalArgumentException("Cannot register enum property with name " + name + " because it has more than 16 values!"); + } + + for (String value : values) { + if (!VALUE_VALIDATION_REGEX.matcher(value).matches()) { + throw new IllegalArgumentException( + "Cannot register enum property with name " + name + " and value " + value + + " because enum values can only contain alphanumeric characters and underscores." + ); + } + } + } + + List allBedrockValues(); + + @Override + default IntEntityProperty defaultValue(int index) { + return new IntEntityProperty(index, defaultIndex()); + } + + @Override + default IntEntityProperty createValue(int index, @Nullable T value) { + if (value == null) { + return defaultValue(index); + } + + int valueIndex = indexOf(value); + if (valueIndex == -1) { + throw new IllegalArgumentException("Enum value " + value + " is not a valid enum value!"); + } + return new IntEntityProperty(index, valueIndex); + } + + int indexOf(T value); + + int defaultIndex(); +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java index 6fc64ad4b..9bbb1cc2d 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/BooleanProperty.java @@ -26,19 +26,33 @@ package org.geysermc.geyser.entity.properties.type; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserBooleanEntityProperty; +import org.geysermc.geyser.api.util.Identifier; -public class BooleanProperty implements PropertyType { - private final String name; - - public BooleanProperty(String name) { - this.name = name; - } +public record BooleanProperty( + Identifier identifier, + Boolean defaultValue +) implements PropertyType, GeyserBooleanEntityProperty { @Override public NbtMap nbtMap() { return NbtMap.builder() - .putString("name", name) + .putString("name", identifier.toString()) .putInt("type", 2) .build(); } -} \ No newline at end of file + + @Override + public IntEntityProperty defaultValue(int index) { + return createValue(index, defaultValue != null && defaultValue); + } + + @Override + public IntEntityProperty createValue(int index, Boolean value) { + if (value == null) { + return defaultValue(index); + } + return new IntEntityProperty(index, value ? 1 : 0); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java index 05e12ba61..42fbb6a81 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/EnumProperty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * 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 @@ -25,37 +25,40 @@ package org.geysermc.geyser.entity.properties.type; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import org.cloudburstmc.nbt.NbtMap; -import org.cloudburstmc.nbt.NbtType; +import org.geysermc.geyser.api.entity.property.type.GeyserEnumEntityProperty; +import org.geysermc.geyser.api.util.Identifier; +import java.util.Arrays; import java.util.List; +import java.util.Locale; -public class EnumProperty implements PropertyType { - private final String name; - private final List values; - private final Object2IntMap valueIndexMap; +public record EnumProperty>( + Identifier identifier, + Class enumClass, + E defaultValue +) implements AbstractEnumProperty, GeyserEnumEntityProperty { - public EnumProperty(String name, List values) { - this.name = name; - this.values = values; - this.valueIndexMap = new Object2IntOpenHashMap<>(values.size()); - for (int i = 0; i < values.size(); i++) { - valueIndexMap.put(values.get(i), i); - } + public EnumProperty { + validateAllValues(identifier, Arrays.stream(enumClass.getEnumConstants()).map(value -> value.name().toLowerCase(Locale.ROOT)).toList()); + } + + public List values() { + return List.of(enumClass.getEnumConstants()); + } + + public List allBedrockValues() { + return values().stream().map( + value -> value.name().toLowerCase(Locale.ROOT) + ).toList(); } @Override - public NbtMap nbtMap() { - return NbtMap.builder() - .putString("name", name) - .putList("enum", NbtType.STRING, values) - .putInt("type", 3) - .build(); + public int indexOf(E value) { + return value.ordinal(); } - public int getIndex(String value) { - return valueIndexMap.getOrDefault(value, -1); + @Override + public int defaultIndex() { + return defaultValue.ordinal(); } -} \ No newline at end of file +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java index 8b808ebc3..16997dc8a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/FloatProperty.java @@ -26,25 +26,48 @@ package org.geysermc.geyser.entity.properties.type; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.FloatEntityProperty; +import org.geysermc.geyser.api.entity.property.type.GeyserFloatEntityProperty; +import org.geysermc.geyser.api.util.Identifier; -public class FloatProperty implements PropertyType { - private final String name; - private final float max; - private final float min; +public record FloatProperty( + Identifier identifier, + float max, + float min, + Float defaultValue +) implements PropertyType, GeyserFloatEntityProperty { - public FloatProperty(String name, float min, float max) { - this.name = name; - this.max = max; - this.min = min; + public FloatProperty { + if (min > max) { + throw new IllegalArgumentException("Cannot create float entity property (%s) with a minimum value (%s) greater than maximum (%s)!" + .formatted(identifier, min, max)); + } + if (defaultValue < min || defaultValue > max) { + throw new IllegalArgumentException("Cannot create float entity property (%s) with a default value (%s) outside of the range (%s - %s)!" + .formatted(identifier, defaultValue, min, max)); + } } @Override public NbtMap nbtMap() { return NbtMap.builder() - .putString("name", name) + .putString("name", identifier.toString()) .putFloat("max", max) .putFloat("min", min) .putInt("type", 1) .build(); } -} \ No newline at end of file + + @Override + public FloatEntityProperty defaultValue(int index) { + return createValue(index, defaultValue == null ? min : defaultValue); + } + + @Override + public FloatEntityProperty createValue(int index, Float value) { + if (value == null) { + return defaultValue(index); + } + return new FloatEntityProperty(index, value); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java index 9e38db7c7..966277696 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/IntProperty.java @@ -26,25 +26,53 @@ package org.geysermc.geyser.entity.properties.type; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.IntEntityProperty; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.entity.property.type.GeyserIntEntityProperty; +import org.geysermc.geyser.api.util.Identifier; -public class IntProperty implements PropertyType { - private final String name; - private final int max; - private final int min; +public record IntProperty( + Identifier identifier, + int max, + int min, + Integer defaultValue +) implements PropertyType, GeyserIntEntityProperty { - public IntProperty(String name, int min, int max) { - this.name = name; - this.max = max; - this.min = min; + public IntProperty { + if (min > max) { + throw new IllegalArgumentException("Cannot create int entity property (%s) with a minimum value (%s) greater than maximum (%s)!" + .formatted(identifier, min, max)); + } + if (defaultValue < min || defaultValue > max) { + throw new IllegalArgumentException("Cannot create int entity property (%s) with a default value (%s) outside of the range (%s - %s)!" + .formatted(identifier, defaultValue, min, max)); + } + if (min < -1000000 || max > 1000000) { + // https://learn.microsoft.com/en-us/minecraft/creator/documents/introductiontoentityproperties?view=minecraft-bedrock-stable#a-note-on-large-integer-entity-property-values + GeyserImpl.getInstance().getLogger().warning("Using int entity properties with min / max values larger than +- 1 million is not recommended!"); + } } @Override public NbtMap nbtMap() { return NbtMap.builder() - .putString("name", name) + .putString("name", identifier.toString()) .putInt("max", max) .putInt("min", min) .putInt("type", 0) .build(); } -} \ No newline at end of file + + @Override + public IntEntityProperty defaultValue(int index) { + return createValue(index, defaultValue == null ? min : defaultValue); + } + + @Override + public IntEntityProperty createValue(int index, Integer value) { + if (value == null) { + return defaultValue(index); + } + return new IntEntityProperty(index, value); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java index a64d7246a..cd1471273 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/PropertyType.java @@ -25,8 +25,20 @@ package org.geysermc.geyser.entity.properties.type; +import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty; +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; +import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager; -public interface PropertyType { +public interface PropertyType extends GeyserEntityProperty { NbtMap nbtMap(); -} \ No newline at end of file + + NetworkRepresentation defaultValue(int index); + + NetworkRepresentation createValue(int index, @Nullable Type value); + + default void apply(GeyserEntityPropertyManager manager, Type value) { + manager.addProperty(this, value); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/properties/type/StringEnumProperty.java b/core/src/main/java/org/geysermc/geyser/entity/properties/type/StringEnumProperty.java new file mode 100644 index 000000000..278f60c7f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/type/StringEnumProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2024 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.entity.properties.type; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.entity.property.type.GeyserStringEnumProperty; +import org.geysermc.geyser.api.util.Identifier; + +import java.util.List; + +public record StringEnumProperty( + Identifier identifier, + List values, + int defaultIndex +) implements AbstractEnumProperty, GeyserStringEnumProperty { + + public StringEnumProperty { + if (defaultIndex < 0) { + throw new IllegalArgumentException("Unable to find default value for enum property with name " + identifier); + } + validateAllValues(identifier, values); + } + + public StringEnumProperty(Identifier name, List values, @Nullable String defaultValue) { + this(name, values, defaultValue == null ? 0 : values.indexOf(defaultValue)); + } + + @Override + public List allBedrockValues() { + return values; + } + + @Override + public int indexOf(String value) { + return values.indexOf(value); + } + + @Override + public @NonNull String defaultValue() { + return values.get(defaultIndex); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java index a8e020f3a..1c672b8bf 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java @@ -29,22 +29,28 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector2f; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityProperty; import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket; import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket; import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket; +import org.geysermc.geyser.api.entity.property.BatchPropertyUpdater; +import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.GeyserDirtyMetadata; +import org.geysermc.geyser.entity.properties.GeyserEntityProperties; import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager; +import org.geysermc.geyser.entity.properties.type.PropertyType; import org.geysermc.geyser.entity.type.living.MobEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.item.Items; @@ -71,6 +77,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; @Getter @Setter @@ -201,6 +208,10 @@ public class Entity implements GeyserEntity { addEntityPacket.setBodyRotation(yaw); // TODO: This should be bodyYaw addEntityPacket.getMetadata().putFlags(flags); dirtyMetadata.apply(addEntityPacket.getMetadata()); + if (propertyManager != null) { + propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); + propertyManager.applyFloatProperties(addEntityPacket.getProperties().getFloatProperties()); + } addAdditionalSpawnData(addEntityPacket); valid = true; @@ -752,4 +763,42 @@ public class Entity implements GeyserEntity { packet.setData(data); session.sendUpstreamPacket(packet); } + + @Override + public void updatePropertiesBatched(Consumer consumer) { + if (this.propertyManager != null) { + Objects.requireNonNull(consumer); + GeyserEntityProperties propertyDefinitions = definition.registeredProperties(); + consumer.accept(new BatchPropertyUpdater() { + @Override + public void update(@NonNull GeyserEntityProperty property, @Nullable T value) { + Objects.requireNonNull(property, "property must not be null!"); + if (!(property instanceof PropertyType propertyType)) { + throw new IllegalArgumentException("Invalid property implementation! Got: " + property.getClass().getSimpleName()); + } + int index = propertyDefinitions.getPropertyIndex(property.identifier().toString()); + if (index < 0) { + throw new IllegalArgumentException("No property with the name " + property.identifier() + " has been registered."); + } + + var expectedProperty = propertyDefinitions.getProperties().get(index); + if (!expectedProperty.equals(propertyType)) { + throw new IllegalArgumentException("The supplied property was not registered with this entity type!"); + } + + propertyType.apply(propertyManager, value); + } + }); + + if (propertyManager.hasProperties()) { + SetEntityDataPacket packet = new SetEntityDataPacket(); + packet.setRuntimeEntityId(getGeyserId()); + propertyManager.applyFloatProperties(packet.getProperties().getFloatProperties()); + propertyManager.applyIntProperties(packet.getProperties().getIntProperties()); + session.sendUpstreamPacket(packet); + } + } else { + throw new IllegalArgumentException("Given entity has no registered properties!"); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java index 3167ed305..a32762abe 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java @@ -28,9 +28,7 @@ package org.geysermc.geyser.entity.type; import lombok.Getter; import net.kyori.adventure.key.Key; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.entity.properties.VanillaEntityProperties; import org.geysermc.geyser.entity.type.living.animal.farm.TemperatureVariantAnimal; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; @@ -41,7 +39,6 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetad import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; -import java.util.Locale; import java.util.UUID; @Getter @@ -54,31 +51,25 @@ public class ThrowableEggEntity extends ThrowableItemEntity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - @Override - public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { - propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); - propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); - } - @Override public void setItem(EntityMetadata entityMetadata) { GeyserItemStack stack = GeyserItemStack.from(entityMetadata.getValue()); - propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, getVariantOrFallback(session, stack)); + TemperatureVariantAnimal.TEMPERATE_VARIANT_PROPERTY.apply(propertyManager, getVariantOrFallback(session, stack)); updateBedrockEntityProperties(); this.itemStack = stack; } - private static String getVariantOrFallback(GeyserSession session, GeyserItemStack stack) { + private static TemperatureVariantAnimal.BuiltInVariant getVariantOrFallback(GeyserSession session, GeyserItemStack stack) { Holder holder = stack.getComponent(DataComponentTypes.CHICKEN_VARIANT); if (holder != null) { Key chickenVariant = holder.getOrCompute(id -> JavaRegistries.CHICKEN_VARIANT.key(session, id)); for (var variant : TemperatureVariantAnimal.BuiltInVariant.values()) { if (chickenVariant.asMinimalString().equalsIgnoreCase(variant.name())) { - return chickenVariant.asMinimalString().toLowerCase(Locale.ROOT); + return variant; } } } - return TemperatureVariantAnimal.BuiltInVariant.TEMPERATE.toBedrock(); + return TemperatureVariantAnimal.BuiltInVariant.TEMPERATE; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/CopperGolemEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/CopperGolemEntity.java index 9a83ec6d4..946d96169 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/CopperGolemEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/CopperGolemEntity.java @@ -27,8 +27,10 @@ package org.geysermc.geyser.entity.type.living; import org.checkerframework.checker.nullness.qual.NonNull; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.BooleanProperty; +import org.geysermc.geyser.entity.properties.type.EnumProperty; +import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.session.GeyserSession; @@ -45,21 +47,42 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import java.util.UUID; public class CopperGolemEntity extends GolemEntity { - public static final String CHEST_INTERACTION = "minecraft:chest_interaction"; - public static final String HAS_FLOWER = "has_flower"; - public static final String OXIDIZATION_LEVEL = "minecraft:oxidation_level"; + public static final BooleanProperty HAS_FLOWER_PROPERTY = new BooleanProperty( + IdentifierImpl.of("has_flower"), + false + ); + + public static final EnumProperty CHEST_INTERACTION_PROPERTY = new EnumProperty<>( + IdentifierImpl.of("chest_interaction"), + ChestInteractionState.class, + ChestInteractionState.NONE + ); + + public static final EnumProperty OXIDATION_LEVEL_STATE_ENUM_PROPERTY = new EnumProperty<>( + IdentifierImpl.of("oxidation_level"), + OxidationLevelState.class, + OxidationLevelState.UNOXIDIZED + ); + + public enum ChestInteractionState { + NONE, + TAKE, + TAKE_FAIL, + PUT, + PUT_FAIL + } + + public enum OxidationLevelState { + UNOXIDIZED, + EXPOSED, + WEATHERED, + OXIDIZED + } public CopperGolemEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - @Override - public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { - propertyManager.add(CHEST_INTERACTION, "none"); - propertyManager.add(HAS_FLOWER, false); - propertyManager.add(OXIDIZATION_LEVEL, "unoxidized"); - } - @Override protected @NonNull InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { if (itemInHand.isEmpty() && !getMainHandItem().isEmpty()) { @@ -95,29 +118,29 @@ public class CopperGolemEntity extends GolemEntity { super.setSaddle(stack); // Equipment on Java, entity property on bedrock - propertyManager.add(HAS_FLOWER, stack.is(Items.POPPY)); + HAS_FLOWER_PROPERTY.apply(propertyManager, stack.is(Items.POPPY)); updateBedrockEntityProperties(); } public void setWeatheringState(EntityMetadata> metadata) { WeatheringCopperState state = metadata.getValue(); - propertyManager.add(OXIDIZATION_LEVEL, switch (state) { - case UNAFFECTED -> "unoxidized"; - case EXPOSED -> "exposed"; - case WEATHERED -> "weathered"; - case OXIDIZED -> "oxidized"; + OXIDATION_LEVEL_STATE_ENUM_PROPERTY.apply(propertyManager, switch (state) { + case UNAFFECTED -> OxidationLevelState.UNOXIDIZED; + case EXPOSED -> OxidationLevelState.EXPOSED; + case WEATHERED -> OxidationLevelState.WEATHERED; + case OXIDIZED -> OxidationLevelState.OXIDIZED; }); updateBedrockEntityProperties(); } public void setGolemState(EntityMetadata> metadata) { CopperGolemState state = metadata.getValue(); - propertyManager.add(CHEST_INTERACTION, switch (state) { - case IDLE -> "none"; - case GETTING_ITEM -> "take"; - case GETTING_NO_ITEM -> "take_fail"; - case DROPPING_ITEM -> "put"; - case DROPPING_NO_ITEM -> "put_fail"; + CHEST_INTERACTION_PROPERTY.apply(propertyManager, switch (state) { + case IDLE -> ChestInteractionState.NONE; + case GETTING_ITEM -> ChestInteractionState.TAKE; + case GETTING_NO_ITEM -> ChestInteractionState.TAKE_FAIL; + case DROPPING_ITEM -> ChestInteractionState.PUT; + case DROPPING_NO_ITEM -> ChestInteractionState.PUT_FAIL; }); updateBedrockEntityProperties(); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java index 2b443f5e4..0ebd92021 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/ArmadilloEntity.java @@ -28,6 +28,8 @@ package org.geysermc.geyser.entity.type.living.animal; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.EnumProperty; +import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; @@ -39,6 +41,13 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; public class ArmadilloEntity extends AnimalEntity { + + public static final EnumProperty STATE_PROPERTY = new EnumProperty<>( + IdentifierImpl.of("armadillo_state"), + State.class, + State.UNROLLED + ); + private ArmadilloState armadilloState = ArmadilloState.IDLE; public ArmadilloEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, @@ -50,10 +59,10 @@ public class ArmadilloEntity extends AnimalEntity { armadilloState = entityMetadata.getValue(); switch (armadilloState) { - case IDLE -> propertyManager.add("minecraft:armadillo_state", "unrolled"); - case ROLLING -> propertyManager.add("minecraft:armadillo_state", "rolled_up"); - case SCARED -> propertyManager.add("minecraft:armadillo_state", "rolled_up_relaxing"); - case UNROLLING -> propertyManager.add("minecraft:armadillo_state", "rolled_up_unrolling"); + case IDLE -> STATE_PROPERTY.apply(propertyManager, State.UNROLLED); + case ROLLING -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP); + case SCARED -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_RELAXING); + case UNROLLING -> STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_UNROLLING); } updateBedrockEntityProperties(); @@ -62,13 +71,13 @@ public class ArmadilloEntity extends AnimalEntity { public void onPeeking() { // Technically we should wait if not currently scared if (armadilloState == ArmadilloState.SCARED) { - propertyManager.add("minecraft:armadillo_state", "rolled_up_peeking"); + STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_PEEKING); updateBedrockEntityProperties(); // Needed for consecutive peeks session.scheduleInEventLoop(() -> { if (armadilloState == ArmadilloState.SCARED) { - propertyManager.add("minecraft:armadillo_state", "rolled_up_relaxing"); + STATE_PROPERTY.apply(propertyManager, State.ROLLED_UP_RELAXING); updateBedrockEntityProperties(); } }, 250, TimeUnit.MILLISECONDS); @@ -80,4 +89,12 @@ public class ArmadilloEntity extends AnimalEntity { protected Tag getFoodTag() { return ItemTag.ARMADILLO_FOOD; } + + public enum State { + UNROLLED, + ROLLED_UP, + ROLLED_UP_PEEKING, + ROLLED_UP_RELAXING, + ROLLED_UP_UNROLLING + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java index 5f8956b6a..919af7c22 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/BeeEntity.java @@ -32,6 +32,8 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.BooleanProperty; +import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; @@ -43,6 +45,11 @@ import java.util.UUID; public class BeeEntity extends AnimalEntity { + public static final BooleanProperty NECTAR_PROPERTY = new BooleanProperty( + IdentifierImpl.of("has_nectar"), + false + ); + public BeeEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } @@ -60,7 +67,7 @@ public class BeeEntity extends AnimalEntity { // If the bee has stung dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, (xd & 0x04) == 0x04 ? 1 : 0); // If the bee has nectar or not - propertyManager.add("minecraft:has_nectar", (xd & 0x08) == 0x08); + NECTAR_PROPERTY.apply(propertyManager, (xd & 0x08) == 0x08); updateBedrockEntityProperties(); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java index b294c4766..51f61c6bf 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java @@ -33,11 +33,13 @@ import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.AttributeData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.BooleanProperty; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.entity.vehicle.ClientVehicle; import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent; import org.geysermc.geyser.entity.vehicle.VehicleComponent; +import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; @@ -61,6 +63,11 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle { public static final float[] X_OFFSETS = {0.0F, -1.7F, 0.0F, 1.7F}; public static final float[] Z_OFFSETS = {1.7F, 0.0F, -1.7F, 0.0F}; + public static final BooleanProperty CAN_MOVE_PROPERTY = new BooleanProperty( + IdentifierImpl.of("can_move"), + true + ); + private final HappyGhastVehicleComponent vehicleComponent = new HappyGhastVehicleComponent(this, 0.0f); private boolean staysStill; @@ -80,8 +87,6 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle { setFlag(EntityFlag.WASD_AIR_CONTROLLED, true); setFlag(EntityFlag.DOES_SERVER_AUTH_ONLY_DISMOUNT, true); - - propertyManager.add("minecraft:can_move", true); } @Override @@ -111,7 +116,7 @@ public class HappyGhastEntity extends AnimalEntity implements ClientVehicle { public void setStaysStill(BooleanEntityMetadata entityMetadata) { staysStill = entityMetadata.getPrimitiveValue(); - propertyManager.add("minecraft:can_move", !entityMetadata.getPrimitiveValue()); + CAN_MOVE_PROPERTY.apply(propertyManager, !entityMetadata.getPrimitiveValue()); updateBedrockEntityProperties(); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java index b61a3a80d..9d772c8a2 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/TemperatureVariantAnimal.java @@ -26,19 +26,24 @@ package org.geysermc.geyser.entity.type.living.animal.farm; import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.geysermc.geyser.entity.EntityDefinition; -import org.geysermc.geyser.entity.properties.VanillaEntityProperties; +import org.geysermc.geyser.entity.properties.type.EnumProperty; import org.geysermc.geyser.entity.type.living.animal.AnimalEntity; import org.geysermc.geyser.entity.type.living.animal.VariantHolder; +import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.RegistryCache; -import java.util.Locale; import java.util.UUID; public abstract class TemperatureVariantAnimal extends AnimalEntity implements VariantHolder { + public static final EnumProperty TEMPERATE_VARIANT_PROPERTY = new EnumProperty<>( + IdentifierImpl.of("climate_variant"), + BuiltInVariant.class, + BuiltInVariant.TEMPERATE + ); + public static final RegistryCache.RegistryReader VARIANT_READER = VariantHolder.reader(BuiltInVariant.class, BuiltInVariant.TEMPERATE); public TemperatureVariantAnimal(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, @@ -46,25 +51,15 @@ public abstract class TemperatureVariantAnimal extends AnimalEntity implements V super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - @Override - public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { - propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, "temperate"); - propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); - } - @Override public void setBedrockVariant(BuiltInVariant variant) { - propertyManager.add(VanillaEntityProperties.CLIMATE_VARIANT_ID, variant.toBedrock()); + TEMPERATE_VARIANT_PROPERTY.apply(propertyManager, variant); updateBedrockEntityProperties(); } public enum BuiltInVariant implements VariantHolder.BuiltIn { - COLD, TEMPERATE, - WARM; - - public String toBedrock() { - return name().toLowerCase(Locale.ROOT); - } + WARM, + COLD; } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java index bc3963ac8..eee7a0052 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/WolfEntity.java @@ -30,10 +30,11 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.StringEnumProperty; import org.geysermc.geyser.entity.type.living.animal.VariantIntHolder; +import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.enchantment.EnchantmentComponent; @@ -56,9 +57,25 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponen import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet; import java.util.Collections; +import java.util.List; import java.util.UUID; public class WolfEntity extends TameableEntity implements VariantIntHolder { + + public static final StringEnumProperty SOUND_VARIANT = new StringEnumProperty( + IdentifierImpl.of("sound_variant"), + List.of( + "default", + "big", + "cute", + "grumpy", + "mad", + "puglin", + "sad" + ), + null + ); + private byte collarColor = 14; // Red - default private HolderSet repairableItems = null; private boolean isCurseOfBinding = false; @@ -67,12 +84,6 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - @Override - public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { - propertyManager.add("minecraft:sound_variant", "default"); - propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); - } - @Override public void setTameableFlags(ByteEntityMetadata entityMetadata) { super.setTameableFlags(entityMetadata); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java index 166cdc053..dc9755a2b 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java @@ -30,9 +30,11 @@ import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.protocol.bedrock.data.LevelEvent; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; -import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket; import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.properties.type.EnumProperty; +import org.geysermc.geyser.entity.properties.type.IntProperty; +import org.geysermc.geyser.impl.IdentifierImpl; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType; @@ -41,8 +43,23 @@ import java.util.Optional; import java.util.UUID; public class CreakingEntity extends MonsterEntity { - public static final String CREAKING_STATE = "minecraft:creaking_state"; - public static final String CREAKING_SWAYING_TICKS = "minecraft:creaking_swaying_ticks"; + + public static final EnumProperty STATE_PROPERTY = new EnumProperty<>( + IdentifierImpl.of("creaking_state"), + CreakingState.class, + CreakingState.NEUTRAL + ); + + // also, the creaking seems to have this minecraft:creaking_swaying_ticks thingy + // which i guess is responsible for some animation? + // it's sent over the network, all 6 "stages" 50ms in between of each other. + // no clue what it's used for tbh, so i'm not gonna bother implementing it + // - chris + // update: this still holds true, even a refactor later :( + public static final IntProperty SWAYING_TICKS_PROPERTY = new IntProperty( + IdentifierImpl.of("creaking_swaying_ticks"), + 6, 0, 0 + ); private Vector3i homePosition; @@ -56,33 +73,21 @@ public class CreakingEntity extends MonsterEntity { setFlag(EntityFlag.FIRE_IMMUNE, true); } - @Override - public void addAdditionalSpawnData(AddEntityPacket addEntityPacket) { - propertyManager.add(CREAKING_STATE, "neutral"); - // also, the creaking seems to have this minecraft:creaking_swaying_ticks thingy - // which i guess is responsible for some animation? - // it's sent over the network, all 6 "stages" 50ms in between of each other. - // no clue what it's used for tbh, so i'm not gonna bother implementing it - // - chris - propertyManager.add(CREAKING_SWAYING_TICKS, 0); - propertyManager.applyIntProperties(addEntityPacket.getProperties().getIntProperties()); - } - public void setCanMove(EntityMetadata> booleanEntityMetadata) { setFlag(EntityFlag.BODY_ROTATION_BLOCKED, !booleanEntityMetadata.getValue()); - propertyManager.add(CREAKING_STATE, booleanEntityMetadata.getValue() ? "hostile_unobserved" : "hostile_observed"); + STATE_PROPERTY.apply(propertyManager, booleanEntityMetadata.getValue() ? CreakingState.HOSTILE_UNOBSERVED : CreakingState.HOSTILE_OBSERVED); updateBedrockEntityProperties(); } public void setActive(EntityMetadata> booleanEntityMetadata) { if (!booleanEntityMetadata.getValue()) { - propertyManager.add(CREAKING_STATE, "neutral"); + STATE_PROPERTY.apply(propertyManager, CreakingState.NEUTRAL); } } public void setIsTearingDown(EntityMetadata> booleanEntityMetadata) { if (booleanEntityMetadata.getValue()) { - propertyManager.add(CREAKING_STATE, "crumbling"); + STATE_PROPERTY.apply(propertyManager, CreakingState.CRUMBLING); updateBedrockEntityProperties(); } } @@ -115,4 +120,12 @@ public class CreakingEntity extends MonsterEntity { session.sendUpstreamPacket(levelEventGenericPacket); } } + + public enum CreakingState { + NEUTRAL, + HOSTILE_OBSERVED, + HOSTILE_UNOBSERVED, + TWITCHING, + CRUMBLING + } } diff --git a/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java b/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java new file mode 100644 index 000000000..ca0324665 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/impl/IdentifierImpl.java @@ -0,0 +1,65 @@ +/* + * 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.impl; + +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()); + } + } + + // FIXME using the identifier interface from the API breaks tests + public static IdentifierImpl of(String value) { + return of(Identifier.DEFAULT_NAMESPACE, value); + } + + @Override + public String namespace() { + return identifier.namespace(); + } + + @Override + public String path() { + return identifier.value(); + } + + @Override + public String toString() { + return identifier.toString(); + } +} 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 b1f62ca99..48e08cc9c 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 @@ -44,8 +44,10 @@ 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.item.GeyserCustomItemData; @@ -74,6 +76,9 @@ public class ProviderRegistryLoader implements RegistryLoader, Prov @Override public Map, ProviderSupplier> load(Map, ProviderSupplier> providers) { + // misc + 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])); 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 031493d72..ae1cedc63 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -1855,6 +1855,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { // It does *not* mean we can dictate the break speed server-sided :( startGamePacket.setServerAuthoritativeBlockBreaking(true); + if (playerEntity.getPropertyManager() != null) { + startGamePacket.setPlayerPropertyData(playerEntity.getPropertyManager().toNbtMap("minecraft:player")); + } + startGamePacket.setServerId(""); startGamePacket.setWorldId(""); startGamePacket.setScenarioId(""); 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 5f1c8e084..891c3ed82 100644 --- a/core/src/main/java/org/geysermc/geyser/util/MinecraftKey.java +++ b/core/src/main/java/org/geysermc/geyser/util/MinecraftKey.java @@ -26,6 +26,9 @@ package org.geysermc.geyser.util; import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.impl.IdentifierImpl; import org.intellij.lang.annotations.Subst; public final class MinecraftKey { @@ -36,4 +39,25 @@ public final class MinecraftKey { public static Key key(@Subst("empty") String s) { return Key.key(s); } + + /** + * 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(identifier.namespace(), identifier.path()); + } + + public static @Nullable Identifier keyToIdentifier(@Nullable Key key) { + if (key == null) { + return null; + } + return new IdentifierImpl(key); + } }