From f2b07e8db1e2a6cf3a5b47393783bb367f2afdb2 Mon Sep 17 00:00:00 2001 From: kashike Date: Wed, 20 Sep 2017 13:03:03 -0700 Subject: [PATCH] Introduce entity entry builder, useful in the Register event replacing needed calls to EntityRegistry. (#4408) --- .../fml/common/registry/EntityEntry.java | 31 +- .../common/registry/EntityEntryBuilder.java | 370 ++++++++++++++++++ .../fml/common/registry/EntityRegistry.java | 7 + .../fml/relauncher/ReflectionHelper.java | 51 +++ .../minecraftforge/registries/GameData.java | 5 + 5 files changed, 443 insertions(+), 21 deletions(-) create mode 100644 src/main/java/net/minecraftforge/fml/common/registry/EntityEntryBuilder.java diff --git a/src/main/java/net/minecraftforge/fml/common/registry/EntityEntry.java b/src/main/java/net/minecraftforge/fml/common/registry/EntityEntry.java index 6bc6e5a9c..0aa5c76c0 100644 --- a/src/main/java/net/minecraftforge/fml/common/registry/EntityEntry.java +++ b/src/main/java/net/minecraftforge/fml/common/registry/EntityEntry.java @@ -18,21 +18,20 @@ */ package net.minecraftforge.fml.common.registry; -import java.lang.reflect.Constructor; - import net.minecraft.entity.Entity; import net.minecraft.entity.EntityList; import net.minecraft.entity.EntityList.EntityEggInfo; import net.minecraft.world.World; -import net.minecraftforge.fml.common.FMLLog; import net.minecraftforge.registries.IForgeRegistryEntry.Impl; +import java.util.function.Function; + public class EntityEntry extends Impl { private Class cls; private String name; private EntityEggInfo egg; - private Constructor ctr; + Function factory; public EntityEntry(Class cls, String name) { @@ -44,14 +43,12 @@ public class EntityEntry extends Impl //Protected method, to make this optional, in case people subclass this to have a better factory. protected void init() { - try - { - this.ctr = this.cls.getConstructor(World.class); - } - catch (NoSuchMethodException e) - { - throw new RuntimeException("Invalid class " + this.cls + " no constructor taking " + World.class.getName()); - } + this.factory = new EntityEntryBuilder.ConstructorFactory(this.cls) { + @Override + protected String describeEntity() { + return String.valueOf(EntityEntry.this.getRegistryName()); + } + }; } public Class getEntityClass(){ return this.cls; } @@ -67,14 +64,6 @@ public class EntityEntry extends Impl public Entity newInstance(World world) { - try - { - return (Entity)this.ctr.newInstance(world); - } - catch (Exception e) - { - FMLLog.log.error("Error creating entity.", e); - return null; - } + return this.factory.apply(world); } } diff --git a/src/main/java/net/minecraftforge/fml/common/registry/EntityEntryBuilder.java b/src/main/java/net/minecraftforge/fml/common/registry/EntityEntryBuilder.java new file mode 100644 index 000000000..325519ee1 --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/common/registry/EntityEntryBuilder.java @@ -0,0 +1,370 @@ +/* + * Minecraft Forge + * Copyright (c) 2016. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package net.minecraftforge.fml.common.registry; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityList; +import net.minecraft.entity.EntityLiving; +import net.minecraft.entity.EnumCreatureType; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraftforge.fml.common.FMLLog; +import net.minecraftforge.fml.common.Loader; +import net.minecraftforge.fml.common.ModContainer; +import net.minecraftforge.fml.relauncher.ReflectionHelper; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +/** + * An entity registry entry builder. + * + * @param The entity type + */ +public final class EntityEntryBuilder +{ + private final ModContainer mod; + @Nullable private Class entity; + @Nullable private Function factory; + @Nullable private ResourceLocation id; + private int network; + @Nullable private String name; + private int trackingRange; + private int trackingUpdateFrequency; + private boolean trackingVelocityUpdates; + private boolean eggProvided; + private int primaryEggColor; + private int secondaryEggColor; + @Nullable private Collection spawns; + + /** + * Creates a new entity entry builder. + * + * @param The entity type + * @return A new entity entry builder + */ + @Nonnull + public static EntityEntryBuilder create() + { + return new EntityEntryBuilder<>(); + } + + private EntityEntryBuilder() + { + this.mod = Loader.instance().activeModContainer(); + } + + /** + * Sets the class of the entity. + * + *

Entities will be constructed using a constructor accepting {@link World}. If you wish + * to use your own factory, use {@link #factory(Function)}.

+ * + * @param entity The entity class + * @return This builder + * @throws NullPointerException If {@code entity} is {@code null} + */ + @Nonnull + public final EntityEntryBuilder entity(@Nonnull final Class entity) + { + this.entity = checkNotNull(entity, "entity class"); + return this; + } + + /** + * Sets the factory of the entity. + * + * @param factory The entity factory + * @return This builder + * @throws NullPointerException If {@code entity} is {@code null} + */ + @Nonnull + public final EntityEntryBuilder factory(@Nonnull final Function factory) + { + this.factory = checkNotNull(factory, "entity factory"); + return this; + } + + /** + * Sets the id of the entity. + * + * @param id The entity id + * @param network The network id + * @return This builder + * @throws NullPointerException If {@code id} is {@code null} + */ + @Nonnull + public final EntityEntryBuilder id(@Nonnull final ResourceLocation id, final int network) + { + this.id = checkNotNull(id, "id"); + this.network = network; + return this; + } + + /** + * Sets the id of the entity. + * + * @param id The entity id + * @param network The network id + * @return This builder + * @throws NullPointerException If {@code id} is {@code null} + */ + @Nonnull + public final EntityEntryBuilder id(@Nonnull final String id, final int network) + { + return this.id(new ResourceLocation(checkNotNull(id, "id")), network); + } + + /** + * Sets the name of the entity. + * + * @param name The entity name + * @return This builder + * @throws NullPointerException If {@code name} is {@code null} + */ + @Nonnull + public final EntityEntryBuilder name(@Nonnull final String name) + { + this.name = checkNotNull(name, "name"); + return this; + } + + /** + * Sets entity tracking information. + * + * @param range The tracking range + * @param updateFrequency The tracking update frequency + * @param sendVelocityUpdates If the entity should send velocity updates + * @return This builder + */ + @Nonnull + public final EntityEntryBuilder tracker(final int range, final int updateFrequency, final boolean sendVelocityUpdates) + { + this.trackingRange = range; + this.trackingUpdateFrequency = updateFrequency; + this.trackingVelocityUpdates = sendVelocityUpdates; + return this; + } + + /** + * Adds a spawn entry. + * + * @param type The creature type + * @param weight The spawn entry weight + * @param min The minimum spawn count + * @param max The maximum spawn count + * @param biomes The biomes to add an entry in + * @return This builder + * @throws IllegalArgumentException If the entity is not a {@link EntityLiving} + * @throws NullPointerException If {@code type} is {@code null} + * @throws NullPointerException If {@code biomes} is {@code null} + */ + @Nonnull + public final EntityEntryBuilder spawn(@Nonnull final EnumCreatureType type, final int weight, final int min, final int max, @Nonnull final Biome... biomes) + { + checkNotNull(biomes, "biomes"); + return this.spawn(type, weight, min, max, Arrays.asList(biomes)); + } + + /** + * Adds a spawn entry. + * + * @param type The creature type + * @param weight The spawn entry weight + * @param min The minimum spawn count + * @param max The maximum spawn count + * @param biomes The biomes to add an entry in + * @return This builder + * @throws IllegalArgumentException If the entity is not a {@link EntityLiving} + * @throws NullPointerException If {@code type} is {@code null} + * @throws NullPointerException If {@code biomes} is {@code null} + */ + @Nonnull + public final EntityEntryBuilder spawn(@Nonnull final EnumCreatureType type, final int weight, final int min, final int max, @Nonnull final Iterable biomes) + { + checkNotNull(type, "type"); + checkNotNull(biomes, "biomes"); + if (this.entity != null) checkArgument(EntityLiving.class.isAssignableFrom(this.entity), "Cannot add spawns to a non-%s", EntityLiving.class.getSimpleName()); + if (this.spawns == null) this.spawns = new ArrayList<>(); + this.spawns.add(new Spawn(type, weight, min, max, biomes)); + return this; + } + + /** + * Sets the egg of the entity. + * + * @param primaryColor the primary egg color + * @param secondaryColor the secondary egg color + * @return This builder + */ + @Nonnull + public final EntityEntryBuilder egg(final int primaryColor, final int secondaryColor) + { + this.eggProvided = true; + this.primaryEggColor = primaryColor; + this.secondaryEggColor = secondaryColor; + return this; + } + + /** + * Create an entity entry based on the data in this builder. + * + * @return The entity entry + * @throws IllegalStateException If the entity class has not been provided + * @throws IllegalStateException If the entity id has not been provided + * @throws IllegalStateException If the entity name has not been provided + * @throws IllegalStateException If spawns have been provided for a non {@link EntityLiving} + * @throws ReflectionHelper.UnknownConstructorException If a {@link #factory} has not been provided + * and {@link #entity} does not have a constructor accepting {@link World} + */ + @Nonnull + public EntityEntry build() + { + checkState(this.entity != null, "entity class not provided"); + checkState(this.id != null, "entity id not provided"); + checkState(this.name != null, "entity name not provided"); + if (this.spawns != null) checkState(EntityLiving.class.isAssignableFrom(EntityEntryBuilder.this.entity), "Cannot add spawns to a non-%s", EntityLiving.class.getSimpleName()); + final BuiltEntityEntry entry = new BuiltEntityEntry(this.entity, this.name); + entry.factory = this.factory != null ? this.factory : new ConstructorFactory(this.entity) { + @Override + protected String describeEntity() { + return String.valueOf(EntityEntryBuilder.this.id); + } + }; + entry.setRegistryName(this.id); + if (this.eggProvided) entry.setEgg(new EntityList.EntityEggInfo(this.id, this.primaryEggColor, this.secondaryEggColor)); + return entry; + } + + @Nonnull + private EntityRegistry.EntityRegistration createRegistration() + { + return EntityRegistry.instance().new EntityRegistration( + this.mod, this.id, this.entity, this.name, this.network, + this.trackingRange, this.trackingUpdateFrequency, this.trackingVelocityUpdates + ); + } + + static abstract class ConstructorFactory implements Function + { + private final Constructor constructor; + + ConstructorFactory(final Class entity) + { + this.constructor = ReflectionHelper.findConstructor(entity, World.class); + } + + @Override + public E apply(final World world) + { + try + { + return this.constructor.newInstance(world); + } + catch (final IllegalAccessException | InstantiationException | InvocationTargetException e) + { + FMLLog.log.error("Encountered an exception while constructing entity '{}'", this.describeEntity(), e); + return null; + } + } + + protected abstract String describeEntity(); + } + + public final class BuiltEntityEntry extends EntityEntry + { + private boolean added; + + BuiltEntityEntry(final Class cls, final String name) + { + super(cls, name); + } + + @Override + protected final void init() { + // NOOP - we handle this in build + } + + @SuppressWarnings("ConstantConditions") + public final void addedToRegistry() + { + if (this.added) return; + this.added = true; + EntityRegistry.instance().insert(EntityEntryBuilder.this.entity, EntityEntryBuilder.this.createRegistration()); + if (EntityEntryBuilder.this.spawns != null) + { + for (final Spawn spawn : EntityEntryBuilder.this.spawns) + { + spawn.insert(); + } + EntityEntryBuilder.this.spawns = null; + } + } + } + + public final class Spawn + { + private final EnumCreatureType type; + private final int weight; + private final int min; + private final int max; + private final Iterable biomes; + + public Spawn(final EnumCreatureType type, final int weight, final int min, final int max, final Iterable biomes) + { + this.type = type; + this.weight = weight; + this.min = min; + this.max = max; + this.biomes = biomes; + } + + @SuppressWarnings("unchecked") + final void insert() + { + for (final Biome biome : this.biomes) { + final List entries = biome.getSpawnableList(this.type); + boolean found = false; + for (final Biome.SpawnListEntry entry : entries) { + if (entry.entityClass == EntityEntryBuilder.this.entity) { + entry.itemWeight = this.weight; + entry.minGroupCount = this.min; + entry.maxGroupCount = this.max; + found = true; + break; + } + } + if (!found) entries.add(new Biome.SpawnListEntry((Class) EntityEntryBuilder.this.entity, this.weight, this.min, this.max)); + } + } + } +} diff --git a/src/main/java/net/minecraftforge/fml/common/registry/EntityRegistry.java b/src/main/java/net/minecraftforge/fml/common/registry/EntityRegistry.java index 77abd68f1..a1de924ab 100644 --- a/src/main/java/net/minecraftforge/fml/common/registry/EntityRegistry.java +++ b/src/main/java/net/minecraftforge/fml/common/registry/EntityRegistry.java @@ -362,4 +362,11 @@ public class EntityRegistry } return null; } + + // This is an internal method - do not touch. + final void insert(final Class entity, final EntityRegistration registration) + { + this.entityClassRegistrations.put(entity, registration); + this.entityRegistrations.put(registration.container, registration); + } } diff --git a/src/main/java/net/minecraftforge/fml/relauncher/ReflectionHelper.java b/src/main/java/net/minecraftforge/fml/relauncher/ReflectionHelper.java index 434d56bfe..e5b7281a7 100644 --- a/src/main/java/net/minecraftforge/fml/relauncher/ReflectionHelper.java +++ b/src/main/java/net/minecraftforge/fml/relauncher/ReflectionHelper.java @@ -24,6 +24,8 @@ import org.apache.commons.lang3.StringUtils; import javax.annotation.Nonnull; import javax.annotation.Nullable; + +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; /** @@ -88,6 +90,14 @@ public class ReflectionHelper } } + public static class UnknownConstructorException extends RuntimeException + { + public UnknownConstructorException(final String message) + { + super(message); + } + } + public static Field findField(Class clazz, String... fieldNames) { Exception failed = null; @@ -220,4 +230,45 @@ public class ReflectionHelper throw new UnableToFindMethodException(e); } } + + /** + * Finds a constructor in the specified class that has matching parameter types. + * + * @param klass The class to find the constructor in + * @param parameterTypes The parameter types of the constructor. + * @param The type + * @return The constructor + * @throws NullPointerException if {@code klass} is null + * @throws NullPointerException if {@code parameterTypes} is null + * @throws UnknownConstructorException if the constructor could not be found + */ + @Nonnull + public static Constructor findConstructor(@Nonnull final Class klass, @Nonnull final Class... parameterTypes) + { + Preconditions.checkNotNull(klass, "class"); + Preconditions.checkNotNull(parameterTypes, "parameter types"); + + final Constructor constructor; + try + { + constructor = klass.getDeclaredConstructor(parameterTypes); + constructor.setAccessible(true); + } + catch (final NoSuchMethodException e) + { + final StringBuilder desc = new StringBuilder(); + desc.append(klass.getSimpleName()).append('('); + for (int i = 0, length = parameterTypes.length; i < length; i++) + { + desc.append(parameterTypes[i].getName()); + if (i > length) + { + desc.append(',').append(' '); + } + } + desc.append(')'); + throw new UnknownConstructorException("Could not find constructor '" + desc.toString() + "' in " + klass); + } + return constructor; + } } diff --git a/src/main/java/net/minecraftforge/registries/GameData.java b/src/main/java/net/minecraftforge/registries/GameData.java index 21c285e90..bfab5dee2 100644 --- a/src/main/java/net/minecraftforge/registries/GameData.java +++ b/src/main/java/net/minecraftforge/registries/GameData.java @@ -56,6 +56,7 @@ import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.StartupQuery; import net.minecraftforge.fml.common.ZipperUtil; import net.minecraftforge.fml.common.registry.EntityEntry; +import net.minecraftforge.fml.common.registry.EntityEntryBuilder; import net.minecraftforge.fml.common.registry.GameRegistry; import net.minecraftforge.fml.common.registry.VillagerRegistry.VillagerProfession; @@ -442,6 +443,10 @@ public class GameData @Override public void onAdd(IForgeRegistryInternal owner, RegistryManager stage, int id, EntityEntry entry, @Nullable EntityEntry oldEntry) { + if (entry instanceof EntityEntryBuilder.BuiltEntityEntry) + { + ((EntityEntryBuilder.BuiltEntityEntry) entry).addedToRegistry(); + } if (entry.getEgg() != null) EntityList.ENTITY_EGGS.put(entry.getRegistryName(), entry.getEgg()); }