Skip to content

Feature: Custom Entity API#6010

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

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

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/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
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 +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 +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.

…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.
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.

Comment on lines 650 to 655
attachedBlocks.forEach((blockPos, state) -> {
blockPos = blockPos.add(movement);
// Don't place blocks that collide with the player
if (!SOLID_BOUNDING_BOX.checkIntersection(blockPos.toDouble(), playerBoundingBox)) {
ChunkUtils.updateBlock(session, state, blockPos);
ChunkUtils.updateBlock(session, session.getGeyser().getWorldManager().blockAt(session, blockPos), blockPos);
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

placeFinalBlocks() iterates attachedBlocks.forEach((blockPos, state) -> ...), but the final block placement ignores state and instead re-reads the current world state via worldManager().blockAt(...). This will typically re-place the moving block/air (or whatever is currently there), not the intended final pushed block state.

Use the state from attachedBlocks when calling ChunkUtils.updateBlock(...) so the piston finalization places the correct block.

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