Skip to content

Feature: Custom Entity API#6010

Open
onebeastchris wants to merge 96 commits into
masterfrom
feature/custom-entities-api
Open

Feature: Custom Entity API#6010
onebeastchris wants to merge 96 commits into
masterfrom
feature/custom-entities-api

Conversation

@onebeastchris
Copy link
Copy Markdown
Member

@onebeastchris onebeastchris commented Nov 25, 2025

Introducing: Custom Entities!

tl;dr: with this PR, you can summon custom bedrock entities that replace the vanilla mapping

Additions

  • CustomEntityDefinition / GeyserEntityDefinition: Representations of custom, and vanilla Bedrock entities. Unlike custom blocks/items, bedrock entities have fewer properties that are defined in advance; it's just the identifier, and the Bedrock Entity Properties. Setting entity properties was introduced in a previous PR; and works the same way.

  • JavaEntityType: Represents a vanilla Java entity type, with the width / height / type identifier, as well as the default Bedrock entity associated with it. Similarly, CustomJavaEntityType represents a non-vanilla Java entity - however, that part of the API still needs some more work and should be regarded as unstable.

  • GeyserEntityDataType / GeyserEntityDataTypes: These are representations of various Bedrock entity metadata types, such as scale, width, height, variant, or color. Further, vertical_offset has been added as a "custom" data type to allow setting a vertical entity offset.

  • The GeyserEntity class has seen major additions! You can now query the entities' associated Bedrock entity definition, Java position, the Geyser id, UUID, or update / query the aforementioned data types.

  • You can now look up GeyserEntity instances using the entity UUID or Geyser entity ID, additionally to the Java entity id.

New Events

  • GeyserDefineEntitiesEvent: Allows registering custom Bedrock entities and querying existing entities.

  • SessionSpawnEntityEvent: Base entity spawn event extended by the server events. With it, you can set a pre-spawn consumer, and switch the Bedrock entity definition, or cancel the entity spawn outright.

  • ServerAttachParrotsEvent: Called every time a parrot is spawned on the player entity

  • ServerSpawnEntityEvent: Called for every non-player entity that is spawned by the Java server. Within this event, you can query the Java entity type, uuid, and entity id (and also have access to the methods provided by the SessionSpawnEntityEvent!

Here's some example code of the API in action:
https://gist.github.com/onebeastchris/1521ab585669792a79a9558d9d069834

image image

Internal changes:

  • Bedrock entity definitions are now split from the Java entity definitions.
  • There is now a EntitySpawnContext that is passed in entity constructors - instead of many arguments. This should make it easier to add new entities, call events, or add more arguments in the future
  • Entity offsets are now properly handled in the base Entity class - no more hacks in TntEntity and the like!

TO-DO's:

  • Finish VanillaEntityBases split; potentially allow non-vanilla entities to extend those?
  • Debug logging for modifications
  • Sensible limits for scale / height / width
  • Docs

EXPERIMENTAL downloads:

Copilot AI review requested due to automatic review settings November 25, 2025 21:38
@onebeastchris onebeastchris added Work in Progress The issue is currently being worked on. PR: Feature When a PR implements a new feature API The issue/feature request relates to the Geyser API labels Nov 25, 2025
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a Custom Entity API for Geyser, refactoring entity creation to use a context-based approach instead of individual constructor parameters. The changes enable support for custom entities while modernizing the entity type system.

Key Changes:

  • Introduced EntitySpawnContext as a unified way to pass entity creation parameters
  • Renamed EntityDefinition to EntityTypeDefinition for clarity
  • Changed EntityType to BuiltinEntityType to distinguish vanilla entities from custom ones
  • Added new registries for custom entities and Bedrock entity definitions
  • Refactored 100+ entity class constructors to use the new context pattern

Reviewed changes

Copilot reviewed 192 out of 193 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
gradle/libs.versions.toml Updated mcprotocollib version to feature branch for custom entities
Test files Updated mock entity creation to use EntitySpawnContext
StatisticsUtils.java Changed entity name translation to use GeyserEntityType
EntityUtils.java Refactored entity type comparisons from switch to if-else with .is() method
Translator classes Updated to use BuiltinEntityType and new entity creation patterns
Session/cache classes Updated entity instantiation with EntitySpawnContext
Registry classes Added new registries for custom entity support
Entity hierarchy All entity constructors refactored to accept EntitySpawnContext

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

Comment thread core/src/main/java/org/geysermc/geyser/util/EntityUtils.java
Comment thread core/src/main/java/org/geysermc/geyser/util/EntityUtils.java
Comment thread core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java Outdated
Comment thread core/src/main/java/org/geysermc/geyser/entity/BedrockEntityDefinition.java Outdated
Copilot AI review requested due to automatic review settings November 27, 2025 21:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 222 out of 223 changed files in this pull request and generated 11 comments.

Comments suppressed due to low confidence (1)

core/src/main/java/org/geysermc/geyser/session/GeyserSession.java:610


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

Comment thread core/src/main/java/org/geysermc/geyser/util/EntityUtils.java
Comment thread core/src/main/java/org/geysermc/geyser/util/EntityUtils.java
Comment thread core/src/main/java/org/geysermc/geyser/session/cache/waypoint/GeyserWaypoint.java Outdated
Comment thread core/src/main/java/org/geysermc/geyser/entity/GeyserEntityType.java Outdated
Comment thread core/src/main/java/org/geysermc/geyser/entity/type/Entity.java Outdated
Comment thread core/src/main/java/org/geysermc/geyser/entity/type/Entity.java Outdated
Comment thread core/src/main/java/org/geysermc/geyser/entity/type/Entity.java Outdated
Comment thread core/src/main/java/org/geysermc/geyser/entity/type/Entity.java
Copilot AI review requested due to automatic review settings April 23, 2026 23:52
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 95 out of 96 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 119 to 126
protected void moveAbsoluteImmediate(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {
float offset = definition.offset();
float offset = this.offset;
if (waterLevel.join() == 0) { // Item is in a full block of water
// Move the item entity down so it doesn't float above the water
offset = -definition.offset();
offset = -this.offset;
}
setOffset(offset);
super.moveAbsoluteImmediate(position, 0, 0, 0, isOnGround, teleported);
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moveAbsoluteImmediate uses this.offset as the base offset and then calls setOffset(offset), which mutates this.offset. Once the item enters water and the offset is negated, the stored offset stays negative and won't reset when the item leaves water. Consider keeping an immutable/base offset (e.g., from the entity type definition) and computing the water adjustment from that, instead of reusing the mutable offset field.

Copilot uses AI. Check for mistakes.
Comment on lines 158 to +164
protected final GeyserDirtyMetadata dirtyMetadata = new GeyserDirtyMetadata();
/**
* A container storing all current metadata for an entity.
*/
// TODO only store what is needed for API
protected final Map<EntityDataType<?>, Object> metadata = new Object2ObjectLinkedOpenHashMap<>();

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metadata is introduced as the source of truth for GeyserEntity#value(...), but nothing in this class ever writes to it (all updates go to dirtyMetadata and then get cleared on apply(...)). As a result, value(...) will always return null/stale values for data types backed by EntityDataTypes (e.g., color/variant/hitboxes). Consider updating metadata whenever a metadata field is changed (e.g., a helper that writes to both dirtyMetadata and metadata, or extending GeyserDirtyMetadata to also persist applied entries).

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 27, 2026 01:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 96 out of 97 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

core/src/main/java/org/geysermc/geyser/entity/type/Entity.java:456

  • The new metadata map is used by the Custom Entity API (GeyserEntityDataImpl.value() reads from entity.getMetadata()), but spawnEntity()/updateBedrockMetadata() only apply dirtyMetadata to outgoing packets and never persist those values into this.metadata. As a result, GeyserEntity#value(...) will always return null for data backed by EntityDataTypes (and list types like hitboxes).

Persist applied dirty metadata into this.metadata (and keep it in sync on subsequent updates), e.g., by merging the dirty entries into metadata before/while clearing them.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…tities-api

# Conflicts:
#	core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
#	core/src/main/java/org/geysermc/geyser/translator/level/block/entity/PistonBlockEntity.java
Copilot AI review requested due to automatic review settings April 29, 2026 00:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 97 out of 98 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

core/src/main/java/org/geysermc/geyser/entity/type/Entity.java:459

  • Entity now exposes API methods to query/update entity data types via a local metadata map, but updateBedrockMetadata() only applies dirtyMetadata to outgoing packets and never persists those values into metadata. As a result, GeyserEntity#value(...) will return null/stale values even after updates. Persist dirty entries (and flags/scale/width/height defaults) into metadata when they change (e.g., when applying dirtyMetadata, or inside each setter/update call).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…tities-api

# Conflicts:
#	core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java
…tities-api

# Conflicts:
#	core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
Copilot AI review requested due to automatic review settings May 3, 2026 20:52
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 97 out of 98 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 159 to +168
/**
* A container to store temporary metadata before it's sent to Bedrock.
*/
protected final GeyserDirtyMetadata dirtyMetadata = new GeyserDirtyMetadata();
/**
* A container storing all current metadata for an entity.
*/
// TODO only store what is needed for API
protected final Map<EntityDataType<?>, Object> metadata = new Object2ObjectLinkedOpenHashMap<>();

Comment on lines +75 to +87
public GeyserEntityDataImpl(Class<T> typeClass, String name, EntityDataType<T> type) {
this.typeClass = typeClass;
this.name = name;
this.consumer = (entity, data) -> entity.getDirtyMetadata().put(type, data);
this.getter = entity -> {
var value = entity.getMetadata().get(type);
// Always the case!
if (typeClass.isInstance(value)) {
return typeClass.cast(value);
} else {
return null;
}
};
Comment on lines +693 to +701
public void setCustomBoundingBoxWidth(float width) {
this.customBoundingBoxWidth = width;
dirtyMetadata.put(EntityDataTypes.WIDTH, customBoundingBoxWidth);
}

public void setCustomBoundingBoxHeight(float height) {
this.customBoundingBoxHeight = height;
dirtyMetadata.put(EntityDataTypes.HEIGHT, customBoundingBoxHeight);
}
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Remove debug exception

Fix: client crash because no furnace recipes are sent

# Conflicts:
#	core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 100 out of 101 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 162 to +168
protected final GeyserDirtyMetadata dirtyMetadata = new GeyserDirtyMetadata();
/**
* A container storing all current metadata for an entity.
*/
// TODO only store what is needed for API
protected final Map<EntityDataType<?>, Object> metadata = new Object2ObjectLinkedOpenHashMap<>();

Comment on lines +146 to +171
if (GameProtocol.is1_26_20orHigher(session.protocolVersion())) {
int placeholderRecipes = 0;

SlotDisplay stoneSlotDisplay = new ItemSlotDisplay(Items.STONE.javaId());
FurnaceRecipeDisplay placeholderFurnaceRecipe = new FurnaceRecipeDisplay(stoneSlotDisplay, new AnyFuelSlotDisplay(), stoneSlotDisplay, stoneSlotDisplay, 1, 1.0F);

for (GeyserShapelessRecipe.FurnaceRecipeType type : GeyserShapelessRecipe.FurnaceRecipeType.values()) {
// Bedrock HAS to have a recipe or else it will crash on 1.26.20 and above, so send a bogus recipe
// Again, very hacky, FIXME please
if (!knownFurnaceRecipes.contains(type)) {
int id = Integer.MIN_VALUE + placeholderRecipes;
GeyserRecipe geyserRecipe = new GeyserShapelessRecipe(id, netId, placeholderFurnaceRecipe, type.categories().get(0));

List<RecipeData> recipeData = geyserRecipe.asRecipeData(session);
craftingDataPacket.getCraftingData().addAll(recipeData);

List<String> bedrockRecipeIds = new ArrayList<>();
for (int i = 0; i < recipeData.size(); i++) {
String recipeId = id + "_" + i;
recipesPacket.getUnlockedRecipes().add(recipeId);
bedrockRecipeIds.add(recipeId);
geyserRecipes.put(netId++, geyserRecipe);
}
javaToBedrockRecipeIds.put(id, List.copyOf(bedrockRecipeIds));
placeholderRecipes++;
}
…tities-api

# Conflicts:
#	api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java
#	api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java
#	api/src/main/java/org/geysermc/geyser/api/entity/type/GeyserEntity.java
#	core/src/main/java/org/geysermc/geyser/item/hashing/DataComponentHashers.java
#	core/src/main/java/org/geysermc/geyser/item/hashing/MinecraftHasher.java
#	core/src/main/java/org/geysermc/geyser/item/parser/ItemStackParser.java
#	core/src/main/java/org/geysermc/geyser/registry/Registries.java
#	core/src/main/java/org/geysermc/geyser/session/cache/EntityCache.java
#	core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRecipeBookAddTranslator.java
#	gradle/libs.versions.toml
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API The issue/feature request relates to the Geyser API PR: Feature When a PR implements a new feature PR: Needs Testing When a PR needs testing but is currently not under review Work in Progress The issue is currently being worked on.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants