Register and Spawn a Custom Entity on 1.13.x

Here is a crude example on how to register and spawn a custom entity on 1.13.x with some comments on whats actually going on.

This is NOT for replacing vanilla entities. This is simply adding a new entity that extends from an existing vanilla entity. Replacing vanilla entities is a bit more involved and will be covered in a different tutorial/resource topic.

This is compatible with CraftBukkit, Spigot, and Paper server implementations.

Any questions or concerns, feel free to ask. I will update this topic as questions/concerns are addressed.

import com.mojang.datafixers.types.Type;
import net.minecraft.server.v1_13_R2.BlockPosition;
import net.minecraft.server.v1_13_R2.DataConverterRegistry;
import net.minecraft.server.v1_13_R2.DataConverterTypes;
import net.minecraft.server.v1_13_R2.EntityTypes;
import net.minecraft.server.v1_13_R2.EntityZombie;
import net.minecraft.server.v1_13_R2.World;
import org.bukkit.Location;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.craftbukkit.v1_13_R2.CraftWorld;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.Map;
import java.util.function.Function;

public class MyAwesomePlugin extends JavaPlugin {
    // this is where we store our custom entity type (for use with spawning, etc)
    public static EntityTypes CUSTOM_ZOMBIE;

    @Override
    public void onLoad() {
        // register the custom entity in the server
        // it is recommended to do this when the server is loading
        // but since we're not replacing vanilla entities it can be
        // done later if wanted
        CUSTOM_ZOMBIE = injectNewEntity("custom_zombie", "zombie", CustomZombie.class, CustomZombie::new);
    }

    @Override
    public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
        if (!(sender instanceof Player)) {
            sender.sendMessage("This is a player only command!");
            return true;
        }

        // spawn CustomZombie where player is standing
        Entity spawnedEntity = spawnEntity(CUSTOM_ZOMBIE, ((Player) sender).getLocation());

        if (spawnedEntity == null) {
            sender.sendMessage("Could not spawn entity!");
        } else {
            sender.sendMessage("Custom zombie spawned!");
        }
        return true;
    }

    /**
     * Spawns entity at specified Location
     *
     * @param entityTypes Type of entity to spawn
     * @param loc Location to spawn at
     * @return Reference to the spawned bukkit Entity
     */
    public Entity spawnEntity(EntityTypes entityTypes, Location loc) {
        net.minecraft.server.v1_13_R2.Entity nmsEntity = entityTypes.a( // NMS method to spawn an entity from an EntityTypes
                ((CraftWorld) loc.getWorld()).getHandle(), // reference to the NMS world
                null, // EntityTag NBT compound
                null, // custom name of entity
                null, // player reference. used to know if player is OP to apply EntityTag NBT compound
                new BlockPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()), // the BlockPosition to spawn at
                true, // center entity on BlockPosition and correct Y position for Entity's height
                false); // not sure. alters the Y position. this is only ever true when using spawn egg and clicked face is UP

        // feel free to further modify your entity here if wanted
        // it's already been added to the world at this point

        return nmsEntity == null ? null : nmsEntity.getBukkitEntity(); // convert to a Bukkit entity
    }

    private EntityTypes injectNewEntity(String name, String extend_from, Class<? extends net.minecraft.server.v1_13_R2.Entity> clazz, Function<? super World, ? extends net.minecraft.server.v1_13_R2.Entity> function) {
        // get the server's datatypes (also referred to as "data fixers")
        Map<Object, Type<?>> dataTypes = (Map<Object, Type<?>>) DataConverterRegistry.a().getSchema(DataFixUtils.makeKey(SharedConstants.a().getWorldVersion())).findChoiceType(DataConverterTypes.n).types();
        // inject the new custom entity (this registers the
        // name/id with the server so you can use it in things
        // like the vanilla /summon command)
        dataTypes.put("minecraft:" + name, dataTypes.get("minecraft:" + extend_from));
        // create and return an EntityTypes for the custom entity
        // store this somewhere so you can reference it later (like for spawning)
        return EntityTypes.a(name, EntityTypes.a.a(clazz, function));
    }

    /**
     * The custom zombie
     */
    public static class CustomZombie extends EntityZombie {
        public CustomZombie(World world) {
            super(world);
        }

        // TODO add custom stuffs to make it custom
    }
}
5 Likes

Thanks for this, Im super stoked to give it a whirl :slight_smile:

Thanks for this indeed! Trying to rewrite a 1.12 plugin that is heavily NMS for custom entities and this is a good start. Would a de-register be simply dataTypes.remove(“mineraft:”+name) ?

I’m honestly not sure about de-registering. The system wasnt designed for that at all, so idk how it would react. Worth a shot, though. Let us know :slight_smile:

Would you have any idea on how to make this mob persistent over a restart (Or even trigger a CreatureSpawnEvent)?

It should persist restarts if you are doing the injection in onLoad.

But it doesn’t persist the custom entity. At least not for me. It persists after a reload, but not after a restart.

I’ve tried to use your code here as a basis for doing it in 1.14 spigot. What i cobbled together mostly works except the entity always looks like a pig, but it’s sounds and behaviors are of the entity im extending from. Curious to know if you have had any luck figuring it out for 1.14.

use DataFixUtils.makeKey(SharedConstants.a().getWorldVersion()) in place of 15190.

I finally figured out where that number came from, and that’s the wrong number for 1.14 :slight_smile:

Updated original post to reflect this.

cool i’ll give that a shot thanks.

I can´t use this. My IDE told me following:
“The method a(String) in the type SharedConstants is not applicable for the arguments ()”

Because it’s an obfuscated methods. There are no guarantees with their names and can change at any time from build to build and version to version. Have to dig into NMS and find the new method name, which should be a breeze for anyone using NMS already. If its not, then this is too advanced for you at this time.