diff --git a/README.md b/README.md
index 601b136f80b..ace19723e65 100644
--- a/README.md
+++ b/README.md
@@ -16,10 +16,10 @@ Special thanks to the DragonProxy project for being a trailblazer in protocol tr
## Supported Versions
-| Edition | Supported Versions |
-|---------|------------------------------------------------------------------------------------------------------------|
+| Edition | Supported Versions |
+|---------|---------------------------------------------------------------------------------------------------|
| Bedrock | 26.0, 26.1, 26.2, 26.3, 26.10, 26.20, 26.21, 26.22, 26.23, 26.30, 26.31, 26.32, 26.33 |
-| Java | 26.1 - 26.1.2 (For older versions, [see this guide](https://geysermc.org/wiki/geyser/supported-versions/)) |
+| Java | 26.2 (For older versions, [see this guide](https://geysermc.org/wiki/geyser/supported-versions/)) |
## Setting Up
Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser.
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..e6ff771ac7c 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
@@ -29,24 +29,57 @@
import org.geysermc.geyser.api.connection.GeyserConnection;
import org.geysermc.geyser.api.entity.type.GeyserEntity;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
+import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
- * This class holds all the methods that relate to entities.
- * Can be accessed through {@link GeyserConnection#entities()}.
+ * Provides entity lookup and input-lock utilities for a specific connection.
+ * Accessed via {@link GeyserConnection#entities()}.
*/
public interface EntityData {
/**
- * Returns a {@link GeyserEntity} to e.g. make them play an emote.
+ * @deprecated use {@link #byJavaId(int)}
+ * @since 2.3.0
+ */
+ @Deprecated(since = "2.11.0")
+ CompletableFuture<@Nullable GeyserEntity> entityByJavaId(@NonNegative int javaId);
+
+ /**
+ * Returns the {@link GeyserEntity} for the given Java entity ID if it is tracked
+ * in this connection's entity cache, or {@code null} if not found.
*
* @param javaId the Java entity ID to look up
- * @return a {@link GeyserEntity} if present in this connection's entity tracker
+ * @return the entity, or {@code null} if not found
+ * @since 2.11.0
*/
- CompletableFuture<@Nullable GeyserEntity> entityByJavaId(@NonNegative int javaId);
+ @ApiStatus.Experimental
+ @Nullable GeyserEntity byJavaId(@NonNegative int javaId);
+
+ /**
+ * Returns the {@link GeyserEntity} for the given Java entity UUID if it is tracked
+ * in this connection's entity cache, or {@code null} if not found.
+ *
+ * @param javaUuid the Java entity UUID to look up
+ * @return the entity, or {@code null} if not found
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ @Nullable GeyserEntity byUuid(UUID javaUuid);
+
+ /**
+ * Returns the {@link GeyserEntity} for the given Geyser runtime entity ID if it is tracked
+ * in this connection's entity cache, or {@code null} if not found.
+ *
+ * @param geyserId the Geyser entity ID (as returned by {@link GeyserEntity#geyserId()})
+ * @return the entity, or {@code null} if not found
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ @Nullable GeyserEntity byGeyserId(@NonNegative long geyserId);
/**
* (Un)locks the client's movement inputs, so that they cannot move.
@@ -79,7 +112,7 @@ public interface EntityData {
void showEmote(GeyserPlayerEntity emoter, String emoteId);
/**
- * @deprecated Use {@link GeyserConnection#playerEntity} instead.
+ * @deprecated Use {@link GeyserConnection#playerEntity()} instead.
*/
@Deprecated(since = "2.9.3")
GeyserPlayerEntity playerEntity();
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..e35c29ad800
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/custom/CustomEntityDefinition.java
@@ -0,0 +1,83 @@
+/*
+ * 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.event.java.ServerAttachParrotsEvent;
+import org.geysermc.geyser.api.event.java.ServerSpawnEntityEvent;
+import org.geysermc.geyser.api.util.Identifier;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Represents a Bedrock entity definition for a custom entity.
+ * These cannot be in the {@code minecraft} namespace, and have to be manually
+ * spawned using either the {@link ServerSpawnEntityEvent} or {@link ServerAttachParrotsEvent}.
+ *
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public interface CustomEntityDefinition extends GeyserEntityDefinition {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @ApiStatus.Experimental
+ default boolean vanilla() {
+ return false;
+ }
+
+ /**
+ * Creates or retrieves a custom entity definition by the Bedrock entity type identifier.
+ *
+ * @param identifier the Bedrock entity identifier
+ * @return customEntityDefinition
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ static CustomEntityDefinition of(Identifier identifier) {
+ if (identifier.vanilla()) {
+ throw new IllegalArgumentException(
+ "Custom entity identifiers cannot use the 'minecraft' namespace. "
+ + "Use a non-vanilla namespace, e.g. 'mymod:my_entity'.");
+ }
+ return GeyserApi.api().provider(CustomEntityDefinition.class, identifier);
+ }
+
+ /**
+ * Creates or retrieves a {@link CustomEntityDefinition} by the Bedrock entity type identifier.
+ * The identifier must not use the {@code minecraft} namespace.
+ *
+ * @param identifier the Bedrock entity identifier, in {@code namespace:path} format
+ * @return the custom entity definition
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ static CustomEntityDefinition of(String identifier) {
+ return of(Identifier.of(identifier));
+ }
+}
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..59308a05819
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserEntityDataType.java
@@ -0,0 +1,115 @@
+/*
+ * 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.entity.type.GeyserEntity;
+import org.geysermc.geyser.api.util.Identifier;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Objects;
+
+/**
+ * Represents a type of entity data that can be sent for an entity.
+ *
+ * Entity data types define the values stored for a particular piece of metadata,
+ * such as a {@code Byte}, {@code Integer}, {@code Float}; and the identifier associated with the types.
+ *
+ * Unlike properties of custom items or blocks, it is possible to update entity metadata at runtime,
+ * which can be done using {@link GeyserEntity#override(GeyserEntityDataType, Object)}.
+ *
+ * Only the built-in types in {@link GeyserEntityDataTypes} are supported. Attempting to use a type
+ * not provided by Geyser will throw an {@link IllegalArgumentException} at runtime.
+ *
+ * @param the value type associated with this entity data type
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public abstract sealed class GeyserEntityDataType permits GeyserEntityDataType.SimpleType, GeyserListEntityDataType {
+
+ private final Identifier identifier;
+ private final Class typeClass;
+
+ @ApiStatus.Internal
+ GeyserEntityDataType(Identifier identifier, Class typeClass) {
+ this.identifier = Objects.requireNonNull(identifier);
+ this.typeClass = Objects.requireNonNull(typeClass);
+ }
+
+ /**
+ * Gets the identifier associated with this entity data type.
+ *
+ * @return the identifier for this type
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public final Identifier identifier() {
+ return identifier;
+ }
+
+ /**
+ * 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
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public final Class typeClass() {
+ return typeClass;
+ }
+
+ @Override
+ @ApiStatus.Experimental
+ public final boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof GeyserEntityDataType> that)) return false;
+ return identifier.equals(that.identifier);
+ }
+
+ @Override
+ @ApiStatus.Experimental
+ public final int hashCode() {
+ return identifier.hashCode();
+ }
+
+ @Override
+ @ApiStatus.Experimental
+ public String toString() {
+ return "GeyserEntityDataType{" + identifier + ", " + typeClass.getSimpleName() + "}";
+ }
+
+ @ApiStatus.Internal
+ static GeyserEntityDataType create(Identifier identifier, Class typeClass) {
+ return new SimpleType<>(identifier, typeClass);
+ }
+
+ @ApiStatus.Internal
+ static final class SimpleType extends GeyserEntityDataType {
+ @ApiStatus.Internal
+ SimpleType(Identifier identifier, Class typeClass) {
+ super(identifier, typeClass);
+ }
+ }
+}
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..4e57dcbeff4
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserEntityDataTypes.java
@@ -0,0 +1,169 @@
+/*
+ * 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;
+import org.geysermc.geyser.api.util.Identifier;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Contains {@link GeyserEntityDataType} types which can be used to change the
+ * current value stored for a specific entity data type.
+ *
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public final class GeyserEntityDataTypes {
+
+ /**
+ * Represents a single-byte value used for the main color, ranged 0 to 15.
+ *
+ * @see Official documentation
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType COLOR =
+ GeyserEntityDataType.create(Identifier.of("color"), Byte.class);
+
+ /**
+ * Represents a numeric variant index that can be queried in resource packs.
+ *
+ * @see Official documentation
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType VARIANT =
+ GeyserEntityDataType.create(Identifier.of("variant"), Integer.class);
+
+ /**
+ * Represents the entity's collision box width.
+ *
+ * @see Official documentation
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType WIDTH =
+ GeyserEntityDataType.create(Identifier.of("geysermc", "width"), Float.class);
+
+ /**
+ * Represents the entity's collision box height.
+ *
+ * @see Official documentation
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType HEIGHT =
+ GeyserEntityDataType.create(Identifier.of("geysermc", "height"), Float.class);
+
+ /**
+ * Represents the entity's vertical offset applied on top of the Java position sent by the server.
+ * Some entities, such as players, boats, and minecarts need such an offset to be displayed in the correct position.
+ *
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType VERTICAL_OFFSET =
+ GeyserEntityDataType.create(Identifier.of("geysermc", "vertical_offset"), Float.class);
+
+ /**
+ * Represents the scale multiplier for visual size.
+ *
+ * @see Official documentation"
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType SCALE =
+ GeyserEntityDataType.create(Identifier.of("scale"), Float.class);
+
+ /**
+ * Represents custom hitboxes for entities, or an empty list if none are set.
+ * To set no hitbox, update hitboxes to an empty list. To restore default behavior, update it to null.
+ *
+ * @see Official documentation
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserListEntityDataType HITBOXES =
+ GeyserListEntityDataType.createList(Identifier.of("custom_hit_test"), Hitbox.class);
+
+ /**
+ * Represents the entity's seat offset, which is used when riding another entity.
+ * Equivalent to "seats" in the {@code minecraft:rideable} entity component, but set on the rider instead of the vehicle.
+ *
+ * @see Official documentation
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType SEAT_OFFSET =
+ GeyserEntityDataType.create(Identifier.of("geysermc", "seat_offset"), Vector3f.class);
+
+ /**
+ * Whether the entity's rotation is locked to the vehicle it is riding.
+ * This is used for boats and happy ghasts in the vanilla game.
+ *
+ * @see Official documentation
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType ROTATION_LOCKED_TO_VEHICLE =
+ GeyserEntityDataType.create(Identifier.of("rotation_locked_to_vehicle"), Boolean.class);
+
+ /**
+ * The degrees of rotation a rider may rotate within, set on the rider entity.
+ * This is used for boats and happy ghasts in the vanilla game to prevent "looking backwards" while riding it
+ *
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType SEAT_LOCK_RIDER_ROTATION_DEGREES =
+ GeyserEntityDataType.create(Identifier.of("seat_lock_rider_rotation_degrees"), Float.class);
+
+ /**
+ * Whether an entity riding another vehicle entity is sitting on a seat which has a rotation.
+ *
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType SEAT_HAS_ROTATION =
+ GeyserEntityDataType.create(Identifier.of("geysermc", "seat_has_rotation"), Boolean.class);
+
+ /**
+ * The degrees of rotation that this seat is offset by. Equivalent to "rotate_rider_by" in the {@code minecraft:rideable} entity component.
+ * This is used for boats to make the player face forward, and is equivalent to {@code rotate_rider_by} in the {@code minecraft:rideable} entity component.
+ *
+ * @see Official documentation
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public static final GeyserEntityDataType ROTATE_RIDER_DEGREES =
+ GeyserEntityDataType.create(Identifier.of("rotate_rider_by"), Float.class);
+
+ @ApiStatus.Internal
+ 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..cca9b08cf28
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/data/GeyserListEntityDataType.java
@@ -0,0 +1,80 @@
+/*
+ * 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.util.Identifier;
+import org.jetbrains.annotations.ApiStatus;
+
+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
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public abstract sealed class GeyserListEntityDataType extends GeyserEntityDataType>
+ permits GeyserListEntityDataType.SimpleListType {
+
+ @SuppressWarnings("unchecked")
+ @ApiStatus.Internal
+ GeyserListEntityDataType(Identifier identifier) {
+ super(identifier, (Class>) (Class>) List.class);
+ }
+
+ /**
+ * @return the class of the list entries
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract Class listEntryClass();
+
+ @ApiStatus.Internal
+ static GeyserListEntityDataType createList(Identifier identifier, Class listEntryClass) {
+ return new SimpleListType<>(identifier, listEntryClass);
+ }
+
+ @ApiStatus.Internal
+ static final class SimpleListType extends GeyserListEntityDataType {
+ private final Class listEntryClass;
+
+ @ApiStatus.Internal
+ SimpleListType(Identifier identifier, Class listEntryClass) {
+ super(identifier);
+ this.listEntryClass = listEntryClass;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Class listEntryClass() {
+ return listEntryClass;
+ }
+ }
+}
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..2e8ed790806
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/data/types/Hitbox.java
@@ -0,0 +1,127 @@
+/*
+ * 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;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Represents an entity hitbox, with min/max representing absolute coordinates.
+ *
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public interface Hitbox {
+
+ /**
+ * The min "corner" of the hitbox as a position in the world.
+ *
+ * @return the vector of the corner
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ Vector3f min();
+
+ /**
+ * The max "corner" of the hitbox as a position in the world.
+ *
+ * @return the vector of the corner
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ Vector3f max();
+
+ /**
+ * The pivot of the hitbox
+ *
+ * @return the pivot
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ Vector3f pivot();
+
+ /**
+ * Creates a new builder for a hitbox.
+ *
+ * @return a new builder
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ static Builder builder() {
+ return GeyserApi.api().provider(Builder.class);
+ }
+
+ /**
+ * The builder for an entity hitbox.
+ *
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ interface Builder {
+
+ /**
+ * Sets the min corner of the hitbox
+ *
+ * @param min the vector of the corner
+ * @return this builder
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ @This Builder min(Vector3f min);
+
+ /**
+ * Sets the max corner of the hitbox.
+ *
+ * @param max the vector of the corner
+ * @return this builder
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ @This Builder max(Vector3f max);
+
+ /**
+ * Sets the pivot of the hitbox.
+ *
+ * @param pivot the pivot vector
+ * @return this builder
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ @This Builder pivot(Vector3f pivot);
+
+ /**
+ * Builds this hitbox, defaulting to {@code Vector3f.ZERO} if
+ * any one vector was not provided.
+ *
+ * @return a new hitbox
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ 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..3aa59e805f2
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/definition/GeyserEntityDefinition.java
@@ -0,0 +1,95 @@
+/*
+ * 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.event.lifecycle.GeyserDefineEntityPropertiesEvent;
+import org.geysermc.geyser.api.util.Identifier;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.List;
+
+/**
+ * Represents a Bedrock entity definition registered in the {@link GeyserDefineEntitiesEvent}.
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public interface GeyserEntityDefinition {
+
+ /**
+ * This entity's identifier as it's known to Bedrock clients and used in resource packs.
+ * See the official docs for further information
+ *
+ * @return the Bedrock entity identifier
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ Identifier identifier();
+
+ /**
+ * Returns the entity properties registered in the {@link GeyserDefineEntityPropertiesEvent} for this entity type.
+ *
+ * @see GeyserEntityProperty
+ * @return an unmodifiable list containing entity properties registered for this entity type
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ List> properties();
+
+ /**
+ * Indicates whether this entity exists in Minecraft: Bedrock Edition, or whether it is a custom entity.
+ *
+ * @return whether this entity exists in the vanilla base game
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ boolean vanilla();
+
+ /**
+ * Returns whether this definition is currently registered via {@link GeyserDefineEntitiesEvent}.
+ * Unregistered definitions cannot be sent to Bedrock clients!
+ *
+ * @return whether this definition is registered
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ boolean registered();
+
+ /**
+ * Retrieves a registered {@link GeyserEntityDefinition} by its Bedrock entity type identifier,
+ * or returns a new unregistered definition if none is found.
+ *
+ * @param identifier the Bedrock entity identifier
+ * @return the GeyserEntityDefinition for the given identifier
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ 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..8dffc557718
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/definition/JavaEntityType.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.definition;
+
+import org.geysermc.geyser.api.util.Identifier;
+import org.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Represents a Java edition entity type.
+ *
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public interface JavaEntityType {
+
+ /**
+ * Each entity type on Java edition has an identifier, such as {@code "minecraft:pig"} that uniquely identifies it.
+ * Modded entities cannot be in the minecraft namespace!
+ *
+ * @return the Java identifier of the entity type
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ Identifier identifier();
+
+ /**
+ * Indicates whether this entity type exists in the Minecraft: Java Edition base game, or whether it is a custom entity.
+ * Note: As of API 2.11.0, this would always be true!
+ *
+ * @return whether this entity exists in the vanilla base game
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ boolean vanilla();
+
+ /**
+ * This entity type's default width, not respective of pose-adjusted width.
+ *
+ * @return the default width of the Java entity type
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ float width();
+
+ /**
+ * This entity type's default height, not respective of pose-adjusted height.
+ *
+ * @return the default height of the Java entity type
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ float height();
+
+ /**
+ * Compares two entity identifiers.
+ *
+ * @param javaIdentifier the other entity identifier
+ * @return true if the entity identifier is the same
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ default boolean is(Identifier javaIdentifier) {
+ return identifier().equals(javaIdentifier);
+ }
+
+ /**
+ * Gets the default Bedrock entity definition which is associated with this Minecraft: Java Edition entity type.
+ *
+ * @return the default Bedrock entity definition
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ @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..9d48289997d 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,31 +26,142 @@
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.custom.CustomEntityDefinition;
+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.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
+import java.util.List;
+import java.util.UUID;
import java.util.function.Consumer;
/**
- * Represents a unique instance of an entity. Each {@link GeyserConnection}
- * have their own sets of entities - no two instances will share the same GeyserEntity instance.
+ * Represents a unique entity instance as seen by a specific {@link GeyserConnection}.
+ * Each connection maintains its own set of entity objects; no two connections share the same
+ * {@code GeyserEntity} instance.
*/
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 -1 if none is present
*/
@NonNegative
int javaId();
/**
- * Updates an entity property with a new value.
- * If the new value is null, the property is reset to the default value.
+ * The entity id used by Geyser to identify this entity with the Bedrock client.
+ *
+ * @return the Geyser entity id that the Bedrock client sees
+ * @since 2.11.0
+ */
+ @Positive
+ @ApiStatus.Experimental
+ long geyserId();
+
+ /**
+ * The entity's UUID, only null if the server has not spawned this entity.
+ *
+ * @return the entity UUID that the server has assigned to this entity,
+ * or null if this entity isn't known to the Java server
+ * @since 2.11.0
+ */
+ @Nullable
+ @ApiStatus.Experimental
+ UUID uuid();
+
+ /**
+ * The Bedrock entity definition for this entity, which can also be a {@link CustomEntityDefinition}.
+ *
+ * @return the Bedrock entity definition
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ GeyserEntityDefinition definition();
+
+ /**
+ * The position of this entity as reported by the Java server, without any Bedrock vertical
+ * offset applied.
+ *
+ * @return the position of the entity as known to the Java server
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ Vector3f position();
+
+ /**
+ * The vehicle this entity is currently mounted on.
+ *
+ * @return the vehicle of this entity, or {@code null} if this entity is not a passenger
+ * @since 2.11.0
+ */
+ @Nullable
+ @ApiStatus.Experimental
+ GeyserEntity vehicle();
+
+ /**
+ * The passengers currently riding this entity.
+ *
+ * @return an immutable snapshot of this entity's passengers, or an empty list if none
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ List passengers();
+
+ /**
+ * Reads the current value of a given {@link GeyserEntityDataType} which
+ * would currently be sent to players - either the {@link #override(GeyserEntityDataType)}, or the
+ * base value, if one is sent by default.
+ *
+ * @see GeyserEntityDataTypes
+ * @param dataType the entity data type to query
+ * @param the type of the value
+ * @return the current value, or {@code null} if none is known
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ @Nullable T value(GeyserEntityDataType dataType);
+
+ /**
+ * Reads the current override of a given {@link GeyserEntityDataType}, or
+ * null if no override was defined.
+ *
+ * @see GeyserEntityDataTypes
+ * @param dataType the entity data type override to query
+ * @param the type of the value
+ * @return the current override value, or {@code null} if no override has been set
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ @Nullable T override(GeyserEntityDataType dataType);
+
+ /**
+ * Overrides an entity data value for this entity.
+ * If {@code value} is {@code null}, the override is cleared and the default value
+ * (from the Java server or entity type definition) is restored.
+ *
+ * @param dataType an entity data type, such as a constant from {@link GeyserEntityDataTypes}
+ * @param value the new value, or {@code null} to reset the override
+ * @param the type of the value
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ void override(GeyserEntityDataType dataType, @Nullable T value);
+
+ /**
+ * Convenience method to update a single entity property.
+ * Equivalent to {@link #updatePropertiesBatched(Consumer, boolean) updatePropertiesBatched}
+ * with a single-property consumer and {@code immediate = false}.
*
* @param property a {@link GeyserEntityProperty} registered for this type in the {@link GeyserDefineEntityPropertiesEvent}
- * @param value the new property value
+ * @param value the new property value, or {@code null} to reset
* @param the type of the value
* @since 2.9.0
*/
@@ -59,24 +170,24 @@ default void updateProperty(GeyserEntityProperty property, @Nullable T va
}
/**
- * Updates multiple properties with just one update packet.
- * @see BatchPropertyUpdater
- *
- * @param consumer a batch updater
+ * @deprecated use {@link #updateProperty(GeyserEntityProperty, Object)} instead
* @since 2.9.0
*/
+ @Deprecated(since = "2.11.0")
default void updatePropertiesBatched(Consumer consumer) {
this.updatePropertiesBatched(consumer, false);
}
/**
- * 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
+ * Updates multiple entity properties with a single update, optionally sending it immediately.
+ *
+ * Usually, sending immediately is unnecessary; batched updates are flushed once per tick.
+ * Pass {@code immediate = true} only when packet ordering requires the properties to be
+ * applied before the next tick (e.g., to avoid a visual flicker on the client).
*
- * @param consumer a batch updater
- * @param immediate whether this update should be sent immediately
+ * @param consumer a {@link BatchPropertyUpdater} callback that applies one or more property changes
+ * @param immediate {@code true} to send the packet immediately rather than at the next tick
+ * @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..cfa3dcaa414
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionSpawnEntityEvent.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2025 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.api.event.bedrock;
+
+import org.geysermc.event.Cancellable;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.entity.EntityData;
+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.jetbrains.annotations.ApiStatus;
+import org.jspecify.annotations.Nullable;
+
+import java.util.function.Consumer;
+
+/**
+ * Base event for entity spawning visible to a specific Bedrock connection.
+ * Specific subtypes are {@link ServerSpawnEntityEvent} (non-player entities) and
+ * {@link ServerAttachParrotsEvent} (shoulder parrots).
+ *
+ * The event is cancellable; cancelling it suppresses spawning the entity on the Bedrock client.
+ * Setting the {@link #definition(GeyserEntityDefinition) definition} to
+ * {@code null} has the same effect.
+ *
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public abstract class SessionSpawnEntityEvent extends ConnectionEvent implements Cancellable {
+
+ @ApiStatus.Internal
+ public SessionSpawnEntityEvent(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * Returns the Bedrock entity definition that will be sent to the Bedrock client.
+ * Setting a definition to null will cancel this entity spawn!
+ *
+ * @return the entity definition, or {@code null} if none is currently set
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract @Nullable GeyserEntityDefinition definition();
+
+ /**
+ * Overrides the Bedrock entity definition when spawning this entity on the Bedrock client.
+ *
+ * The supplied definition must have been previously registered in the
+ * {@link GeyserDefineEntitiesEvent}; passing an unregistered definition will throw
+ * {@link IllegalStateException}. Passing {@code null} cancels the spawn for this connection.
+ *
+ * @param definition the replacement entity definition, or {@code null} to suppress the spawn
+ * @throws IllegalStateException if the provided definition has not been registered
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract void definition(@Nullable GeyserEntityDefinition definition);
+
+ /**
+ * Registers a callback that is invoked with the {@link GeyserEntity} instance just before the entity is spawned.
+ * This means that {@link EntityData#byJavaId}, {@link EntityData#byUuid}, and related lookups
+ * will not find the entity from within the consumer.
+ *
+ * Use this callback to apply initial overrides such as entity data values (scale, width,
+ * height, hitboxes) or entity properties before the spawn packet is sent to the client.
+ * If multiple consumers are registered, they run in the order they were added.
+ *
+ * The consumer is not called if the event is cancelled.
+ *
+ * @param consumer the callback receiving the freshly created entity
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract void preSpawnConsumer(Consumer consumer);
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/connection/SessionDefineCustomWaypointsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/connection/SessionDefineCustomWaypointsEvent.java
new file mode 100644
index 00000000000..d286c066fb4
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/connection/SessionDefineCustomWaypointsEvent.java
@@ -0,0 +1,75 @@
+/*
+ * 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.connection;
+
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.util.Identifier;
+import org.geysermc.geyser.api.waypoint.CustomWaypointStyle;
+import org.geysermc.geyser.api.waypoint.CustomWaypointStyleRegisterException;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Map;
+
+/**
+ * Called once for every {@link GeyserConnection} to register custom waypoint styles. Custom waypoint styles must be registered through this event.
+ *
+ * {@link CustomWaypointStyle}s may be registered multiple times for different Java Edition waypoint style {@link Identifier}s, however, each Java Edition waypoint style
+ * may only have one {@link CustomWaypointStyle}.
+ *
+ * @see CustomWaypointStyle
+ * @since 2.11.0
+ */
+@ApiStatus.NonExtendable
+public abstract class SessionDefineCustomWaypointsEvent extends ConnectionEvent {
+
+ @ApiStatus.Internal
+ public SessionDefineCustomWaypointsEvent(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * Returns a map of all the currently registered custom waypoint styles for this {@link GeyserConnection}.
+ *
+ * @return an unmodifiable map of all currently registered custom waypoint styles
+ * @since 2.11.0
+ */
+ public abstract Map customWaypointStyles();
+
+ /**
+ * Registers a {@link CustomWaypointStyle} for this {@link GeyserConnection}, for the given Java Edition waypoint style {@code identifier}.
+ *
+ * The given {@link CustomWaypointStyle} will be used for every waypoint that uses the given {@code identifier} as waypoint style. Vanilla
+ * waypoint styles, like {@code minecraft:default}, may be overridden.
+ *
+ * When a waypoint style is already registered for the given {@code identifier}, this method will fail without throwing an exception, but an error will be logged.
+ *
+ * @param identifier the identifier of the waypoint style on Java Edition
+ * @param style the {@link CustomWaypointStyle}
+ * @throws CustomWaypointStyleRegisterException when an error occurred while registering the custom waypoint style
+ * @since 2.11.0
+ */
+ public abstract void register(Identifier identifier, CustomWaypointStyle style);
+}
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..22ff1ff1b28
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerAttachParrotsEvent.java
@@ -0,0 +1,72 @@
+/*
+ * 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;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Called when the Java server attaches parrots to a player.
+ *
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public abstract class ServerAttachParrotsEvent extends SessionSpawnEntityEvent {
+
+ @ApiStatus.Internal
+ public ServerAttachParrotsEvent(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * The player for which the Java server attached parrots.
+ *
+ * @return the player with bird friends
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract GeyserPlayerEntity player();
+
+ /**
+ * The variant of the parrot.
+ *
+ * @return the parrot variant
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract int variant();
+
+ /**
+ * Whether this parrot is on the right shoulder of the player.
+ *
+ * @return true if parrot is on the right shoulder, left otherwise
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ 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..0766fe39872
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerSpawnEntityEvent.java
@@ -0,0 +1,75 @@
+/*
+ * 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 org.jetbrains.annotations.ApiStatus;
+
+import java.util.UUID;
+
+/**
+ * Called when the downstream server spawns a non-player entity.
+ *
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public abstract class ServerSpawnEntityEvent extends SessionSpawnEntityEvent implements Cancellable {
+
+ @ApiStatus.Internal
+ public ServerSpawnEntityEvent(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * Gets the entity id of the entity being spawned, which is unique per session.
+ *
+ * @return the entity id of the entity being spawned
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract int entityId();
+
+ /**
+ * Gets the uuid of the entity being spawned.
+ *
+ * @return the uuid of the entity being spawned
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract UUID uuid();
+
+ /**
+ * Gets the Java entity type sent by the server.
+ *
+ * @return the Java edition entity type of the entity being spawned
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ 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..9bd7fa54281
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerUpdateEntityPassengersEvent.java
@@ -0,0 +1,94 @@
+/*
+ * 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;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * This event is called when an entity's passengers are updated.
+ *
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+public abstract class ServerUpdateEntityPassengersEvent extends ConnectionEvent {
+
+ /**
+ * The vehicle entity that gets a passenger update.
+ *
+ * @return the vehicle entity
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract GeyserEntity vehicle();
+
+ @ApiStatus.Internal
+ public ServerUpdateEntityPassengersEvent(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * This event is called when entities are mounted to a vehicle.
+ *
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract static class Mount extends ServerUpdateEntityPassengersEvent {
+ @ApiStatus.Internal
+ public Mount(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * @return the passenger that was added to the vehicle
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract GeyserEntity addedPassenger();
+ }
+
+ /**
+ * This event is called when entities are dismounted from a vehicle.
+ *
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ public abstract static class Dismount extends ServerUpdateEntityPassengersEvent {
+ @ApiStatus.Internal
+ public Dismount(GeyserConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * @return the passenger that was removed from the vehicle
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ 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..a4e096b82d1
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineEntitiesEvent.java
@@ -0,0 +1,81 @@
+/*
+ * 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.definition.GeyserEntityDefinition;
+import org.geysermc.geyser.api.util.Identifier;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Collection;
+
+/**
+ * Called once during Geyser startup to allow users to register custom Bedrock entity types.
+ *
+ * All {@link CustomEntityDefinition}'s must be registered in this event before being used!
+ *
+ * To register a custom entity, create a definition with {@link CustomEntityDefinition#of(Identifier)}
+ * and pass it to {@link #register(CustomEntityDefinition)}. Additional Bedrock entity properties
+ * can be registered in the subsequent {@link GeyserDefineEntityPropertiesEvent}.
+ *
+ * @since 2.11.0
+ */
+@ApiStatus.Experimental
+@ApiStatus.NonExtendable
+public interface GeyserDefineEntitiesEvent extends Event {
+
+ /**
+ * Returns an immutable view of all currently registered Bedrock entity definitions,
+ * including both vanilla and any custom definitions registered so far in this event.
+ *
+ * @return an immutable collection of all registered entity definitions
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ Collection entities();
+
+ /**
+ * Returns an immutable view of all custom entity definitions registered so far.
+ *
+ * @return an immutable collection of registered custom entity definitions
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ Collection customEntities();
+
+ /**
+ * Registers a custom entity definition. Using the same identifier twice in different definitions
+ * or providing a definition in the vanilla namespace will throw an exception.
+ *
+ * @param definition the custom entity definition to register; must not be null
+ * @throws IllegalArgumentException if {@code definition} was not created via {@link CustomEntityDefinition#of}
+ * @throws IllegalStateException if a custom entity definition with this identifier has already been registered
+ * @since 2.11.0
+ */
+ @ApiStatus.Experimental
+ void register(CustomEntityDefinition definition);
+}
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/api/src/main/java/org/geysermc/geyser/api/waypoint/CustomWaypointStyle.java b/api/src/main/java/org/geysermc/geyser/api/waypoint/CustomWaypointStyle.java
new file mode 100644
index 00000000000..becaa857583
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/waypoint/CustomWaypointStyle.java
@@ -0,0 +1,199 @@
+/*
+ * 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.waypoint;
+
+import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.checker.index.qual.Positive;
+import org.checkerframework.common.returnsreceiver.qual.This;
+import org.cloudburstmc.math.vector.Vector2f;
+import org.geysermc.geyser.api.GeyserApi;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.event.connection.SessionDefineCustomWaypointsEvent;
+import org.geysermc.geyser.api.util.GenericBuilder;
+import org.geysermc.geyser.api.util.Identifier;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Objects;
+
+/**
+ * Custom waypoint styles apply to icons/players (known as waypoints) on the locator bar on the client.
+ * Java Edition allows defining custom waypoint styles for an {@link Identifier} in a resourcepack,
+ * and Bedrock Edition allows specifying custom textures for waypoints through packets.
+ *
+ * This interface may be implemented by developers if they wish to display custom textures on the locator
+ * bar on Bedrock Edition. Implementations must always implement both {@link CustomWaypointStyle#texturePath(Identifier, float)} and
+ * {@link CustomWaypointStyle#textureSize(Identifier, float)}, and should be registered for one or more waypoint style identifiers
+ * in the {@link SessionDefineCustomWaypointsEvent}. For implementation details, see the Javadocs at the respective methods.
+ *
+ * If you wish to define a custom waypoint style that acts like custom waypoint styles on Java Edition, use {@link CustomWaypointStyle#vanillaLike(int, int)}.
+ *
+ * @see VanillaBuilder
+ * @see SessionDefineCustomWaypointsEvent
+ * @since 2.11.0
+ */
+public interface CustomWaypointStyle {
+
+ /**
+ * Returns the path of the texture to display on the locator bar for a waypoint with this {@code style} identifier, at the specified {@code distance}.
+ * This method will be called whenever the position of a waypoint changes.
+ *
+ * Please note that the {@code distance} parameter may not represent an accurate distance between the waypoint and the player:
+ *
+ *
+ * - For {@code coordinate} waypoints, the accuracy will be within 1 block.
+ * - For {@code chunk} waypoints, the accuracy will be within 8 blocks.
+ * - For {@code azimuth} waypoints, the distance is fixed at {@link Integer#MAX_VALUE}.
+ *
+ *
+ * @implSpec
+ * Implementations should not prefix the path with {@code "textures/"}, as this is done by Geyser. Implementations should never
+ * return null, and upon doing so, Geyser will log a warning and use the {@code "textures/ui/locator_bar_dot_0"} texture as fallback.
+ *
+ * @param style the {@link Identifier} of the waypoint's custom style on Java Edition
+ * @param distance the distance between the {@link GeyserConnection} and the waypoint
+ * @return the path of the texture to display on the locator bar
+ * @see CustomWaypointStyle#textureSize(Identifier, float)
+ * @since 2.11.0
+ */
+ String texturePath(Identifier style, float distance);
+
+ /**
+ * Returns the size of the texture to display on the locator bar for a waypoint with this {@code style} identifier, at the specified {@code distance}.
+ * This method will be called whenever the position of a waypoint changes.
+ *
+ Please note that the {@code distance} parameter may not represent an accurate distance between the waypoint and the player:
+ *
+ *
+ * - For {@code coordinate} waypoints, the accuracy will be within 1 block.
+ * - For {@code chunk} waypoints, the accuracy will be within 8 blocks.
+ * - For {@code azimuth} waypoints, the distance is fixed at {@link Integer#MAX_VALUE}.
+ *
+ *
+ * @implSpec
+ * Implementations should return a reasonable texture size. None of the returned vector's components may ever be below 0 - when this happens,
+ * Geyser will log a warning and use [0;0] as fallback.
+ *
+ * @param style the {@link Identifier} of the waypoint's custom style on Java Edition
+ * @param distance the distance between the {@link GeyserConnection} and the waypoint
+ * @return the size of the texture to display on the locator bar
+ * @see CustomWaypointStyle#texturePath(Identifier, float)
+ * @since 2.11.0
+ */
+ Vector2f textureSize(Identifier style, float distance);
+
+ /**
+ * This builder can be used to create an implementation of {@link CustomWaypointStyle} that acts like the vanilla Java Edition
+ * custom waypoint styles.
+ *
+ * When creating an instance of this builder, a {@code nearDistance} and {@code farDistance} have to be specified,
+ * which act the same as their Java Edition counterparts. Then, 1 or multiple textures must be added. The builder uses these textures as follows:
+ *
+ *
+ * - The first texture is shown for any distance below the {@code nearDistance}.
+ * - The last texture is shown for any distance at or above the {@code farDistance}.
+ * - Any textures between the first and last are spaced out equally between {@code nearDistance} and {@code farDistance}, and will be used as such.
+ *
+ *
+ * This behaviour matches the Java Edition custom waypoint styles, which also allow specifying a list of textures. For convenience,
+ * a shorthand {@link VanillaBuilder#withTexture(Identifier)} method exists, which takes a texture {@link Identifier} and turns it into a texture string.
+ *
+ * @see VanillaBuilder#withTexture(Identifier)
+ * @see VanillaBuilder#withTexture(String)
+ * @see CustomWaypointStyle#vanillaLike(int, int)
+ * @since 2.11.0
+ */
+ @ApiStatus.NonExtendable
+ interface VanillaBuilder extends GenericBuilder {
+
+ /**
+ * Adds a new texture to this {@link CustomWaypointStyle} in the form of a texture {@link Identifier}.
+ *
+ * This {@link Identifier} is turned into a texture string using the following format: {@code "ui//locator_bar_dot/"}.
+ *
+ * @param texture the texture to add
+ * @return this builder
+ * @since 2.11.0
+ */
+ @This
+ default VanillaBuilder withTexture(Identifier texture) {
+ Objects.requireNonNull(texture, "texture may not be null");
+ return withTexture("ui/" + texture.namespace() + "/locator_bar_dot/" + texture.path());
+ }
+
+ /**
+ * Adds a new texture to this {@link CustomWaypointStyle}.
+ *
+ * @param texture the texture to add
+ * @return this builder
+ * @since 2.11.0
+ */
+ @This
+ VanillaBuilder withTexture(String texture);
+
+ /**
+ * Creates an implementation of {@link CustomWaypointStyle}, using the specified {@code nearDistance} and {@code farDistance}, and the textures added to this builder.
+ *
+ * @return the created {@link CustomWaypointStyle}
+ * @throws IllegalArgumentException when no textures were added to this builder
+ * @since 2.11.0
+ */
+ @Override
+ CustomWaypointStyle build();
+ }
+
+ /**
+ * Creates a new {@link VanillaBuilder} with the given {@code nearDistance} and {@code farDistance} values.
+ *
+ * In Java Edition, the default value for {@code nearDistance} is {@code 128}, and for {@code farDistance} is {@code 332}.
+ *
+ * Please see {@link VanillaBuilder} for a throughout description of {@link VanillaBuilder}s and the {@code nearDistance} and {@code farDistance} values.
+ *
+ * @param nearDistance the nearDistance to be used
+ * @param farDistance the farDistance to be used
+ * @return the created {@link VanillaBuilder}
+ * @throws IllegalArgumentException when the {@code nearDistance} is below 0, or at or above the {@code farDistance}
+ * @see VanillaBuilder
+ * @since 2.11.0
+ */
+ static CustomWaypointStyle.VanillaBuilder vanillaLike(@NonNegative int nearDistance, @Positive int farDistance) {
+ return GeyserApi.api().provider(VanillaBuilder.class, nearDistance, farDistance);
+ }
+
+ /**
+ * Creates a new {@link VanillaBuilder} with the default values of {@code 128} for {@code nearDistance} and {@code 332} for {@code far}.
+ *
+ * Please see {@link VanillaBuilder} for a throughout description of {@link VanillaBuilder}s and the {@code nearDistance} and {@code farDistance} values.
+ *
+ * @return the created {@link VanillaBuilder}
+ * @throws IllegalArgumentException when the {@code nearDistance} is below 0, or at or above the {@code farDistance}
+ * @see VanillaBuilder
+ * @see CustomWaypointStyle#vanillaLike(int, int)
+ * @since 2.11.0
+ */
+ static CustomWaypointStyle.VanillaBuilder vanillaLike() {
+ return vanillaLike(128, 332);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedBlockEntityTranslator.java b/api/src/main/java/org/geysermc/geyser/api/waypoint/CustomWaypointStyleRegisterException.java
similarity index 58%
rename from core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedBlockEntityTranslator.java
rename to api/src/main/java/org/geysermc/geyser/api/waypoint/CustomWaypointStyleRegisterException.java
index 7622e6d28d7..ac7aefd9418 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/BedBlockEntityTranslator.java
+++ b/api/src/main/java/org/geysermc/geyser/api/waypoint/CustomWaypointStyleRegisterException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ * 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
@@ -23,19 +23,24 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.translator.level.block.entity;
+package org.geysermc.geyser.api.waypoint;
-import org.cloudburstmc.nbt.NbtMap;
-import org.cloudburstmc.nbt.NbtMapBuilder;
-import org.geysermc.geyser.level.block.type.BedBlock;
-import org.geysermc.geyser.level.block.type.BlockState;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityType;
+import org.jetbrains.annotations.ApiStatus;
-@BlockEntity(type = BlockEntityType.BED)
-public class BedBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
- @Override
- public void translateTag(GeyserSession session, NbtMapBuilder bedrockNbt, NbtMap javaNbt, BlockState blockState) {
- bedrockNbt.putByte("color", (byte) (blockState.block() instanceof BedBlock bed ? bed.dyeColor() : 0));
+import java.io.Serial;
+
+/**
+ * Thrown when there was an error registering the custom waypoint style. The exception message will have details as to what went wrong.
+ * @since 2.11.0
+ */
+@ApiStatus.NonExtendable
+public class CustomWaypointStyleRegisterException extends RuntimeException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @ApiStatus.Internal
+ public CustomWaypointStyleRegisterException(String message) {
+ super(message);
}
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/waypoint/package-info.java b/api/src/main/java/org/geysermc/geyser/api/waypoint/package-info.java
new file mode 100644
index 00000000000..539aa8869ef
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/waypoint/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.waypoint;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/bootstrap/bungeecord/build.gradle.kts b/bootstrap/bungeecord/build.gradle.kts
index 5f593975e12..8f469d44169 100644
--- a/bootstrap/bungeecord/build.gradle.kts
+++ b/bootstrap/bungeecord/build.gradle.kts
@@ -7,7 +7,6 @@ dependencies {
api(projects.core)
implementation(libs.cloud.bungee)
- implementation(libs.adventure.text.serializer.bungeecord)
compileOnlyApi(libs.bungeecord.proxy) {
isTransitive = false
}
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java
index 17c402037db..2f34404e4ce 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java
@@ -26,8 +26,6 @@
package org.geysermc.geyser.platform.bungeecord;
import lombok.AllArgsConstructor;
-import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
-import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.ServerPing;
import net.md_5.bungee.api.chat.BaseComponent;
@@ -36,6 +34,7 @@
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.ProxyPingEvent;
import net.md_5.bungee.api.plugin.Listener;
+import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.protocol.ProtocolConstants;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
@@ -76,7 +75,7 @@ public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) {
ServerPing response = event.getResponse();
return new GeyserPingInfo(
- GsonComponentSerializer.gson().serialize(BungeeComponentSerializer.get().deserialize(new BaseComponent[]{ response.getDescriptionComponent() })),
+ ComponentSerializer.toString(new BaseComponent[]{ response.getDescriptionComponent() }),
response.getPlayers().getMax(),
response.getPlayers().getOnline()
);
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java
index 10ccc5bac35..670f7927da6 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java
@@ -26,10 +26,11 @@
package org.geysermc.geyser.platform.bungeecord.command;
import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
+import net.md_5.bungee.chat.ComponentSerializer;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.command.GeyserCommandSource;
@@ -64,10 +65,10 @@ public void sendMessage(@NonNull String message) {
public void sendMessage(Component message) {
if (handle instanceof ProxiedPlayer player && player.getPendingConnection().getVersion() >= PROTOCOL_HEX_COLOR) {
// Include hex colors
- handle.sendMessage(BungeeComponentSerializer.get().serialize(message));
+ handle.sendMessage(ComponentSerializer.parse(GsonComponentSerializer.gson().serialize(message)));
return;
}
- handle.sendMessage(BungeeComponentSerializer.legacy().serialize(message));
+ handle.sendMessage(ComponentSerializer.parse(GsonComponentSerializer.colorDownsamplingGson().serialize(message)));
}
@Override
diff --git a/bootstrap/mod/fabric/src/main/resources/fabric.mod.json b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json
index 2932bfb3b18..a7d649063ac 100644
--- a/bootstrap/mod/fabric/src/main/resources/fabric.mod.json
+++ b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json
@@ -23,8 +23,8 @@
"geyser.mixins.json"
],
"depends": {
- "fabricloader": ">=0.18.2",
+ "fabricloader": ">=0.19.3",
"fabric-api": "*",
- "minecraft": "~26.1"
+ "minecraft": "~${minecraft}"
}
}
diff --git a/bootstrap/mod/gametest/build.gradle.kts b/bootstrap/mod/gametest/build.gradle.kts
index 6464a666697..2264c3ac3e4 100644
--- a/bootstrap/mod/gametest/build.gradle.kts
+++ b/bootstrap/mod/gametest/build.gradle.kts
@@ -49,18 +49,4 @@ tasks {
withType(PublishToMavenRepository::class).configureEach {
enabled = false
}
-
- // it'd be processGametestResources if we had a separate source set for tests
- getByName("processResources", ProcessResources::class) {
- filesMatching("fabric.mod.json") {
- expand(
- "id" to "geyser",
- "name" to "Geyser",
- "version" to project.version,
- "description" to project.description!!,
- "url" to "https://geysermc.org",
- "author" to "GeyserMC"
- )
- }
- }
}
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/entity_metadata/sulfur_cube.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/entity_metadata/sulfur_cube.json
new file mode 100644
index 00000000000..817c315b6de
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/entity_metadata/sulfur_cube.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:entity_metadata",
+ "entity_type": "minecraft:sulfur_cube"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/minecraft_version.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/minecraft_version.json
index 0e34d4ab216..6ac386fd614 100644
--- a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/minecraft_version.json
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/minecraft_version.json
@@ -1,4 +1,4 @@
{
"type": "geyser:minecraft_version",
- "version": "26.1.2"
+ "version": "26.2"
}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/amethyst_shard.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/amethyst_shard.json
new file mode 100644
index 00000000000..9de7cbc8188
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/amethyst_shard.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:amethyst_shard"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/blue_egg.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/blue_egg.json
new file mode 100644
index 00000000000..20e2eecc6da
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/blue_egg.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:blue_egg"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/brown_egg.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/brown_egg.json
new file mode 100644
index 00000000000..f869ebfdecc
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/brown_egg.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:brown_egg"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/copper_ingot.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/copper_ingot.json
new file mode 100644
index 00000000000..01c998ac86b
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/copper_ingot.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:copper_ingot"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/copper_spear.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/copper_spear.json
new file mode 100644
index 00000000000..2b82923e10e
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/copper_spear.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:copper_spear"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/diamond.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/diamond.json
new file mode 100644
index 00000000000..076fcf6105e
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/diamond.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:diamond"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/diamond_spear.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/diamond_spear.json
new file mode 100644
index 00000000000..5c0426313d5
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/diamond_spear.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:diamond_spear"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/egg.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/egg.json
new file mode 100644
index 00000000000..0150b038236
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/egg.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:egg"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/emerald.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/emerald.json
new file mode 100644
index 00000000000..2780526a6f5
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/emerald.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:emerald"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/goat_horn.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/goat_horn.json
new file mode 100644
index 00000000000..56e008faac1
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/goat_horn.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:goat_horn"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/gold_ingot.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/gold_ingot.json
new file mode 100644
index 00000000000..d30e8a8f517
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/gold_ingot.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:gold_ingot"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/golden_spear.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/golden_spear.json
new file mode 100644
index 00000000000..bede35e01d8
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/golden_spear.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:golden_spear"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/iron_ingot.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/iron_ingot.json
new file mode 100644
index 00000000000..9eea2a8ef43
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/iron_ingot.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:iron_ingot"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/iron_spear.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/iron_spear.json
new file mode 100644
index 00000000000..22b3c942151
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/iron_spear.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:iron_spear"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/lapis_lazuli.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/lapis_lazuli.json
new file mode 100644
index 00000000000..b049c0b5c20
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/lapis_lazuli.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:lapis_lazuli"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_11.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_11.json
new file mode 100644
index 00000000000..d0620d5079f
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_11.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_11"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_13.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_13.json
new file mode 100644
index 00000000000..952016d70d0
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_13.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_13"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_5.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_5.json
new file mode 100644
index 00000000000..cc265084e3e
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_5.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_5"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_blocks.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_blocks.json
new file mode 100644
index 00000000000..d80ac742c5c
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_blocks.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_blocks"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_bounce.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_bounce.json
new file mode 100644
index 00000000000..a2e2c1a15f4
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_bounce.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_bounce"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_cat.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_cat.json
new file mode 100644
index 00000000000..49b7ccbad4b
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_cat.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_cat"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_chirp.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_chirp.json
new file mode 100644
index 00000000000..c8874bfedf7
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_chirp.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_chirp"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_creator.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_creator.json
new file mode 100644
index 00000000000..7de54bb5778
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_creator.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_creator"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_creator_music_box.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_creator_music_box.json
new file mode 100644
index 00000000000..7da178b4ec4
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_creator_music_box.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_creator_music_box"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_far.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_far.json
new file mode 100644
index 00000000000..96770302785
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_far.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_far"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_lava_chicken.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_lava_chicken.json
new file mode 100644
index 00000000000..6b3bdb8e9f6
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_lava_chicken.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_lava_chicken"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_mall.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_mall.json
new file mode 100644
index 00000000000..c5befbcc3e9
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_mall.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_mall"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_mellohi.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_mellohi.json
new file mode 100644
index 00000000000..7ed192af893
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_mellohi.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_mellohi"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_otherside.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_otherside.json
new file mode 100644
index 00000000000..338848a994d
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_otherside.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_otherside"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_pigstep.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_pigstep.json
new file mode 100644
index 00000000000..8053caa3ec4
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_pigstep.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_pigstep"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_precipice.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_precipice.json
new file mode 100644
index 00000000000..f82acadf095
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_precipice.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_precipice"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_relic.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_relic.json
new file mode 100644
index 00000000000..93fd1f32833
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_relic.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_relic"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_stal.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_stal.json
new file mode 100644
index 00000000000..4d191738181
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_stal.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_stal"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_strad.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_strad.json
new file mode 100644
index 00000000000..65b1f65d558
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_strad.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_strad"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_tears.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_tears.json
new file mode 100644
index 00000000000..dac5d42cba7
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_tears.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_tears"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_wait.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_wait.json
new file mode 100644
index 00000000000..f87d90e0ed2
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_wait.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_wait"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_ward.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_ward.json
new file mode 100644
index 00000000000..88d4d8b7f89
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/music_disc_ward.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:music_disc_ward"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/netherite_ingot.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/netherite_ingot.json
new file mode 100644
index 00000000000..7756c812cda
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/netherite_ingot.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:netherite_ingot"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/netherite_spear.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/netherite_spear.json
new file mode 100644
index 00000000000..a24c7c2ad24
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/netherite_spear.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:netherite_spear"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/quartz.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/quartz.json
new file mode 100644
index 00000000000..e7dd44de35e
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/quartz.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:quartz"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/redstone.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/redstone.json
new file mode 100644
index 00000000000..967866e475c
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/redstone.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:redstone"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/resin_brick.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/resin_brick.json
new file mode 100644
index 00000000000..c8a0be2e209
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/resin_brick.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:resin_brick"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/stone_spear.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/stone_spear.json
new file mode 100644
index 00000000000..125656eeb98
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/stone_spear.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:stone_spear"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/wooden_spear.json b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/wooden_spear.json
new file mode 100644
index 00000000000..2c9b7c74a28
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/generated/data/geyser/test_instance/resolvable_components/wooden_spear.json
@@ -0,0 +1,4 @@
+{
+ "type": "geyser:resolvable_components",
+ "item": "minecraft:wooden_spear"
+}
\ No newline at end of file
diff --git a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTestBootstrap.java b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTestBootstrap.java
index c2bc29df2a0..3eb5373c252 100644
--- a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTestBootstrap.java
+++ b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTestBootstrap.java
@@ -78,8 +78,8 @@ public void onInitialize() {
ModCommandSource::new
);
CommandManager cloud = new FabricServerCommandManager<>(
- ExecutionCoordinator.simpleCoordinator(),
- sourceConverter
+ ExecutionCoordinator.simpleCoordinator(),
+ sourceConverter
);
this.setCommandRegistry(new CommandRegistry(GeyserImpl.getInstance(), cloud, false)); // applying root permission would be a breaking change because we can't register permission defaults
diff --git a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTestTypes.java b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTestTypes.java
index bcc7301d132..68c9d6b48ad 100644
--- a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTestTypes.java
+++ b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTestTypes.java
@@ -26,21 +26,24 @@
package org.geysermc.geyser.gametest;
import com.mojang.serialization.MapCodec;
-import net.minecraft.core.HolderLookup;
+import net.minecraft.core.HolderGetter;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.gametest.framework.GameTestInstance;
+import net.minecraft.gametest.framework.TestEnvironmentDefinition;
import net.minecraft.resources.Identifier;
import org.geysermc.geyser.gametest.tests.ComponentHashTestInstance;
import org.geysermc.geyser.gametest.tests.EntityMetadataTest;
import org.geysermc.geyser.gametest.tests.MinecraftVersionTestInstance;
import org.geysermc.geyser.gametest.tests.RequiredComponentsForHashingTestInstance;
+import org.geysermc.geyser.gametest.tests.ResolvableComponentLoadingTestInstance;
public interface GeyserGameTestTypes {
Identifier COMPONENT_HASH = createKey("component_hash");
SingletonTestType REQUIRED_COMPONENTS_FOR_HASHING = createSingleton("required_components_for_hashing", RequiredComponentsForHashingTestInstance::new);
Identifier ENTITY_METADATA = createKey("entity_metadata");
SingletonTestType MINECRAFT_VERSION = createSingleton("minecraft_version", MinecraftVersionTestInstance::new);
+ Identifier RESOLVABLE_COMPONENTS = createKey("resolvable_components");
private static Identifier createKey(String name) {
return Identifier.fromNamespaceAndPath("geyser", name);
@@ -63,6 +66,7 @@ static void bootstrap() {
register(REQUIRED_COMPONENTS_FOR_HASHING, RequiredComponentsForHashingTestInstance.MAP_CODEC);
register(ENTITY_METADATA, EntityMetadataTest.MAP_CODEC);
register(MINECRAFT_VERSION, MinecraftVersionTestInstance.MAP_CODEC);
+ register(RESOLVABLE_COMPONENTS, ResolvableComponentLoadingTestInstance.MAP_CODEC);
}
record SingletonTestType(Identifier type, Constructor constructor) {
@@ -70,7 +74,7 @@ record SingletonTestType(Identifier type, Constructor constructor) {
@FunctionalInterface
interface Constructor {
- GameTestInstance create(HolderLookup.Provider registries, boolean required);
+ GameTestInstance create(HolderGetter> testEnvironments, boolean required);
}
}
}
diff --git a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTests.java b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTests.java
index b5713893941..ad035e554fc 100644
--- a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTests.java
+++ b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/GeyserGameTests.java
@@ -25,20 +25,34 @@
package org.geysermc.geyser.gametest;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricDynamicRegistryProvider;
+import net.minecraft.core.Holder;
+import net.minecraft.core.HolderGetter;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestInstance;
+import net.minecraft.gametest.framework.TestEnvironmentDefinition;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.world.item.Item;
import org.geysermc.geyser.gametest.tests.EntityMetadataTest;
+import org.geysermc.geyser.gametest.tests.ResolvableComponentLoadingTestInstance;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
public final class GeyserGameTests {
- private static final List> UNSUPPORTED_ENTITY_TYPES = List.of(EntityType.BLOCK_DISPLAY, EntityType.ITEM_DISPLAY, EntityType.MARKER);
+ private static final List> UNSUPPORTED_ENTITY_TYPES = List.of(EntityTypes.BLOCK_DISPLAY, EntityTypes.ITEM_DISPLAY, EntityTypes.MARKER);
private GeyserGameTests() {}
@@ -54,24 +68,57 @@ private static ResourceKey createSingletonKey(Identifier testT
return createKey(testType.getPath());
}
- private static void registerSingletonTest(HolderLookup.Provider registries, FabricDynamicRegistryProvider.Entries entries, GeyserGameTestTypes.SingletonTestType type, boolean required) {
- entries.add(createSingletonKey(type.type()), type.constructor().create(registries, required));
+ private static void registerSingletonTest(HolderGetter> testEnvironments, FabricDynamicRegistryProvider.Entries entries, GeyserGameTestTypes.SingletonTestType type, boolean required) {
+ entries.add(createSingletonKey(type.type()), type.constructor().create(testEnvironments, required));
}
- private static void registerSingletonTest(HolderLookup.Provider registries, FabricDynamicRegistryProvider.Entries entries, GeyserGameTestTypes.SingletonTestType type) {
- registerSingletonTest(registries, entries, type, true);
+ private static void registerSingletonTest(HolderGetter> testEnvironments, FabricDynamicRegistryProvider.Entries entries, GeyserGameTestTypes.SingletonTestType type) {
+ registerSingletonTest(testEnvironments, entries, type, true);
}
- private static void registerEntityTypeTests(HolderLookup.Provider registries, FabricDynamicRegistryProvider.Entries entries) {
+ private static void registerEntityTypeTests(HolderGetter> testEnvironments, FabricDynamicRegistryProvider.Entries entries) {
for (EntityType> entityType : BuiltInRegistries.ENTITY_TYPE) {
entries.add(createKey(GeyserGameTestTypes.ENTITY_METADATA, BuiltInRegistries.ENTITY_TYPE.getKey(entityType).getPath()),
- new EntityMetadataTest(registries, !UNSUPPORTED_ENTITY_TYPES.contains(entityType), entityType));
+ new EntityMetadataTest(testEnvironments, !UNSUPPORTED_ENTITY_TYPES.contains(entityType), entityType));
+ }
+ }
+
+ private static void registerResolvableComponentLoadingTests(HolderGetter> testEnvironments, GeyserGameTestPlatform platform, FabricDynamicRegistryProvider.Entries entries) {
+ for (Holder- item : findItemsWithResolvableComponents(platform)) {
+ entries.add(createKey(GeyserGameTestTypes.RESOLVABLE_COMPONENTS, item.unwrapKey().orElseThrow().identifier().getPath()),
+ new ResolvableComponentLoadingTestInstance(testEnvironments, true, item));
}
}
public static void bootstrap(HolderLookup.Provider registries, FabricDynamicRegistryProvider.Entries entries) {
- registerEntityTypeTests(registries, entries);
- registerSingletonTest(registries, entries, GeyserGameTestTypes.REQUIRED_COMPONENTS_FOR_HASHING);
- registerSingletonTest(registries, entries, GeyserGameTestTypes.MINECRAFT_VERSION);
+ GeyserGameTestPlatform platform = new GeyserGameTestPlatform();
+ HolderGetter> testEnvironments = registries.lookupOrThrow(Registries.TEST_ENVIRONMENT);
+
+ registerEntityTypeTests(testEnvironments, entries);
+ registerSingletonTest(testEnvironments, entries, GeyserGameTestTypes.REQUIRED_COMPONENTS_FOR_HASHING);
+ registerSingletonTest(testEnvironments, entries, GeyserGameTestTypes.MINECRAFT_VERSION);
+ registerResolvableComponentLoadingTests(testEnvironments, platform, entries);
+ }
+
+ private static List> findItemsWithResolvableComponents(GeyserGameTestPlatform platform) {
+ try (InputStream stream = platform.resolveResource("mappings/resolvable_item_data_components.json")) {
+ assert stream != null;
+ JsonElement rootElement = JsonParser.parseReader(new InputStreamReader(stream));
+ JsonArray items = rootElement.getAsJsonObject().get("value").getAsJsonArray();
+
+ List> itemsWithResolvableComponents = new ArrayList<>();
+
+ for (int i = 0; i < items.size(); i++) {
+ JsonElement item = items.get(i);
+ JsonArray itemComponentArray = item.getAsJsonArray();
+ if (!itemComponentArray.isEmpty()) {
+ itemsWithResolvableComponents.add(Objects.requireNonNull(BuiltInRegistries.ITEM.asHolderIdMap().byId(i)));
+ }
+ }
+
+ return itemsWithResolvableComponents;
+ } catch (IOException exception) {
+ throw new RuntimeException(exception);
+ }
}
}
diff --git a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/registries/CloudburstNbtOps.java b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/registries/CloudburstNbtOps.java
index 1b33a71f778..20aed5a572c 100644
--- a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/registries/CloudburstNbtOps.java
+++ b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/registries/CloudburstNbtOps.java
@@ -80,6 +80,16 @@ public Object createNumeric(Number i) {
return i;
}
+ @Override
+ public DataResult getBooleanValue(Object input) {
+ return this.getNumberValue(input).map(value -> value.doubleValue() != 0.0);
+ }
+
+ @Override
+ public Object createBoolean(boolean value) {
+ return value;
+ }
+
@Override
public DataResult getStringValue(Object input) {
if (input instanceof String) {
diff --git a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/ComponentHashTestInstance.java b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/ComponentHashTestInstance.java
index 2954d5e1a49..362e997bc8f 100644
--- a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/ComponentHashTestInstance.java
+++ b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/ComponentHashTestInstance.java
@@ -32,10 +32,12 @@
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import io.netty.buffer.Unpooled;
+import net.minecraft.core.HolderGetter;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.component.TypedDataComponent;
import net.minecraft.gametest.framework.GameTestAssertException;
import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.gametest.framework.TestEnvironmentDefinition;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
@@ -43,9 +45,9 @@
import net.minecraft.util.ExtraCodecs;
import net.minecraft.util.HashOps;
import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.gametest.registries.GameTestJavaRegistryProvider;
import org.geysermc.geyser.item.hashing.DataComponentHashers;
import org.geysermc.geyser.item.hashing.MapHasher;
+import org.geysermc.geyser.session.cache.registry.JavaRegistryProvider;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftTypes;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponent;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
@@ -62,7 +64,7 @@ public class ComponentHashTestInstance extends GeyserTestInstance {
.xmap(listOfLists -> listOfLists.stream().flatMap(List::stream).toList(), list -> {
Map, List>> split = new HashMap<>();
list.forEach(component -> {
- split.compute(component.type(), (type, typeList) -> {
+ split.compute(component.type(), (_, typeList) -> {
if (typeList == null) {
typeList = new ArrayList<>();
}
@@ -82,8 +84,8 @@ public class ComponentHashTestInstance extends GeyserTestInstance {
private final List> testCases;
- private ComponentHashTestInstance(RegistryOps> ops, boolean required, List> testCases) {
- super(ops, required);
+ private ComponentHashTestInstance(HolderGetter> testEnvironments, boolean required, List> testCases) {
+ super(testEnvironments, required);
this.testCases = testCases;
}
@@ -108,7 +110,7 @@ public void run(GameTestHelper helper) {
Object encodedJavaValue = testCase.encodeValue(javaOps).getOrThrow();
// Hash both and compare
int expected = testCase.encodeValue(hashOps).getOrThrow().asInt();
- GameTestJavaRegistryProvider registries = new GameTestJavaRegistryProvider(helper.getLevel().registryAccess());
+ JavaRegistryProvider registries = createRegistryProvider(helper);
int geyser = DataComponentHashers.hash(registries, mcplComponent).asInt();
try {
diff --git a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/EntityMetadataTest.java b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/EntityMetadataTest.java
index 50e5068fb0e..5ac7fda9a27 100644
--- a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/EntityMetadataTest.java
+++ b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/EntityMetadataTest.java
@@ -27,22 +27,24 @@
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
-import net.minecraft.core.HolderLookup;
+import net.minecraft.core.HolderGetter;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.gametest.framework.GameTestAssertException;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.gametest.framework.GameTestInstance;
+import net.minecraft.gametest.framework.TestEnvironmentDefinition;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData;
-import net.minecraft.resources.RegistryOps;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntitySpawnReason;
import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.level.GameType;
import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.EntityTypeDefinition;
+import org.geysermc.geyser.entity.GeyserEntityType;
import org.geysermc.geyser.gametest.mixin.SynchedEntityDataAccessor;
import org.geysermc.geyser.gametest.util.SynchedEntityDataDebugger;
import org.geysermc.geyser.registry.Registries;
@@ -56,27 +58,21 @@ public class EntityMetadataTest extends GeyserTestInstance {
);
private final EntityType> entityType;
- private EntityMetadataTest(RegistryOps> ops, boolean required, EntityType> entityType) {
- super(ops, required);
- this.entityType = entityType;
- }
-
- public EntityMetadataTest(HolderLookup.Provider registries, boolean required, EntityType> entityType) {
- super(registries, required);
+ public EntityMetadataTest(HolderGetter> testEnvironments, boolean required, EntityType> entityType) {
+ super(testEnvironments, required);
this.entityType = entityType;
}
@Override
public void run(GameTestHelper helper) {
- // Nice full qualified name, lovely
- org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType geyserEntityType = org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType.valueOf(BuiltInRegistries.ENTITY_TYPE.getKey(entityType).getPath().toUpperCase());
- EntityDefinition> definition = Registries.ENTITY_DEFINITIONS.get(geyserEntityType);
+ GeyserEntityType geyserEntityType = GeyserEntityType.of(BuiltInRegistries.ENTITY_TYPE.getId(entityType));
+ EntityTypeDefinition> definition = Registries.JAVA_ENTITY_TYPES.get(geyserEntityType);
if (definition == null) {
helper.fail("No entity definition found for type " + entityType);
} else {
Entity javaEntity;
- if (entityType == EntityType.PLAYER) {
+ if (entityType == EntityTypes.PLAYER) {
javaEntity = helper.makeMockPlayer(GameType.SURVIVAL);
} else {
javaEntity = entityType.create(helper.getLevel(), EntitySpawnReason.COMMAND);
diff --git a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/GeyserTestInstance.java b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/GeyserTestInstance.java
index 0c7e175bb04..dd8f271e512 100644
--- a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/GeyserTestInstance.java
+++ b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/GeyserTestInstance.java
@@ -27,70 +27,39 @@
import com.mojang.datafixers.Products;
import com.mojang.serialization.Codec;
-import com.mojang.serialization.DataResult;
-import com.mojang.serialization.DynamicOps;
-import com.mojang.serialization.MapCodec;
-import com.mojang.serialization.MapLike;
-import com.mojang.serialization.RecordBuilder;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.Holder;
-import net.minecraft.core.HolderLookup;
+import net.minecraft.core.HolderGetter;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestEnvironments;
+import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.gametest.framework.GameTestInstance;
import net.minecraft.gametest.framework.TestData;
import net.minecraft.gametest.framework.TestEnvironmentDefinition;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
-
-import java.util.stream.Stream;
+import org.geysermc.geyser.gametest.registries.GameTestJavaRegistryProvider;
+import org.geysermc.geyser.session.cache.registry.JavaRegistryProvider;
public abstract class GeyserTestInstance extends GameTestInstance {
- // Cursed codec to extract a RegistryOps
- private static final MapCodec> REGISTRY_OPS_MAP_CODEC = new MapCodec<>() {
- @Override
- public Stream keys(DynamicOps ops) {
- return Stream.empty();
- }
-
- @Override
- public DataResult> decode(DynamicOps ops, MapLike input) {
- if (ops instanceof RegistryOps registryOps) {
- return DataResult.success(registryOps);
- }
- return DataResult.error(() -> "Registry ops required for parsing");
- }
-
- @Override
- public RecordBuilder encode(RegistryOps> input, DynamicOps ops, RecordBuilder prefix) {
- // noop
- return prefix;
- }
- };
- protected GeyserTestInstance(RegistryOps> ops, boolean required) {
- super(createEmptyTestData(ops, required));
+ protected GeyserTestInstance(HolderGetter> testEnvironments, boolean required) {
+ super(createEmptyTestData(testEnvironments, required));
}
- protected GeyserTestInstance(HolderLookup.Provider registries, boolean required) {
- super(createEmptyTestData(registries, required));
+ protected JavaRegistryProvider createRegistryProvider(GameTestHelper helper) {
+ return new GameTestJavaRegistryProvider(helper.getLevel().registryAccess());
}
- protected static Products.P2, RegistryOps>, Boolean> commonFields(RecordCodecBuilder.Instance instance) {
+ protected static Products.P2, HolderGetter>, Boolean> commonFields(RecordCodecBuilder.Instance instance) {
return instance.group(
- REGISTRY_OPS_MAP_CODEC.forGetter(_ -> null),
+ RegistryOps.retrieveGetter(Registries.TEST_ENVIRONMENT),
Codec.BOOL.optionalFieldOf("required", true).forGetter(GameTestInstance::required)
);
}
- private static TestData>> createEmptyTestData(HolderLookup.Provider registries, boolean required) {
- return new TestData<>(registries.lookupOrThrow(Registries.TEST_ENVIRONMENT).getOrThrow(GameTestEnvironments.DEFAULT_KEY),
- Identifier.withDefaultNamespace("empty"), 1, 1, required);
- }
-
- private static TestData>> createEmptyTestData(RegistryOps> ops, boolean required) {
- return new TestData<>(ops.getter(Registries.TEST_ENVIRONMENT)
- .flatMap(getter -> getter.get(GameTestEnvironments.DEFAULT_KEY)).orElseThrow(),
+ private static TestData>> createEmptyTestData(HolderGetter> testEnvironments, boolean required) {
+ return new TestData<>(testEnvironments.getOrThrow(GameTestEnvironments.DEFAULT_KEY),
Identifier.withDefaultNamespace("empty"), 1, 1, required);
}
}
diff --git a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/MinecraftVersionTestInstance.java b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/MinecraftVersionTestInstance.java
index 97460de3b4b..b18bb1b0789 100644
--- a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/MinecraftVersionTestInstance.java
+++ b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/MinecraftVersionTestInstance.java
@@ -29,11 +29,13 @@
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.SharedConstants;
-import net.minecraft.core.HolderLookup;
+import net.minecraft.core.HolderGetter;
+import net.minecraft.gametest.framework.GameTestAssertException;
import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.gametest.framework.TestEnvironmentDefinition;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
-import net.minecraft.resources.RegistryOps;
+import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.network.GameProtocol;
public class MinecraftVersionTestInstance extends GeyserTestInstance {
@@ -44,22 +46,32 @@ public class MinecraftVersionTestInstance extends GeyserTestInstance {
);
private final String version;
- private MinecraftVersionTestInstance(RegistryOps> ops, boolean required, String version) {
- super(ops, required);
+ private MinecraftVersionTestInstance(HolderGetter> testEnvironments, boolean required, String version) {
+ super(testEnvironments, required);
this.version = version;
}
- public MinecraftVersionTestInstance(HolderLookup.Provider registries, boolean required) {
- super(registries, required);
- this.version = SharedConstants.getCurrentVersion().id();
+ public MinecraftVersionTestInstance(HolderGetter> testEnvironments, boolean required) {
+ this(testEnvironments, required, SharedConstants.getCurrentVersion().id());
}
@Override
public void run(GameTestHelper helper) {
String current = SharedConstants.getCurrentVersion().id();
- helper.assertValueEqual(current, version, "running Minecraft version");
- helper.assertValueEqual(SharedConstants.getProtocolVersion(), GameProtocol.getJavaProtocolVersion(), "Java protocol version");
- helper.assertTrue(GameProtocol.getAllSupportedJavaVersions().contains(current), "GameProtocol must mark " + current + " as supported");
+ try {
+ helper.assertValueEqual(current, version, "running Minecraft version");
+ } catch (GameTestAssertException exception) {
+ GeyserImpl.getInstance().getLogger().error("Please re-run the \":gametest:runDatagen\" gradle task!");
+ throw exception;
+ }
+
+ try {
+ helper.assertValueEqual(SharedConstants.getProtocolVersion(), GameProtocol.getJavaProtocolVersion(), "Java protocol version");
+ helper.assertTrue(GameProtocol.getAllSupportedJavaVersions().contains(current), "GameProtocol must mark " + current + " as supported");
+ } catch (GameTestAssertException exception) {
+ GeyserImpl.getInstance().getLogger().error("Please ensure MCPL is updated!");
+ throw exception;
+ }
helper.succeed();
}
diff --git a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/RequiredComponentsForHashingTestInstance.java b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/RequiredComponentsForHashingTestInstance.java
index b3b84501186..e357da21ba6 100644
--- a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/RequiredComponentsForHashingTestInstance.java
+++ b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/RequiredComponentsForHashingTestInstance.java
@@ -27,16 +27,16 @@
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
-import net.minecraft.core.HolderLookup;
+import net.minecraft.core.HolderGetter;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.component.TypedDataComponent;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.gametest.framework.GameTestInstance;
+import net.minecraft.gametest.framework.TestEnvironmentDefinition;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
-import net.minecraft.resources.RegistryOps;
import org.geysermc.geyser.item.hashing.DataComponentHashers;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes;
@@ -48,12 +48,8 @@ public class RequiredComponentsForHashingTestInstance extends GeyserTestInstance
commonFields(instance).apply(instance, RequiredComponentsForHashingTestInstance::new)
);
- private RequiredComponentsForHashingTestInstance(RegistryOps> ops, boolean required) {
- super(ops, required);
- }
-
- public RequiredComponentsForHashingTestInstance(HolderLookup.Provider registries, boolean required) {
- super(registries, required);
+ public RequiredComponentsForHashingTestInstance(HolderGetter> testEnvironments, boolean required) {
+ super(testEnvironments, required);
}
@Override
diff --git a/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/ResolvableComponentLoadingTestInstance.java b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/ResolvableComponentLoadingTestInstance.java
new file mode 100644
index 00000000000..8a2c36c2386
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/java/org/geysermc/geyser/gametest/tests/ResolvableComponentLoadingTestInstance.java
@@ -0,0 +1,86 @@
+/*
+ * 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.gametest.tests;
+
+import com.mojang.serialization.MapCodec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+import net.minecraft.core.Holder;
+import net.minecraft.core.HolderGetter;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.gametest.framework.GameTestHelper;
+import net.minecraft.gametest.framework.TestEnvironmentDefinition;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.MutableComponent;
+import net.minecraft.world.item.Item;
+import org.geysermc.geyser.item.components.resolvable.ResolvableComponent;
+import org.geysermc.geyser.registry.Registries;
+import org.geysermc.geyser.session.cache.registry.JavaRegistryProvider;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
+
+import java.util.HashMap;
+
+public class ResolvableComponentLoadingTestInstance extends GeyserTestInstance {
+ public static final MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec(instance ->
+ commonFields(instance)
+ .and(Item.CODEC.fieldOf("item").forGetter(test -> test.item))
+ .apply(instance, ResolvableComponentLoadingTestInstance::new)
+ );
+ private final Holder
- item;
+
+ public ResolvableComponentLoadingTestInstance(HolderGetter> testEnvironments, boolean required, Holder
- item) {
+ super(testEnvironments, required);
+ this.item = item;
+ }
+
+ @Override
+ public void run(GameTestHelper helper) {
+ JavaRegistryProvider registries = createRegistryProvider(helper);
+ var geyserItem = Registries.JAVA_ITEMS.get(BuiltInRegistries.ITEM.getId(item.value()));
+
+ helper.assertFalse(geyserItem.resolvableComponents().isEmpty(), "a test was created for this item but it does not have any resolvable components loaded");
+
+ DataComponents components = new DataComponents(new HashMap<>());
+ for (ResolvableComponent> component : geyserItem.resolvableComponents()) {
+ try {
+ // Have to resolve into map to make sure casts and stuff are checked
+ component.resolve(registries, components);
+ } catch (Exception exception) {
+ helper.fail("component " + component + " failed to resolve: " + exception);
+ }
+ }
+ helper.succeed();
+ }
+
+ @Override
+ public MapCodec codec() {
+ return MAP_CODEC;
+ }
+
+ @Override
+ protected MutableComponent typeDescription() {
+ return Component.literal("Resolvable Component Loading for Item: " + item.unwrapKey().orElseThrow().identifier());
+ }
+}
diff --git a/bootstrap/mod/gametest/src/main/resources/data/geyser/test_instance/hashing/sulfur_cube_content.json b/bootstrap/mod/gametest/src/main/resources/data/geyser/test_instance/hashing/sulfur_cube_content.json
new file mode 100644
index 00000000000..a535b4527d3
--- /dev/null
+++ b/bootstrap/mod/gametest/src/main/resources/data/geyser/test_instance/hashing/sulfur_cube_content.json
@@ -0,0 +1,46 @@
+{
+ "type": "geyser:component_hash",
+ "component": "minecraft:sulfur_cube_content",
+ "value": [
+ {
+ "id": "minecraft:stone"
+ },
+ {
+ "id": "minecraft:slime_block",
+ "count": 5
+ },
+ {
+ "id": "minecraft:diamond_block",
+ "count": 3,
+ "components": {
+ "minecraft:enchantment_glint_override": true
+ }
+ },
+ {
+ "id": "minecraft:enchanted_golden_apple",
+ "count": 68,
+ "components": {
+ "minecraft:enchantment_glint_override": false,
+ "minecraft:max_stack_size": 96,
+ "minecraft:bundle_contents": [
+ {
+ "id": "minecraft:bundle",
+ "count": 2,
+ "components": {
+ "minecraft:max_stack_size": 2,
+ "minecraft:bundle_contents": [
+ {
+ "id": "minecraft:diamond",
+ "components": {
+ "minecraft:custom_name": "Now that's a fun item!",
+ "!minecraft:item_name": {}
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/bootstrap/mod/gametest/src/main/resources/fabric.mod.json b/bootstrap/mod/gametest/src/main/resources/fabric.mod.json
index e4ceb984b3c..f28ac76d9b8 100644
--- a/bootstrap/mod/gametest/src/main/resources/fabric.mod.json
+++ b/bootstrap/mod/gametest/src/main/resources/fabric.mod.json
@@ -27,8 +27,8 @@
"geyser-gametest.mixins.json"
],
"depends": {
- "fabricloader": ">=0.18.2",
+ "fabricloader": ">=0.19.3",
"fabric-api": "*",
- "minecraft": "~26.1"
+ "minecraft": "~${minecraft}"
}
}
diff --git a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml
index 8c864b83ecc..9e1deb5b9be 100644
--- a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml
+++ b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml
@@ -16,12 +16,12 @@ config = "geyser_neoforge.mixins.json"
[[dependencies.geyser_neoforge]]
modId="neoforge"
type="required"
- versionRange="[26.1.0.1-beta,)"
+ versionRange="[26.2.0.0-beta,26.3.0.0)"
ordering="NONE"
side="BOTH"
[[dependencies.geyser_neoforge]]
modId="minecraft"
type="required"
- versionRange="[26.1,)"
+ versionRange="[${minecraft},${minecraftNext})"
ordering="NONE"
side="BOTH"
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java
index 035abd701a1..a14b0a6edde 100644
--- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java
@@ -50,8 +50,8 @@ public class IntegratedServerMixin implements GeyserServerPortGetter {
@Shadow @Final private Minecraft minecraft;
- @Inject(method = "publishServer", at = @At("RETURN"))
- private void onOpenToLan(GameType gameType, boolean cheatsAllowed, int port, CallbackInfoReturnable cir) {
+ @Inject(method = "publishServer(Lnet/minecraft/server/MinecraftServer$MultiplayerScope;I)Z", at = @At("RETURN"))
+ private void onOpenToLan(MinecraftServer.MultiplayerScope scope, int port, CallbackInfoReturnable cir) {
if (cir.getReturnValueZ()) {
// If the LAN is opened, starts Geyser.
GeyserModBootstrap instance = GeyserModBootstrap.getInstance();
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java
index 41cd48510b3..1d34e73636f 100644
--- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java
@@ -31,6 +31,7 @@
import net.minecraft.server.WorldStem;
import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.server.level.progress.LevelLoadListener;
+import net.minecraft.server.notifications.NotificationManager;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.level.storage.LevelStorageSource;
@@ -43,8 +44,8 @@
@Mixin(DedicatedServer.class)
public abstract class DedicatedServerMixin extends MinecraftServer implements GeyserServerPortGetter {
- public DedicatedServerMixin(Thread thread, LevelStorageSource.LevelStorageAccess levelStorageAccess, PackRepository packRepository, WorldStem worldStem, Optional gameRules, Proxy proxy, DataFixer dataFixer, Services services, LevelLoadListener levelLoadListener) {
- super(thread, levelStorageAccess, packRepository, worldStem, gameRules, proxy, dataFixer, services, levelLoadListener, true);
+ public DedicatedServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Optional gameRules, Proxy proxy, DataFixer fixerUpper, Services services, LevelLoadListener levelLoadListener, boolean propagatesCrashes, NotificationManager notificationManager) {
+ super(serverThread, storageSource, packRepository, worldStem, gameRules, proxy, fixerUpper, services, levelLoadListener, propagatesCrashes, notificationManager);
}
@Override
diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts
index 83d17521858..c1264312bbe 100644
--- a/bootstrap/spigot/build.gradle.kts
+++ b/bootstrap/spigot/build.gradle.kts
@@ -26,8 +26,6 @@ dependencies {
implementation(libs.cloud.paper)
implementation(libs.commodore)
- implementation(libs.adventure.text.serializer.bungeecord)
-
compileOnly(libs.folia.api)
compileOnlyApi(libs.viaversion)
@@ -78,10 +76,8 @@ tasks.withType {
modrinth {
uploadFile.set(tasks.getByPath("shadowJar"))
- gameVersions.addAll("1.16.5", "1.17", "1.17.1", "1.18", "1.18.1", "1.18.2", "1.19",
- "1.19.1", "1.19.2", "1.19.3", "1.19.4", "1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6",
- "1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5", "1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10",
- "1.21.11")
+ gameVersions.addAll("1.20.5", "1.20.6", "1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4",
+ "1.21.5", "1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11", "26.1", "26.1.1", "26.1.2", "26.2")
loaders.addAll("spigot", "paper")
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java
index 9f897b09538..47b5a23a510 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java
@@ -26,7 +26,8 @@
package org.geysermc.geyser.platform.spigot.command;
import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.md_5.bungee.chat.ComponentSerializer;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.RemoteConsoleCommandSender;
@@ -67,7 +68,7 @@ public void sendMessage(Component message) {
}
// CommandSender#sendMessage(BaseComponent[]) is Paper-only
- handle.spigot().sendMessage(BungeeComponentSerializer.get().serialize(message));
+ handle.spigot().sendMessage(ComponentSerializer.parse(GsonComponentSerializer.gson().serialize(message)));
}
@Override
diff --git a/bootstrap/velocity/build.gradle.kts b/bootstrap/velocity/build.gradle.kts
index dec2e050720..0da1faf7db9 100644
--- a/bootstrap/velocity/build.gradle.kts
+++ b/bootstrap/velocity/build.gradle.kts
@@ -17,7 +17,6 @@ dependencies {
}
platformRelocate("it.unimi.dsi.fastutil")
-platformRelocate("net.kyori.adventure.text.serializer.gson.legacyimpl")
platformRelocate("org.yaml")
platformRelocate("org.spongepowered")
platformRelocate("org.bstats")
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java
index a61a65805af..b39c14a8ef4 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java
@@ -53,10 +53,10 @@ public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) {
ProxyPingEvent event;
try {
event = server.getEventManager().fire(new ProxyPingEvent(new GeyserInboundConnection(inetSocketAddress), ServerPing.builder()
- .description(server.getConfiguration().getMotd()).onlinePlayers(server.getPlayerCount())
- .maximumPlayers(server.getConfiguration().getShowMaxPlayers())
- .version(new Version(GameProtocol.getJavaProtocolVersion(), GameProtocol.getJavaMinecraftVersion()))
- .build())).get();
+ .description(server.getConfiguration().getMotd())
+ .onlinePlayers(server.getPlayerCount())
+ .maximumPlayers(server.getConfiguration().getShowMaxPlayers())
+ .version(new Version(GameProtocol.getJavaProtocolVersion(), GameProtocol.getJavaMinecraftVersion())).build())).get();
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java
index f55f145ed63..bbfd01ca161 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java
@@ -49,7 +49,7 @@ public VelocityCommandSource(CommandSource handle) {
}
@Override
- public String name() {
+ public @NonNull String name() {
if (handle instanceof Player) {
return ((Player) handle).getUsername();
} else if (handle instanceof ConsoleCommandSource) {
@@ -65,7 +65,6 @@ public void sendMessage(@NonNull String message) {
@Override
public void sendMessage(Component message) {
- // Be careful that we don't shade in Adventure!!
handle.sendMessage(message);
}
@@ -83,9 +82,9 @@ public boolean isConsole() {
}
@Override
- public String locale() {
- if (handle instanceof Player) {
- Locale locale = ((Player) handle).getPlayerSettings().getLocale();
+ public @NonNull String locale() {
+ if (handle instanceof Player player) {
+ Locale locale = player.getPlayerSettings().getLocale();
return GeyserLocale.formatLocale(locale.getLanguage() + "_" + locale.getCountry());
}
return GeyserLocale.getDefaultLocale();
diff --git a/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts
index dd54b79ffc2..8bfa62bf510 100644
--- a/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts
+++ b/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts
@@ -13,7 +13,7 @@ modrinth {
versionNumber.set(projectVersion(project))
versionType.set("beta")
changelog.set(System.getenv("CHANGELOG") ?: "")
- gameVersions.addAll("26.1", "26.1.1", libs.minecraft.get().version as String)
+ gameVersions.addAll(libs.minecraft.get().version as String)
failSilently.set(true)
syncBodyFrom.set(rootProject.file("README.md").readText())
diff --git a/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts
index bbb18ff0e87..ffc8bb10d37 100644
--- a/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts
+++ b/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts
@@ -3,6 +3,13 @@ plugins {
id("io.freefair.lombok")
}
+// Major.minor + the next minor, for the mod metadata version ranges.
+// Strips any patch/pre-release suffix (26.2.1 / 26.2-rc-2 -> 26.2).
+val minecraftVersion = libs.minecraft.get().version as String
+val minecraftLine = Regex("^\\d+\\.\\d+").find(minecraftVersion)?.value ?: minecraftVersion
+val (minecraftMajor, minecraftMinor) = minecraftLine.split(".")
+val minecraftNext = "$minecraftMajor.${minecraftMinor.toInt() + 1}"
+
tasks {
processResources {
// Spigot, BungeeCord, Velocity, Fabric, ViaProxy, NeoForge
@@ -17,7 +24,9 @@ tasks {
),
"description" to project.description as String,
"url" to "https://geysermc.org",
- "author" to "GeyserMC"
+ "author" to "GeyserMC",
+ "minecraft" to minecraftLine,
+ "minecraftNext" to minecraftNext
)
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
index d63bf2e7747..6653bab914e 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
@@ -314,6 +314,7 @@ private void startInstance() {
SkinProvider.registerCacheImageTask(this);
Registries.RESOURCE_PACKS.load();
+ Registries.WAYPOINT_STYLE_MAPPINGS.load();
// Warnings to users who enable options that they might not need.
if (config.advanced().bedrock().useHaproxyProtocol()) {
@@ -600,6 +601,9 @@ public void disable() {
}
ResourcePackLoader.clear();
+ if (Registries.WAYPOINT_STYLE_MAPPINGS.loaded()) {
+ Registries.WAYPOINT_STYLE_MAPPINGS.get().clear();
+ }
CodeOfConductManager.trySave();
this.setEnabled(false);
diff --git a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
index 3dcb1e13bba..1dcacdbd882 100644
--- a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
+++ b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
@@ -42,6 +42,7 @@
import org.geysermc.floodgate.util.DeviceOs;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.GeyserApi;
+import org.geysermc.geyser.api.entity.custom.CustomEntityDefinition;
import org.geysermc.geyser.api.extension.Extension;
import org.geysermc.geyser.api.util.MinecraftVersion;
import org.geysermc.geyser.network.GameProtocol;
@@ -169,7 +170,8 @@ public DumpInfo(GeyserImpl geyser, boolean addLog) {
.toList();
this.mappingInfo = new MappingInfo(BlockRegistries.CUSTOM_BLOCKS.get().length,
BlockRegistries.CUSTOM_SKULLS.get().size(),
- Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_PROTOCOL).getCustomIdMappings().size()
+ Registries.ITEMS.forVersion(GameProtocol.DEFAULT_BEDROCK_PROTOCOL).getCustomIdMappings().size(),
+ Registries.BEDROCK_ENTITY_DEFINITIONS.get().values().stream().filter(def -> def instanceof CustomEntityDefinition).toArray().length
);
}
@@ -340,6 +342,6 @@ public PackInfo(ResourcePackHolder holder) {
}
}
- public record MappingInfo(int customBlocks, int customSkulls, int customItems) {
+ public record MappingInfo(int customBlocks, int customSkulls, int customItems, int customEntities) {
}
}
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..1c73619ce4b
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/BedrockEntityDefinition.java
@@ -0,0 +1,137 @@
+/*
+ * 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.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 class BedrockEntityDefinition implements GeyserEntityDefinition {
+
+ private final Identifier identifier;
+ private final GeyserEntityProperties registeredProperties;
+
+ protected BedrockEntityDefinition(@NonNull Identifier identifier, @NonNull GeyserEntityProperties registeredProperties) {
+ this.identifier = Objects.requireNonNull(identifier, "identifier");
+ this.registeredProperties = Objects.requireNonNull(registeredProperties, "registeredProperties");
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ static BedrockEntityDefinition ofVanilla(Identifier identifier) {
+ BedrockEntityDefinition bedrockEntityDefinition = builder().identifier(identifier).build();
+ Registries.BEDROCK_ENTITY_DEFINITIONS.register(identifier, bedrockEntityDefinition);
+ return bedrockEntityDefinition;
+ }
+
+ public static BedrockEntityDefinition getVanilla(@NonNull Identifier identifier) {
+ Objects.requireNonNull(identifier, "identifier");
+ BedrockEntityDefinition existing = Registries.BEDROCK_ENTITY_DEFINITIONS.get().get(identifier);
+ if (existing == null || existing instanceof CustomBedrockEntityDefinition) {
+ throw new IllegalArgumentException("Unknown vanilla Bedrock entity definition: " + identifier);
+ }
+ return existing;
+ }
+
+ @Override
+ public @NonNull Identifier identifier() {
+ return identifier;
+ }
+
+ public @NonNull GeyserEntityProperties registeredProperties() {
+ return registeredProperties;
+ }
+
+ @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);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BedrockEntityDefinition that)) return false;
+ return identifier.equals(that.identifier) && registeredProperties.equals(that.registeredProperties);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(identifier, registeredProperties);
+ }
+
+ @Override
+ public String toString() {
+ return "BedrockEntityDefinition[identifier=" + identifier + ", registeredProperties=" + registeredProperties + "]";
+ }
+
+ 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..88f43f88410
--- /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.api.util.Identifier;
+
+/**
+ * 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(Identifier.of("armor_stand"));
+ ARROW = BedrockEntityDefinition.ofVanilla(Identifier.of("arrow"));
+ BOAT = BedrockEntityDefinition.ofVanilla(Identifier.of("boat"));
+ CHEST_BOAT = BedrockEntityDefinition.ofVanilla(Identifier.of("chest_boat"));
+ EVOCATION_ILLAGER = BedrockEntityDefinition.ofVanilla(Identifier.of("evocation_illager"));
+ LLAMA = BedrockEntityDefinition.ofVanilla(Identifier.of("llama"));
+ MINECART = BedrockEntityDefinition.ofVanilla(Identifier.of("minecart"));
+ SPLASH_POTION = BedrockEntityDefinition.ofVanilla(Identifier.of("splash_potion"));
+ ZOMBIE = BedrockEntityDefinition.ofVanilla(Identifier.of("zombie"));
+ }
+
+ public static void init() {
+ // no-op
+ }
+
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/CustomBedrockEntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/CustomBedrockEntityDefinition.java
new file mode 100644
index 00000000000..cc7e808117b
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/CustomBedrockEntityDefinition.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2025-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.entity;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.api.entity.custom.CustomEntityDefinition;
+import org.geysermc.geyser.api.util.Identifier;
+import org.geysermc.geyser.entity.properties.GeyserEntityProperties;
+import org.geysermc.geyser.registry.Registries;
+
+import java.util.Objects;
+
+public final class CustomBedrockEntityDefinition extends BedrockEntityDefinition implements CustomEntityDefinition {
+
+ public CustomBedrockEntityDefinition(@NonNull Identifier identifier, @NonNull GeyserEntityProperties registeredProperties) {
+ super(identifier, registeredProperties);
+ }
+
+ /**
+ * Retrieves an existing registered entity definition by identifier, or creates a new unregistered
+ * {@link CustomBedrockEntityDefinition} for the given identifier.
+ * Custom identifiers must not use the "minecraft" namespace.
+ */
+ public static CustomBedrockEntityDefinition getOrCreate(@NonNull Identifier identifier) {
+ Objects.requireNonNull(identifier, "identifier");
+ BedrockEntityDefinition existing = Registries.BEDROCK_ENTITY_DEFINITIONS.get().get(identifier);
+ if (existing instanceof CustomBedrockEntityDefinition custom) {
+ return custom;
+ }
+ if (identifier.vanilla()) {
+ throw new IllegalArgumentException("Cannot create custom entity in vanilla namespace! " + identifier);
+ }
+ return new CustomBedrockEntityDefinition(identifier, new GeyserEntityProperties());
+ }
+
+ @Override
+ public boolean vanilla() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "CustomBedrockEntityDefinition[identifier=" + identifier() + ", registeredProperties=" + registeredProperties() + "]";
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDataBehaviorRegistry.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDataBehaviorRegistry.java
new file mode 100644
index 00000000000..cfb2c5d45c0
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDataBehaviorRegistry.java
@@ -0,0 +1,193 @@
+/*
+ * 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.Object2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataType;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.GeyserLogger;
+import org.geysermc.geyser.api.entity.data.GeyserEntityDataType;
+import org.geysermc.geyser.api.entity.data.GeyserEntityDataTypes;
+import org.geysermc.geyser.api.entity.data.types.Hitbox;
+import org.geysermc.geyser.entity.type.Entity;
+import org.geysermc.geyser.impl.entity.HitboxImpl;
+import org.geysermc.geyser.session.GeyserSession;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public final class EntityDataBehaviorRegistry {
+
+ public static final Set> TRACKED_ENTITY_DATA;
+ private static final Map, EntityDataBehavior>> BEHAVIORS;
+ private static final GeyserLogger logger = GeyserImpl.getInstance().getLogger();
+
+ static {
+ Set> tracked = new ObjectOpenHashSet<>();
+ Map, EntityDataBehavior>> behaviors = new Object2ObjectOpenHashMap<>();
+
+ register(behaviors, tracked, GeyserEntityDataTypes.COLOR, EntityDataTypes.COLOR);
+ register(behaviors, tracked, GeyserEntityDataTypes.VARIANT, EntityDataTypes.VARIANT);
+ register(behaviors, tracked, GeyserEntityDataTypes.WIDTH, EntityDataTypes.WIDTH);
+ register(behaviors, tracked, GeyserEntityDataTypes.HEIGHT, EntityDataTypes.HEIGHT);
+ register(behaviors, tracked, GeyserEntityDataTypes.SCALE, EntityDataTypes.SCALE);
+ register(behaviors, tracked, GeyserEntityDataTypes.SEAT_OFFSET, EntityDataTypes.SEAT_OFFSET);
+ register(behaviors, tracked, GeyserEntityDataTypes.ROTATION_LOCKED_TO_VEHICLE, EntityDataTypes.SEAT_LOCK_RIDER_ROTATION);
+ register(behaviors, tracked, GeyserEntityDataTypes.SEAT_LOCK_RIDER_ROTATION_DEGREES, EntityDataTypes.SEAT_LOCK_RIDER_ROTATION_DEGREES);
+ register(behaviors, tracked, GeyserEntityDataTypes.SEAT_HAS_ROTATION, EntityDataTypes.SEAT_HAS_ROTATION);
+ register(behaviors, tracked, GeyserEntityDataTypes.ROTATE_RIDER_DEGREES, EntityDataTypes.SEAT_ROTATION_OFFSET_DEGREES);
+
+ // The vertical offset lives on the entity itself rather than as metadata, but it
+ // still exposes a value (the effective offset) and an override (the API-set offset, if any).
+ behaviors.put(GeyserEntityDataTypes.VERTICAL_OFFSET, new EntityDataBehavior() {
+ @Override
+ public void set(@NonNull Entity entity, @Nullable Float value) {
+ entity.setOffsetOverride(value);
+ }
+
+ @Override
+ public Float value(@NonNull Entity entity) {
+ return entity.getOffset();
+ }
+
+ @Override
+ public @Nullable Float override(@NonNull Entity entity) {
+ return entity.getOffsetOverride();
+ }
+ });
+
+ // Hitboxes are stored in the metadata manager as an NbtMap
+ tracked.add(EntityDataTypes.HITBOX);
+ behaviors.put(GeyserEntityDataTypes.HITBOXES, new EntityDataBehavior
>() {
+ @Override
+ public void set(@NonNull Entity entity, @Nullable List value) {
+ entity.getMetadata().updateOverride(EntityDataTypes.HITBOX, HitboxImpl.toNbtMap(value));
+ }
+
+ @Override
+ public @Nullable List value(@NonNull Entity entity) {
+ return HitboxImpl.fromMetaData(entity.getMetadata().value(EntityDataTypes.HITBOX));
+ }
+
+ @Override
+ public @Nullable List override(@NonNull Entity entity) {
+ return HitboxImpl.fromMetaData(entity.getMetadata().override(EntityDataTypes.HITBOX));
+ }
+ });
+
+ TRACKED_ENTITY_DATA = Collections.unmodifiableSet(tracked);
+ BEHAVIORS = Collections.unmodifiableMap(behaviors);
+ }
+
+ /**
+ * Registers a data type whose value and override are stored in the entity's
+ * {@link GeyserEntityDataManager} under the given Bedrock data type.
+ */
+ private static void register(Map, EntityDataBehavior>> map,
+ Set> tracked,
+ GeyserEntityDataType type,
+ EntityDataType bedrockType) {
+ map.put(type, new EntityDataBehavior() {
+ @Override
+ public void set(@NonNull Entity entity, @Nullable T value) {
+ entity.getMetadata().updateOverride(bedrockType, value);
+ }
+
+ @Override
+ public @Nullable T value(@NonNull Entity entity) {
+ return entity.getMetadata().value(bedrockType);
+ }
+
+ @Override
+ public @Nullable T override(@NonNull Entity entity) {
+ return entity.getMetadata().override(bedrockType);
+ }
+ });
+ tracked.add(bedrockType);
+ }
+
+ public static void update(@NonNull Entity entity, @NonNull GeyserEntityDataType type, @Nullable T value) {
+ EntityDataBehavior behavior = behavior(type);
+ GeyserSession session = entity.getSession();
+ if (logger.isDebug()) {
+ logger.debug(session, "Custom entity API: Updating %s to %s", type.identifier(), value);
+ }
+
+ behavior.set(entity, value);
+ }
+
+ /**
+ * Reads the value currently shown to the Bedrock client: the override if one is set, otherwise the base value.
+ */
+ public static @Nullable T value(@NonNull Entity entity, @NonNull GeyserEntityDataType type) {
+ return behavior(type).value(entity);
+ }
+
+ /**
+ * Reads only the active API override, or {@code null} if none has been set.
+ */
+ public static @Nullable T override(@NonNull Entity entity, @NonNull GeyserEntityDataType type) {
+ return behavior(type).override(entity);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static EntityDataBehavior behavior(@NonNull GeyserEntityDataType type) {
+ EntityDataBehavior behavior = (EntityDataBehavior) BEHAVIORS.get(type);
+ if (behavior == null) {
+ throw new IllegalArgumentException("Unknown entity data type: " + type.identifier()
+ + "; only types defined in GeyserEntityDataTypes are supported");
+ }
+ return behavior;
+ }
+
+ private EntityDataBehaviorRegistry() {}
+
+ /**
+ * Encapsulates how a single {@link GeyserEntityDataType} is stored and read for an entity
+ */
+ private interface EntityDataBehavior {
+ /**
+ * Applies the API override for this data type, or clears it if {@code value} is {@code null}.
+ */
+ void set(@NonNull Entity entity, @Nullable T value);
+
+ /**
+ * The value currently shown to the Bedrock client: the override if set, otherwise the base value.
+ */
+ @Nullable T value(@NonNull Entity entity);
+
+ /**
+ * The active API override, or {@code null} if none has been set.
+ */
+ @Nullable T override(@NonNull Entity entity);
+ }
+}
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/EntitySpectateHelper.java b/core/src/main/java/org/geysermc/geyser/entity/EntitySpectateHelper.java
new file mode 100644
index 00000000000..978b99e8797
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/EntitySpectateHelper.java
@@ -0,0 +1,184 @@
+/*
+ * 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.entity;
+
+import org.cloudburstmc.math.vector.Vector2f;
+import org.cloudburstmc.math.vector.Vector3f;
+import org.cloudburstmc.protocol.bedrock.data.camera.CameraEase;
+import org.cloudburstmc.protocol.bedrock.data.camera.CameraSetInstruction;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
+import org.cloudburstmc.protocol.bedrock.packet.CameraInstructionPacket;
+import org.cloudburstmc.protocol.common.util.OptionalBoolean;
+import org.geysermc.geyser.api.bedrock.camera.CameraPerspective;
+import org.geysermc.geyser.api.entity.data.GeyserEntityDataTypes;
+import org.geysermc.geyser.entity.type.Entity;
+import org.geysermc.geyser.impl.camera.CameraDefinitions;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.util.MathUtils;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+public final class EntitySpectateHelper {
+
+ private static final int FIRST_PERSON_PRESET = CameraPerspective.FIRST_PERSON.ordinal();
+ private static final int FREE_PRESET = CameraPerspective.FREE.ordinal();
+ private static final double DISTANCE = 4.0;
+ private static final float EYE_HEIGHT_RATIO = 0.85f;
+
+ private EntitySpectateHelper() {
+ }
+
+ public enum SpectateMode {
+ FIRST_PERSON,
+ THIRD_PERSON_BACK,
+ THIRD_PERSON_FRONT;
+
+ private static final SpectateMode[] VALUES = values();
+
+ public SpectateMode next() {
+ return VALUES[(ordinal() + 1) % VALUES.length];
+ }
+ }
+
+ public static boolean isSpectating(GeyserSession session) {
+ return session.getSpectatedEntity() != null;
+ }
+
+ public static void start(GeyserSession session, Entity target) {
+ Entity previous = session.getSpectatedEntity();
+ session.setSpectatedEntity(target);
+ session.setSpectateMode(SpectateMode.FIRST_PERSON);
+ if (previous != null && previous != target) {
+ previous.sendSpectateInvisible(false);
+ }
+ setSelfHidden(session, true);
+ apply(session, target, SpectateMode.FIRST_PERSON);
+ }
+
+ public static void stop(GeyserSession session) {
+ Entity target = session.getSpectatedEntity();
+ if (target == null) {
+ return;
+ }
+ session.setSpectatedEntity(null);
+ session.setSpectateMode(SpectateMode.FIRST_PERSON);
+ if (target.isValid()) {
+ target.sendSpectateInvisible(false);
+ }
+ setSelfHidden(session, false);
+ // Hand the camera back as the default first-person preset, then clear a few ticks later to re-enable F5. Clearing directly
+ // gets ignored after a reconnect while a camera set is still in flight, but a set instruction restores the view either way.
+ CameraInstructionPacket reset = new CameraInstructionPacket();
+ CameraSetInstruction defaultCamera = new CameraSetInstruction();
+ defaultCamera.setPreset(CameraDefinitions.getById(FIRST_PERSON_PRESET));
+ defaultCamera.setDefaultPreset(OptionalBoolean.of(true));
+ reset.setSetInstruction(defaultCamera);
+ session.sendUpstreamPacket(reset);
+ session.scheduleInEventLoop(() -> {
+ CameraInstructionPacket clear = new CameraInstructionPacket();
+ clear.setClear(true);
+ session.sendUpstreamPacket(clear);
+ }, 150, TimeUnit.MILLISECONDS);
+ }
+
+ public static void cycleMode(GeyserSession session) {
+ Entity target = session.getSpectatedEntity();
+ if (target == null) {
+ return;
+ }
+ SpectateMode mode = session.getSpectateMode().next();
+ session.setSpectateMode(mode);
+ apply(session, target, mode);
+ }
+
+ public static void tick(GeyserSession session) {
+ Entity target = session.getSpectatedEntity();
+ if (target == null) {
+ return;
+ }
+ if (!target.isValid()) {
+ stop(session);
+ return;
+ }
+ sendCamera(session, target, session.getSpectateMode());
+ }
+
+ private static void apply(GeyserSession session, Entity target, SpectateMode mode) {
+ target.sendSpectateInvisible(mode == SpectateMode.FIRST_PERSON);
+ sendCamera(session, target, mode);
+ }
+
+ private static void setSelfHidden(GeyserSession session, boolean hidden) {
+ Entity self = session.getPlayerEntity();
+ self.setFlag(EntityFlag.INVISIBLE, hidden);
+ self.updateBedrockMetadata();
+ }
+
+ private static void sendCamera(GeyserSession session, Entity entity, SpectateMode mode) {
+ float yaw = entity.getHeadYaw();
+ float pitch = entity.getPitch();
+ Vector3f forward = MathUtils.calculateViewVector(pitch, yaw);
+
+ Vector3f pos = entity.position();
+ double eyeY = pos.getY() + eyeHeight(entity);
+
+ // First-person sits at the eye. Third-person offsets along the view vector (behind, or in front looking back)
+ double distance;
+ Vector2f rot;
+ switch (mode) {
+ case FIRST_PERSON -> {
+ distance = 0.0;
+ rot = Vector2f.from(pitch, yaw);
+ }
+ case THIRD_PERSON_BACK -> {
+ distance = -DISTANCE;
+ rot = Vector2f.from(pitch, yaw);
+ }
+ case THIRD_PERSON_FRONT -> {
+ distance = DISTANCE;
+ rot = Vector2f.from(-pitch, yaw + 180f);
+ }
+ default -> throw new IllegalStateException("Unexpected spectate mode: " + mode);
+ }
+ double cx = pos.getX() + forward.getX() * distance;
+ double cy = eyeY + forward.getY() * distance;
+ double cz = pos.getZ() + forward.getZ() * distance;
+
+ CameraInstructionPacket packet = new CameraInstructionPacket();
+ CameraSetInstruction set = new CameraSetInstruction();
+ set.setPreset(CameraDefinitions.getById(FREE_PRESET));
+ set.setPos(Vector3f.from(cx, cy, cz));
+ set.setRot(rot);
+ set.setEase(new CameraSetInstruction.EaseData(CameraEase.LINEAR, 0.15f));
+ packet.setSetInstruction(set);
+ session.sendUpstreamPacket(packet);
+ }
+
+ private static float eyeHeight(Entity entity) {
+ return Objects.requireNonNullElse(entity.override(GeyserEntityDataTypes.HEIGHT), entity.getBoundingBoxHeight()) * EYE_HEIGHT_RATIO;
+ }
+}
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..e476c737f0e
--- /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.getJavaDefinition().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..7fad9f9a6c9
--- /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.EntityType;
+
+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(EntityType EntityType);
+
+ @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/GeyserDirtyMetadata.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserDirtyMetadata.java
deleted file mode 100644
index 1ee84f4b5ef..00000000000
--- a/core/src/main/java/org/geysermc/geyser/entity/GeyserDirtyMetadata.java
+++ /dev/null
@@ -1,68 +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.Object2ObjectLinkedOpenHashMap;
-import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataMap;
-import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataType;
-
-import java.util.Map;
-
-/**
- * A wrapper for temporarily storing entity metadata that will be sent to Bedrock.
- */
-public final class GeyserDirtyMetadata {
- private final Map, Object> metadata = new Object2ObjectLinkedOpenHashMap<>();
-
- public void put(EntityDataType entityData, T value) {
- metadata.put(entityData, value);
- }
-
- /**
- * Applies the contents of the dirty metadata into the input and clears the contents of our map.
- */
- public void apply(EntityDataMap map) {
- map.putAll(metadata);
- metadata.clear();
- }
-
- public boolean hasEntries() {
- return !metadata.isEmpty();
- }
-
- /**
- * Intended for testing purposes only
- */
- public T get(EntityDataType entityData) {
- //noinspection unchecked
- return (T) metadata.get(entityData);
- }
-
- @Override
- public String toString() {
- return metadata.toString();
- }
-}
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..5d7f77d6372 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
@@ -29,23 +29,23 @@
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;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
public class GeyserEntityData implements EntityData {
private final GeyserSession session;
- private final Set movementLockOwners = new HashSet<>();
+ private final Set movementLockOwners = ConcurrentHashMap.newKeySet();
public GeyserEntityData(GeyserSession session) {
this.session = session;
@@ -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!");
@@ -88,8 +112,10 @@ public boolean lockMovement(boolean lock, @NonNull UUID owner) {
movementLockOwners.remove(owner);
}
- session.setLockInput(InputLocksFlag.MOVEMENT, isMovementLocked());
- session.updateInputLocks();
+ session.executeInEventLoop(() -> {
+ session.setLockInput(InputLocksFlag.MOVEMENT, isMovementLocked());
+ session.updateInputLocks();
+ });
return isMovementLocked();
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityDataManager.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityDataManager.java
new file mode 100644
index 00000000000..2ea5f271059
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityDataManager.java
@@ -0,0 +1,130 @@
+/*
+ * 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.Object2ObjectLinkedOpenHashMap;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataMap;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataType;
+import org.geysermc.geyser.api.entity.data.GeyserEntityDataType;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A wrapper for temporarily storing entity metadata that will be sent to Bedrock.
+ */
+public final class GeyserEntityDataManager {
+
+ /**
+ * Map storing all current values of tracked metadata that's made available in the API.
+ */
+ private final Map, Object> metadata = new ConcurrentHashMap<>();
+
+ /**
+ * Map storing the metadata updates until they're sent to Bedrock, then cleared.
+ */
+ private final Map