1
0
mirror of https://github.com/GeyserMC/Geyser.git synced 2025-12-19 14:59:27 +00:00

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 <github@onechris.mozmail.com>
This commit is contained in:
Zigy
2025-10-08 16:09:34 +03:30
committed by GitHub
parent 2295814b22
commit d869e745e0
36 changed files with 1555 additions and 389 deletions

View File

@@ -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.
* <p>
* Notes:
* <ul>
* <li>Passing {@code null} as a value resets the property to its default.</li>
* <li>Numeric properties must be within declared ranges; enum properties must use an allowed value.</li>
* <li>Multiple updates to the same property within a single batch will result in the last value being applied.</li>
* <li>The updater is short-lived and should not be retained outside the batching callback.</li>
* </ul>
*
* <pre>{@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
* });
* }</pre>
*
* @since 2.9.0
*/
@FunctionalInterface
public interface BatchPropertyUpdater {
/**
* Queues an update for the given property within the current batch.
* <p>
* 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 <T> the property's value type
*
* @since 2.9.0
*/
<T> void update(@NonNull GeyserEntityProperty<T> property, @Nullable T value);
}

View File

@@ -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.
* <p>
* Entity properties are used to describe metadata about an entity, such as
* integers, floats, booleans, or enums.
* @see <a href="https://learn.microsoft.com/en-us/minecraft/creator/documents/introductiontoentityproperties?view=minecraft-bedrock-stable#number-of-entity-properties-per-entity-type">
* Official documentation for info</a>
*
* @param <T> the type of value stored by this property
*
* @since 2.9.0
*/
public interface GeyserEntityProperty<T> {
/**
* 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();
}

View File

@@ -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<Boolean> {
}

View File

@@ -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:
* <ul>
* <li>There cannot be more than 16 values</li>
* <li>Enum names cannot be longer than 32 chars, must start with a letter, and may contain numbers and underscores</li>
* </ul>
*
* @param <E> the enum type
* @since 2.9.0
*/
public interface GeyserEnumEntityProperty<E extends Enum<E>> extends GeyserEntityProperty<E> {
}

View File

@@ -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<Float> {
/**
* @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();
}

View File

@@ -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:
* <ul>
* <li>Values must be always within the {@code [min(), max()]} bounds</li>
* <li>Molang evaluation uses floats under the hood; very large integers can lose precision.
* Prefer keeping values in a practical range to avoid rounding issues.</li>
* </ul>
*
* @see GeyserDefineEntityPropertiesEvent#registerIntegerProperty(Identifier, Identifier, int, int, Integer)
* @since 2.9.0
*/
public interface GeyserIntEntityProperty extends GeyserEntityProperty<Integer> {
/**
* @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();
}

View File

@@ -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:
* <ul>
* <li>There cannot be more than 16 values</li>
* <li>The values' names cannot be longer than 32 chars, must start with a letter, and may contain numbers and underscores</li>
* </ul>
*
* @since 2.9.0
*/
public interface GeyserStringEnumProperty extends GeyserEntityProperty<String> {
/**
* @return an unmodifiable list of all registered values
* @since 2.9.0
*/
List<String> values();
}

View File

@@ -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 <T> the type of the value
* @since 2.9.0
*/
default <T> void updateProperty(@NonNull GeyserEntityProperty<T> 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<BatchPropertyUpdater> consumer);
}

View File

@@ -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.
* <p>
* 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.
*
* <h2>Example usage</h2>
* <pre>{@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);
* }
* }</pre>
*
* 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.
*
* <p><b>Notes:</b>
* <ul>
* <li>Default values must fall within the provided bounds.</li>
* <li>There cannot be more than 32 properties registered per entity type in total</li>
* <li>{@link #properties(Identifier)} returns properties registered for the given entity
* (including those added earlier in the same callback), including vanilla properties.</li>
* </ul>
*
* @since 2.9.0
*/
public interface GeyserDefineEntityPropertiesEvent extends Event {
/**
* Returns an <em>unmodifiable</em> 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<GeyserEntityProperty<?>> 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.
* <p>
* 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 <E> the enum type
* @return the created enum property
*
* @since 2.9.0
*/
<E extends Enum<E>> GeyserEnumEntityProperty<E> registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull Class<E> 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 <E> the enum type
* @return the created enum property
*
* @since 2.9.0
*/
default <E extends Enum<E>> GeyserEnumEntityProperty<E> registerEnumProperty(@NonNull Identifier entityType, @NonNull Identifier propertyIdentifier, @NonNull Class<E> 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<String> 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<String> values) {
return registerEnumProperty(entityType, propertyIdentifier, values, null);
}
}

View File

@@ -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:
* <ul>
* <li>
* a namespace, which is usually a name identifying your work
* </li>
* <li>
* a path, which holds a value.
* </li>
* </ul>
*
* Examples of identifiers:
* <ul>
* <li>{@code minecraft:fox}</li>
* <li>{@code geysermc:one_fun_example}</li>
* </ul>
*
* 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);
}
}

View File

@@ -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<T extends Entity>(EntityFactory<T> factory, Entit
float width, float height, float offset, GeyserEntityProperties registeredProperties, List<EntityMetadataTranslator<? super T, ?, ?>> translators) {
public static <T extends Entity> Builder<T> inherited(EntityFactory<T> factory, EntityDefinition<? super T> 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 <T extends Entity> Builder<T> builder(EntityFactory<T> factory) {
@@ -89,7 +90,7 @@ public record EntityDefinition<T extends Entity>(EntityFactory<T> factory, Entit
private float width;
private float height;
private float offset = 0.00001f;
private GeyserEntityProperties registeredProperties;
private GeyserEntityProperties.Builder propertiesBuilder;
private final List<EntityMetadataTranslator<? super T, ?, ?>> translators;
private Builder(EntityFactory<T> factory) {
@@ -97,14 +98,13 @@ public record EntityDefinition<T extends Entity>(EntityFactory<T> factory, Entit
translators = new ObjectArrayList<>();
}
public Builder(EntityFactory<T> factory, EntityType type, String identifier, float width, float height, float offset, GeyserEntityProperties registeredProperties, List<EntityMetadataTranslator<? super T, ?, ?>> translators) {
public Builder(EntityFactory<T> factory, EntityType type, String identifier, float width, float height, float offset, List<EntityMetadataTranslator<? super T, ?, ?>> 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<T extends Entity>(EntityFactory<T> factory, Entit
return this;
}
public Builder<T> properties(GeyserEntityProperties registeredProperties) {
this.registeredProperties = registeredProperties;
public Builder<T> 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<T extends Entity>(EntityFactory<T> factory, Entit
if (identifier == null && type != null) {
identifier = "minecraft:" + type.name().toLowerCase(Locale.ROOT);
}
GeyserEntityProperties registeredProperties = propertiesBuilder == null ? null : propertiesBuilder.build();
EntityDefinition<T> 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;
}

View File

@@ -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<BoatEntity> ACACIA_BOAT;
public static final EntityDefinition<ChestBoatEntity> 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 <E extends Enum<E>> EnumProperty<E> registerEnumProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, @NonNull Class<E> 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<E> 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<String> 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<GeyserEntityProperty<?>> 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 <T> void registerProperty(Identifier entityType, PropertyType<T, ?> 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() {

View File

@@ -44,7 +44,6 @@ import java.util.concurrent.CompletableFuture;
public class GeyserEntityData implements EntityData {
private final GeyserSession session;
private final Set<UUID> movementLockOwners = new HashSet<>();
public GeyserEntityData(GeyserSession session) {

View File

@@ -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<PropertyType> properties;
private final static Pattern ENTITY_PROPERTY_PATTERN = Pattern.compile("^[a-z0-9_.:-]*:[a-z0-9_.:-]*$");
private final ObjectArrayList<PropertyType<?, ?>> properties;
private final Object2IntMap<String> propertyIndices;
private GeyserEntityProperties(ObjectArrayList<PropertyType> properties,
Object2IntMap<String> 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<NbtMap> 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<PropertyType> getProperties() {
public <T> void add(String entityType, @NonNull PropertyType<T, ? extends EntityProperty> 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<PropertyType<?, ?>> getProperties() {
return properties;
}
@@ -77,89 +102,24 @@ public class GeyserEntityProperties {
}
public static class Builder {
private final ObjectArrayList<PropertyType> properties = new ObjectArrayList<>();
private final Object2IntMap<String> 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 <T> Builder add(@NonNull PropertyType<T, ? extends EntityProperty> 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<String> 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<String> 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;
}
}
}

View File

@@ -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<IntEntityProperty> intEntityProperties = new ObjectArrayList<>();
private final ObjectArrayList<FloatEntityProperty> floatEntityProperties = new ObjectArrayList<>();
private final Object2ObjectMap<String, IntEntityProperty> intEntityProperties = new Object2ObjectArrayMap<>();
private final Object2ObjectMap<String, FloatEntityProperty> 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 <T> void addProperty(PropertyType<T, ? extends EntityProperty> 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<IntEntityProperty> intProperties() {
return this.intEntityProperties;
}
public void applyIntProperties(List<IntEntityProperty> properties) {
properties.addAll(intEntityProperties);
properties.addAll(intEntityProperties.values());
intEntityProperties.clear();
}
public ObjectArrayList<FloatEntityProperty> floatProperties() {
return this.floatEntityProperties;
}
public void applyFloatProperties(List<FloatEntityProperty> properties) {
properties.addAll(floatEntityProperties);
properties.addAll(floatEntityProperties.values());
floatEntityProperties.clear();
}
}
public NbtMap toNbtMap(String entityType) {
return this.properties.toNbtMap(entityType);
}
}

View File

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

View File

@@ -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<T> extends PropertyType<T, IntEntityProperty> {
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<String> 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<String> 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();
}

View File

@@ -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<Boolean, IntEntityProperty>, GeyserBooleanEntityProperty {
@Override
public NbtMap nbtMap() {
return NbtMap.builder()
.putString("name", name)
.putString("name", identifier.toString())
.putInt("type", 2)
.build();
}
}
@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);
}
}

View File

@@ -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<String> values;
private final Object2IntMap<String> valueIndexMap;
public record EnumProperty<E extends Enum<E>>(
Identifier identifier,
Class<E> enumClass,
E defaultValue
) implements AbstractEnumProperty<E>, GeyserEnumEntityProperty<E> {
public EnumProperty(String name, List<String> 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<E> values() {
return List.of(enumClass.getEnumConstants());
}
public List<String> 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();
}
}
}

View File

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

View File

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

View File

@@ -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<Type, NetworkRepresentation extends EntityProperty> extends GeyserEntityProperty<Type> {
NbtMap nbtMap();
}
NetworkRepresentation defaultValue(int index);
NetworkRepresentation createValue(int index, @Nullable Type value);
default void apply(GeyserEntityPropertyManager manager, Type value) {
manager.addProperty(this, value);
}
}

View File

@@ -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<String> values,
int defaultIndex
) implements AbstractEnumProperty<String>, 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<String> values, @Nullable String defaultValue) {
this(name, values, defaultValue == null ? 0 : values.indexOf(defaultValue));
}
@Override
public List<String> allBedrockValues() {
return values;
}
@Override
public int indexOf(String value) {
return values.indexOf(value);
}
@Override
public @NonNull String defaultValue() {
return values.get(defaultIndex);
}
}

View File

@@ -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<BatchPropertyUpdater> consumer) {
if (this.propertyManager != null) {
Objects.requireNonNull(consumer);
GeyserEntityProperties propertyDefinitions = definition.registeredProperties();
consumer.accept(new BatchPropertyUpdater() {
@Override
public <T> void update(@NonNull GeyserEntityProperty<T> property, @Nullable T value) {
Objects.requireNonNull(property, "property must not be null!");
if (!(property instanceof PropertyType<T, ? extends EntityProperty> 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!");
}
}
}

View File

@@ -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<ItemStack, ?> 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<Key> 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;
}
}

View File

@@ -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<ChestInteractionState> CHEST_INTERACTION_PROPERTY = new EnumProperty<>(
IdentifierImpl.of("chest_interaction"),
ChestInteractionState.class,
ChestInteractionState.NONE
);
public static final EnumProperty<OxidationLevelState> 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<WeatheringCopperState, ? extends MetadataType<WeatheringCopperState>> 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<CopperGolemState, ? extends MetadataType<CopperGolemState>> 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();
}

View File

@@ -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> 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<Item> getFoodTag() {
return ItemTag.ARMADILLO_FOOD;
}
public enum State {
UNROLLED,
ROLLED_UP,
ROLLED_UP_PEEKING,
ROLLED_UP_RELAXING,
ROLLED_UP_UNROLLING
}
}

View File

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

View File

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

View File

@@ -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<TemperatureVariantAnimal.BuiltInVariant> {
public static final EnumProperty<BuiltInVariant> TEMPERATE_VARIANT_PROPERTY = new EnumProperty<>(
IdentifierImpl.of("climate_variant"),
BuiltInVariant.class,
BuiltInVariant.TEMPERATE
);
public static final RegistryCache.RegistryReader<BuiltInVariant> 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;
}
}

View File

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

View File

@@ -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<CreakingState> 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<Boolean,? extends MetadataType<Boolean>> 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<Boolean,? extends MetadataType<Boolean>> booleanEntityMetadata) {
if (!booleanEntityMetadata.getValue()) {
propertyManager.add(CREAKING_STATE, "neutral");
STATE_PROPERTY.apply(propertyManager, CreakingState.NEUTRAL);
}
}
public void setIsTearingDown(EntityMetadata<Boolean,? extends MetadataType<Boolean>> 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
}
}

View File

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

View File

@@ -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<Map<Class<?>, Prov
@Override
public Map<Class<?>, ProviderSupplier> load(Map<Class<?>, 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]));

View File

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

View File

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