Resource 1.11.2 / 1.12 - Class for easily registering custom NMS entities/items

Discussion in 'Spigot Plugin Development' started by jetp250, Apr 23, 2017.

  1. EDIT: Open for ideas for a better title

    Get the class here!

    Update 8/14/17:
    Small additions. Still for 1.12 / 1.12.1, update if you feel like.
    Changelog:
    • Added another method to add random spawn - but unlike the other one, this allows you to set the mob's category: Creature, Monster, Ambient or Water Creature. Creatures'll spawn during the day, monsters'll spawn during the night and so on. Note though, you may still have to override your mobs canSpawn method (P() in 1.12 / 1.12.1, cM() in 1.11.2)
    • Changed ZOMBIE_PIGMAN to PIG_ZOMBIE as per request
    • Added a way to register a mob without ID (savegame-id, however, is required), can still be summoned via /summon. Intended for entities without internal id (players, lightning bolts etc.)

    Update 5/21/17:
    The 1.12 class can now be found here. I re-wrote the file, so you may or may not have to change the registration from the 1.11 class a little.
    Changelog:
    • More accurate biome specification! You can now choose from every single Minecraft biome rather than just 'forest' or 'ocean' or 'desert' -kind of stuff.
    • All mobs are now in the same enum, called 'Type'; to avoid further confusion (and make it more clean)
    • Updated attributes
    • Updated field names that changed on the update
    • Added new 1.12 mobs (parrot, illusionist) to the list
    Have fun!

    For 1.12, change MobType to Type; should otherwise be pretty much the same
    --------------------------
    Hey Spigot!

    I've seen lately a lot of threads about NMS entities, how to spawn them, how to create them; and as of what I've seen, they're all somewhat outdated.

    I've been working with such for quite a while, and have made a class I use to easily do such things. Since it's only one class; no depedencies, no jar files, no complicated anything, I thought I'd share it with you all.

    The class has:
    - Simple method(s) to register custom entities with no digging
    - Simple method to register custom items
    - Fields to make working with NBT tags easier
    - Fields to make working with Attributes easier
    - a Simple method to add mob random spawns without runnables / trickery
    - an option to override vanilla mobs of certain type with your own one
    Pretty much everything has comments, too.

    Without further do, here's the class.

    Now, example of the usage!

    Let's create a simple custom zombie that always wears a pumpkin, attacks by shooting arrows with a bow, has 40 base health and 5 defense, attacks creepers and skeletons and spawns randomly around the world!

    Let's get into it.
    First off, we'll create the class like so:
    Code (Java):
    import net.minecraft.server.v1_11_R1.EntityZombie;
    import net.minecraft.server.v1_11_R1.World;

    public class CustomZombie extends EntityZombie {

        public CustomZombie(World world) {
            super(world);
        }

    }
    Now, it'd be just a normal; boring zombie that does absolutely nothing differently.
    First off, we'll clear the Zombie's AI and add our custom ones. To do this, we'll override the r() method, clear the current, default ones; and add our custom ones in.

    Next! We'll add in the pathfindergoals. To do so, we'll add in the following:
    Code (Java):
    // Make sure you implement IRangedEntity if you want to make the mob shoot arrows!
    public class CustomZombie extends EntityZombie implements IRangedEntity {
    Code (Java):
    @Override
    protected void r() {
        // Adding our custom pathfinder selectors.
        // Grants our zombie the ability to swim.
        this.goalSelector.a(0, new PathfinderGoalFloat(this));
        // This causes our zombie to shoot arrows.
        // The parameters are: The ranged entity, movement speed, cooldown,
        // maxDistance
        // Or, with the second constructor: The ranged entity, movement speed,
        // mincooldown, maxcooldown, maxDistance
        this.goalSelector.a(2, new PathfinderGoalArrowAttack(this, 1.0, 12, 20));
        // Gets our zombie to attack creepers and skeletons!
        this.targetSelector.a(3, new PathfinderGoalNearestAttackableTarget<>(this, EntityCreeper.class, true));
        this.targetSelector.a(3, new PathfinderGoalNearestAttackableTarget<>(this, EntitySkeleton.class, true));
        // Causes our zombie to walk towards it restriction.
        this.goalSelector.a(5, new PathfinderGoalMoveTowardsRestriction(this, 1.0));
        // Causes the zombie to walk around randomly.
        this.goalSelector.a(7, new PathfinderGoalRandomStrollLand(this, 1.0));
        // Causes the zombie to look at players. Optional in our case. Last
        // argument is range.
        this.goalSelector.a(8, new PathfinderGoalLookAtPlayer(this, EntityHuman.class, 8.0f));
        // Causes the zombie to randomly look around.
        this.goalSelector.a(8, new PathfinderGoalRandomLookaround(this));
    }
    Code (Java):
    @Override
    public void a(final EntityLiving target, final float f) {
        // Preparing the projectile
        final EntityArrow entityarrow = this.prepareProjectile(f);
        // Calculating the motion for the arrow to hit
        final double motX = target.locX - this.locX;
        final double motY = target.getBoundingBox().b + target.length / 3.0f - entityarrow.locY;
        final double motZ = target.locZ - this.locZ;
        final double horizontalMot = MathHelper.sqrt(motX * motX + motZ * motZ);
        // 'Shooting' the projectile (aka preparing it for being added to the
        // world.)
        entityarrow.shoot(motX, motY + horizontalMot * 0.2, motZ, 1.6f, 14 - world.getDifficulty().a() * 4);

        // OPTIONAL! Calls the event for shooting, that can be cancelled. I'd
        // keep it for other plugins that could cancel it.
        final EntityShootBowEvent event = CraftEventFactory.callEntityShootBowEvent(this, this.getItemInMainHand(),
                    entityarrow, 0.8f);
        if (event.isCancelled()) {
            event.getProjectile().remove();
            return;
        }
        // Checking if the projectile has been changed thru the event..
        if (event.getProjectile() == entityarrow.getBukkitEntity()) {
            this.world.addEntity(entityarrow);
        }
        // And last, playing the shooting sound.
        this.a(SoundEffects.fV, 1.0f, 1.0f / (this.getRandom().nextFloat() * 0.4f + 0.8f));
    }

    protected EntityArrow prepareProjectile(final float unknown) {
        // Creating the arrow instance. Now, you see, it's a Tipped Arrow. No
        // idea why, but EntityArrow is abstract and we can't instantiate it
        // without creating a custom class.
        // This is why the arrows nowadays have the odd particle effect!
        final EntityArrow arrow = new EntityTippedArrow(this.world, this);
        // No idea what this does, copied from the sourcecode
        arrow.a(this, unknown);
        return arrow;
    }
    That's a lot to add. Let's break this down, shall we?
    All EntityInsentients (extends EntityLiving) have an r() method. This'll be called when the entity is created, and represents a method where the pathfindergoals should be added in.
    All entities, EntityZombie included, add their pathfindergoals there, so by overriding it and NOT calling super.r(), we'll have full control over the pathfindergoals and our Zombie's actions.
    This is also why we needn't to clear the AI, and haven't done so. If you really want to clear it, for whatever reason, you can do that as follows:
    Code (Java):
    this.goalSelector = new PathfinderGoalSelector(world.methodProfiler);
    But as I said, this is not needed.

    Next! We're implementing IRangedEntity! Why?
    We wanted our dear Zombie to shoot arrows, didn't we? To do so easily, we can use the pre-made PathfinderGoalArrowAttack, which - though - takes an IRangedEntity as a parameter. So, by implementing that, we can now pass our Zombie as that parameter (just as we should) and the Zombie'll shoot arrows.
    This is also why we have that ugly a(EntityLiving, Float) method added in; the ArrowAttack pathfindergoal calls that every time the entity should shoot. Sourcecode is taken from EntitySkeleton.class.
    We could move the prepareProjectile's code inside the a(EntityLiving, Float) method, but it felt more readable the way EntitySkeleton and I did it.

    So, in theory, our Zombie now shoots arrows, walks, acts like a living entity and attacks creepers & skeletons! Great! Our zombie doesn't have a bow yet, though. We'll add the bow and the pumpkin next!
    Every time an entity is spawned, a method called prepare is called. Bukkit gives the entities their equipment inside this, so we'll do that too! To add the bow and pumpkin, we'll do the following, but of course, play around with it!

    Code (Java):
    @Override
    public GroupDataEntity prepare(DifficultyDamageScaler dds, GroupDataEntity gde) {
        // Calling the super method FIRST, so in case it changes the equipment, our equipment overrides it.
        gde = super.prepare(dds, gde);
        // We'll set the main hand to a bow and head to a pumpkin now!
        this.setSlot(EnumItemSlot.MAINHAND, new ItemStack(Items.BOW));
        this.setSlot(EnumItemSlot.HEAD, new ItemStack(Blocks.PUMPKIN));
        // Last, returning the GroupDataEntity called gde.
        return gde;
    }
    Notice that I first call the super method - as commented in the code snippet - so we override the possible equipment it could have. Other than that, pretty straight-forward, getting the specific slot and assigning a NMS ItemStack to it. You can also turn a Bukkit ItemStack to a NMS one by calling
    Code (Java):
    CraftItemStack.asNMSCopy(Bukkit ItemStack)
    if you really want to.

    We're almost done now! Of course, we haven't registered the entity yet, but we'll cover that last.
    We'll give it 40 Health and 5 defense next. To do this, we'll override the initAttributes() method.
    Now, we can take advantage of the Attributes class provided in the NMSUtils class:
    Code (Java):
    @Override
    protected void initAttributes() {
        // Calling the super method for the rest of the attributes.
        super.initAttributes();
        // Next, overriding armor and max health!
        // Setting the max health to 40:
        this.getAttributeInstance(Attributes.MAX_HEALTH.getValue()).setValue(40.0);
        // Setting the 'defense' (armor) to 5:
        this.getAttributeInstance(Attributes.ARMOR.getValue()).setValue(5.0);
    }
    Nothing super-interesting here. Now you know how to set attribute-values via NMS though, so I guess it was worth adding.

    And we're finally done with the CustomZombie class! Everything we wanted is now implemented and should be working.
    In case something was still unclear for whatever reason, the full zombie's code you should now have can be found here.

    However, as I said earlier, we need to register the zombie too. This seems to be what most of the questions are about nowadays.
    So, in your main class (the class that extends JavaPlugin) in onEnable(), add the following:
    Code (Java):
    NMSUtils.registerEntity("ranged_zombie", MobType.ZOMBIE, CustomZombie.class, false);
    And your Zombie is now registered and ready to be used. You can try it out by hopping on your server and typing:
    Code (Text):
    /summon minecraft:ranged_zombie
    and it should spawn in the zombie we just created. If - for whatever reason - it doesn't work, be sure to leave a reply below and I'll do my best to help .-.

    Now, have you tested or not, I assume it's working at this point. The last thing left for us to do is to add the random spawns like I said we'd do.
    We'll use the NMSUtils.addRandomSpawn(EntityType, SpawnData, Biome[]) method for this purpose.
    The first argument is the MobType of your entity, in this case MobType.ZOMBIE.
    The second argument, SpawnData, can be simply created by calling new SpawnData() with your parameters. I'll go with the following:
    Code (Java):
    final SpawnData data = new SpawnData(CustomZombie.class, 80, 1, 5);
    where CustomZombie.class is our custom class, 80 is the spawn weight, 1 and 5 are the min-max spawn counts.
    The third argument in the method is the list of biomes in which the mobs will spawn. Biome.ALL is also an option, and I'll go with that this time. I ended up with this:
    Code (Java):
    import org.bukkit.plugin.java.JavaPlugin;

    public class YourMainClass extends JavaPlugin {
        @Override
        public void onEnable() {
            NMSUtils.registerEntity("ranged_zombie", MobType.ZOMBIE, CustomZombie.class, false);
            final SpawnData data = new SpawnData(CustomZombie.class, 100, 1, 5);
            NMSUtils.addRandomSpawn(MobType.ZOMBIE, data, Biome.ALL);
        }
    }
    And that should be it! You can add a debug message to the constructor or something to make sure it actually gets called, and if it doesn't, keep increasing the spawn weight until you see some results.

    If I forgot something important somewhere or something isn't right, please make sure to tell me ;)
    - jet
     
    #1 jetp250, Apr 23, 2017
    Last edited: Aug 14, 2017
    • Useful Useful x 36
    • Like Like x 8
    • Winner Winner x 2
    • Informative Informative x 1
  2. Awesome tutorial, really helped me out with registering my entities. Not sure if this is a me-problem, but my entities are sometimes unable to jump and walk down ledges. Is there a PathfinderGoal to fix this?
     
    #2 Mats9799, Apr 24, 2017
    Last edited: Apr 24, 2017
  3. That sounds strange, especially if that only happens sometimes. I admit I'm somewhat clueless here, since that has never happened to me :(
    And no, there isn't a pathfinder goal for that to my knowledge; PathfinderGoalMoveTowardsRestriction should call the AbstractNavigation of the mob and make it move wherever it should, and as far as I'm aware it should do the jumping and dropping too. If someone else has experienced such, please do leave a comment <3

    However, I don't know how your entity class looks. Could you show what you have, and I'd take a look if I can reproduce it?
     
  4. I think the PathfinderGoals aren't as accurate when the entity is a strange position such as being one block underground or standing in front of a slab with carpet around it. A minor issue but may be something to keep in mind in more advanced things involving PathfinderGoals.
     
  5. Thanks for the thread ;)
     
  6. Glad you like it!
     
  7. The class is good but it's only for 1.11 server
     
  8. Can't we change the imports to match the server's version? And, I guess, update the field names. I see what you mean; I'll gladly make it for another version if someone needs.
     
  9. What is the difference between goalSelector and targetSelector?
     
  10. Well, both are PathfinderGoalSelectors, but generally, targetSelector manages the 'target' of the EntityInsentient, while goalSelector tells the entity what to do. Every entity with goalSelector and targetSelector fields are instances of EntityInsentient (i.e they extend EntityInsentient). The EntityInsentient class does all the 'updating' and these with the goal/targetSelectors, and by what I've seen; goal- and targetSelectors are 'treated' differently; targetSelector having some more methods called - haven't checked what they do, though.

    Generally, you should leave the 'target' managing for targetSelectors and 'action'-related PathfinderGoals to the goalSelector; the EntityInsentient class will take care of the rest.

    Also, this is why like every PathfinderGoal take either an 'EntityInsentient' or 'EntityCreature' as the argument, that being your entity. There may be some exceptions to this, however, that I may not be aware of.

    But, your entities can have goal and targetSelectors even if they weren't instances of the EntityInsentient! I created an Ender Crystal entity (which extends Entity directly; isn't an EntityLiving, EntityCreature nor EntityInsentient, obviously) and just manually created the target- and goalSelector fields. Then looked how EntityInsentient does it; took the most important parts and modified it for my needs. I now have a Ender Crystal with health, ability to attack targets from range, ability to get target entities of certain type and act like any other Entity with pathfindergoals.

    If this left you unclear on something, make sure to leave a reply! :)
     
    • Informative Informative x 1
  11. Bumping, I myself had also forgotten about this.
     
    • Creative Creative x 1
  12. What if I want to spawn my entity at a specific location through my code?
     
  13. Hmmh? This should do the trick.
    Code (Java):
    MyCustomEntity entity = new MyCustomEntity(world);
    entity.setPosition(x, y, z);
    entity.world.addEntity(entity, SpawnReason.CUSTOM);
     
  14. Would it be more efficient to use your method or force the server to call /summon?
     
  15. Way more efficient to do it via code. That's basically what /summon does, except it also has to use reflection (it creates the instance of the mob via reflection, which is known to be slower - not that it'd be extremely slow, all natural mob spawning is done that way too.) plus it has to parse the mob type and position (and NBT from String if given).
     
  16. Thanks for the replies. Another silly question, how would I add a potion effect to the mob. I can't cast it to LivingEntity.
     
  17. Is it possile to do custom AI without nms by extending the mob in the bukkit class
     
  18. No it isn't possible. The Bukkit implementation is only a wrapper class of the nms entity. That means it wraps around and delegating the readable methodcalls to nms versions.
     
  19. I mean in net.Minecraft.server its EntityWither (I think so...) so what if I did

    public class FriendlyWither extends Wither

    Instead of extends EntityWither?

    EDIT: I'm talking about the mob class in the bukkit.jar not the craftbukkit.jar
     
  20. The wither is only an interface. Then your class does nothing useful.