Resource 1.16.2 Overriding Villager Behavior With NMS

Discussion in 'Spigot Plugin Development' started by DevSock, Sep 15, 2020 at 10:10 PM.

Thread Status:
Not open for further replies.
  1. Hello World, Welcome to my First Resource
    As a disclaimer, this resource expects you to understand how to code in Java with at least intermediate proficiency. This resource also does not cover restart persistence nor does it cover custom path finding or collision handling. With that said, it WILL teach you how to start with a clean slate for your villager entity. Lets get into the meat and potatoes.



    Key Differences Between Villagers and Other Entities

    • The biggest Key difference is Villagers are controlled by both PathFinderGoals AND a BehaviorController whereas something like an EntityZombie is controlled by ONLY PathFinderGoals.
    • When a villager entity is created, it is given a preset list of Activities and PathfinderGoals, so we have to clear both of these in order to fully control our custom villager from a clean slate.First we need to create a custom NMS Entity that extends the EntityVillager.class. For the purposes of demonstration, I've combined everything into one class for you. You will eventually want to create a reflection utility class. It isn't essential, but I will eventually cover it at a later date.

    Here is our EntityNPC.class Base
    Code (Java):
    public class EntityNPC extends EntityVillager {
        //Here is our custom entity constructor. This gets called when a new custom entity object is being created. We call the super() method
        //because it's required, and it will handle all of the background setup for the custom entity.
        public EntityNPC(EntityTypes<? extends EntityVillager> entitytypes, World world, VillagerType villagertype){
            super(entitytypes, world, villagertype);
        }
    }

    Good job finishing step one! Now you have a class that is essentially an exact copy of the EntityVillager.class. In the next step we're going to create the method that removes the behavior of our villager. The reason we need to do this is because in our constructor we call the super(); method.
    The super method defines our entity the exact same way it defines a regular villager when the server creates one naturally. This means that calling super(); in our constructor automatically sets our custom villager's behavior. What do we do about this? We use reflection to change those behaviors after they've been set.

    Now that you've got your base class, you're ready to create a new method. Time to create the method that this whole resource is about. Below the constructor you defined, create a new method like what is shown below.​
    Code (Java):

    private void removeAI() {

    }
     

    Here is our EntityNPC.class structure now, after we put our removeAI() method in place.

    Code (Java):

    public class EntityNPC extends EntityVillager {

        public EntityNPC(EntityTypes<? extends EntityVillager> entitytypes, World world, VillagerType villagertype){
            super(entitytypes, world, villagertype);
        }

        private void removeAI() {

        }

    }
     


    Time for some reflection. This part is version dependant, and I'm writing this resource for 1.16.2. However, there are plenty of websites that map NMS classes for you. Below is a list of the NMS classes we will use, if you click them you'll be redirected to an NMS Mapping website. Using this, you can find the unobfuscated names of most methods from NMS classes.​


    Lets add the first part of our reflection. Below we will use reflection to get three different fields from runtime. One of these fields is from the PathFinderGoalSelector, and the other two are from the BehaviorController.class. These field names will change, which is why I suggest creating a reflection utility class as soon as you know how.
    Code (Java):

    private void removeAI() {
        //Since we're using reflection, we're going to surround our code with a try/catch to make it safe for execution.
        try {
            //This is the "availableGoals" field. It's named "d" in version 1.16.2.
            // Again, you can find all this information on the mappings site I provided.
            //One other important thing to know about these fields is their actual object type.
            // You need to know this so when you set it with reflection you know what type of object it needs.
            // Otherwise you're going see an IllegalArgumentException in console.

            Field availableGoalsField = PathfinderGoalSelector.class.getDeclaredField("d"); //This field has an object value of a LinkedHashSet
            Field priorityBehaviorsField = BehaviorController.class.getDeclaredField("e"); //This field has an object value of a Map
            Field coreActivitysField = BehaviorController.class.getDeclaredField("i"); //This field has an object value of a HashSet

            //Now take all of those fields, and set their accessibility to true. This gives us the ability to change their value.
            availableGoalsField.setAccessible(true);
            priorityBehaviorsField.setAccessible(true);
            coreActivitysField.setAccessible(true);

        } catch (IllegalAccessException | NoSuchFieldException | IllegalArgumentException exception) {
            exception.printStackTrace();
        }

    }
     
    Now we've got our goals ready to be modified. For this part, it's important to know what object type we actually need to pass through in order to avoid an IllegalArgumentException. For example, if we passed tried to set a hashmap to an arraylist, it wouldn't work. It's the same thing with our reflection here. So make sure you check the object type of the field you're trying to change with an NMS mapper to make sure you've got the right type. Lets get to it!​
    Code (Java):

    private void removeAI() {

        try {
            Field availableGoalsField = PathfinderGoalSelector.class.getDeclaredField("d");
            Field priorityBehaviorsField = BehaviorController.class.getDeclaredField("e");
            Field coreActivitysField = BehaviorController.class.getDeclaredField("i");

            availableGoalsField.setAccessible(true);
            priorityBehaviorsField.setAccessible(true);
            coreActivitysField.setAccessible(true);

            //Here we take our field value that we defined, and we use it to access this classes goalSelector / targetSelector / behaviorController.
            availableGoalsField.set(this.goalSelector, Sets.newLinkedHashSet()); //goalSelector and targetSelector are accessed with the same field.
            availableGoalsField.set(this.targetSelector, Sets.newLinkedHashSet());
            priorityBehaviorsField.set(this.getBehaviorController(), Collections.emptyMap());
            coreActivitysField.set(this.getBehaviorController(), Sets.newHashSet());

        } catch (IllegalAccessException | NoSuchFieldException | IllegalArgumentException exception) {
            exception.printStackTrace();
        }

    }
     
    Good job finishing step two, only one more step to go! Now you've got your EntityNPC.class populated with a constructor and the removeAI() method. This is where we will create the method that actually modifies and spawns our custom villager. Now time to put that removeAI() method to use!
    First things first, lets create a new method called create() directly underneath our constructor, and above our removeAI() method. This is the method we're going to be using to actually spawn our new custom entity, so we're going to make it static so we can reference the method from anywhere with a single call.​
    Code (Java):

    //We pass a Location so we know where to spawn the entity, a String so we know what to name it, and a
    //boolean so we know whether or not we want this specific mob to be invulnerable to survival players or not
    public static void create(Location location, String entityName, Boolean invulnerable) {

    }
     
    Here is our EntityNPC.class after putting our create() method in it's place.​
    Code (Java):

    public class EntityNPC extends EntityVillager {

        public EntityNPC(EntityTypes<? extends EntityVillager> entitytypes, World world, VillagerType villagertype) {
            super(entitytypes, world, villagertype);
        }

        public static void create(Location location, String entityName, Boolean invulnerable) {

        }

        private void removeAI() {
                Field availableGoalsField = PathfinderGoalSelector.class.getDeclaredField("d");
                Field priorityBehaviorsField = BehaviorController.class.getDeclaredField("e");
                Field coreActivitysField = BehaviorController.class.getDeclaredField("i");

                availableGoalsField.setAccessible(true);
                priorityBehaviorsField.setAccessible(true);
                coreActivitysField.setAccessible(true);

                availableGoalsField.set(this.goalSelector, Sets.newLinkedHashSet());
                availableGoalsField.set(this.targetSelector, Sets.newLinkedHashSet());
                priorityBehaviorsField.set(this.getBehaviorController(), Collections.emptyMap());
                coreActivitysField.set(this.getBehaviorController(), Sets.newHashSet());
            } catch (IllegalAccessException | NoSuchFieldException | IllegalArgumentException exception) {
                exception.printStackTrace();
            }
        }
    }
     
    Now lets get our server's WorldServer and use our constructor to create a new EntityNPC object! The WorldServer is what minecraft uses to change a lot of variables about the current world you're in. ​
    Code (Java):

    public static void create(Location location, String entityName, Boolean invulnerable) {
        //Get the world from our location argument, and cast it to it's NMS-equivalent object,
        //then get it's handle. This is the NMS WorldServer for the specified world.
        WorldServer nmsWorld = ((CraftWorld) location.getWorld()).getHandle();

        //Now we call our constructor. We give it an EntityType of Villager, give it the
        // WorldServer that will be handling it, and then specify the type of villager.
        EntityNPC npc = new EntityNPC(EntityTypes.VILLAGER, nmsWorld, VillagerType.TAIGA);

    }
     

    Now below what we've defined, we're going to set some of the Entity's attributes then add them to the world.​
    Code (Java):

    public static void create(Location location, String entityName, Boolean invulnerable) {

        WorldServer nmsWorld = ((CraftWorld) location.getWorld()).getHandle();
        EntityNPC npc = new EntityNPC(EntityTypes.VILLAGER, nmsWorld, VillagerType.TAIGA);
        npc.setCustomName(new ChatComponentText(entityName));
        npc.setCustomNameVisible(true);
        npc.setInvulnerable(invulnerable);

        //keep in mind this is the NMS method setLocation. This doesn't spawn the entity, just preps the worldserver for where the entity is going to be.
        npc.setLocation(location.getX(), location.getY(), location.getZ(), (byte) location.getYaw() * 256 / 360, location.getPitch());

        //Now we remove the NPC's AI, and set their first goal selector to whatever we want. In this case I want the NPC to
        //Look at any entity that comes near it that is handled by the EntityPlayer.class, within a range of 5 blocks
        //With a 100 percent probability to activate.
        npc.removeAI();
        npc.goalSelector.a(0, new PathfinderGoalLookAtPlayer(npc, EntityPlayer.class, 5, 100)); // (Priority, Goal(EntityToApplyTo, Type to Look at, look range, execution probability))

        //Then we add the NPC to the world!
        nmsWorld.addEntity(npc);

    }
     
    Here is Our Completed EntityNPC.class after adding everything to it.​
    Code (Java):

    public class EntityNPC extends EntityVillager {

        public EntityNPC(EntityTypes<? extends EntityVillager> entitytypes, World world, VillagerType villagertype) {
            super(entitytypes, world, villagertype);
        }

        public static void create(Location location, String entityName, Boolean invulnerable) {
            WorldServer nmsWorld = ((CraftWorld) location.getWorld()).getHandle();
            EntityNPC npc = new EntityNPC(EntityTypes.VILLAGER, nmsWorld, VillagerType.TAIGA);
            npc.setCustomName(new ChatComponentText(entityName));
            npc.setCustomNameVisible(true);
            npc.setInvulnerable(invulnerable);
            npc.setLocation(location.getX(), location.getY(), location.getZ(), (byte) location.getYaw() * 256 / 360, location.getPitch());

            npc.removeAI();
            npc.goalSelector.a(0, new PathfinderGoalLookAtPlayer(npc, EntityPlayer.class, 5, 100));

            nmsWorld.addEntity(npc);
        }


        private void removeAI() {
            try {
                Field availableGoalsField = PathfinderGoalSelector.class.getDeclaredField("d");
                Field priorityBehaviorsField = BehaviorController.class.getDeclaredField("e");
                Field coreActivitysField = BehaviorController.class.getDeclaredField("i");

                availableGoalsField.setAccessible(true);
                priorityBehaviorsField.setAccessible(true);
                coreActivitysField.setAccessible(true);

                availableGoalsField.set(this.goalSelector, Sets.newLinkedHashSet());
                availableGoalsField.set(this.targetSelector, Sets.newLinkedHashSet());
                priorityBehaviorsField.set(this.getBehaviorController(), Collections.emptyMap());
                coreActivitysField.set(this.getBehaviorController(), Sets.newHashSet());
            } catch (IllegalAccessException | NoSuchFieldException | IllegalArgumentException exception) {
                exception.printStackTrace();
            }
        }

    }
     
    Congrats, You've Reached the End!
    Now you have the entire EntityNPC.class created! Here's how to spawn your brand new entity!
    Code (Java):

    //Call this method from any class, and pass it a Location, a String, and a Boolean and like magic, you've got
    //a custom villager standing at the location specified while giving you a run for your money for your middle school
    //staring competition record.
    EntityNPC.create(Location spawnPoint, String entityName, Boolean shouldBeInvulnerable);
     

    If you wish to make your entities restart-persistent, I suggest saving your entities's Bukkit entity ID somewhere, and then looping through the entities within a chunk when the OnChunkLoad event is called, comparing the IDs, and then if they match you remove the entity there, and spawn a custom one in it's place. I won't go more in depth here but if you've made it through this tutorial and actually understand it, I'm confident you can figure this step out yourself with that snippet of guidance.
     
    #1 DevSock, Sep 15, 2020 at 10:10 PM
    Last edited: Sep 16, 2020 at 9:41 PM
    • Useful Useful x 7
  2. I would leave a like if the whole text was properly aligned left, black, not bold and in default font size
     
    • Agree Agree x 4
  3. I didn't read it because of this formatting. So for me he's getting a like if reformatted correct.
     
  4. On the bright side, I like the distinct headers and use of spoilers. Topic's been covered before, but it's a nice tutorial.
     
    • Like Like x 1
  5. I do love the fact you catch the actual exceptions thrown and not just Exception like many people do. I do agree with how you've laid the thread out but maybe changing the colours back to the default would be better:p
     
    • Friendly Friendly x 1
  6. Nice tutorial, this might help a beginner to some extent, but yes, the text is a little strange
     
  7. I formatted it this way so users aren't met with an intimidating wall of stagnant text. The format of this post not suiting your fancy doesn't make an impact on the usefulness of the resource I feel. It's completely laid out in an easy to read, step-by-step process. Each step has a clearly defined header so if you can skip to exactly where you want. This perspective confuses me.

    Can you show me where it's been covered before? I'd really like to see another resource on the topic.

    I used colors to give a much more clear directive while not being too distracting. I think I may remove most of the bolding, because that was actually unintentional and I hadn't realized I left the bold format active.

    I would really hope a beginner doesn't tackle this task. NMS and reflection is NOT something a beginner should be working with until they've got a real foothold with how spigot even works.


    Thank you all for the input. I'll be removing the bolding and possibly resizing some text, but the majority of the formatting will be staying the same. If you disagree with this choice, feel free to write your own resource. This forum needs more of that anyway.
     
    • Like Like x 1
  8. Updated with different formatting on account of people here enjoying reading hardcopies of the javadocs.
     
  9. Use that link you've sent, and actually try to find a link on there that explains to you how to override EntityVillager mechanics. There's not a single link there that explains how to handle it. Villagers are controlled completely differently when compared to a Zombie or an NMS EntityPlayer. I understand you're trying to help but you're misdirecting people with this comment. The whole reason I took the time to create this resource was because there was no resource for this task specifically.
     
  10. Completely differently? You wrote yourself, "The biggest Key difference is Villagers are controlled by both PathFinderGoals AND a BehaviorController whereas something like an EntityZombie is controlled by ONLY PathFinderGoals." Besides a few lines of code to clear the BehaviorController, I don't see anything that couldn't be ascertained through the majority of those bukkit(dot)org and spigotmc(dot)org results.

    Thank you for adjusting some of the text formatting.
     
  11. Yes, completely differently. I don't know how many times I have to reiterate what I'm saying. For every other regular entity, you can just override the initPathFinder() method from the class you're extending. Villagers don't have this. So not only do you have to override their pathfinders differently, you also have to access a behaviorcontroller, which is ALSO something you don't have to do for regular entities.

    They are completely different dude.
     
  12. Well, no. The post's bizarre formatting does impact its usefulness: it's much harder to read. Center-justification should be used sparingly, especially for paragraphs. Fixing that alone would vastly improve the post's readability, in my opinion. Also, I don't think anyone is complaining about headers - as you said, they're incredibly useful for skipping down to relevant information.

    The main post's formatting is a lot like this bench. Sure, it functions as a bench, much like your thread has useful and valid information, but it is uncomfortable by design.

    [​IMG]

    A bench like this, actively hostile against its primary purpose: the simple act of sitting, probably shouldn't exist. So please be considerate and format your post with readers in mind.
     
  13. You can do that for newer versions (v1_14_R1 and up, I think?), but many threads, especially older ones, don't. Might want to mention which versions this resource is compatible with. Also, forgive me if I'm wrong, but I don't see any mention of that in the OP. Make a new bullet?
     
  14. I do specifically mention that this code is oriented towards 1.16.2 right before we get into version dependent code in step 2. Offering support for backdated versions only creates more work and inhibits the forward motion of a productive workflow in my opinion. Especially for something like minecraft.

    This resource is oriented around the headache I received when I discovered that the way you override behavior for literally EVERY SINGLE OTHER MOB doesn't work for villagers in this current version. Why would you ever want to send someone to an extremely outdated, bloated resource with 95% of the methods in those classes not even existing anymore, just to obtain that 5% of information before they have to hop to another resource with another 5% pertinent information and so on. Just to build up enough bits and pieces to try to put the puzzle together.

    Is it really valid to say that this topic has been covered before in that case? No, I don't feel it is. Has the reflection been written before? Yeah probably, but which of those resources cover the other quintessential half of the topic being the behaviorcontroller?.

    All I'm going to say after this experience, I hope someone sees this and it helps them out. I also now understand why nobody ever posts resources to this forum. "muh text justification" "muh culur" etc. To those of you that found this thread helpful, I'm happy I could help. To the rest, I sincerely no longer care.
     
    • Optimistic Optimistic x 3
    • Winner Winner x 1
Thread Status:
Not open for further replies.