diff --git a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java
index 389b721a436..714082a3c65 100644
--- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java
+++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java
@@ -25,14 +25,12 @@
package org.geysermc.geyser.api.connection;
-import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.index.qual.Positive;
import org.geysermc.api.connection.Connection;
import org.geysermc.geyser.api.bedrock.camera.CameraData;
import org.geysermc.geyser.api.bedrock.camera.CameraShake;
import org.geysermc.geyser.api.command.CommandSource;
import org.geysermc.geyser.api.entity.EntityData;
-import org.geysermc.geyser.api.entity.type.GeyserEntity;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.api.skin.SkinData;
import org.jspecify.annotations.Nullable;
@@ -40,7 +38,6 @@
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
/**
* Represents a player connection used in Geyser.
@@ -57,7 +54,7 @@ public interface GeyserConnection extends Connection, CommandSource {
/**
* Exposes the {@link EntityData} for this connection.
- * It allows you to get entities by their Java entity ID, show emotes, and get the player entity.
+ * It allows you to look up other entities through various methods.
*
* @return the EntityData for this connection.
*/
@@ -164,14 +161,6 @@ public interface GeyserConnection extends Connection, CommandSource {
*/
void sendSkin(UUID player, SkinData skinData);
- /**
- * @param javaId the Java entity ID to look up.
- * @return a {@link GeyserEntity} if present in this connection's entity tracker.
- * @deprecated Use {@link EntityData#entityByJavaId(int)} instead
- */
- @Deprecated
- CompletableFuture<@Nullable GeyserEntity> entityByJavaId(@NonNegative int javaId);
-
/**
* Displays a player entity as emoting to this client.
*
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java b/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java
index 44c0254e0ef..4bf5682782e 100644
--- a/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java
@@ -40,13 +40,29 @@
*/
public interface EntityData {
+ /**
+ * @deprecated use {@link #byJavaId(int)}
+ */
+ @Deprecated
+ CompletableFuture<@Nullable GeyserEntity> entityByJavaId(@NonNegative int javaId);
+
/**
* Returns a {@link GeyserEntity} to e.g. make them play an emote.
*
* @param javaId the Java entity ID to look up
* @return a {@link GeyserEntity} if present in this connection's entity tracker
*/
- CompletableFuture<@Nullable GeyserEntity> entityByJavaId(@NonNegative int javaId);
+ @Nullable GeyserEntity byJavaId(@NonNegative int javaId);
+
+ /**
+ * Returns a {@link GeyserEntity} to e.g. update entity properties.
+ */
+ @Nullable GeyserEntity byUuid(UUID javaUuid);
+
+ /**
+ * Returns a {@link GeyserEntity} based on a Geyser entity id
+ */
+ @Nullable GeyserEntity byGeyserId(@NonNegative long geyserId);
/**
* (Un)locks the client's movement inputs, so that they cannot move.
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/custom/CustomEntityDefinition.java b/api/src/main/java/org/geysermc/geyser/api/entity/custom/CustomEntityDefinition.java
new file mode 100644
index 00000000000..929707b7a79
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/custom/CustomEntityDefinition.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2025 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.api.entity.custom;
+
+import org.geysermc.geyser.api.GeyserApi;
+import org.geysermc.geyser.api.entity.definition.GeyserEntityDefinition;
+import org.geysermc.geyser.api.util.Identifier;
+
+/**
+ * Represents a custom entity definition for a non-vanilla, custom Bedrock entity.
+ */
+public interface CustomEntityDefinition extends GeyserEntityDefinition {
+
+ @Override
+ default boolean vanilla() {
+ return false;
+ }
+
+ /**
+ * Creates or retrieves a GeyserEntityDefinition by the Bedrock entity type identifier.
+ *
+ * @param identifier the Bedrock entity identifier
+ * @return the CustomEntityDefinition
+ */
+ static CustomEntityDefinition of(Identifier identifier) {
+ if (identifier.vanilla()) {
+ throw new IllegalArgumentException("Use GeyserEntityDefinition#of for vanilla entity lookups!");
+ }
+ return GeyserApi.api().provider(CustomEntityDefinition.class, identifier);
+ }
+
+ /**
+ * Creates or retrieves a GeyserEntityDefinition by the Bedrock entity type identifier.
+ *
+ * @param identifier the Bedrock entity identifier, in string format
+ * @return the CustomEntityDefinition
+ */
+ static CustomEntityDefinition of(String identifier) {
+ return of(Identifier.of(identifier));
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/custom/CustomJavaEntityType.java b/api/src/main/java/org/geysermc/geyser/api/entity/custom/CustomJavaEntityType.java
new file mode 100644
index 00000000000..be5d27eacdf
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/custom/CustomJavaEntityType.java
@@ -0,0 +1,90 @@
+/*
+ * 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.custom;
+
+import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.common.returnsreceiver.qual.This;
+import org.geysermc.geyser.api.entity.definition.GeyserEntityDefinition;
+import org.geysermc.geyser.api.entity.definition.JavaEntityType;
+import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntitiesEvent;
+import org.geysermc.geyser.api.util.Identifier;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Represents a custom Minecraft: Java Edition entity type.
+ * This can only be used with modded servers!
+ */
+public interface CustomJavaEntityType extends JavaEntityType {
+
+ @Override
+ default boolean vanilla() {
+ return false;
+ }
+
+ interface Builder {
+
+ /**
+ * The entity type's identifier. It cannot be in the Minecraft namespace
+ * for custom entities!
+ *
+ * @param entityType the identifier
+ * @return this builder
+ */
+ @This Builder type(Identifier entityType);
+
+ /**
+ * The entity type's numeric network id.
+ * @param javaId the java id
+ * @return this builder
+ */
+ @This Builder javaId(int javaId);
+
+ /**
+ * The width of this entity.
+ * @param width the width of this entity
+ * @return this builder
+ */
+ @This Builder width(@NonNegative float width);
+
+ /**
+ * The height of this entity
+ * @param height the height
+ * @return this builder
+ */
+ @This Builder height(@NonNegative float height);
+
+ /**
+ * The default Bedrock edition entity definition.
+ * You can define custom Bedrock entities, or use vanilla definitions
+ * obtainable via the {@link GeyserDefineEntitiesEvent#entities()} collection.
+ * This entity has to be registered before calling this method!
+ *
+ * @param defaultBedrockDefinition the default Bedrock definition
+ * @return this builder
+ */
+ @This Builder definition(@Nullable GeyserEntityDefinition defaultBedrockDefinition);
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/custom/package-info.java b/api/src/main/java/org/geysermc/geyser/api/entity/custom/package-info.java
new file mode 100644
index 00000000000..30ed743b1c6
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/custom/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2026 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
+ */
+
+@NullMarked
+package org.geysermc.geyser.api.entity.custom;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserEntityDataType.java b/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserEntityDataType.java
new file mode 100644
index 00000000000..3484f217bf4
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserEntityDataType.java
@@ -0,0 +1,67 @@
+/*
+ * 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.data;
+
+import org.geysermc.geyser.api.GeyserApi;
+import org.geysermc.geyser.api.entity.type.GeyserEntity;
+
+/**
+ * Represents a type of entity data that can be sent for an entity.
+ *
+ * Entity data types define the kind of value stored for a particular piece of metadata,
+ * such as a {@code Byte}, {@code Integer}, {@code Float}; and the name associated with them.
+ *
+ * Unlike custom items or blocks, it is possible to update entity metadata at runtime,
+ * which can be done using {@link GeyserEntity#update(GeyserEntityDataType, Object)}.
+ *
+ * @param the value type associated with this entity data type
+ */
+public interface GeyserEntityDataType {
+
+ /**
+ * Gets the Java class representing the value type associated with this data type.
+ *
+ * @return the class of the value used by this entity data type
+ */
+ Class typeClass();
+
+ /**
+ * Gets the unique name of this data type.
+ *
+ * The name is used internally to identify and register the data type so it can be
+ * referenced when reading or writing entity metadata.
+ *
+ * @return the name of this entity data type
+ */
+ String name();
+
+ /**
+ * For API usage only; use the types defined in {@link GeyserEntityDataTypes}
+ */
+ static GeyserEntityDataType of(Class typeClass, String name) {
+ return GeyserApi.api().provider(GeyserEntityDataType.class, typeClass, name);
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserEntityDataTypes.java b/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserEntityDataTypes.java
new file mode 100644
index 00000000000..e526d0f74c1
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserEntityDataTypes.java
@@ -0,0 +1,96 @@
+/*
+ * 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.data;
+
+import org.cloudburstmc.math.vector.Vector3f;
+import org.geysermc.geyser.api.entity.data.types.Hitbox;
+
+/**
+ * Contains commonly used {@link GeyserEntityDataType} constants for built-in entity
+ * metadata fields.
+ *
+ * These data types define the structure of certain primitive or numeric metadata
+ * values that can be used for Bedrock entities. Each constant is backed by a
+ * pre-registered entity data type that can be used when reading or writing metadata
+ * through the Geyser API.
+ */
+public final class GeyserEntityDataTypes {
+
+ /**
+ * Represents a single-byte value used for color types
+ * (e.g., sheep wool color).
+ */
+ public static final GeyserEntityDataType COLOR =
+ GeyserEntityDataType.of(Byte.class, "color");
+
+ /**
+ * Represents a numeric variant index that can be queried in resource packs.
+ */
+ public static final GeyserEntityDataType VARIANT =
+ GeyserEntityDataType.of(Integer.class, "variant");
+
+ /**
+ * Represents the entity's width.
+ */
+ public static final GeyserEntityDataType WIDTH =
+ GeyserEntityDataType.of(Float.class, "width");
+
+ /**
+ * Represents the entity's height.
+ */
+ public static final GeyserEntityDataType HEIGHT =
+ GeyserEntityDataType.of(Float.class, "height");
+
+ /**
+ * Represents the entity's vertical offset.
+ */
+ public static final GeyserEntityDataType VERTICAL_OFFSET =
+ GeyserEntityDataType.of(Float.class, "vertical_offset");
+
+ /**
+ * Represents the scale multiplier.
+ */
+ public static final GeyserEntityDataType SCALE =
+ GeyserEntityDataType.of(Float.class, "scale");
+
+ /**
+ * Represents custom hitboxes for entities
+ */
+ public static final GeyserListEntityDataType HITBOXES =
+ GeyserListEntityDataType.of(Hitbox.class, "hitboxes");
+
+ /**
+ * Represents the entity's seat offset. Applied when mounting an entity.
+ * Note: This can get overridden when a new entity is mounted, in which case, the seat offset
+ * would need to be updated again!
+ */
+ public static final GeyserEntityDataType SEAT_OFFSET =
+ GeyserEntityDataType.of(Vector3f.class, "seat_offset");
+
+ private GeyserEntityDataTypes() {
+ // no-op
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserListEntityDataType.java b/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserListEntityDataType.java
new file mode 100644
index 00000000000..cf60351fecd
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserListEntityDataType.java
@@ -0,0 +1,51 @@
+/*
+ * 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.data;
+
+import org.geysermc.geyser.api.GeyserApi;
+
+import java.util.List;
+
+/**
+ * Represents a list of objects for specific entity data types.
+ * For example, there can be multiple hitboxes on an entity.
+ *
+ * @param the object type in the list
+ */
+public interface GeyserListEntityDataType extends GeyserEntityDataType> {
+
+ /**
+ * @return the class of the list entries
+ */
+ Class listEntryClass();
+
+ /**
+ * API usage only, use the types defined in {@link GeyserEntityDataTypes}
+ */
+ static GeyserListEntityDataType of(Class typeClass, String name) {
+ return GeyserApi.api().provider(GeyserListEntityDataType.class, List.class, typeClass, name);
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/data/package-info.java b/api/src/main/java/org/geysermc/geyser/api/entity/data/package-info.java
new file mode 100644
index 00000000000..4111ccac8d6
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/data/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2026 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
+ */
+
+@NullMarked
+package org.geysermc.geyser.api.entity.data;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/data/types/Hitbox.java b/api/src/main/java/org/geysermc/geyser/api/entity/data/types/Hitbox.java
new file mode 100644
index 00000000000..4861d3101b9
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/data/types/Hitbox.java
@@ -0,0 +1,98 @@
+/*
+ * 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.data.types;
+
+import org.checkerframework.common.returnsreceiver.qual.This;
+import org.cloudburstmc.math.vector.Vector3f;
+import org.geysermc.geyser.api.GeyserApi;
+
+/**
+ * Represents an entity hitbox.
+ */
+public interface Hitbox {
+
+ /**
+ * Represents an empty / disabled hitbox.
+ */
+ Hitbox EMPTY = GeyserApi.api().provider(Hitbox.class, true);
+
+ /**
+ * The min "corner" of the hitbox
+ * @return the vector of the corner
+ */
+ Vector3f min();
+
+ /**
+ * The max "corner" of the hitbox
+ * @return the vector of the corner
+ */
+ Vector3f max();
+
+ /**
+ * The pivot of the hitbox
+ * @return the pivot
+ */
+ Vector3f pivot();
+
+ static Builder builder() {
+ return GeyserApi.api().provider(Builder.class);
+ }
+
+ /**
+ * The builder for the hitbox
+ */
+ interface Builder {
+
+ /**
+ * Sets the min corner of the hitbox
+ * @param min the vector of the corner
+ * @return this builder
+ */
+ @This Builder min(Vector3f min);
+
+ /**
+ * Sets the max corner of the hitbox
+ * @param max the vector of the corner
+ * @return this builder
+ */
+ @This Builder max(Vector3f max);
+
+ /**
+ * Sets the pivot of the hitbox
+ * @param pivot the pivot vector
+ * @return this builder
+ */
+ @This Builder pivot(Vector3f pivot);
+
+ /**
+ * Builds this hitbox, defaulting to {@code Vector3f.ZERO} if
+ * any one vector was not provided.
+ *
+ * @return a new hitbox
+ */
+ Hitbox build();
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/data/types/package-info.java b/api/src/main/java/org/geysermc/geyser/api/entity/data/types/package-info.java
new file mode 100644
index 00000000000..13e48938f5f
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/data/types/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2026 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
+ */
+
+@NullMarked
+package org.geysermc.geyser.api.entity.data.types;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/definition/GeyserEntityDefinition.java b/api/src/main/java/org/geysermc/geyser/api/entity/definition/GeyserEntityDefinition.java
new file mode 100644
index 00000000000..e6d5ade143f
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/definition/GeyserEntityDefinition.java
@@ -0,0 +1,71 @@
+/*
+ * 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.definition;
+
+import org.geysermc.geyser.api.GeyserApi;
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
+import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntitiesEvent;
+import org.geysermc.geyser.api.util.Identifier;
+
+import java.util.List;
+
+/**
+ * Represents a Bedrock entity definition.
+ * Custom Bedrock entity definitions must be registered in the
+ * {@link GeyserDefineEntitiesEvent} before usage!
+ */
+public interface GeyserEntityDefinition {
+
+ /**
+ * @return the Bedrock entity identifier
+ */
+ Identifier identifier();
+
+ /**
+ * @return the properties registered for this entity type
+ */
+ List> properties();
+
+ /**
+ * @return whether this entity is a vanilla entity
+ */
+ boolean vanilla();
+
+ /**
+ * @return whether this definition has been registered
+ */
+ boolean registered();
+
+ /**
+ * Creates or retrieves a GeyserEntityDefinition by the Bedrock entity type identifier.
+ *
+ * @param identifier the Bedrock entity identifier
+ * @return the GeyserEntityDefinition
+ */
+ static GeyserEntityDefinition of(Identifier identifier) {
+ return GeyserApi.api().provider(GeyserEntityDefinition.class, identifier);
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/definition/JavaEntityType.java b/api/src/main/java/org/geysermc/geyser/api/entity/definition/JavaEntityType.java
new file mode 100644
index 00000000000..1b0d245983b
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/definition/JavaEntityType.java
@@ -0,0 +1,78 @@
+/*
+ * 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.definition;
+
+import org.geysermc.geyser.api.util.Identifier;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Represents a Java edition entity type
+ */
+public interface JavaEntityType {
+
+ /**
+ * @return the Java identifier of the type
+ */
+ Identifier identifier();
+
+ /**
+ * @return the numeric Java entity type id
+ */
+ int javaId();
+
+ /**
+ * @return whether this entity exists in the vanilla base game
+ */
+ boolean vanilla();
+
+ /**
+ * @return the width of the Java entity
+ */
+ float width();
+
+ /**
+ * @return the height of the Java entity
+ */
+ float height();
+
+ /**
+ * Compares two entity identifiers.
+ *
+ * @param javaIdentifier the other entity identifier
+ * @return true if the entity identifier is the same
+ */
+ default boolean is(Identifier javaIdentifier) {
+ return identifier().equals(javaIdentifier);
+ }
+
+ /**
+ * Gets the default Bedrock entity definition, if available,
+ * that is associated with this Minecraft: Java Edition entity type.
+ *
+ * @return the default Bedrock entity definition
+ */
+ @Nullable GeyserEntityDefinition defaultBedrockDefinition();
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/definition/package-info.java b/api/src/main/java/org/geysermc/geyser/api/entity/definition/package-info.java
new file mode 100644
index 00000000000..3ccab92d450
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/definition/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2026 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
+ */
+
+@NullMarked
+package org.geysermc.geyser.api.entity.definition;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java b/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java
index 922c87491a8..9f923e2d216 100644
--- a/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java
@@ -26,12 +26,19 @@
package org.geysermc.geyser.api.entity.type;
import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.checker.index.qual.Positive;
+import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.entity.data.GeyserEntityDataType;
+import org.geysermc.geyser.api.entity.data.GeyserEntityDataTypes;
+import org.geysermc.geyser.api.entity.definition.GeyserEntityDefinition;
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 org.jspecify.annotations.Nullable;
+import java.util.List;
+import java.util.UUID;
import java.util.function.Consumer;
/**
@@ -40,11 +47,68 @@
*/
public interface GeyserEntity {
/**
- * @return the entity ID that the server has assigned to this entity.
+ * @return the entity ID that the server has assigned to this entity, or 0 if none is present
*/
@NonNegative
int javaId();
+ /**
+ * @return the Geyser entity id that the Bedrock client sees
+ */
+ @Positive
+ long geyserId();
+
+ /**
+ * @return the entity uuid that the server has assigned to this entity,
+ * or null if none is assigned
+ */
+ @Nullable
+ UUID uuid();
+
+ /**
+ * @return the Bedrock entity definition
+ */
+ GeyserEntityDefinition definition();
+
+ /**
+ * The position of this entity, without the Bedrock edition offset
+ * defined in the Bedrock entity definition.
+ *
+ * @return the position of the entity, as it is known to the Java server.
+ */
+ Vector3f position();
+
+ /**
+ * The vehicle this entity is currently on, or null if not present.
+ */
+ @Nullable
+ GeyserEntity vehicle();
+
+ /**
+ * The passengers of this entity, or an empty list if none are present.
+ */
+ List passengers();
+
+ /**
+ * Queries the current value of a given {@link GeyserEntityDataType}.
+ *
+ * @see GeyserEntityDataTypes
+ * @param dataType the entity data type to query
+ * @return the value, or null if not set
+ * @param the type of the value
+ */
+ @Nullable T value(GeyserEntityDataType dataType);
+
+ /**
+ * Updates an entity property with a new value.
+ * If the new value is null, the property is reset to the default value.
+ *
+ * @param dataType an entity data type, such as from {@link GeyserEntityDataTypes}
+ * @param value the new property value
+ * @param the type of the value
+ */
+ void update(GeyserEntityDataType dataType, @Nullable T value);
+
/**
* Updates an entity property with a new value.
* If the new value is null, the property is reset to the default value.
@@ -59,12 +123,9 @@ default void updateProperty(GeyserEntityProperty property, @Nullable T va
}
/**
- * Updates multiple properties with just one update packet.
- * @see BatchPropertyUpdater
- *
- * @param consumer a batch updater
- * @since 2.9.0
+ * @deprecated - use {@link #updateProperty(GeyserEntityProperty, Object)} instead
*/
+ @Deprecated
default void updatePropertiesBatched(Consumer consumer) {
this.updatePropertiesBatched(consumer, false);
}
@@ -73,10 +134,10 @@ default void updatePropertiesBatched(Consumer consumer) {
* Updates multiple properties with just one update packet, which can be sent immediately to the client.
* Usually, sending updates immediately is not required except for specific situations where packet batching
* would result in update order issues.
- * @see BatchPropertyUpdater
*
* @param consumer a batch updater
* @param immediate whether this update should be sent immediately
+ * @see BatchPropertyUpdater
* @since 2.9.1
*/
void updatePropertiesBatched(Consumer consumer, boolean immediate);
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java b/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java
index d31def99670..da2e286090b 100644
--- a/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java
@@ -25,15 +25,7 @@
package org.geysermc.geyser.api.entity.type.player;
-import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.api.entity.type.GeyserEntity;
public interface GeyserPlayerEntity extends GeyserEntity {
-
- /**
- * Gets the position of the player, as it is known to the Java server.
- *
- * @return the player's position
- */
- Vector3f position();
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionSpawnEntityEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionSpawnEntityEvent.java
new file mode 100644
index 00000000000..8feda193da3
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionSpawnEntityEvent.java
@@ -0,0 +1,73 @@
+/*
+ * 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.bedrock;
+
+import org.geysermc.event.Cancellable;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.entity.definition.GeyserEntityDefinition;
+import org.geysermc.geyser.api.entity.type.GeyserEntity;
+import org.geysermc.geyser.api.event.connection.ConnectionEvent;
+import org.geysermc.geyser.api.event.java.ServerAttachParrotsEvent;
+import org.geysermc.geyser.api.event.java.ServerSpawnEntityEvent;
+import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntitiesEvent;
+import org.jspecify.annotations.Nullable;
+
+import java.util.function.Consumer;
+
+/**
+ * See {@link ServerSpawnEntityEvent} and {@link ServerAttachParrotsEvent}
+ */
+public abstract class SessionSpawnEntityEvent extends ConnectionEvent implements Cancellable {
+
+ public SessionSpawnEntityEvent(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * Gets the entity definition sent to the connection when the entity is spawned.
+ *
+ * @return the entity definition sent to the connection when the entity is spawned
+ */
+ public abstract @Nullable GeyserEntityDefinition definition();
+
+ /**
+ * Sets the entity definition sent to the connection when the entity is spawned.
+ * This entity definition MUST have been registered in the {@link GeyserDefineEntitiesEvent} before
+ * using it here!
+ *
+ * @param entityDefinition the entity definition sent to the connection when the entity is spawned
+ */
+ public abstract void definition(@Nullable GeyserEntityDefinition entityDefinition);
+
+ /**
+ * Adds a consumer for the {@link GeyserEntity} that will be called once the entity is created,
+ * assuming that it hasn't been cancelled.
+ * Using this method, you can modify the entities' entity properties, scale, with, and other entity data.
+ *
+ * @param consumer the consumer for the new GeyserEntity
+ */
+ public abstract void preSpawnConsumer(Consumer consumer);
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerAttachParrotsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerAttachParrotsEvent.java
new file mode 100644
index 00000000000..6d4c9fb0949
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerAttachParrotsEvent.java
@@ -0,0 +1,55 @@
+/*
+ * 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.java;
+
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
+import org.geysermc.geyser.api.event.bedrock.SessionSpawnEntityEvent;
+
+/**
+ * Called when the Java server attaches parrots to a player.
+ */
+public abstract class ServerAttachParrotsEvent extends SessionSpawnEntityEvent {
+
+ public ServerAttachParrotsEvent(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * @return the player with bird friends
+ */
+ public abstract GeyserPlayerEntity player();
+
+ /**
+ * @return the parrot variant
+ */
+ public abstract int variant();
+
+ /**
+ * @return true if parrot is on the right shoulder, left otherwise
+ */
+ public abstract boolean right();
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerSpawnEntityEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerSpawnEntityEvent.java
new file mode 100644
index 00000000000..338f3faa9a4
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerSpawnEntityEvent.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2025 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.api.event.java;
+
+import org.geysermc.event.Cancellable;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.entity.definition.JavaEntityType;
+import org.geysermc.geyser.api.event.bedrock.SessionSpawnEntityEvent;
+
+import java.util.UUID;
+
+/**
+ * Called when the downstream server spawns a non-player entity.
+ */
+public abstract class ServerSpawnEntityEvent extends SessionSpawnEntityEvent implements Cancellable {
+
+ public ServerSpawnEntityEvent(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * Gets the entity id of the entity being spawned.
+ *
+ * @return the entity id of the entity being spawned
+ */
+ public abstract int entityId();
+
+ /**
+ * Gets the uuid of the entity being spawned.
+ *
+ * @return the uuid of the entity being spawned
+ */
+ public abstract UUID uuid();
+
+ /**
+ * Gets the Java entity type sent by the server
+ *
+ * @return the Java edition entity type of the entity being spawned
+ */
+ public abstract JavaEntityType entityType();
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerUpdateEntityPassengersEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerUpdateEntityPassengersEvent.java
new file mode 100644
index 00000000000..21c299e46aa
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerUpdateEntityPassengersEvent.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2026 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.java;
+
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.entity.type.GeyserEntity;
+import org.geysermc.geyser.api.event.connection.ConnectionEvent;
+
+/**
+ * This event is called when an entity's passengers are updated.
+ * To avoid de-syncs, you cannot cancel this event.
+ */
+public abstract class ServerUpdateEntityPassengersEvent extends ConnectionEvent {
+
+ /**
+ * The vehicle entity that gets a passenger update.
+ *
+ * @return the vehicle entity
+ */
+ public abstract GeyserEntity vehicle();
+
+ public ServerUpdateEntityPassengersEvent(GeyserConnection connection) {
+ super(connection);
+ }
+
+ public abstract static class Mount extends ServerUpdateEntityPassengersEvent {
+ public Mount(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * @return the passenger that was added to the vehicle
+ */
+ public abstract GeyserEntity addedPassenger();
+ }
+
+ public abstract static class Dismount extends ServerUpdateEntityPassengersEvent {
+ public Dismount(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * @return the passenger that was removed from the vehicle
+ */
+ public abstract GeyserEntity removedPassenger();
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntitiesEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntitiesEvent.java
new file mode 100644
index 00000000000..80bb9183bdb
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntitiesEvent.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2025 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.api.event.lifecycle;
+
+import org.geysermc.event.Event;
+import org.geysermc.geyser.api.entity.custom.CustomEntityDefinition;
+import org.geysermc.geyser.api.entity.custom.CustomJavaEntityType;
+import org.geysermc.geyser.api.entity.definition.GeyserEntityDefinition;
+
+import java.util.Collection;
+import java.util.function.Consumer;
+
+/**
+ * Called when entities are defined within Geyser.
+ *
+ * This event can be used to add custom entities to Geyser.
+ */
+public interface GeyserDefineEntitiesEvent extends Event {
+
+ /**
+ * @return an immutable collection of all registered entity definitions
+ */
+ Collection entities();
+
+ /**
+ * @return an immutable collection of all registered custom entity definitions
+ */
+ Collection customEntities();
+
+ /**
+ * Registers a custom entity definition
+ * @param definition the custom entity definition to register
+ */
+ void register(CustomEntityDefinition definition);
+
+ /**
+ * Registers a non-vanilla Java entity type.
+ *
+ * @param builderConsumer the builder for the non-vanilla type
+ */
+ void registerEntityType(Consumer builderConsumer);
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java
index 1230e3551fe..d50054d2741 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntityPropertiesEvent.java
@@ -26,6 +26,7 @@
package org.geysermc.geyser.api.event.lifecycle;
import org.geysermc.event.Event;
+import org.geysermc.geyser.api.connection.GeyserConnection;
import org.geysermc.geyser.api.entity.EntityData;
import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
import org.geysermc.geyser.api.entity.property.type.GeyserBooleanEntityProperty;
@@ -39,7 +40,6 @@
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
@@ -61,10 +61,9 @@
* }
* }
*
- * 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.
+ * Retrieving entity instances is possible with, for example, the {@link EntityData#byJavaId(int)} method, or
+ * {@link GeyserConnection#playerEntity()} for the connection player entity.
+ * To update the value of a property on a specific entity, use {@link GeyserEntity#updateProperty(GeyserEntityProperty, Object)}.
*
* Notes:
*
@@ -83,7 +82,7 @@ public interface GeyserDefineEntityPropertiesEvent extends Event {
* 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
+ * @param entityType the Bedrock edition entity type identifier
* @return an unmodifiable collection of registered properties
*
* @since 2.9.0
@@ -93,7 +92,7 @@ public interface GeyserDefineEntityPropertiesEvent extends Event {
/**
* Registers a {@code float}-backed entity property.
*
- * @param entityType the Java edition entity type identifier
+ * @param entityType the Bedrock edition entity type identifier
* @param propertyIdentifier the unique property identifier
* @param min the minimum allowed value (inclusive)
* @param max the maximum allowed value (inclusive)
@@ -108,7 +107,7 @@ public interface GeyserDefineEntityPropertiesEvent extends Event {
* 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 entityType the Bedrock edition entity type identifier
* @param propertyIdentifier the unique property identifier
* @param min the minimum allowed value (inclusive)
* @param max the maximum allowed value (inclusive)
@@ -123,7 +122,7 @@ default GeyserFloatEntityProperty registerFloatProperty(Identifier entityType, I
/**
* Registers an {@code int}-backed entity property.
*
- * @param entityType the Java edition entity type identifier
+ * @param entityType the Bedrock edition entity type identifier
* @param propertyIdentifier the unique property identifier
* @param min the minimum allowed value (inclusive)
* @param max the maximum allowed value (inclusive)
@@ -137,7 +136,7 @@ default GeyserFloatEntityProperty registerFloatProperty(Identifier entityType, I
/**
* 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 entityType the Bedrock edition entity type identifier
* @param propertyIdentifier the unique property identifier
* @param min the minimum allowed value (inclusive)
* @param max the maximum allowed value (inclusive)
@@ -152,7 +151,7 @@ default GeyserIntEntityProperty registerIntegerProperty(Identifier entityType, I
/**
* Registers a {@code boolean}-backed entity property.
*
- * @param entityType the Java edition entity type identifier
+ * @param entityType the Bedrock edition entity type identifier
* @param propertyIdentifier the unique property identifier
* @param defaultValue the default boolean value
* @return the created boolean property handle
@@ -165,7 +164,7 @@ default GeyserIntEntityProperty registerIntegerProperty(Identifier entityType, I
* 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 entityType the Bedrock edition entity type identifier
* @param propertyIdentifier the unique property identifier
* @return the created boolean property
* @since 2.9.0
@@ -181,7 +180,7 @@ default GeyserBooleanEntityProperty registerBooleanProperty(Identifier entityTyp
* the first enum value is set as the default.
* @see GeyserEnumEntityProperty for further limitations
*
- * @param entityType the Java edition entity type identifier
+ * @param entityType the Bedrock 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
@@ -196,7 +195,7 @@ default GeyserBooleanEntityProperty registerBooleanProperty(Identifier entityTyp
* 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 entityType the Bedrock edition entity type identifier
* @param propertyIdentifier the unique property identifier
* @param enumClass the enum class that defines allowed values
* @param the enum type
@@ -214,7 +213,7 @@ default > GeyserEnumEntityProperty registerEnumProperty(Ide
* 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 entityType the Bedrock 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
@@ -228,7 +227,7 @@ default > GeyserEnumEntityProperty registerEnumProperty(Ide
* 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 entityType the Bedrock edition entity type identifier
* @param propertyIdentifier the unique property identifier
* @param values the allowed string values
* @return the created string-enum property handle
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
index e510e524357..e672168ab5d 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
@@ -73,7 +73,7 @@
import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.configuration.GeyserConfig;
import org.geysermc.geyser.configuration.GeyserPluginConfig;
-import org.geysermc.geyser.entity.EntityDefinitions;
+import org.geysermc.geyser.entity.VanillaEntities;
import org.geysermc.geyser.erosion.UnixSocketClientListener;
import org.geysermc.geyser.event.GeyserEventBus;
import org.geysermc.geyser.event.type.SessionDisconnectEventImpl;
@@ -253,7 +253,7 @@ public void initialize() {
RegistryCache.init();
/* Initialize translators */
- EntityDefinitions.init();
+ VanillaEntities.init();
MessageTranslator.init();
// Download the latest asset list and cache it
diff --git a/core/src/main/java/org/geysermc/geyser/entity/BedrockEntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/BedrockEntityDefinition.java
new file mode 100644
index 00000000000..6bb318c988f
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/BedrockEntityDefinition.java
@@ -0,0 +1,110 @@
+/*
+ * 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;
+
+import lombok.AccessLevel;
+import lombok.Setter;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.api.entity.custom.CustomEntityDefinition;
+import org.geysermc.geyser.api.entity.definition.GeyserEntityDefinition;
+import org.geysermc.geyser.api.entity.property.GeyserEntityProperty;
+import org.geysermc.geyser.api.util.Identifier;
+import org.geysermc.geyser.entity.properties.GeyserEntityProperties;
+import org.geysermc.geyser.registry.Registries;
+
+import java.util.List;
+import java.util.Objects;
+
+public record BedrockEntityDefinition(
+ @NonNull Identifier identifier,
+ @NonNull GeyserEntityProperties registeredProperties
+) implements GeyserEntityDefinition, CustomEntityDefinition {
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static BedrockEntityDefinition ofVanilla(Identifier identifier) {
+ BedrockEntityDefinition bedrockEntityDefinition = builder().identifier(identifier).build();
+ Registries.BEDROCK_ENTITY_DEFINITIONS.register(identifier, bedrockEntityDefinition);
+ return bedrockEntityDefinition;
+ }
+
+ public static BedrockEntityDefinition getOrCreate(@NonNull Identifier identifier) {
+ Objects.requireNonNull(identifier, "identifier");
+ if (Registries.BEDROCK_ENTITY_DEFINITIONS.get().containsKey(identifier)) {
+ return Registries.BEDROCK_ENTITY_DEFINITIONS.get().get(identifier);
+ }
+
+ if (identifier.vanilla()) {
+ throw new IllegalArgumentException("Cannot create custom entity in vanilla namespace! " + identifier);
+ }
+ return builder().identifier(identifier).build();
+ }
+
+ @Override
+ public @NonNull List> properties() {
+ if (registeredProperties.isEmpty()) {
+ return List.of();
+ }
+ return List.copyOf(registeredProperties.getProperties());
+ }
+
+ @Override
+ public boolean vanilla() {
+ return identifier.vanilla();
+ }
+
+ @Override
+ public boolean registered() {
+ return Registries.BEDROCK_ENTITY_DEFINITIONS.get().containsKey(identifier);
+ }
+
+ public static class Builder {
+ private Identifier identifier;
+ @Setter(AccessLevel.NONE)
+ protected GeyserEntityProperties.Builder propertiesBuilder;
+
+ public Builder() {
+ }
+
+ public Builder identifier(Identifier identifier) {
+ this.identifier = identifier;
+ return this;
+ }
+
+ public Builder properties(GeyserEntityProperties.@Nullable Builder propertiesBuilder) {
+ this.propertiesBuilder = propertiesBuilder;
+ return this;
+ }
+
+ BedrockEntityDefinition build() {
+ return new BedrockEntityDefinition(identifier, propertiesBuilder != null ?
+ propertiesBuilder.build() : new GeyserEntityProperties());
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/BedrockEntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/BedrockEntityDefinitions.java
new file mode 100644
index 00000000000..b780b9bc24a
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/BedrockEntityDefinitions.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+import org.geysermc.geyser.impl.IdentifierImpl;
+
+/**
+ * Most Bedrock entities are registered in {@link VanillaEntities} - however, some are
+ * done here to be able to re-use the same bedrock entity across multiple Java types
+ */
+public class BedrockEntityDefinitions {
+ public static final BedrockEntityDefinition ARMOR_STAND;
+ public static final BedrockEntityDefinition ARROW;
+ public static final BedrockEntityDefinition BOAT;
+ public static final BedrockEntityDefinition CHEST_BOAT;
+ public static final BedrockEntityDefinition EVOCATION_ILLAGER;
+ public static final BedrockEntityDefinition LLAMA;
+ public static final BedrockEntityDefinition MINECART;
+ public static final BedrockEntityDefinition SPLASH_POTION;
+ public static final BedrockEntityDefinition ZOMBIE;
+
+ static {
+ ARMOR_STAND = BedrockEntityDefinition.ofVanilla(IdentifierImpl.of("armor_stand"));
+ ARROW = BedrockEntityDefinition.ofVanilla(IdentifierImpl.of("arrow"));
+ BOAT = BedrockEntityDefinition.ofVanilla(IdentifierImpl.of("boat"));
+ CHEST_BOAT = BedrockEntityDefinition.ofVanilla(IdentifierImpl.of("chest_boat"));
+ EVOCATION_ILLAGER = BedrockEntityDefinition.ofVanilla(IdentifierImpl.of("evocation_illager"));
+ LLAMA = BedrockEntityDefinition.ofVanilla(IdentifierImpl.of("llama"));
+ MINECART = BedrockEntityDefinition.ofVanilla(IdentifierImpl.of("minecart"));
+ SPLASH_POTION = BedrockEntityDefinition.ofVanilla(IdentifierImpl.of("splash_potion"));
+ ZOMBIE = BedrockEntityDefinition.ofVanilla(IdentifierImpl.of("zombie"));
+ }
+
+ public static void init() {
+ // no-op
+ }
+
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java
deleted file mode 100644
index 64ed99ef96d..00000000000
--- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (c) 2019-2022 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;
-
-import it.unimi.dsi.fastutil.objects.ObjectArrayList;
-import lombok.Setter;
-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;
-import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
-import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType;
-import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
-
-import java.util.List;
-import java.util.Locale;
-import java.util.function.BiConsumer;
-
-/**
- * Represents data for an entity. This includes properties such as height and width, as well as the list of entity
- * metadata translators needed to translate the properties sent from the server. The translators are structured in such
- * a way that inserting a new one (for example in version updates) is convenient.
- *
- * @param identifier the Bedrock identifier of this entity
- * @param the entity type this definition represents
- */
-public record EntityDefinition(EntityFactory factory, EntityType entityType, String identifier,
- float width, float height, float offset, GeyserEntityProperties registeredProperties, List> translators) {
-
- public static Builder inherited(EntityFactory factory, EntityDefinition super T> parent) {
- return new Builder<>(factory, parent.entityType, parent.identifier, parent.width, parent.height, parent.offset, new ObjectArrayList<>(parent.translators));
- }
-
- public static Builder builder(EntityFactory factory) {
- return new Builder<>(factory);
- }
-
- @SuppressWarnings("unchecked")
- public void translateMetadata(T entity, EntityMetadata> metadata) {
- EntityMetadataTranslator super T, M, EntityMetadata>> translator = (EntityMetadataTranslator super T, M, EntityMetadata>>) this.translators.get(metadata.getId());
- if (translator == null) {
- // This can safely happen; it means we don't translate this entity metadata
- return;
- }
-
- if (translator.acceptedType() != metadata.getType()) {
- GeyserImpl.getInstance().getLogger().warning("Metadata ID " + metadata.getId() + " was received with type " + metadata.getType() + " but we expected " + translator.acceptedType() + " for " + entity.getDefinition().entityType());
- if (GeyserImpl.getInstance().config().debugMode()) {
- GeyserImpl.getInstance().getLogger().debug(metadata.toString());
- }
- return;
- }
-
- translator.translate(entity, metadata);
- }
-
- @Setter
- @Accessors(fluent = true, chain = true)
- public static class Builder {
- private final EntityFactory factory;
- private EntityType type;
- private String identifier;
- private float width;
- private float height;
- private float offset;
- private GeyserEntityProperties.Builder propertiesBuilder;
- private final List> translators;
-
- private Builder(EntityFactory factory) {
- this.factory = factory;
- translators = new ObjectArrayList<>();
- }
-
- public Builder(EntityFactory factory, EntityType type, String identifier, float width, float height, float offset, List> translators) {
- this.factory = factory;
- this.type = type;
- this.identifier = identifier;
- this.width = width;
- this.height = height;
- this.offset = offset;
- this.translators = translators;
- }
-
- /**
- * Sets the height and width as one value
- */
- public Builder heightAndWidth(float value) {
- height = value;
- width = value;
- return this;
- }
-
- public Builder offset(float offset) {
- this.offset = offset;
- return this;
- }
-
- /**
- * Resets the identifier as well
- */
- public Builder type(EntityType type) {
- this.type = type;
- identifier = null;
- return this;
- }
-
- public Builder property(PropertyType, ?> propertyType) {
- if (this.propertiesBuilder == null) {
- this.propertiesBuilder = new GeyserEntityProperties.Builder(this.identifier);
- }
- propertiesBuilder.add(propertyType);
- return this;
- }
-
- public >> Builder addTranslator(MetadataType type, BiConsumer translateFunction) {
- translators.add(new EntityMetadataTranslator<>(type, translateFunction));
- return this;
- }
-
- public Builder addTranslator(EntityMetadataTranslator translator) {
- translators.add(translator);
- return this;
- }
-
- public EntityDefinition build() {
- return build(true);
- }
-
- /**
- * @param register whether to register this entity in the Registries for entity types. Generally this should be
- * set to false if we're not expecting this entity to spawn from the network.
- */
- public EntityDefinition build(boolean register) {
- if (identifier == null && type != null) {
- identifier = "minecraft:" + type.name().toLowerCase(Locale.ROOT);
- }
- GeyserEntityProperties registeredProperties = propertiesBuilder == null ? new GeyserEntityProperties() : propertiesBuilder.build();
- EntityDefinition definition = new EntityDefinition<>(factory, type, identifier, width, height, offset, registeredProperties, translators);
- if (register && definition.entityType() != null) {
- Registries.ENTITY_DEFINITIONS.get().putIfAbsent(definition.entityType(), definition);
- Registries.JAVA_ENTITY_IDENTIFIERS.get().putIfAbsent("minecraft:" + type.name().toLowerCase(Locale.ROOT), definition);
- }
- return definition;
- }
- }
-}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityTypeBase.java b/core/src/main/java/org/geysermc/geyser/entity/EntityTypeBase.java
new file mode 100644
index 00000000000..621b10beed2
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/EntityTypeBase.java
@@ -0,0 +1,151 @@
+/*
+ * 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;
+
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.entity.type.Entity;
+import org.geysermc.geyser.translator.entity.EntityMetadataTranslator;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+
+@EqualsAndHashCode
+@ToString
+@Getter
+@Accessors(fluent = true)
+public class EntityTypeBase {
+ /**
+ * The width of the Java entity type.
+ */
+ protected final float width;
+ /**
+ * The height of the Java entity type
+ */
+ protected final float height;
+ /**
+ * The vertical offset for the default Bedrock entity.
+ */
+ protected final float offset;
+ protected final List> translators;
+
+ public EntityTypeBase(float width, float height, float offset, List> translators) {
+ this.width = width;
+ this.height = height;
+ this.offset = offset;
+ this.translators = translators;
+ }
+
+ public static Builder baseBuilder(Class clazz) {
+ return new Builder<>(clazz);
+ }
+
+ // Unused param so Java knows what entity we're talking about
+ @SuppressWarnings("unused")
+ public static Builder baseInherited(Class clazz, EntityTypeBase super T> parent) {
+ return new Builder(parent.width, parent.height, parent.offset, new ObjectArrayList<>(parent.translators));
+ }
+
+ @SuppressWarnings("unchecked")
+ public void translateMetadata(T entity, EntityMetadata> metadata) {
+ EntityMetadataTranslator super T, M, EntityMetadata>> translator = (EntityMetadataTranslator super T, M, EntityMetadata>>) this.translators.get(metadata.getId());
+ if (translator == null) {
+ // This can safely happen; it means we don't translate this entity metadata
+ return;
+ }
+
+ if (translator.acceptedType() != metadata.getType()) {
+ GeyserImpl.getInstance().getLogger().warning("Metadata ID " + metadata.getId() + " was received with type " + metadata.getType() + " but we expected " + translator.acceptedType() + " for " + entity.getJavaTypeDefinition().type());
+ if (GeyserImpl.getInstance().config().debugMode()) {
+ GeyserImpl.getInstance().getLogger().debug(metadata.toString());
+ }
+ return;
+ }
+
+ translator.translate(entity, metadata);
+ }
+
+ @Setter
+ @Accessors(fluent = true, chain = true)
+ public static class Builder {
+ protected float width;
+ protected float height;
+ protected float offset;
+ protected final List> translators;
+
+ protected Builder() {
+ translators = new ObjectArrayList<>();
+ }
+
+ // Unused param so Java knows what entity we're talking about
+ @SuppressWarnings("unused")
+ protected Builder(Class clazz) {
+ this();
+ }
+
+ protected Builder(float width, float height, float offset, List> translators) {
+ this.width = width;
+ this.height = height;
+ this.offset = offset;
+ this.translators = translators;
+ }
+
+ /**
+ * Sets the height and width as one value
+ */
+ public Builder heightAndWidth(float value) {
+ height = value;
+ width = value;
+ return this;
+ }
+
+ public Builder offset(float offset) {
+ this.offset = offset;
+ return this;
+ }
+
+ public >> Builder addTranslator(MetadataType type, BiConsumer translateFunction) {
+ translators.add(new EntityMetadataTranslator<>(type, translateFunction));
+ return this;
+ }
+
+ public Builder addTranslator(EntityMetadataTranslator translator) {
+ translators.add(translator);
+ return this;
+ }
+
+ public EntityTypeBase build() {
+ return new EntityTypeBase<>(width, height, offset, translators);
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityTypeDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/EntityTypeDefinition.java
new file mode 100644
index 00000000000..225be45bd04
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/EntityTypeDefinition.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2019-2022 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;
+
+import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+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.translator.entity.EntityMetadataTranslator;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.type.BuiltinEntityType;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+
+/**
+ * Represents data for an entity. This includes the default bedrock entity definition, as well as the list of Java entity
+ * metadata translators needed to translate the properties sent from the server. The translators are structured in such
+ * a way that inserting a new one (for example in version updates) is convenient.
+ *
+ * @param the entity type this definition represents
+ */
+@Getter
+@Accessors(fluent = true)
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public abstract class EntityTypeDefinition extends EntityTypeBase {
+ private final EntityFactory factory;
+ private final GeyserEntityType type;
+ private final BedrockEntityDefinition defaultBedrockDefinition;
+
+ public EntityTypeDefinition(EntityFactory factory, GeyserEntityType type, float width, float height, float offset, BedrockEntityDefinition defaultBedrockDefinition, List> translators) {
+ super(width, height, offset, translators);
+ this.type = type;
+ this.factory = factory;
+ this.defaultBedrockDefinition = defaultBedrockDefinition;
+ }
+
+ public abstract boolean is(BuiltinEntityType builtinEntityType);
+
+ @Setter
+ @Accessors(fluent = true, chain = true)
+ public static abstract class Builder extends EntityTypeBase.Builder {
+ protected final EntityFactory factory;
+ protected String bedrockIdentifier;
+ @Setter(AccessLevel.NONE)
+ protected GeyserEntityProperties.Builder propertiesBuilder;
+
+ protected Builder(EntityFactory factory) {
+ super();
+ this.factory = factory;
+ }
+
+ protected Builder(EntityFactory factory, float width, float height, float offset, List> translators) {
+ super(width, height, offset, translators);
+ this.factory = factory;
+ }
+
+ @Override
+ public Builder width(float width) {
+ return (Builder) super.width(width);
+ }
+
+ @Override
+ public Builder height(float height) {
+ return (Builder) super.height(height);
+ }
+
+ @Override
+ public Builder heightAndWidth(float value) {
+ return (Builder) super.heightAndWidth(value);
+ }
+
+ @Override
+ public Builder offset(float offset) {
+ return (Builder) super.offset(offset);
+ }
+
+ @Override
+ public >> Builder addTranslator(MetadataType type, BiConsumer translateFunction) {
+ return (Builder) super.addTranslator(type, translateFunction);
+ }
+
+ @Override
+ public Builder addTranslator(EntityMetadataTranslator translator) {
+ return (Builder) super.addTranslator(translator);
+ }
+
+ public Builder property(PropertyType, ?> propertyType) {
+ if (this.propertiesBuilder == null) {
+ this.propertiesBuilder = new GeyserEntityProperties.Builder(this.bedrockIdentifier);
+ }
+ propertiesBuilder.add(propertyType);
+ return this;
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
index 1263be24b0c..7e4180dc005 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
@@ -29,11 +29,11 @@
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.packet.EmotePacket;
-import org.geysermc.geyser.input.InputLocksFlag;
import org.geysermc.geyser.api.entity.EntityData;
import org.geysermc.geyser.api.entity.type.GeyserEntity;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.entity.type.Entity;
+import org.geysermc.geyser.input.InputLocksFlag;
import org.geysermc.geyser.session.GeyserSession;
import java.util.HashSet;
@@ -58,6 +58,30 @@ public GeyserEntityData(GeyserSession session) {
return future;
}
+ @Override
+ public @Nullable GeyserEntity byJavaId(@NonNegative int javaId) {
+ //noinspection ConstantValue
+ if (javaId < 0) {
+ throw new IllegalArgumentException("entity id cannot be negative! (got: " + javaId + ")");
+ }
+ return session.getEntityCache().getEntityByJavaId(javaId);
+ }
+
+ @Override
+ public @Nullable GeyserEntity byUuid(@NonNull UUID javaUuid) {
+ Objects.requireNonNull(javaUuid, "javaUuid");
+ return session.getEntityCache().getEntityByUuid(javaUuid);
+ }
+
+ @Override
+ public @Nullable GeyserEntity byGeyserId(@NonNegative long geyserId) {
+ //noinspection ConstantValue
+ if (geyserId < 0) {
+ throw new IllegalArgumentException("geyser entity id cannot be negative! (got: " + geyserId + ")");
+ }
+ return session.getEntityCache().getEntityByGeyserId(geyserId);
+ }
+
@Override
public void showEmote(@NonNull GeyserPlayerEntity emoter, @NonNull String emoteId) {
Objects.requireNonNull(emoter, "emoter must not be null!");
diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityType.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityType.java
new file mode 100644
index 00000000000..619252a82c5
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityType.java
@@ -0,0 +1,229 @@
+/*
+ * 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;
+
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import lombok.Getter;
+import net.kyori.adventure.key.Key;
+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.Constants;
+import org.geysermc.geyser.api.entity.custom.CustomJavaEntityType;
+import org.geysermc.geyser.api.entity.definition.GeyserEntityDefinition;
+import org.geysermc.geyser.api.entity.definition.JavaEntityType;
+import org.geysermc.geyser.api.util.Identifier;
+import org.geysermc.geyser.impl.IdentifierImpl;
+import org.geysermc.geyser.registry.Registries;
+import org.geysermc.geyser.util.MinecraftKey;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.type.BuiltinEntityType;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
+
+import java.util.EnumMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+public record GeyserEntityType(Identifier identifier, int javaId) implements JavaEntityType, CustomJavaEntityType {
+ private static final Identifier UNREGISTERED = IdentifierImpl.of(Constants.GEYSER_CUSTOM_NAMESPACE, "unregistered_sadface");
+
+ private static final Map VANILLA = new EnumMap<>(BuiltinEntityType.class);
+ private static final Int2ObjectMap CUSTOM = new Int2ObjectOpenHashMap<>();
+ private static final Object2ObjectMap CUSTOM_BY_IDENTIFIER = new Object2ObjectOpenHashMap<>();
+
+ private GeyserEntityType(BuiltinEntityType builtin) {
+ this(IdentifierImpl.of(builtin.name().toLowerCase(Locale.ROOT)), builtin.id());
+ }
+
+ private GeyserEntityType(int javaId) {
+ this(UNREGISTERED, javaId);
+ }
+
+ public boolean isUnregistered() {
+ return identifier.equals(UNREGISTERED);
+ }
+
+ @Override
+ public boolean vanilla() {
+ return VANILLA.containsValue(this);
+ }
+
+ @Override
+ public float width() {
+ var definition = Registries.JAVA_ENTITY_TYPES.get(this);
+ if (definition == null) {
+ throw new IllegalStateException("No entity definition registered for " + this);
+ }
+ return definition.width();
+ }
+
+ @Override
+ public float height() {
+ var definition = Registries.JAVA_ENTITY_TYPES.get(this);
+ if (definition == null) {
+ throw new IllegalStateException("No entity definition registered for " + this);
+ }
+ return definition.height();
+ }
+
+ @Override
+ public @Nullable GeyserEntityDefinition defaultBedrockDefinition() {
+ var definition = Registries.JAVA_ENTITY_TYPES.get(this);
+ if (definition == null) {
+ throw new IllegalStateException("No entity definition registered for " + this);
+ }
+ return definition.defaultBedrockDefinition();
+ }
+
+ public boolean is(EntityType type) {
+ return javaId == type.id();
+ }
+
+ public static GeyserEntityType ofVanilla(BuiltinEntityType builtin) {
+ return VANILLA.computeIfAbsent(builtin, GeyserEntityType::new);
+ }
+
+ /**
+ * @throws IllegalArgumentException document this in API
+ */
+ public static GeyserEntityType ofVanilla(Identifier javaIdentifier) {
+ return ofVanilla(BuiltinEntityType.valueOf(javaIdentifier.path().toUpperCase(Locale.ROOT)));
+ }
+
+ public static GeyserEntityType of(int javaId) {
+ if (javaId >= 0 && javaId < BuiltinEntityType.VALUES.length) {
+ return ofVanilla(BuiltinEntityType.VALUES[javaId]);
+ }
+
+ GeyserEntityType type = CUSTOM.get(javaId);
+ return type == null ? new GeyserEntityType(javaId) : type;
+ }
+
+ @Nullable
+ public static GeyserEntityType of(Key javaKey) {
+ if (javaKey.namespace().equals(Key.MINECRAFT_NAMESPACE)) {
+ try {
+ return ofVanilla(MinecraftKey.keyToIdentifier(javaKey));
+ } catch (IllegalArgumentException exception) {
+ return null;
+ }
+ }
+ return CUSTOM_BY_IDENTIFIER.get(MinecraftKey.keyToIdentifier(javaKey));
+ }
+
+ public static GeyserEntityType of(EntityType type) {
+ return type instanceof BuiltinEntityType builtin ? ofVanilla(builtin) : of(type.id());
+ }
+
+ public static GeyserEntityType createCustomAndRegister(GeyserJavaEntityTypeBuild builder) {
+ Identifier javaIdentifier = Objects.requireNonNull(builder.identifier, "javaIdentifier may not be null");
+ int javaId = builder.javaId;
+
+ if (javaIdentifier.vanilla()) {
+ throw new IllegalArgumentException("Cannot register custom entity type in vanilla namespace!" + javaIdentifier);
+ } else if (javaId < 0) {
+ throw new IllegalArgumentException("Invalid ID (may not be below zero): " + javaId);
+ } else if (javaId < BuiltinEntityType.VALUES.length) {
+ throw new IllegalArgumentException("Invalid ID (conflicts with vanilla entity type): " + javaId);
+ } else if (CUSTOM.containsKey(javaId) || CUSTOM_BY_IDENTIFIER.containsKey(javaIdentifier)) {
+ throw new IllegalArgumentException("Custom entity type with identifier " + javaIdentifier + " and ID " + javaId + " conflicts with existing custom entity type");
+ }
+ GeyserEntityType type = new GeyserEntityType(javaIdentifier, javaId);
+ CUSTOM.put(javaId, type);
+ CUSTOM_BY_IDENTIFIER.put(javaIdentifier, type);
+ return type;
+ }
+
+ @Getter
+ public static class GeyserJavaEntityTypeBuild implements CustomJavaEntityType.Builder {
+ private Identifier identifier;
+ private int javaId;
+ private float width;
+ private float height;
+ private BedrockEntityDefinition defaultBedrockDefinition;
+
+ @Override
+ public GeyserJavaEntityTypeBuild type(@NonNull Identifier entityType) {
+ Objects.requireNonNull(entityType, "entityType may not be null");
+ if (entityType.vanilla()) {
+ throw new IllegalArgumentException("Cannot register custom entity type in vanilla namespace!" + entityType);
+ }
+ if (CUSTOM_BY_IDENTIFIER.containsKey(entityType)) {
+ throw new IllegalArgumentException("Custom entity type with identifier " + entityType + " already exists!");
+ }
+ this.identifier = entityType;
+ return this;
+ }
+
+ @Override
+ public GeyserJavaEntityTypeBuild javaId(int javaId) {
+ if (javaId < 0) {
+ throw new IllegalArgumentException("Invalid custom entity type id (may not be negative): " + javaId);
+ }
+ if (javaId < BuiltinEntityType.VALUES.length) {
+ throw new IllegalArgumentException("Invalid custom entity type id (conflicts with vanilla entity type): " + javaId);
+ }
+ if (CUSTOM.containsKey(javaId)) {
+ throw new IllegalArgumentException("Custom entity type with id " + javaId + " already exists!");
+ }
+ this.javaId = javaId;
+ return this;
+ }
+
+ @Override
+ public GeyserJavaEntityTypeBuild width(@NonNegative float width) {
+ if (width < 0) {
+ throw new IllegalArgumentException("Invalid custom entity type width (may not be negative): " + width);
+ }
+ this.width = width;
+ return this;
+ }
+
+ @Override
+ public GeyserJavaEntityTypeBuild height(@NonNegative float height) {
+ if (height < 0) {
+ throw new IllegalArgumentException("Invalid custom entity type height (may not be negative): " + height);
+ }
+ this.height = height;
+ return this;
+ }
+
+ @Override
+ public GeyserJavaEntityTypeBuild definition(@Nullable GeyserEntityDefinition defaultBedrockDefinition) {
+ if (defaultBedrockDefinition == null) {
+ this.defaultBedrockDefinition = null;
+ } else if (defaultBedrockDefinition instanceof BedrockEntityDefinition bedrockEntityDefinition) {
+ this.defaultBedrockDefinition = bedrockEntityDefinition;
+ } else {
+ throw new IllegalArgumentException("Invalid default geyser definition: " + defaultBedrockDefinition);
+ }
+ return this;
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/NonVanillaEntityTypeDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/NonVanillaEntityTypeDefinition.java
new file mode 100644
index 00000000000..6fe92885c41
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/NonVanillaEntityTypeDefinition.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.entity;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+import org.geysermc.geyser.entity.type.Entity;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.type.BuiltinEntityType;
+
+@Getter
+@Accessors(fluent = true)
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class NonVanillaEntityTypeDefinition extends EntityTypeDefinition {
+
+ public NonVanillaEntityTypeDefinition(GeyserEntityType.GeyserJavaEntityTypeBuild builder, GeyserEntityType entityType) {
+ super(Entity::new, entityType, builder.getWidth(), builder.getHeight(), 0f, builder.getDefaultBedrockDefinition(), VanillaEntityBases.ENTITY.translators);
+ }
+
+ @Override
+ public boolean is(BuiltinEntityType builtinEntityType) {
+ return false;
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/VanillaEntities.java
similarity index 50%
rename from core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java
rename to core/src/main/java/org/geysermc/geyser/entity/VanillaEntities.java
index c866c5c9a5d..8ad7a9e3e07 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/VanillaEntities.java
@@ -25,23 +25,9 @@
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.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;
@@ -169,183 +155,180 @@
import org.geysermc.geyser.entity.type.player.AvatarEntity;
import org.geysermc.geyser.entity.type.player.MannequinEntity;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
+import org.geysermc.geyser.impl.IdentifierImpl;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.translator.text.MessageTranslator;
+import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataTypes;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.type.BuiltinEntityType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
-import java.util.Collection;
-import java.util.List;
-import java.util.Objects;
-
-public final class EntityDefinitions {
- public static final EntityDefinition ACACIA_BOAT;
- public static final EntityDefinition ACACIA_CHEST_BOAT;
- public static final EntityDefinition ALLAY;
- public static final EntityDefinition AREA_EFFECT_CLOUD;
- public static final EntityDefinition ARMADILLO;
- public static final EntityDefinition ARMOR_STAND;
- public static final EntityDefinition ARROW;
- public static final EntityDefinition AXOLOTL;
- public static final EntityDefinition BAMBOO_RAFT;
- public static final EntityDefinition BAMBOO_CHEST_RAFT;
- public static final EntityDefinition BAT;
- public static final EntityDefinition BEE;
- public static final EntityDefinition BIRCH_BOAT;
- public static final EntityDefinition BIRCH_CHEST_BOAT;
- public static final EntityDefinition BLAZE;
- public static final EntityDefinition BOGGED;
- public static final EntityDefinition BREEZE;
- public static final EntityDefinition BREEZE_WIND_CHARGE;
- public static final EntityDefinition CAMEL;
- public static final EntityDefinition CAMEL_HUSK;
- public static final EntityDefinition CAT;
- public static final EntityDefinition CAVE_SPIDER;
- public static final EntityDefinition CHERRY_BOAT;
- public static final EntityDefinition CHERRY_CHEST_BOAT;
- public static final EntityDefinition CHEST_MINECART;
- public static final EntityDefinition CHICKEN;
- public static final EntityDefinition COD;
- public static final EntityDefinition COPPER_GOLEM;
- public static final EntityDefinition COMMAND_BLOCK_MINECART;
- public static final EntityDefinition COW;
- public static final EntityDefinition CREAKING;
- public static final EntityDefinition CREEPER;
- public static final EntityDefinition DARK_OAK_BOAT;
- public static final EntityDefinition DARK_OAK_CHEST_BOAT;
- public static final EntityDefinition DOLPHIN;
- public static final EntityDefinition DONKEY;
- public static final EntityDefinition DRAGON_FIREBALL;
- public static final EntityDefinition DROWNED;
- public static final EntityDefinition EGG;
- public static final EntityDefinition ELDER_GUARDIAN;
- public static final EntityDefinition ENDERMAN;
- public static final EntityDefinition ENDERMITE;
- public static final EntityDefinition ENDER_DRAGON;
- public static final EntityDefinition ENDER_PEARL;
- public static final EntityDefinition END_CRYSTAL;
- public static final EntityDefinition EVOKER;
- public static final EntityDefinition EVOKER_FANGS;
- public static final EntityDefinition EXPERIENCE_BOTTLE;
- public static final EntityDefinition EXPERIENCE_ORB;
- public static final EntityDefinition EYE_OF_ENDER;
- public static final EntityDefinition FALLING_BLOCK;
- public static final EntityDefinition FIREBALL;
- public static final EntityDefinition FIREWORK_ROCKET;
- public static final EntityDefinition FISHING_BOBBER;
- public static final EntityDefinition FOX;
- public static final EntityDefinition FROG;
- public static final EntityDefinition FURNACE_MINECART; // Not present on Bedrock
- public static final EntityDefinition GHAST;
- public static final EntityDefinition GIANT;
- public static final EntityDefinition GLOW_ITEM_FRAME;
- public static final EntityDefinition GLOW_SQUID;
- public static final EntityDefinition GOAT;
- public static final EntityDefinition GUARDIAN;
- public static final EntityDefinition HAPPY_GHAST;
- public static final EntityDefinition HOGLIN;
- public static final EntityDefinition HOPPER_MINECART;
- public static final EntityDefinition HORSE;
- public static final EntityDefinition HUSK;
- public static final EntityDefinition ILLUSIONER; // Not present on Bedrock
- public static final EntityDefinition INTERACTION;
- public static final EntityDefinition IRON_GOLEM;
- public static final EntityDefinition ITEM;
- public static final EntityDefinition ITEM_FRAME;
- public static final EntityDefinition JUNGLE_BOAT;
- public static final EntityDefinition JUNGLE_CHEST_BOAT;
- public static final EntityDefinition LEASH_KNOT;
- public static final EntityDefinition LIGHTNING_BOLT;
- public static final EntityDefinition LLAMA;
- public static final EntityDefinition LLAMA_SPIT;
- public static final EntityDefinition MAGMA_CUBE;
- public static final EntityDefinition MANGROVE_BOAT;
- public static final EntityDefinition MANGROVE_CHEST_BOAT;
- public static final EntityDefinition MANNEQUIN;
- public static final EntityDefinition MINECART;
- public static final EntityDefinition MOOSHROOM;
- public static final EntityDefinition MULE;
- public static final EntityDefinition NAUTILUS;
- public static final EntityDefinition OAK_BOAT;
- public static final EntityDefinition OAK_CHEST_BOAT;
- public static final EntityDefinition OCELOT;
- public static final EntityDefinition PAINTING;
- public static final EntityDefinition PALE_OAK_BOAT;
- public static final EntityDefinition PALE_OAK_CHEST_BOAT;
- public static final EntityDefinition PANDA;
- public static final EntityDefinition PARCHED;
- public static final EntityDefinition PARROT;
- public static final EntityDefinition PHANTOM;
- public static final EntityDefinition PIG;
- public static final EntityDefinition PIGLIN;
- public static final EntityDefinition PIGLIN_BRUTE;
- public static final EntityDefinition PILLAGER;
- public static final EntityDefinition PLAYER;
- public static final EntityDefinition POLAR_BEAR;
- public static final EntityDefinition SPLASH_POTION;
- public static final EntityDefinition LINGERING_POTION;
- public static final EntityDefinition PUFFERFISH;
- public static final EntityDefinition RABBIT;
- public static final EntityDefinition RAVAGER;
- public static final EntityDefinition SALMON;
- public static final EntityDefinition SHEEP;
- public static final EntityDefinition SHULKER;
- public static final EntityDefinition