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 index 18e99b966..0b409bd16 100644 --- a/api/src/main/java/org/geysermc/geyser/api/entity/CustomEntityDefinition.java +++ b/api/src/main/java/org/geysermc/geyser/api/entity/CustomEntityDefinition.java @@ -29,12 +29,6 @@ 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 { @@ -47,15 +41,11 @@ public interface CustomEntityDefinition { 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 { + interface Builder { @This Builder width(@Positive float width); @@ -69,12 +59,6 @@ public interface CustomEntityDefinition { @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 853a014c6..07374b184 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 @@ -48,7 +48,7 @@ public interface JavaEntityType { return GeyserApi.api().provider(JavaEntityType.class, javaIdentifier); } - static JavaEntityType create(@NonNull Identifier javaIdentifier, @NonNegative int javaId) { + static JavaEntityType createAndRegister(@NonNull Identifier javaIdentifier, @NonNegative int javaId) { return GeyserApi.api().provider(JavaEntityType.class, javaIdentifier, javaId); } } diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomEntitiesEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomEntitiesEvent.java new file mode 100644 index 000000000..65fd52552 --- /dev/null +++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomEntitiesEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.event.lifecycle; + +import org.geysermc.event.Event; +import org.geysermc.geyser.api.entity.CustomEntityDefinition; + +import java.util.List; + +public interface GeyserDefineCustomEntitiesEvent extends Event { + + List existingCustomEntityDefinitions(); + + default void register(CustomEntityDefinition.Builder builder) { + register(builder.build()); + } + + void register(CustomEntityDefinition customEntityDefinition); +} 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 bf3008930..4e9030ed2 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -30,9 +30,11 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.entity.CustomEntityDefinition; import org.geysermc.geyser.api.entity.property.GeyserEntityProperty; import org.geysermc.geyser.api.entity.property.type.GeyserFloatEntityProperty; import org.geysermc.geyser.api.entity.property.type.GeyserStringEnumProperty; +import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomEntitiesEvent; import org.geysermc.geyser.api.event.lifecycle.GeyserDefineEntityPropertiesEvent; import org.geysermc.geyser.api.util.Identifier; import org.geysermc.geyser.entity.factory.EntityFactory; @@ -173,6 +175,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatE import org.geysermc.mcprotocollib.protocol.data.game.entity.type.BuiltinEntityType; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -1263,7 +1266,19 @@ public final class EntityDefinitions { } public static void init() { - // entities would be initialized before this event is called + // entities would be initialized before these events are called + GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineCustomEntitiesEvent() { + @Override + public List existingCustomEntityDefinitions() { + return Collections.unmodifiableList(Registries.CUSTOM_ENTITY_DEFINITIONS.get()); + } + + @Override + public void register(CustomEntityDefinition customEntityDefinition) { + Registries.CUSTOM_ENTITY_DEFINITIONS.register(Registries.CUSTOM_ENTITY_DEFINITIONS.get().size(), customEntityDefinition); + } + }); + GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineEntityPropertiesEvent() { @Override public GeyserFloatEntityProperty registerFloatProperty(@NonNull Identifier identifier, @NonNull Identifier propertyId, float min, float max, @Nullable Float defaultValue) { @@ -1345,13 +1360,13 @@ public final class EntityDefinitions { } } - private static void registerProperty(Identifier BuiltinEntityType, PropertyType property) { - var definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(BuiltinEntityType.toString()); + private static void registerProperty(Identifier entityType, PropertyType property) { + var definition = Registries.JAVA_ENTITY_IDENTIFIERS.get(entityType.toString()); if (definition == null) { - throw new IllegalArgumentException("Unknown entity type: " + BuiltinEntityType); + throw new IllegalArgumentException("Unknown entity type: " + entityType); } - definition.registeredProperties().add(BuiltinEntityType.toString(), property); + definition.registeredProperties().add(entityType.toString(), property); } private EntityDefinitions() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserCustomEntityDefinition.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserCustomEntityDefinition.java index 734d68119..84af91ce9 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/GeyserCustomEntityDefinition.java +++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserCustomEntityDefinition.java @@ -36,7 +36,7 @@ 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.api.predicate.context.entity.EntitySpawnPredicateContext; import org.geysermc.geyser.entity.factory.EntityFactory; import org.geysermc.geyser.entity.properties.GeyserEntityProperties; import org.geysermc.geyser.entity.properties.type.PropertyType; @@ -56,10 +56,10 @@ import java.util.function.BiConsumer; @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class GeyserCustomEntityDefinition extends EntityDefinition implements CustomEntityDefinition { - private final List> predicates; + private final List> predicates; private final PredicateStrategy predicateStrategy; - public GeyserCustomEntityDefinition(EntityFactory factory, String bedrockIdentifier, List> predicates, 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; @@ -84,7 +84,7 @@ public class GeyserCustomEntityDefinition extends EntityDefini } public static class Builder extends EntityDefinition.Builder implements CustomEntityDefinition.Builder { - protected List> predicates; + protected List> predicates; protected PredicateStrategy predicateStrategy = PredicateStrategy.AND; protected Builder(EntityFactory factory, String bedrockIdentifier) { @@ -122,7 +122,7 @@ public class GeyserCustomEntityDefinition extends EntityDefini return (Builder) super.offset(offset); } - public Builder predicate(@NonNull MinecraftPredicate predicate) { + public Builder predicate(@NonNull MinecraftPredicate predicate) { predicates.add(Objects.requireNonNull(predicate, "predicate must not be null")); return this; } 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 db961c916..7eb60e308 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityType.java +++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityType.java @@ -117,7 +117,7 @@ public record GeyserEntityType(Identifier javaIdentifier, int javaId) implements return type instanceof BuiltinEntityType builtin ? ofVanilla(builtin) : of(type.id()); } - public static GeyserEntityType createCustom(@NonNull Identifier javaIdentifier, @NonNegative int javaId) { + public static GeyserEntityType createCustomAndRegister(@NonNull Identifier javaIdentifier, @NonNegative int javaId) { Objects.requireNonNull(javaIdentifier, "javaIdentifier may not be null"); if (javaIdentifier.vanilla()) { throw new IllegalArgumentException("Cannot register custom entity type in vanilla namespace!" + javaIdentifier); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java index 8313226da..90ff6fa42 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Entity.java @@ -221,9 +221,7 @@ public class Entity implements GeyserEntity { flagsDirty = false; if (session.getGeyser().config().debugMode() && PRINT_ENTITY_SPAWN_DEBUG) { - JavaEntityType type = definition.entityType(); - String name = type != null ? type.javaIdentifier().toString() : getClass().getSimpleName(); - session.getGeyser().getLogger().debug("Spawned entity " + name + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")"); + session.getGeyser().getLogger().debug("Spawned entity " + definition.bedrockIdentifier() + " at location " + position + " with id " + geyserId + " (java id " + entityId + ")"); } } diff --git a/core/src/main/java/org/geysermc/geyser/registry/ListRegistry.java b/core/src/main/java/org/geysermc/geyser/registry/ListRegistry.java index 084835c2e..85dc5644b 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/ListRegistry.java +++ b/core/src/main/java/org/geysermc/geyser/registry/ListRegistry.java @@ -27,13 +27,15 @@ package org.geysermc.geyser.registry; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.registry.loader.RegistryLoader; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.function.Supplier; -public class ListRegistry extends Registry> { +public class ListRegistry extends Registry> implements Iterable { private boolean frozen = false; /** @@ -114,6 +116,11 @@ public class ListRegistry extends Registry> { return this.mappings.set(index, value); } + @Override + public @NotNull Iterator iterator() { + return get().iterator(); + } + /** * Mark this registry as unsuitable for new additions. The backing list will then be optimized for storage. */ 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 9c70d892d..a3ad2a0ac 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java +++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java @@ -37,7 +37,9 @@ import org.cloudburstmc.protocol.bedrock.data.biome.BiomeDefinitions; 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.CustomEntityDefinition; import org.geysermc.geyser.api.entity.JavaEntityType; +import org.geysermc.geyser.entity.GeyserCustomEntityDefinition; import org.geysermc.geyser.entity.VanillaEntityDefinition; import org.geysermc.geyser.inventory.recipe.GeyserRecipe; import org.geysermc.geyser.item.type.Item; @@ -127,6 +129,8 @@ public final class Registries { // TODO rename to VANILLA_ENTITY_DEFINITIONS public static final SimpleMappedRegistry> ENTITY_DEFINITIONS = SimpleMappedRegistry.create(RegistryLoaders.empty(Reference2ObjectOpenHashMap::new)); + public static final ListRegistry> CUSTOM_ENTITY_DEFINITIONS = ListRegistry.create(RegistryLoaders.empty(ArrayList::new)); + /** * A registry holding a list of all the known entity properties to be sent to the client after start game. */ 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 5a2960c5f..6f387aeee 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 @@ -114,7 +114,7 @@ public class ProviderRegistryLoader implements RegistryLoader, Prov providers.put(CameraPosition.Builder.class, args -> new GeyserCameraPosition.Builder()); // entities - providers.put(JavaEntityType.class, args -> args.length == 1 ? GeyserEntityType.ofVanilla((Identifier) args[0]) : GeyserEntityType.createCustom((Identifier) args[0], (int) args[1])); + providers.put(JavaEntityType.class, args -> args.length == 1 ? GeyserEntityType.ofVanilla((Identifier) args[0]) : GeyserEntityType.createCustomAndRegister((Identifier) args[0], (int) args[1])); providers.put(CustomEntityDefinition.class, args -> GeyserCustomEntityDefinition.inherited((String) args[0], (JavaEntityType) args[1])); return providers; diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java index b39785c20..77cec1f9c 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaAddEntityTranslator.java @@ -27,13 +27,16 @@ package org.geysermc.geyser.translator.protocol.java.entity; import org.cloudburstmc.math.vector.Vector3f; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.predicate.context.entity.EntitySpawnPredicateContext; import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.GeyserCustomEntityDefinition; import org.geysermc.geyser.entity.GeyserEntityType; import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.FallingBlockEntity; import org.geysermc.geyser.entity.type.FishingHookEntity; import org.geysermc.geyser.entity.type.HangingEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.impl.predicate.GeyserEntitySpawnPredicateContext; import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.skin.SkinManager; @@ -47,7 +50,6 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.object.FallingBlockD import org.geysermc.mcprotocollib.protocol.data.game.entity.object.ProjectileData; import org.geysermc.mcprotocollib.protocol.data.game.entity.object.WardenData; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.BuiltinEntityType; -import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundAddEntityPacket; @Translator(packet = ClientboundAddEntityPacket.class) @@ -58,9 +60,14 @@ public class JavaAddEntityTranslator extends PacketTranslator definition = Registries.ENTITY_DEFINITIONS.get(type); + if (type.isUnregistered()) { + session.getGeyser().getLogger().warning("Received unregistered entity type " + type + " in add entity packet"); + return; + } + + EntityDefinition definition = getEntityDefinition(session, type, packet); if (definition == null) { - session.getGeyser().getLogger().warning("Could not find an entity definition with type " + type); + session.getGeyser().getLogger().warning("Could not find an entity definition for add entity packet " + packet); return; } @@ -138,4 +145,14 @@ public class JavaAddEntityTranslator extends PacketTranslator getEntityDefinition(GeyserSession session, GeyserEntityType entityType, ClientboundAddEntityPacket packet) { + EntitySpawnPredicateContext context = new GeyserEntitySpawnPredicateContext(session, entityType, packet); + for (GeyserCustomEntityDefinition customEntityDefinition : Registries.CUSTOM_ENTITY_DEFINITIONS) { + if (customEntityDefinition.test(context)) { + return customEntityDefinition; + } + } + return Registries.ENTITY_DEFINITIONS.get(context.entityType()); + } }