diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/CustomEntityDefinition.java b/api/src/main/java/org/geysermc/geyser/api/entity/CustomEntityDefinition.java new file mode 100644 index 000000000..18e99b966 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/entity/CustomEntityDefinition.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.entity; + +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.geyser.api.GeyserApi; +import org.geysermc.geyser.api.predicate.MinecraftPredicate; +import org.geysermc.geyser.api.predicate.PredicateStrategy; +import org.geysermc.geyser.api.predicate.context.entity.EntitySpawnContext; +import org.geysermc.geyser.api.util.GenericBuilder; + +import java.util.List; + +public interface CustomEntityDefinition { + + // TODO Identifier + String bedrockIdentifier(); + + float width(); + + float height(); + + float offset(); + + List> predicates(); + + PredicateStrategy predicateStrategy(); + + static Builder builder(@NonNull String bedrockIdentifier, @NonNull JavaEntityType vanillaType) { + return GeyserApi.api().provider(Builder.class, bedrockIdentifier, vanillaType); + } + + interface Builder extends GenericBuilder { + + @This + Builder width(@Positive float width); + + @This + Builder height(@Positive float height); + + @This + Builder heightAndWidth(@Positive float value); + + @This + Builder offset(@Positive float offset); + + @This + Builder predicate(@NonNull MinecraftPredicate predicate); + + @This + Builder predicateStrategy(@NonNull PredicateStrategy strategy); + + @Override + CustomEntityDefinition build(); + } +} diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/JavaEntityType.java b/api/src/main/java/org/geysermc/geyser/api/entity/JavaEntityType.java index 454aa2735..853a014c6 100644 --- a/api/src/main/java/org/geysermc/geyser/api/entity/JavaEntityType.java +++ b/api/src/main/java/org/geysermc/geyser/api/entity/JavaEntityType.java @@ -38,6 +38,8 @@ public interface JavaEntityType { boolean isUnregistered(); + boolean vanilla(); + default boolean is(Identifier javaIdentifier) { return javaIdentifier().equals(javaIdentifier); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java index 4311b564e..28978bb89 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinition.java @@ -55,15 +55,13 @@ import java.util.function.BiConsumer; @ToString(callSuper = true) public abstract class EntityDefinition extends EntityDefinitionBase { private final EntityFactory factory; - private final GeyserEntityType entityType; private final String bedrockIdentifier; private final GeyserEntityProperties registeredProperties; - public EntityDefinition(EntityFactory factory, GeyserEntityType entityType, String bedrockIdentifier, + public EntityDefinition(EntityFactory factory, String bedrockIdentifier, float width, float height, float offset, GeyserEntityProperties registeredProperties, List> translators) { super(width, height, offset, translators); this.factory = factory; - this.entityType = entityType; this.bedrockIdentifier = bedrockIdentifier; this.registeredProperties = registeredProperties; } @@ -72,8 +70,6 @@ public abstract class EntityDefinition extends EntityDefinitio @Accessors(fluent = true, chain = true) public static abstract class Builder extends EntityDefinitionBase.Builder { protected final EntityFactory factory; - @Setter(AccessLevel.NONE) - protected GeyserEntityType type; protected String bedrockIdentifier; @Setter(AccessLevel.NONE) protected GeyserEntityProperties.Builder propertiesBuilder; @@ -91,15 +87,6 @@ public abstract class EntityDefinition extends EntityDefinitio this.offset = offset; } - /** - * Resets the bedrock identifier as well - */ - public Builder type(GeyserEntityType type) { - this.type = type; - this.bedrockIdentifier = null; - return this; - } - @Override public Builder width(float width) { return (Builder) super.width(width); @@ -137,13 +124,5 @@ public abstract class EntityDefinition extends EntityDefinitio propertiesBuilder.add(propertyType); return this; } - - protected void validateTypeAndIdentifier() { - if (type == null) { - throw new IllegalStateException("Missing entity type!"); - } else if (bedrockIdentifier == null) { - bedrockIdentifier = type.javaIdentifier().toString(); - } - } } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitionBase.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitionBase.java index 518491e66..cfcb3d8ca 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitionBase.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitionBase.java @@ -61,7 +61,9 @@ public class EntityDefinitionBase { return new Builder<>(clazz); } - public static Builder baseInherited(EntityDefinitionBase parent) { + // Unused param so Java knows what entity we're talking about + @SuppressWarnings("unused") + public static Builder baseInherited(Class clazz, EntityDefinitionBase parent) { return new Builder<>(parent.width(), parent.height(), parent.offset(), new ObjectArrayList<>(parent.translators())); } @@ -97,7 +99,8 @@ public class EntityDefinitionBase { } // Unused param so Java knows what entity we're talking about - protected Builder(@SuppressWarnings("unused") Class clazz) { + @SuppressWarnings("unused") + protected Builder(Class clazz) { this(); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitionBases.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitionBases.java new file mode 100644 index 000000000..d3a9a6a6d --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitionBases.java @@ -0,0 +1,165 @@ +/* + * 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.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.geysermc.geyser.entity.type.BoatEntity; +import org.geysermc.geyser.entity.type.ChestBoatEntity; +import org.geysermc.geyser.entity.type.DisplayBaseEntity; +import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.FireballEntity; +import org.geysermc.geyser.entity.type.HangingEntity; +import org.geysermc.geyser.entity.type.LivingEntity; +import org.geysermc.geyser.entity.type.ThrowableItemEntity; +import org.geysermc.geyser.entity.type.living.AbstractFishEntity; +import org.geysermc.geyser.entity.type.living.AgeableEntity; +import org.geysermc.geyser.entity.type.living.MobEntity; +import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity; +import org.geysermc.geyser.entity.type.living.animal.tameable.TameableEntity; +import org.geysermc.geyser.entity.type.living.monster.BasePiglinEntity; +import org.geysermc.geyser.entity.type.living.monster.raid.RaidParticipantEntity; +import org.geysermc.geyser.entity.type.living.monster.raid.SpellcasterIllagerEntity; +import org.geysermc.geyser.entity.type.player.AvatarEntity; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataTypes; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; + +public final class EntityDefinitionBases { + public static final EntityDefinitionBase ENTITY; + public static final EntityDefinitionBase DISPLAY; + public static final EntityDefinitionBase FIREBALL; + public static final EntityDefinitionBase THROWABLE; + public static final EntityDefinitionBase HANGING; + public static final EntityDefinitionBase BOAT; + public static final EntityDefinitionBase CHEST_BOAT; + public static final EntityDefinitionBase LIVING_ENTITY; + public static final EntityDefinitionBase AVATAR; + public static final EntityDefinitionBase MOB; + public static final EntityDefinitionBase FISH; + public static final EntityDefinitionBase PIGLIN; + public static final EntityDefinitionBase RAID_PARTICIPANT; + public static final EntityDefinitionBase SPELLCASTER; + public static final EntityDefinitionBase AGEABLE; + public static final EntityDefinitionBase HORSE; + public static final EntityDefinitionBase TAMABLE; + + static { + ENTITY = EntityDefinition.baseBuilder(Entity.class) + .addTranslator(MetadataTypes.BYTE, Entity::setFlags) + .addTranslator(MetadataTypes.INT, Entity::setAir) // Air/bubbles + .addTranslator(MetadataTypes.OPTIONAL_COMPONENT, Entity::setDisplayName) + .addTranslator(MetadataTypes.BOOLEAN, Entity::setDisplayNameVisible) + .addTranslator(MetadataTypes.BOOLEAN, Entity::setSilent) + .addTranslator(MetadataTypes.BOOLEAN, Entity::setGravity) + .addTranslator(MetadataTypes.POSE, (entity, entityMetadata) -> entity.setPose(entityMetadata.getValue())) + .addTranslator(MetadataTypes.INT, Entity::setFreezing) + .build(); + DISPLAY = EntityDefinitionBase.baseInherited(DisplayBaseEntity.class, ENTITY) + .addTranslator(null) // Interpolation delay + .addTranslator(null) // Transformation interpolation duration + .addTranslator(null) // Position/Rotation interpolation duration + .addTranslator(MetadataTypes.VECTOR3, DisplayBaseEntity::setTranslation) // Translation + .addTranslator(null) // Scale + .addTranslator(null) // Left rotation + .addTranslator(null) // Right rotation + .addTranslator(null) // Billboard render constraints + .addTranslator(null) // Brightness override + .addTranslator(null) // View range + .addTranslator(null) // Shadow radius + .addTranslator(null) // Shadow strength + .addTranslator(null) // Width + .addTranslator(null) // Height + .addTranslator(null) // Glow color override + .build(); + FIREBALL = EntityDefinitionBase.baseInherited(FireballEntity.class, ENTITY) + .addTranslator(null) // Item + .build(); + THROWABLE = EntityDefinitionBase.baseInherited(ThrowableItemEntity.class, ENTITY) + .addTranslator(MetadataTypes.ITEM_STACK, ThrowableItemEntity::setItem) + .build(); + HANGING = EntityDefinitionBase.baseInherited(HangingEntity.class, ENTITY) + .addTranslator(MetadataTypes.DIRECTION, HangingEntity::setDirectionMetadata) + .build(); + BOAT = EntityDefinitionBase.baseInherited(BoatEntity.class, ENTITY) + .height(0.6f).width(1.6f) + .offset(0.35f) + .addTranslator(MetadataTypes.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityDataTypes.HURT_TICKS, entityMetadata.getValue())) // Time since last hit + .addTranslator(MetadataTypes.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityDataTypes.HURT_DIRECTION, entityMetadata.getValue())) // Rocking direction + .addTranslator(MetadataTypes.FLOAT, (boatEntity, entityMetadata) -> + // 'Health' in Bedrock, damage taken in Java - it makes motion in Bedrock + boatEntity.getDirtyMetadata().put(EntityDataTypes.STRUCTURAL_INTEGRITY, 40 - ((int) ((FloatEntityMetadata) entityMetadata).getPrimitiveValue()))) + .addTranslator(MetadataTypes.BOOLEAN, BoatEntity::setPaddlingLeft) + .addTranslator(MetadataTypes.BOOLEAN, BoatEntity::setPaddlingRight) + .addTranslator(MetadataTypes.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityDataTypes.BOAT_BUBBLE_TIME, entityMetadata.getValue())) // May not actually do anything + .build(); + CHEST_BOAT = EntityDefinitionBase.baseInherited(ChestBoatEntity.class, BOAT) + .build(); + LIVING_ENTITY = EntityDefinitionBase.baseInherited(LivingEntity.class, ENTITY) + .addTranslator(MetadataTypes.BYTE, LivingEntity::setLivingEntityFlags) + .addTranslator(MetadataTypes.FLOAT, LivingEntity::setHealth) + .addTranslator(MetadataTypes.PARTICLES, LivingEntity::setParticles) + .addTranslator(MetadataTypes.BOOLEAN, + (livingEntity, entityMetadata) -> livingEntity.getDirtyMetadata().put(EntityDataTypes.EFFECT_AMBIENCE, (byte) (((BooleanEntityMetadata) entityMetadata).getPrimitiveValue() ? 1 : 0))) + .addTranslator(null) // Arrow count + .addTranslator(null) // Stinger count + .addTranslator(MetadataTypes.OPTIONAL_BLOCK_POS, LivingEntity::setBedPosition) + .build(); + AVATAR = EntityDefinitionBase.baseInherited(AvatarEntity.class, LIVING_ENTITY) + .height(1.8f).width(0.6f) + .offset(1.62f) + .addTranslator(null) // Player main hand + .addTranslator(MetadataTypes.BYTE, AvatarEntity::setSkinVisibility) + .build(); + MOB = EntityDefinitionBase.baseInherited(MobEntity.class, LIVING_ENTITY) + .addTranslator(MetadataTypes.BYTE, MobEntity::setMobFlags) + .build(); + FISH = EntityDefinitionBase.baseInherited(AbstractFishEntity.class, MOB) + .addTranslator(null) // From bucket + .build(); + PIGLIN = EntityDefinitionBase.baseInherited(BasePiglinEntity.class, MOB) + .addTranslator(MetadataTypes.BOOLEAN, BasePiglinEntity::setImmuneToZombification) + .build(); + RAID_PARTICIPANT = EntityDefinitionBase.baseInherited(RaidParticipantEntity.class, MOB) + .addTranslator(null) // Celebrating //TODO + .build(); + SPELLCASTER = EntityDefinitionBase.baseInherited(SpellcasterIllagerEntity.class, RAID_PARTICIPANT) + .addTranslator(MetadataTypes.BYTE, SpellcasterIllagerEntity::setSpellType) + .build(); + AGEABLE = EntityDefinitionBase.baseInherited(AgeableEntity.class, MOB) + .addTranslator(MetadataTypes.BOOLEAN, AgeableEntity::setBaby) + .build(); + HORSE = EntityDefinitionBase.baseInherited(AbstractHorseEntity.class, AGEABLE) + .addTranslator(MetadataTypes.BYTE, AbstractHorseEntity::setHorseFlags) + .build(); + TAMABLE = EntityDefinitionBase.baseInherited(TameableEntity.class, AGEABLE) + .addTranslator(MetadataTypes.BYTE, TameableEntity::setTameableFlags) + .addTranslator(MetadataTypes.OPTIONAL_LIVING_ENTITY_REFERENCE, TameableEntity::setOwner) + .build(); + } + + private EntityDefinitionBases() { + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java index 7796b6346..bf3008930 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -337,7 +337,7 @@ public final class EntityDefinitions { public static final VanillaEntityDefinition WITHER_SKULL_DANGEROUS; static { - EntityDefinition entityBase = VanillaEntityDefinition.builder(Entity::new) + EntityDefinitionBase entityBase = EntityDefinition.baseBuilder(Entity.class) .addTranslator(MetadataTypes.BYTE, Entity::setFlags) .addTranslator(MetadataTypes.INT, Entity::setAir) // Air/bubbles .addTranslator(MetadataTypes.OPTIONAL_COMPONENT, Entity::setDisplayName) @@ -432,7 +432,7 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.INT, TNTEntity::setFuseLength) .build(); - VanillaEntityDefinition displayBase = VanillaEntityDefinition.inherited(DisplayBaseEntity::new, entityBase) + EntityDefinitionBase displayBase = EntityDefinitionBase.baseInherited(DisplayBaseEntity.class, entityBase) .addTranslator(null) // Interpolation delay .addTranslator(null) // Transformation interpolation duration .addTranslator(null) // Position/Rotation interpolation duration @@ -469,7 +469,7 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.BOOLEAN, InteractionEntity::setResponse) .build(); - VanillaEntityDefinition fireballBase = VanillaEntityDefinition.inherited(FireballEntity::new, entityBase) + EntityDefinitionBase fireballBase = EntityDefinitionBase.baseInherited(FireballEntity.class, entityBase) .addTranslator(null) // Item .build(); FIREBALL = VanillaEntityDefinition.inherited(FireballEntity::new, fireballBase) @@ -481,7 +481,7 @@ public final class EntityDefinitions { .heightAndWidth(0.3125f) .build(); - VanillaEntityDefinition throwableItemBase = VanillaEntityDefinition.inherited(ThrowableItemEntity::new, entityBase) + EntityDefinitionBase throwableItemBase = EntityDefinitionBase.baseInherited(ThrowableItemEntity.class, entityBase) .addTranslator(MetadataTypes.ITEM_STACK, ThrowableItemEntity::setItem) .build(); EGG = VanillaEntityDefinition.inherited(ThrowableEggEntity::new, throwableItemBase) @@ -525,7 +525,7 @@ public final class EntityDefinitions { .heightAndWidth(0.3125f) .build(); - VanillaEntityDefinition abstractArrowBase = VanillaEntityDefinition.inherited(AbstractArrowEntity::new, entityBase) + EntityDefinitionBase abstractArrowBase = EntityDefinitionBase.baseInherited(AbstractArrowEntity.class, entityBase) .addTranslator(MetadataTypes.BYTE, AbstractArrowEntity::setArrowFlags) .addTranslator(null) // "Piercing level" .addTranslator(null) // If the arrow is in the ground @@ -535,7 +535,7 @@ public final class EntityDefinitions { .heightAndWidth(0.25f) .addTranslator(MetadataTypes.INT, ArrowEntity::setPotionEffectColor) .build(); - SPECTRAL_ARROW = VanillaEntityDefinition.inherited(abstractArrowBase.factory(), abstractArrowBase) + SPECTRAL_ARROW = VanillaEntityDefinition.inherited(AbstractArrowEntity::new, abstractArrowBase) .type(BuiltinEntityType.SPECTRAL_ARROW) .heightAndWidth(0.25f) .bedrockIdentifier("minecraft:arrow") @@ -547,7 +547,7 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.BOOLEAN, (tridentEntity, entityMetadata) -> tridentEntity.setFlag(EntityFlag.ENCHANTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .build(); - VanillaEntityDefinition hangingEntityBase = VanillaEntityDefinition.inherited(null, entityBase) + EntityDefinitionBase hangingEntityBase = EntityDefinitionBase.baseInherited(HangingEntity.class, entityBase) .addTranslator(MetadataTypes.DIRECTION, HangingEntity::setDirectionMetadata) .build(); @@ -613,7 +613,7 @@ public final class EntityDefinitions { // Boats { - VanillaEntityDefinition boatBase = VanillaEntityDefinition.inherited(null, entityBase) + EntityDefinitionBase boatBase = EntityDefinitionBase.baseInherited(BoatEntity.class, entityBase) .height(0.6f).width(1.6f) .offset(0.35f) .addTranslator(MetadataTypes.INT, (boatEntity, entityMetadata) -> boatEntity.getDirtyMetadata().put(EntityDataTypes.HURT_TICKS, entityMetadata.getValue())) // Time since last hit @@ -637,7 +637,7 @@ public final class EntityDefinitions { SPRUCE_BOAT = buildBoat(boatBase, BuiltinEntityType.SPRUCE_BOAT, BoatEntity.BoatVariant.SPRUCE); PALE_OAK_BOAT = buildBoat(boatBase, BuiltinEntityType.PALE_OAK_BOAT, BoatEntity.BoatVariant.PALE_OAK); - VanillaEntityDefinition chestBoatBase = VanillaEntityDefinition.inherited(null, boatBase) + EntityDefinitionBase chestBoatBase = EntityDefinitionBase.baseInherited(ChestBoatEntity.class, boatBase) .build(); ACACIA_CHEST_BOAT = buildChestBoat(chestBoatBase, BuiltinEntityType.ACACIA_CHEST_BOAT, BoatEntity.BoatVariant.ACACIA); @@ -652,7 +652,7 @@ public final class EntityDefinitions { PALE_OAK_CHEST_BOAT = buildChestBoat(chestBoatBase, BuiltinEntityType.PALE_OAK_CHEST_BOAT, BoatEntity.BoatVariant.PALE_OAK); } - VanillaEntityDefinition livingEntityBase = VanillaEntityDefinition.inherited(LivingEntity::new, entityBase) + EntityDefinitionBase livingEntityBase = EntityDefinitionBase.baseInherited(LivingEntity.class, entityBase) .addTranslator(MetadataTypes.BYTE, LivingEntity::setLivingEntityFlags) .addTranslator(MetadataTypes.FLOAT, LivingEntity::setHealth) .addTranslator(MetadataTypes.PARTICLES, LivingEntity::setParticles) @@ -675,7 +675,7 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.ROTATIONS, ArmorStandEntity::setRightLegRotation) .build(); - VanillaEntityDefinition avatarEntityBase = VanillaEntityDefinition.inherited(null, livingEntityBase) + EntityDefinitionBase avatarEntityBase = EntityDefinitionBase.baseInherited(AvatarEntity.class, livingEntityBase) .height(1.8f).width(0.6f) .offset(1.62f) .addTranslator(null) // Player main hand @@ -697,7 +697,7 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, PlayerEntity::setRightParrot) .build(); - VanillaEntityDefinition mobEntityBase = VanillaEntityDefinition.inherited(MobEntity::new, livingEntityBase) + EntityDefinitionBase mobEntityBase = EntityDefinitionBase.baseInherited(MobEntity.class, livingEntityBase) .addTranslator(MetadataTypes.BYTE, MobEntity::setMobFlags) .build(); @@ -907,10 +907,10 @@ public final class EntityDefinitions { .type(BuiltinEntityType.MAGMA_CUBE) .build(); - VanillaEntityDefinition abstractFishEntityBase = VanillaEntityDefinition.inherited(AbstractFishEntity::new, mobEntityBase) + EntityDefinitionBase abstractFishEntityBase = EntityDefinitionBase.baseInherited(AbstractFishEntity.class, mobEntityBase) .addTranslator(null) // From bucket .build(); - COD = VanillaEntityDefinition.inherited(abstractFishEntityBase.factory(), abstractFishEntityBase) + COD = VanillaEntityDefinition.inherited(AbstractFishEntity::new, abstractFishEntityBase) .type(BuiltinEntityType.COD) .height(0.25f).width(0.5f) .build(); @@ -919,7 +919,7 @@ public final class EntityDefinitions { .heightAndWidth(0.7f) .addTranslator(MetadataTypes.INT, PufferFishEntity::setPufferfishSize) .build(); - SALMON = VanillaEntityDefinition.inherited(abstractFishEntityBase.factory(), abstractFishEntityBase) + SALMON = VanillaEntityDefinition.inherited(AbstractFishEntity::new, abstractFishEntityBase) .type(BuiltinEntityType.SALMON) .height(0.5f).width(0.7f) .addTranslator(null) // Scale/variant - TODO @@ -935,7 +935,7 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.INT, TropicalFishEntity::setFishVariant) .build(); - VanillaEntityDefinition abstractPiglinEntityBase = VanillaEntityDefinition.inherited(BasePiglinEntity::new, mobEntityBase) + EntityDefinitionBase abstractPiglinEntityBase = EntityDefinitionBase.baseInherited(BasePiglinEntity.class, mobEntityBase) .addTranslator(MetadataTypes.BOOLEAN, BasePiglinEntity::setImmuneToZombification) .build(); PIGLIN = VanillaEntityDefinition.inherited(PiglinEntity::new, abstractPiglinEntityBase) @@ -945,23 +945,23 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.BOOLEAN, PiglinEntity::setChargingCrossbow) .addTranslator(MetadataTypes.BOOLEAN, PiglinEntity::setDancing) .build(); - PIGLIN_BRUTE = VanillaEntityDefinition.inherited(abstractPiglinEntityBase.factory(), abstractPiglinEntityBase) + PIGLIN_BRUTE = VanillaEntityDefinition.inherited(BasePiglinEntity::new, abstractPiglinEntityBase) .type(BuiltinEntityType.PIGLIN_BRUTE) .height(1.95f).width(0.6f) .build(); - VanillaEntityDefinition raidParticipantEntityBase = VanillaEntityDefinition.inherited(RaidParticipantEntity::new, mobEntityBase) + EntityDefinitionBase raidParticipantEntityBase = EntityDefinitionBase.baseInherited(RaidParticipantEntity.class, mobEntityBase) .addTranslator(null) // Celebrating //TODO .build(); - VanillaEntityDefinition spellcasterEntityBase = VanillaEntityDefinition.inherited(SpellcasterIllagerEntity::new, raidParticipantEntityBase) + EntityDefinitionBase spellcasterEntityBase = EntityDefinitionBase.baseInherited(SpellcasterIllagerEntity.class, raidParticipantEntityBase) .addTranslator(MetadataTypes.BYTE, SpellcasterIllagerEntity::setSpellType) .build(); - EVOKER = VanillaEntityDefinition.inherited(spellcasterEntityBase.factory(), spellcasterEntityBase) + EVOKER = VanillaEntityDefinition.inherited(SpellcasterIllagerEntity::new, spellcasterEntityBase) .type(BuiltinEntityType.EVOKER) .height(1.95f).width(0.6f) .bedrockIdentifier("minecraft:evocation_illager") .build(); - ILLUSIONER = VanillaEntityDefinition.inherited(spellcasterEntityBase.factory(), spellcasterEntityBase) + ILLUSIONER = VanillaEntityDefinition.inherited(SpellcasterIllagerEntity::new, spellcasterEntityBase) .type(BuiltinEntityType.ILLUSIONER) .height(1.95f).width(0.6f) .bedrockIdentifier("minecraft:evocation_illager") @@ -981,7 +981,7 @@ public final class EntityDefinitions { .height(1.8f).width(0.6f) .offset(1.62f) .build(); - WITCH = VanillaEntityDefinition.inherited(raidParticipantEntityBase.factory(), raidParticipantEntityBase) + WITCH = VanillaEntityDefinition.inherited(RaidParticipantEntity::new, raidParticipantEntityBase) .type(BuiltinEntityType.WITCH) .height(1.8f).width(0.6f) .offset(1.62f) @@ -989,7 +989,7 @@ public final class EntityDefinitions { .build(); } - VanillaEntityDefinition ageableEntityBase = VanillaEntityDefinition.inherited(AgeableEntity::new, mobEntityBase) + EntityDefinitionBase ageableEntityBase = EntityDefinitionBase.baseInherited(AgeableEntity.class, mobEntityBase) .addTranslator(MetadataTypes.BOOLEAN, AgeableEntity::setBaby) .build(); @@ -1164,7 +1164,7 @@ public final class EntityDefinitions { // Horses { - VanillaEntityDefinition abstractHorseEntityBase = VanillaEntityDefinition.inherited(AbstractHorseEntity::new, ageableEntityBase) + EntityDefinitionBase abstractHorseEntityBase = EntityDefinitionBase.baseInherited(AbstractHorseEntity.class, ageableEntityBase) .addTranslator(MetadataTypes.BYTE, AbstractHorseEntity::setHorseFlags) .build(); CAMEL = VanillaEntityDefinition.inherited(CamelEntity::new, abstractHorseEntityBase) @@ -1186,14 +1186,14 @@ public final class EntityDefinitions { .type(BuiltinEntityType.ZOMBIE_HORSE) .height(1.6f).width(1.3965f) .build(); - VanillaEntityDefinition chestedHorseEntityBase = VanillaEntityDefinition.inherited(ChestedHorseEntity::new, abstractHorseEntityBase) + EntityDefinitionBase chestedHorseEntityBase = EntityDefinitionBase.baseInherited(ChestedHorseEntity.class, abstractHorseEntityBase) .addTranslator(MetadataTypes.BOOLEAN, (horseEntity, entityMetadata) -> horseEntity.setFlag(EntityFlag.CHESTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .build(); - DONKEY = VanillaEntityDefinition.inherited(chestedHorseEntityBase.factory(), chestedHorseEntityBase) + DONKEY = VanillaEntityDefinition.inherited(ChestedHorseEntity::new, chestedHorseEntityBase) .type(BuiltinEntityType.DONKEY) .height(1.6f).width(1.3965f) .build(); - MULE = VanillaEntityDefinition.inherited(chestedHorseEntityBase.factory(), chestedHorseEntityBase) + MULE = VanillaEntityDefinition.inherited(ChestedHorseEntity::new, chestedHorseEntityBase) .type(BuiltinEntityType.MULE) .height(1.6f).width(1.3965f) .build(); @@ -1209,7 +1209,7 @@ public final class EntityDefinitions { .build(); } - VanillaEntityDefinition tameableEntityBase = VanillaEntityDefinition.inherited(null, ageableEntityBase) // No factory, is abstract + EntityDefinitionBase tameableEntityBase = EntityDefinitionBase.baseInherited(TameableEntity.class, ageableEntityBase) .addTranslator(MetadataTypes.BYTE, TameableEntity::setTameableFlags) .addTranslator(MetadataTypes.OPTIONAL_LIVING_ENTITY_REFERENCE, TameableEntity::setOwner) .build(); @@ -1246,7 +1246,7 @@ public final class EntityDefinitions { Registries.JAVA_ENTITY_IDENTIFIERS.get().put("minecraft:marker", null); // We don't need an entity definition for this as it is never sent over the network } - private static VanillaEntityDefinition buildBoat(VanillaEntityDefinition base, BuiltinEntityType BuiltinEntityType, BoatEntity.BoatVariant variant) { + private static VanillaEntityDefinition buildBoat(EntityDefinitionBase base, BuiltinEntityType BuiltinEntityType, BoatEntity.BoatVariant variant) { return VanillaEntityDefinition.inherited((session, javaId, bedrockId, uuid, definition, position, motion, yaw, pitch, headYaw) -> new BoatEntity(session, javaId, bedrockId, uuid, definition, position, motion, yaw, variant), base) .type(BuiltinEntityType) @@ -1254,7 +1254,7 @@ public final class EntityDefinitions { .build(); } - private static VanillaEntityDefinition buildChestBoat(VanillaEntityDefinition base, BuiltinEntityType BuiltinEntityType, BoatEntity.BoatVariant variant) { + private static VanillaEntityDefinition buildChestBoat(EntityDefinitionBase base, BuiltinEntityType BuiltinEntityType, BoatEntity.BoatVariant variant) { return VanillaEntityDefinition.inherited((session, javaId, bedrockId, uuid, definition, position, motion, yaw, pitch, headYaw) -> new ChestBoatEntity(session, javaId, bedrockId, uuid, definition, position, motion, yaw, variant), base) .type(BuiltinEntityType) diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserCustomEntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserCustomEntityDefinition.java new file mode 100644 index 000000000..899b79824 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserCustomEntityDefinition.java @@ -0,0 +1,152 @@ +/* + * 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.ToString; +import lombok.experimental.Accessors; +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.geyser.api.entity.CustomEntityDefinition; +import org.geysermc.geyser.api.entity.JavaEntityType; +import org.geysermc.geyser.api.predicate.MinecraftPredicate; +import org.geysermc.geyser.api.predicate.PredicateStrategy; +import org.geysermc.geyser.api.predicate.context.entity.EntitySpawnContext; +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 java.util.List; +import java.util.Objects; +import java.util.function.BiConsumer; + +@Getter +@Accessors(fluent = true) +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class GeyserCustomEntityDefinition extends EntityDefinition implements CustomEntityDefinition { + private final List> predicates; + private final PredicateStrategy predicateStrategy; + + public GeyserCustomEntityDefinition(EntityFactory factory, String bedrockIdentifier, List> predicates, PredicateStrategy predicateStrategy, + float width, float height, float offset, GeyserEntityProperties registeredProperties, List> translators) { + super(factory, bedrockIdentifier, width, height, offset, registeredProperties, translators); + this.predicates = predicates; + this.predicateStrategy = predicateStrategy; + } + + public static Builder inherited(String bedrockIdentifier, JavaEntityType vanillaType) { + if (!vanillaType.vanilla()) { + throw new IllegalArgumentException("vanillaType must be a vanilla entity type, was: " + vanillaType); + } + VanillaEntityDefinition parent = Registries.ENTITY_DEFINITIONS.get(vanillaType); + if (parent == null) { + throw new IllegalArgumentException("No vanilla entity definition registered for vanilla entity type " + vanillaType); + } + // TODO fix the rawtypes/unchecked + return new Builder<>(bedrockIdentifier, parent.factory(), parent.width(), parent.height(), parent.offset(), new ObjectArrayList(parent.translators())); + } + + public static class Builder extends EntityDefinition.Builder implements CustomEntityDefinition.Builder { + protected List> predicates; + protected PredicateStrategy predicateStrategy = PredicateStrategy.AND; + + protected Builder(EntityFactory factory, String bedrockIdentifier) { + super(factory); + this.bedrockIdentifier = Objects.requireNonNull(bedrockIdentifier, "bedrockIdentifier must not be null"); + } + + protected Builder(String bedrockIdentifier, EntityFactory factory, float width, float height, float offset, List> translators) { + super(factory, width, height, offset, translators); + this.bedrockIdentifier = Objects.requireNonNull(bedrockIdentifier, "bedrockIdentifier must not be null"); + } + + @Override + public EntityDefinition.Builder bedrockIdentifier(String bedrockIdentifier) { + throw new UnsupportedOperationException("bedrockIdentifier should be immutable"); + } + + @Override + public Builder width(@Positive float width) { + return (Builder) super.width(width); + } + + @Override + public Builder height(@Positive float height) { + return (Builder) super.height(height); + } + + @Override + public Builder heightAndWidth(@Positive float value) { + return (Builder) super.heightAndWidth(value); + } + + @Override + public Builder offset(@Positive float offset) { + return (Builder) super.offset(offset); + } + + public Builder predicate(@NonNull MinecraftPredicate predicate) { + predicates.add(Objects.requireNonNull(predicate, "predicate must not be null")); + return this; + } + + public Builder predicateStrategy(@NonNull PredicateStrategy strategy) { + predicateStrategy = Objects.requireNonNull(strategy, "strategy must not be null"); + return this; + } + + @Override + public Builder property(PropertyType propertyType) { + return (Builder) super.property(propertyType); + } + + @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); + } + + @Override + public GeyserCustomEntityDefinition build() { + if (predicates.isEmpty()) { + throw new IllegalStateException("predicates must not be empty!"); + } + return new GeyserCustomEntityDefinition<>(factory, bedrockIdentifier, predicates, predicateStrategy, width, height, offset, propertiesBuilder.build(), translators); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityType.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityType.java index 3a3a156cb..db961c916 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityType.java +++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityType.java @@ -72,6 +72,11 @@ public record GeyserEntityType(Identifier javaIdentifier, int javaId) implements return javaIdentifier.equals(UNREGISTERED); } + @Override + public boolean vanilla() { + return VANILLA.containsValue(this); + } + public boolean is(EntityType type) { return javaId == type.id(); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/VanillaEntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/VanillaEntityDefinition.java index 6350407dc..8bb3423a7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/VanillaEntityDefinition.java +++ b/core/src/main/java/org/geysermc/geyser/entity/VanillaEntityDefinition.java @@ -48,21 +48,24 @@ import java.util.function.BiConsumer; @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class VanillaEntityDefinition extends EntityDefinition { + private final GeyserEntityType entityType; public VanillaEntityDefinition(EntityFactory factory, GeyserEntityType entityType, String bedrockIdentifier, float width, float height, float offset, GeyserEntityProperties registeredProperties, List> translators) { - super(factory, entityType, bedrockIdentifier, width, height, offset, registeredProperties, translators); + super(factory, bedrockIdentifier, width, height, offset, registeredProperties, translators); + this.entityType = entityType; } public static Builder builder(EntityFactory factory) { return new Builder<>(factory); } - public static Builder inherited(EntityFactory factory, EntityDefinition parent) { + public static Builder inherited(EntityFactory factory, EntityDefinitionBase parent) { return new Builder<>(factory, parent.width(), parent.height(), parent.offset(), new ObjectArrayList<>(parent.translators())); } public static class Builder extends EntityDefinition.Builder { + protected GeyserEntityType type; protected Builder(EntityFactory factory) { super(factory); @@ -72,8 +75,13 @@ public class VanillaEntityDefinition extends EntityDefinition< super(factory, width, height, offset, translators); } + /** + * Resets the bedrock identifier as well + */ public Builder type(BuiltinEntityType type) { - return (Builder) super.type(GeyserEntityType.ofVanilla(type)); + this.type = GeyserEntityType.ofVanilla(type); + this.bedrockIdentifier = null; + return this; } @Override @@ -121,6 +129,14 @@ public class VanillaEntityDefinition extends EntityDefinition< return build(true); } + private void validateTypeAndIdentifier() { + if (type == null) { + throw new IllegalStateException("Missing entity type!"); + } else if (bedrockIdentifier == null) { + bedrockIdentifier = type.javaIdentifier().toString(); + } + } + /** * @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. diff --git a/core/src/main/java/org/geysermc/geyser/registry/Registries.java b/core/src/main/java/org/geysermc/geyser/registry/Registries.java index e20efc192..9c70d892d 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -38,7 +38,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.PotionMixData; import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.api.entity.JavaEntityType; -import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.VanillaEntityDefinition; import org.geysermc.geyser.inventory.recipe.GeyserRecipe; import org.geysermc.geyser.item.type.Item; import org.geysermc.geyser.pack.ResourcePackHolder; @@ -65,14 +65,12 @@ import org.geysermc.geyser.translator.level.event.LevelEventTranslator; import org.geysermc.geyser.translator.sound.SoundInteractionTranslator; import org.geysermc.geyser.translator.sound.SoundTranslator; import org.geysermc.mcprotocollib.network.packet.Packet; -import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents; import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityType; import org.geysermc.mcprotocollib.protocol.data.game.level.event.LevelEvent; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType; import java.util.ArrayList; -import java.util.EnumMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Map; @@ -126,7 +124,8 @@ public final class Registries { * A map containing all entity types and their respective Geyser definitions */ // Is a Reference2ObjectMap since GeyserEntityType, the implementation of JavaEntityType, only ever keeps one instance per registered entity type - public static final SimpleMappedRegistry> ENTITY_DEFINITIONS = SimpleMappedRegistry.create(RegistryLoaders.empty(Reference2ObjectOpenHashMap::new)); + // TODO rename to VANILLA_ENTITY_DEFINITIONS + public static final SimpleMappedRegistry> ENTITY_DEFINITIONS = SimpleMappedRegistry.create(RegistryLoaders.empty(Reference2ObjectOpenHashMap::new)); /** * A registry holding a list of all the known entity properties to be sent to the client after start game. @@ -136,7 +135,7 @@ public final class Registries { /** * A map containing all Java entity identifiers and their respective Geyser definitions */ - public static final SimpleMappedRegistry> JAVA_ENTITY_IDENTIFIERS = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); + public static final SimpleMappedRegistry> JAVA_ENTITY_IDENTIFIERS = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new)); /** * A registry containing all the Java packet translators. diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java index 31ec971b4..5a2960c5f 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java @@ -34,6 +34,7 @@ import org.geysermc.geyser.api.block.custom.component.GeometryComponent; import org.geysermc.geyser.api.block.custom.component.MaterialInstance; import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState; import org.geysermc.geyser.api.command.Command; +import org.geysermc.geyser.api.entity.CustomEntityDefinition; import org.geysermc.geyser.api.entity.JavaEntityType; import org.geysermc.geyser.api.event.EventRegistrar; import org.geysermc.geyser.api.extension.Extension; @@ -46,6 +47,7 @@ import org.geysermc.geyser.api.pack.option.PriorityOption; import org.geysermc.geyser.api.pack.option.SubpackOption; import org.geysermc.geyser.api.pack.option.UrlFallbackOption; import org.geysermc.geyser.api.util.Identifier; +import org.geysermc.geyser.entity.GeyserCustomEntityDefinition; import org.geysermc.geyser.entity.GeyserEntityType; import org.geysermc.geyser.event.GeyserEventRegistrar; import org.geysermc.geyser.extension.command.GeyserExtensionCommand; @@ -113,6 +115,7 @@ public class ProviderRegistryLoader implements RegistryLoader, Prov // entities providers.put(JavaEntityType.class, args -> args.length == 1 ? GeyserEntityType.ofVanilla((Identifier) args[0]) : GeyserEntityType.createCustom((Identifier) args[0], (int) args[1])); + providers.put(CustomEntityDefinition.class, args -> GeyserCustomEntityDefinition.inherited((String) args[0], (JavaEntityType) args[1])); return providers; }