Skip to content

Feature: Custom Entity API#6010

Open
onebeastchris wants to merge 81 commits intomasterfrom
feature/custom-entities-api
Open

Feature: Custom Entity API#6010
onebeastchris wants to merge 81 commits intomasterfrom
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.

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.

Copilot AI review requested due to automatic review settings February 28, 2026 17:12
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 130 out of 131 changed files in this pull request and generated no new comments.


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

Copilot AI review requested due to automatic review settings March 6, 2026 08:39
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 130 out of 131 changed files in this pull request and generated 3 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

Copilot AI review requested due to automatic review settings March 14, 2026 19:42
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 130 out of 131 changed files in this pull request and generated 2 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +195 to +197
if (!context.callParrotEvent(this, variant.getAsInt(), !isLeft)) {
GeyserImpl.getInstance().getLogger().debug(session, "Cancelled parrot spawn as definition is null!");
return;
Comment on lines +449 to 450
GeyserImpl.getInstance().getLogger().debug("Client %s tried to request pack with an invalid id %s)",
session.bedrockUsername(), id);
This will be later reworked to allow setting a height + width combo that accounts for the entity pose. For now, it's a general override
Copilot AI review requested due to automatic review settings March 15, 2026 16:36
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 130 out of 131 changed files in this pull request and generated 2 comments.


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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 482 to 485
} else if (entity == null && this.vehicle instanceof BoatEntity) {
this.vehicle.setBoundingBoxWidth(this.vehicle.getDefinition().width());
this.vehicle.setBoundingBoxHeight(this.vehicle.getDefinition().height());
this.vehicle.setBoundingBoxWidth(this.vehicle.getBoundingBoxWidth());
this.vehicle.setBoundingBoxHeight(this.vehicle.getBoundingBoxHeight());
this.vehicle.updateBedrockMetadata();
Comment on lines +61 to +83
@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);
}
Copilot AI review requested due to automatic review settings March 20, 2026 23:07
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 130 out of 131 changed files in this pull request and generated 1 comment.


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

Comment on lines 477 to 486
if (entity instanceof BoatEntity) {
// These bounding box values are based off 1.21.7
entity.setBoundingBoxWidth(1.375F);
entity.setBoundingBoxHeight(0.5625F);
entity.updateBedrockMetadata();
} else if (entity == null && this.vehicle instanceof BoatEntity) {
this.vehicle.setBoundingBoxWidth(this.vehicle.getDefinition().width());
this.vehicle.setBoundingBoxHeight(this.vehicle.getDefinition().height());
this.vehicle.setBoundingBoxWidth(this.vehicle.getBoundingBoxWidth());
this.vehicle.setBoundingBoxHeight(this.vehicle.getBoundingBoxHeight());
this.vehicle.updateBedrockMetadata();
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

When dismounting a boat, this branch attempts to restore the boat bounding box back to its normal size, but it currently sets width/height to their existing values (no-op). As a result, the boat can remain with the enlarged bounding box after the player dismounts, contradicting the comment above and potentially affecting collision/movement. Restore the boat dimensions using the boat’s default size (e.g., from its type definition/base dimensions) instead of the current bounding box values.

Copilot uses AI. Check for mistakes.
…tities-api

# Conflicts:
#	core/src/main/java/org/geysermc/geyser/entity/spawn/EntitySpawnContext.java
#	core/src/main/java/org/geysermc/geyser/entity/type/BoatEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/Entity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/EvokerFangsEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/ItemEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/LightningEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/MinecartEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/TextDisplayEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/ThrowableItemEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/living/animal/OcelotEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/living/animal/SnifferEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/living/merchant/VillagerEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/living/monster/CreakingEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EnderDragonPartEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/living/monster/EndermanEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/player/AvatarEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java
#	core/src/main/java/org/geysermc/geyser/entity/vehicle/BoatVehicleComponent.java
#	core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java
#	core/src/main/java/org/geysermc/geyser/inventory/holder/BlockInventoryHolder.java
#	core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java
#	core/src/main/java/org/geysermc/geyser/session/cache/BlockBreakHandler.java
#	core/src/main/java/org/geysermc/geyser/session/cache/BossBar.java
#	core/src/main/java/org/geysermc/geyser/session/cache/WorldBorder.java
#	core/src/main/java/org/geysermc/geyser/translator/inventory/chest/DoubleChestInventoryTranslator.java
#	core/src/main/java/org/geysermc/geyser/translator/level/event/PlaySoundEventTranslator.java
#	core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
#	core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockMovePlayer.java
#	core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/input/BedrockPlayerAuthInputTranslator.java
#	core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java
#	core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerLookAtTranslator.java
#	core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java
#	core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaGameEventTranslator.java
#	core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelParticlesTranslator.java
#	core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java
Copilot AI review requested due to automatic review settings March 28, 2026 20:50
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 101 out of 102 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/ItemEntity.java:127

  • ItemEntity's offset field is updated inside the setOffset override, but moveAbsoluteImmediate uses this.offset as the base offset and then calls setOffset again (including with a negated value when in water). This means that once the offset is negated, the next tick in water will negate it again, causing the entity to flip offset every tick. Keep an immutable/base offset (e.g., from the entity type/initial context) and only apply the temporary sign change to the Bedrock offset without overwriting the base.

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

Comment on lines +97 to 106
public EntitySpawnContext(GeyserSession session, EntityTypeDefinition<?> definition, int javaId, UUID uuid, BedrockEntityDefinition bedrockEntityDefinition, Vector3f position,
Vector3f motion, float yaw, float pitch, float headYaw, float offset, @Nullable Long geyserId) {
this.session = session;
this.entityTypeDefinition = definition;
this.javaId = javaId;
this.uuid = uuid;
this.position = position;
this.offset = definition.offset();
this.bedrockEntityDefinition = bedrockEntityDefinition;
this.motion = motion;
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

EntitySpawnContext's constructor accepts an offset parameter but ignores it and always assigns this.offset = definition.offset();. This makes it impossible to override the per-spawn offset (e.g., for event-driven/custom entity adjustments) and contradicts the call sites that pass an explicit offset value. Use the provided offset parameter when assigning this.offset (and ensure the other constructors pass the intended value).

Copilot uses AI. Check for mistakes.
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