Tutorial - How to Customize the Behaviour of a Mob or Entity

Discussion in 'Resources' started by Jacek, Jan 13, 2012.

Thread Status:
Not open for further replies.
  1. Offline

    Jacek

    Each entity in the game has it's own class that defines how it behaves, for example the way skeletons work is controlled by EntitySkeleton. To modify these behaviours we need to create our own class that gets used instead of the vanilla one, luckily they are constructed in such a way that makes that fairly easy to do.

    The first thing we'll do is create an enum that holds all of our entity types, this is mostly for convenience as it gives a nice way to attach methods to each type. For this example we'll only modify one type of entity, the code will be written so that it will work for however many you add to the enum.
    Code:
    public enum CustomEntityType {
     
        SKELETON("Skeleton", 51, EntityType.SKELETON, EntitySkeleton.class, CustomEntitySkeleton.class),
     
        private String name;
        private int id;
        private EntityType entityType;
        private Class<? extends EntityInsentient> nmsClass;
        private Class<? extends EntityInsentient> customClass;
     
        private CustomEntityType(String name, int id, EntityType entityType, Class<? extends EntityInsentient> nmsClass, Class<? extends EntityInsentient> customClass){
            this.name = name;
            this.id = id;
            this.entityType = entityType;
            this.nmsClass = nmsClass;
            this.customClass = customClass;
        }
     
        public String getName(){
            return this.name;
        }
     
        public int getID(){
            return this.id;
        }
     
        public EntityType getEntityType(){
            return this.entityType;
        }
     
        public Class<? extends EntityInsentient> getNMSClass(){
            return this.nmsClass;
        }
     
        public Class<? extends EntityInsentient> getCustomClass(){
            return this.customClass;
        }
     
    }
    That may seem a bit daunting so I'll go through the constructor parameters and where to get them from;
    • name - This is the name of the entity as listed at the end of EntityTypes.
    • id - This is the ID linked with the name, also listed in EntityTypes.
    • entityType - The Bukkit EntityType, included for convenience.
    • nmsClass - This is the vanilla class for the entity, can also be found from EntityTypes.
    • customClass - This is the class we will make to replace the vanilla one. It can have any name you like also it makes it easier if you follow the same conventions as the vanilla server.
    You'll need to check all of these values with every game update to make sure they don't change, this is the main reason that we put them all together in this enum.

    Next we'll create our custom class that extends the vanilla one, for now it can just do nothing
    Code:
    public class CustomEntitySkeleton extends EntitySkeleton {
     
        public CustomEntitySkeleton(World world){
            super(world);
        }
     
    }
    you need to define the constructor and call super(world) to prevent the server spamming an error each time it tried to spawn a creature.

    With that defined we can tell the server about the change and get it to spawn our custom entity instead of the vanilla one, to do this we need to replace the defined class in 2 places. The first is the list that is set at the bottom of EntityTypes and the second is in the meta data for each biome. This step is a bit hacky so we'll do it in two steps, we're going to add a method to the enum that registers the entity information with the server
    Code:
    public static void registerEntities(){
     
    }
    in this method we'll loop over every entity we've listed in the enum and do the replacement. first we want to call the static method EntityTypes.a(Class oclass, String s, int i) with our new class, since that's a private method we need to do it via reflection.
    Code:
    public static void registerEntities(){
        for (CustomEntityType entity : values()){
            try{
                Method a = EntityTypes.class.getDeclaredMethod("a", new Class<?>[]{Class.class, String.class, int.class});
                a.setAccessible(true);
                a.invoke(null, entity.getCustomClass(), entity.getName(), entity.getID());
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    The next step is pretty messy, we need to loop over every biome and then for each biome we also need to loop over the defined meta data. We need a way to do this dynamically so that if the biomes change we don't have to modify the code, luckily all of the biome fields in BiomeBase get put into the BiomeBase.biomes array so we can just use that. Each biomes spawning meta data is stored in 4 fields that are named K, J, L and M at the time of writing, these are all private so need to be accessed via reflection.
    Code:
    for (BiomeBase biomeBase : BiomeBase.biomes){
        if (biomeBase == null){
            break;
        }
     
        for (String field : new String[]{"K", "J", "L", "M"}){
            try{
                Field list = BiomeBase.class.getDeclaredField(field);
                list.setAccessible(true);
                @SuppressWarnings("unchecked")
                List<BiomeMeta> mobList = (List<BiomeMeta>) list.get(biomeBase);
             
                for (BiomeMeta meta : mobList){
                 
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    The null check is needed because the biomes array is much longer than it needs to be so after a certain point all values will be null. Next we just need to check each of our custom entities and replace the class if there is a match for the corresponding vanilla class.
    Code:
                for (BiomeMeta meta : mobList){
                    for (CustomEntityType entity : values()){
                        if (entity.getNMSClass().equals(meta.b)){
                            meta.b = entity.getCustomClass();
                        }
                    }
                }
    putting all of this together we get a complete method that we can call in our plugin's onEnable method to register all of the custom entities that are listed in the enum.
    Code:
        public static void registerEntities(){
            for (CustomEntityType entity : values()){
                try{
                    Method a = EntityTypes.class.getDeclaredMethod("a", new Class<?>[]{Class.class, String.class, int.class});
                    a.setAccessible(true);
                    a.invoke(null, entity.getCustomClass(), entity.getName(), entity.getID());
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
         
            for (BiomeBase biomeBase : BiomeBase.biomes){
                if (biomeBase == null){
                    break;
                }
             
                for (String field : new String[]{"K", "J", "L", "M"}){
                    try{
                        Field list = BiomeBase.class.getDeclaredField(field);
                        list.setAccessible(true);
                        @SuppressWarnings("unchecked")
                        List<BiomeMeta> mobList = (List<BiomeMeta>) list.get(biomeBase);
                     
                        for (BiomeMeta meta : mobList){
                            for (CustomEntityType entity : values()){
                                if (entity.getNMSClass().equals(meta.b)){
                                    meta.b = entity.getCustomClass();
                                }
                            }
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }
    That's all of the hard work done, now any time a zombie spawns it will be using out custom class instead of the vanilla one. Customising the way they behave is a matter of overriding the methods you want from the vanilla class and adding your changes, for example if I wanted to make the skeletons shoot double arrows I'd override the EntitySkeleton.a(EntityLiving entityliving, float f); method.
    Code:
    public class CustomSkeleton extends Skeleton {
     
        public CustomSkeleton(World world){
            super(world);
        }
     
        @Override
        public void a(EntityLiving entityliving, float f){
            for (int i = 0; i < 2; ++i){
                super.a(entityliving, f);
            }
        }
     
    }
    The loop is a bit daft but shows how you could make it shoot an arbitrary number of times. You have to work out the names based on the code, this is a pretty easy one since it's the only place you see "new EntityArrow".
     
  2. Offline

    MrMag518

    Would be cleaner for the sake of our eyes if you used ['syntax=java]"the java code"['/syntax] xD
    Else, very good! :)
     
  3. Offline

    tips48

    Great tutorial :) Glad to see something like this. Should help people alot!
    EDIT:
    1) What the guy above said ^_^
    2) mcWorld.removeEntity((net.minecraft.server.EntityZombie) mcEntity); Do you really have to cast?
     
  4. Offline

    Jaker232

    What the? Nice advanced topic. Now let me decode what you just said into English.
     
  5. Offline

    Jacek

    Looking at it, probably not, I must have done that for some reason though.Perhaps there was no remove() for a general entity.

    Also I changed the syntax tags :D
     
    MrMag518 likes this.
  6. Offline

    tips48

    Looks much better :) Just thought you could remove the cast, as it might be kinda confusing to new people
     
  7. Offline

    Jacek

    I'll test without it and remove if it's not actually needed. I still think I didn't just do it for fun ;) Updating SkylandsPlus for 1.1 at the moment.
     
  8. Offline

    ZNickq

    Looks good! :)
     
  9. Offline

    coldandtired

    The onEnable() section needs to be explained a lot more. What's the 54 parameter? What's the "a" parameter?
     
  10. Offline

    Jacek

  11. Offline

    coldandtired

    Thanks for that, it's all clear now :)

    One issue I found, is that with 1.1 (didn't try with earlier builds) the line "Fetching addPacket for removed entity: CraftZombie" would be output to the console every time.

    Do yo know how to avoid this?
     
  12. Offline

    Jacek

    That has been a problem since 1.8, it happens every time a entity is removed form the world. The only fix would be to modify the craftbukkit source. :(
     
  13. Offline

    coldandtired

    I've found a workaround, but I haven't done any testing to find out if it's a suitable solution.
    Code:Java
    1. bloodMoonEntityZombie.setPosition(location.getX(), location.getY(), location.getZ());
    2. event.setCancelled(true);
    3. mcWorld.addEntity(bloodMoonEntityZombie, event.getSpawnReason());
    4. return;


    This will produce the desired effect (the original mob doesn't spawn while the new one does) without the message.
     
  14. Offline

    Jacek

    You cant really rely on cancelling the event since another plugin may uncancel it leading to double mobs and a crash because of the modifications we made to the enetitytypes list.
     
  15. Offline

    coldandtired

    I've been building on this quite heavily, with many issues that need to be resolved, but there's one thing I can't seem to do. Hopefully it's something easy and I'm being stupid :)

    Is there an easy way to do the reverse of this line:
    net.minecraft.server.Entity mcEntity = (((CraftEntity) entity).getHandle());

    i.e. cast a Minecraft entity back to a Bukkit entity?
     
  16. Offline

    Jacek

    There is often a .getBukkitEntity() method, try that ?

    EDIT:
    Example on this line form the above code
    Code:java
    1.  
    2. Zombie zombie = (Zombie) this.getBukkitEntity();
     
    coldandtired likes this.
  17. Offline

    Dark_Balor

    Jacek
    Good tutorial, using reflection :)

    but I have a question about that :
    Because you replaced the entity Class in the EntityType, that if I right, is used by the game to spawn Entities. Then why do you need to replace them on spawn ?

    Maybe bukkit changed the way that the creature are spawned and use their own CreatureType ( https://github.com/Bukkit/Bukkit/blob/master/src/main/java/org/bukkit/entity/CreatureType.java ), if you change there the class used, I think it will do the trick and avoid to listen to the event. (Just an hypothesis)

    Oh and of course by doing that, you have to create another Zombie (a Bukkit entity) that will be linked to your minecraft entity.
     
  18. Offline

    coldandtired

    It doesn't seem to replace all of them. Spawn eggs and spawners will spawn the new class without needing to be removed and added, but mobs that are spawned from code (and naturally I believe) use the old class.

    Thanks, that was exactly what I needed. No idea how I missed it :)

    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: May 23, 2016
  19. Offline

    Chaemelion

    I'm trying to create a custom zombie class that can see farther than 16 blocks, but it doesnt seem to work. If I extend the EntityZombie class with my own, I can spawn the new class and everything works just perfectly. The sight for the mobs is set in their parent class however... Somehow I need to override the method in EntityMonster I believe. Since I don't think it's possible from a grandchild class, I changed my custom class to also extend EntityMonster like the original EntityZombie does. This more or less seems to run ok, but no zombie appears when I attempt to spawn it. Any ideas?
     
  20. Offline

    Jacek

    If you don't extend EntityZombie you will need to copy all of the code from it into your class.

    You should be able to override a method from EntityMonster if you extend EntityZombie though, is it actually a method and not a field ?
     
  21. Offline

    Chaemelion

    I did that. Copied all the code and all. It would appear to be a near perfect clone of EntityZombie except for the override for findTarget. And I just said method, it may be a field, I have no idea. I'm teaching myself Java coming from a C background and I don't know all the terms and definitions.
     
  22. Offline

    Jacek

    Okay well a field is a property or a variable or not-a-function :) I don't think copying the entire zombie class is the way to go really, it will be impossible to keep up with updates to the game.
     
  23. Offline

    Chaemelion

    Ahh, ok. Well do you have any other suggestions? I can't seem to find another way to do it... :/
     
  24. Offline

    Dark_Balor

    After looking at the source code and the bukkit modification, we can't change "easily" which class is used, because of the code used in CraftWorld to spawn an entity.
    https://github.com/Bukkit/CraftBukk...a/org/bukkit/craftbukkit/CraftWorld.java#L699
    It's checking the Assignable Class and change the class by the Bukkit Entity in all the case.

    That explain why we need to use the onSpawn event.
     
  25. Offline

    bergerkiller

    May anyone be interested in this, this is the source code of TrainCarts where I swap two minecarts:
    Code:
        public static void replaceMinecarts(EntityMinecart toreplace, EntityMinecart with) {
            with.yaw = toreplace.yaw;
            with.pitch = toreplace.pitch;
            with.locX = toreplace.locX;
            with.locY = toreplace.locY;
            with.locZ = toreplace.locZ;
            with.motX = toreplace.motX;
            with.motY = toreplace.motY;
            with.motZ = toreplace.motZ;
            with.b = toreplace.b;
            with.c = toreplace.c;
            with.fallDistance = toreplace.fallDistance;
            with.ticksLived = toreplace.ticksLived;
            with.uniqueId = toreplace.uniqueId;
            with.setDamage(toreplace.getDamage());
            ItemUtil.transfer(toreplace, with);
            with.dead = false;
            toreplace.dead = true;
           
            with.setDerailedVelocityMod(toreplace.getDerailedVelocityMod());
            with.setFlyingVelocityMod(toreplace.getFlyingVelocityMod());
           
            //longer public in 1.0.0... :-(
            //with.e = toreplace.e;
           
            //swap
            MinecartSwapEvent.call(toreplace, with);
            ((WorldServer) toreplace.world).tracker.untrackEntity(toreplace);
            toreplace.world.removeEntity(toreplace);
            with.world.addEntity(with);
            if (toreplace.passenger != null) toreplace.passenger.setPassengerOf(with);
        }
     
  26. Offline

    LinkterSHD

    Very nice might implement this into my Plugin.
     
  27. Offline

    Josvth

    This might be a dumb question. But why do you use net.minecraft.server? Is the bukkit api not sufficient enough?
     
  28. Offline

    Jacek

    Exactly that, there is no move event for mobs so I had to add my own.
     
  29. Offline

    ZNickq

    Hell no, in most advanced cases!
     
  30. Offline

    CorrieKay

    Bumping... does this still work? im getting undefined for super.s_(); :x
     
Thread Status:
Not open for further replies.

Share This Page