Resource Tamable mobs without custom entities

Discussion in 'Plugin Help/Development/Requests' started by sirrus86, Feb 3, 2015.

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

    sirrus86

    I saw a tutorial awhile back that explained how to not only create custom mobs but also control their pathfinding. I was personally working on a plugin that incorporated have mobs as pets that follow and defend you, so this resource was invaluable to me.

    Unfortunately, I had two hurdles to overcome: make a custom mob that would actually defend its owner, and make the methods used as version-proof as possible.

    The first issue became simple: create my own PathfinderGoal classes. Fortunately, it was literally as easy as copy/pasting the existing classes used by Wolves and substituting a few methods here and there.

    The second issue is actually a lot of why I created this post. Initially I was able to create mobs that would defend and follow their owner using custom entity classes, but because I had to delve into NMS to do it, in order to maintain backwards compatibility I'd have to recreate the custom class with each new version. To top it off, fields and methods may change due to obfuscation.

    And that's when I came up with an idea: If the main intention is to make an existing mob became tame, and you do this by modifying the pathfinding it already has, why would you need a custom entity when you can use reflection?
    Code:
    package me.sirrus86.mobtamer;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    
    import org.bukkit.Bukkit;
    import org.bukkit.entity.Creature;
    import org.bukkit.entity.Endermite;
    import org.bukkit.entity.Entity;
    import org.bukkit.entity.Player;
    import org.bukkit.entity.Silverfish;
    import org.bukkit.entity.Skeleton;
    import org.bukkit.entity.Skeleton.SkeletonType;
    import org.bukkit.entity.Spider;
    import org.bukkit.entity.Zombie;
    
    public class MobTamer {
    
        private static final String OBC_PREFIX = Bukkit.getServer().getClass().getPackage().getName() + ".";
        private static final String NMS_PREFIX = OBC_PREFIX.replace("org.bukkit.craftbukkit", "net.minecraft.server");
        private static final String CUSTOM_PREFIX = OBC_PREFIX.replace("org.bukkit.craftbukkit", "me.sirrus86.mobtamer");
       
        private static Field b = null, c = null;
        private static Class<?> entityCreature = null,
                entityHuman = null,
                entityInsentient = null,
                pathfinderGoal = null,
                unsafeList = null;
       
        // Checks the current object's class for the specified field.
        // If it isn't found, it checks the superclass, and so on.
        private static Field getField(Object obj, String name) {
            Class<?> check = obj.getClass();
            do {
                for (Field field : check.getDeclaredFields()) {
                    if (field.getName().equalsIgnoreCase(name)) return field;
                }
                check = check.getSuperclass();
            } while (check != null);
            return null;
        }
       
        // Gets the NMS entity class of the Bukkit entity without using NMS.
        private static Object getHandle(Entity entity) {
            try {
                return getValue(entity, "entity");
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
       
        // Shortcut to get the value of a given field.
        private static Object getValue(Object instance, String fieldName) {
            try {
                Field field = getField(instance, fieldName);
                if (field != null) {
                    field.setAccessible(true);
                    return field.get(instance);
                }
                else throw new NullPointerException();
            }
            catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
       
        /**
         * Sets a specified entity as tamed by the specified player.
         * @param entity - Entity to be tamed. Must be an instance of {@link Creature}.
         * @param player - Player which this entity will belong to.
         */
        public static void setTamed(Creature entity, Player player) {
            try {
                Object handle = getHandle(entity);
                Field goalTarget = getField(handle, "goalTarget");
                goalTarget.setAccessible(true);
                goalTarget.set(handle, null); // Tell entity to stop targeting anything.
                if (b == null) {
                    b = Class.forName(NMS_PREFIX + "PathfinderGoalSelector").getDeclaredField("b");
                }
                if (c == null) {
                    c = Class.forName(NMS_PREFIX + "PathfinderGoalSelector").getDeclaredField("c");
                }
                Field goalSelector = getField(handle, "goalSelector");
                Field targetSelector = getField(handle, "targetSelector");
                b.setAccessible(true);
                c.setAccessible(true);
                goalSelector.setAccessible(true);
                targetSelector.setAccessible(true);
                if (unsafeList == null) {
                    unsafeList = Class.forName(OBC_PREFIX + "util.UnsafeList");
                }
                b.set(targetSelector.get(handle), unsafeList.newInstance());
                c.set(targetSelector.get(handle), unsafeList.newInstance());
                if (pathfinderGoal == null) {
                    pathfinderGoal = Class.forName(NMS_PREFIX + "PathfinderGoal");
                }
                Method gA = goalSelector.get(handle).getClass().getMethod("a", int.class, pathfinderGoal);
                Method tA = targetSelector.get(handle).getClass().getMethod("a", int.class, pathfinderGoal);
                gA.setAccessible(true);
                tA.setAccessible(true);
                if (entityCreature == null) {
                    entityCreature = Class.forName(NMS_PREFIX + "EntityCreature");
                }
                if (entityHuman == null) {
                    entityHuman = Class.forName(NMS_PREFIX + "EntityHuman");
                }
                if (entityInsentient == null) {
                    entityInsentient = Class.forName(NMS_PREFIX + "EntityInsentient");
                }
                // By default these mobs will run to the nearest player when they have a target.
                // Adding this allows them to run to their target instead.
                if (entity instanceof Spider
                        || entity instanceof Endermite
                        || entity instanceof Silverfish
                        || (entity instanceof Skeleton && ((Skeleton)entity).getSkeletonType() == SkeletonType.WITHER)
                        || entity instanceof Zombie) {
                    gA.invoke(goalSelector.get(handle), 4, Class.forName(NMS_PREFIX + "PathfinderGoalMeleeAttack").getConstructor(entityCreature, double.class, boolean.class).newInstance(handle, 1.2D, true));
                }
                gA.invoke(goalSelector.get(handle), 5, Class.forName(CUSTOM_PREFIX + "PathfinderGoalFollowTamer").getConstructor(entityInsentient, entityHuman, double.class, float.class, float.class).newInstance(handle, getHandle(player), 1.2D, 10.0F, 2.0F));
                tA.invoke(targetSelector.get(handle), 1, Class.forName(CUSTOM_PREFIX + "PathfinderGoalTamerHurtByTarget").getConstructor(entityCreature, entityHuman).newInstance(handle, getHandle(player)));
                tA.invoke(targetSelector.get(handle), 2, Class.forName(CUSTOM_PREFIX + "PathfinderGoalTamerHurtTarget").getConstructor(entityCreature, entityHuman).newInstance(handle, getHandle(player)));
                tA.invoke(targetSelector.get(handle), 3, Class.forName(NMS_PREFIX + "PathfinderGoalHurtByTarget").getConstructor(entityCreature, boolean.class, Class[].class).newInstance(handle, true, new Class[0]));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
       
    }
    What this class essentially does is dynamically change the pathfinding AI of a given mob. No replacements here, just taking the original mob and changing it. And unless some field or method names change (or if they overhaul pathfinding again), this should be version-proof. The mob's targetting is completely replaced as well, so it will only attack in order to assist the owner, defend the owner, or defend itself.

    The main caveat of this is that it only works on mobs that are instances of Creature. This mainly just excludes Bats, Ender Drgaons, Ghasts, Magma Cubes and Slimes.

    Now in order for the mob to follow and defend its owner, there are three custom PathfinderGoal classes in use here: PathfinderGoalFollowTamer, PathfinderGoalTamerHurtByTarget, and PathfinderGoalTamerHurtTarget. These are literally copy/pastes of similar classes used by Wolves.

    PathfinderGoalFollowTamer:
    Code:
    package me.sirrus86.mobtamer.v1_8_R1;
    
    import net.minecraft.server.v1_8_R1.BlockPosition;
    import net.minecraft.server.v1_8_R1.EntityHuman;
    import net.minecraft.server.v1_8_R1.EntityInsentient;
    import net.minecraft.server.v1_8_R1.EntityLiving;
    import net.minecraft.server.v1_8_R1.MathHelper;
    import net.minecraft.server.v1_8_R1.Navigation;
    import net.minecraft.server.v1_8_R1.NavigationAbstract;
    import net.minecraft.server.v1_8_R1.PathfinderGoal;
    import net.minecraft.server.v1_8_R1.World;
    
    public class PathfinderGoalFollowTamer extends PathfinderGoal {
    
        private EntityInsentient d;
        private EntityLiving e;
        World a;
        private double f;
        private NavigationAbstract g;
        private int h;
        float b;
        float c;
        @SuppressWarnings("unused")
        private boolean i;
        private EntityHuman owner;
    
        public PathfinderGoalFollowTamer(EntityInsentient paramEntityInsentient, EntityHuman owner, double paramDouble, float paramFloat1, float paramFloat2) {
            this.d = paramEntityInsentient;
            this.a = paramEntityInsentient.world;
            this.f = paramDouble;
            this.g = paramEntityInsentient.getNavigation();
            this.c = paramFloat1;
            this.b = paramFloat2;
            a(3);
            this.owner = owner;
            if (!(paramEntityInsentient.getNavigation() instanceof Navigation))
                throw new IllegalArgumentException("Unsupported mob type for FollowTamerGoal");
        }
    
        public boolean a() {
            EntityLiving localEntityLiving = owner;
            if (localEntityLiving == null) {
                return false;
            }
            if (this.d.h(localEntityLiving) < this.c * this.c) {
                return false;
            }
            this.e = localEntityLiving;
            return true;
        }
    
        public boolean b() {
            return (!this.g.m()) && (this.d.h(this.e) > this.b * this.b);
        }
    
        public void c() {
            this.h = 0;
            this.i = ((Navigation)this.d.getNavigation()).e();
            ((Navigation)this.d.getNavigation()).a(false);
        }
    
        public void d() {
            this.e = null;
            this.g.n();
            ((Navigation)this.d.getNavigation()).a(true);
        }
    
        public void e() {
            this.d.getControllerLook().a(this.e, 10.0F, this.d.bP());
    
            if (--this.h > 0) {
                return;
            }
            this.h = 10;
    
            if (this.g.a(this.e, this.f)) {
                return;
            }
            if (this.d.cb()) {
                return;
            }
            if (this.d.h(this.e) < 144.0D) {
                return;
            }
    
            int j = MathHelper.floor(this.e.locX) - 2;
            int k = MathHelper.floor(this.e.locZ) - 2;
            int m = MathHelper.floor(this.e.getBoundingBox().b);
            for (int n = 0; n <= 4; n++) {
                for (int i1 = 0; i1 <= 4; i1++) {
                    if ((n < 1) || (i1 < 1) || (n > 3) || (i1 > 3)) {
                        if ((World.a(this.a, new BlockPosition(j + n, m - 1, k + i1))) && (!this.a.getType(new BlockPosition(j + n, m, k + i1)).getBlock().d()) && (!this.a.getType(new BlockPosition(j + n, m + 1, k + i1)).getBlock().d())) {
                            this.d.setPositionRotation(j + n + 0.5F, m, k + i1 + 0.5F, this.d.yaw, this.d.pitch);
                            this.g.n();
                            return;
                        }
                    }
                }
            }
        }
    
    }
    PathfinderGoalTamerHurtByTarget:
    Code:
    package me.sirrus86.mobtamer.v1_8_R1;
    
    import net.minecraft.server.v1_8_R1.EntityCreature;
    import net.minecraft.server.v1_8_R1.EntityHuman;
    import net.minecraft.server.v1_8_R1.EntityLiving;
    import net.minecraft.server.v1_8_R1.PathfinderGoalTarget;
    
    import org.bukkit.event.entity.EntityTargetEvent;
    
    public class PathfinderGoalTamerHurtByTarget extends PathfinderGoalTarget {
    
        EntityCreature a;
        EntityLiving b;
        private int c;
        private EntityHuman owner;
    
        public PathfinderGoalTamerHurtByTarget(EntityCreature entitycreature, EntityHuman owner) {
            super(entitycreature, false);
            this.a = entitycreature;
            this.owner = owner;
            a(1);
        }
    
        public boolean a() {
            EntityLiving entityliving = owner;
    
            if (entityliving == null) {
                return false;
            }
            this.b = entityliving.getLastDamager();
            int i = entityliving.bd();
    
            return (i != this.c) && (a(this.b, false));
        }
    
        public void c() {
            this.e.setGoalTarget(this.b, EntityTargetEvent.TargetReason.TARGET_ATTACKED_OWNER, true);
            EntityLiving entityliving = owner;
    
            if (entityliving != null) {
                this.c = entityliving.bd();
            }
    
            super.c();
        }
    
    }
    PathfinderGoalTamerHurtTarget:
    Code:
    package me.sirrus86.mobtamer.v1_8_R1;
    
    import net.minecraft.server.v1_8_R1.EntityCreature;
    import net.minecraft.server.v1_8_R1.EntityHuman;
    import net.minecraft.server.v1_8_R1.EntityLiving;
    import net.minecraft.server.v1_8_R1.PathfinderGoalTarget;
    
    import org.bukkit.event.entity.EntityTargetEvent;
    
    public class PathfinderGoalTamerHurtTarget extends PathfinderGoalTarget {
       
        EntityCreature a;
        EntityLiving b;
        private int c;
        private EntityHuman owner;
    
        public PathfinderGoalTamerHurtTarget(EntityCreature entitycreature, EntityHuman owner) {
            super(entitycreature, false);
            this.a = entitycreature;
            this.owner = owner;
            a(1);
        }
    
        public boolean a() {
            EntityLiving entityliving = owner;
    
            if (entityliving == null) {
                return false;
            }
            this.b = entityliving.be();
            int i = entityliving.bf();
    
            return (i != this.c) && (a(this.b, false));
        }
    
        public void c() {
            this.e.setGoalTarget(this.b, EntityTargetEvent.TargetReason.OWNER_ATTACKED_TARGET, true);
            EntityLiving entityliving = owner;
    
            if (entityliving != null) {
                this.c = entityliving.bf();
            }
    
            super.c();
        }
    }
    Unfortunately as you can see the PathfinderGoal classes do use NMS, meaning they would need to be rebuilt with each version. I couldn't personally find any way to avoid this. The above examples will work with Bukkit 1.8, though to my knowledge aside from a few field/method names it should work the same in 1.7.x as well.

    I don't claim to be an expert in Java or NMS. I'm sure parts of this are sloppy, incorrect, could crash servers, etc. This is just to show a different way to simulate "tamed" mobs using as little NMS as possible.
     
    Totom3 likes this.
  2. Offline

    mrCookieSlime

    Moved to Alternatives Section.
     
Thread Status:
Not open for further replies.

Share This Page