From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: MiniDigger <admin@minidigger.me>
Date: Fri, 3 Jan 2020 16:26:19 +0100
Subject: [PATCH] Implement Mob Goal API


diff --git a/pom.xml b/pom.xml
index 2349491a15393a450d80397dd92c5733b28a3482..80f165291385f5f54def1a8e8487b209fb06a4a3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -134,6 +134,13 @@
             <version>1.3</version>
             <scope>test</scope>
         </dependency>
+        <!-- for vanilla goal scanning -->
+        <dependency>
+            <groupId>io.github.classgraph</groupId>
+            <artifactId>classgraph</artifactId>
+            <version>4.8.47</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <repositories>
diff --git a/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java b/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..89b56de7567ae54be75e0735b712e4dd713f1bf4
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java
@@ -0,0 +1,356 @@
+package com.destroystokyo.paper.entity.ai;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+
+import com.destroystokyo.paper.entity.RangedEntity;
+import com.destroystokyo.paper.entity.ai.GoalKey;
+import com.destroystokyo.paper.entity.ai.GoalType;
+import com.destroystokyo.paper.util.set.OptimizedSmallEnumSet;
+
+import net.minecraft.server.*; // intentional star import
+
+import java.lang.reflect.Constructor;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.*; // intentional star import
+
+public class MobGoalHelper {
+
+    private static final BiMap<String, String> deobfuscationMap = HashBiMap.create();
+    private static final Map<Class<? extends PathfinderGoal>, Class<? extends Mob>> entityClassCache = new HashMap<>();
+    private static final Map<Class<? extends EntityInsentient>, Class<? extends Mob>> bukkitMap = new HashMap<>();
+
+    static final Set<String> ignored = new HashSet<>();
+
+    static {
+        // TODO these kinda should be checked on each release, in case obfuscation changes
+        deobfuscationMap.put("bee_b", "bee_attack");
+        deobfuscationMap.put("bee_c", "bee_become_angry");
+        deobfuscationMap.put("bee_d", "bee_enter_hive");
+        deobfuscationMap.put("bee_e", "bee_go_to_hive");
+        deobfuscationMap.put("bee_f", "bee_go_to_known_flower");
+        deobfuscationMap.put("bee_g", "bee_grow_crop");
+        deobfuscationMap.put("bee_h", "bee_hurt_by_other");
+        deobfuscationMap.put("bee_i", "bee_locate_hive");
+        deobfuscationMap.put("bee_k", "bee_pollinate");
+        deobfuscationMap.put("bee_l", "bee_wander");
+        deobfuscationMap.put("cat_a", "cat_avoid_entity");
+        deobfuscationMap.put("cat_b", "cat_relax_on_owner");
+        deobfuscationMap.put("dolphin_b", "dolphin_swim_to_treasure");
+        deobfuscationMap.put("dolphin_c", "dolphin_swim_with_player");
+        deobfuscationMap.put("dolphin_d", "dolphin_play_with_items");
+        deobfuscationMap.put("drowned_a", "drowned_attack");
+        deobfuscationMap.put("drowned_b", "drowned_goto_beach");
+        deobfuscationMap.put("drowned_c", "drowned_goto_water");
+        deobfuscationMap.put("drowned_e", "drowned_swim_up");
+        deobfuscationMap.put("drowned_f", "drowned_trident_attack");
+        deobfuscationMap.put("enderman_a", "enderman_freeze_when_looked_at");
+        deobfuscationMap.put("evoker_a", "evoker_attack_spell");
+        deobfuscationMap.put("evoker_b", "evoker_cast_spell");
+        deobfuscationMap.put("evoker_c", "evoker_summon_spell");
+        deobfuscationMap.put("evoker_d", "evoker_wololo_spell");
+        deobfuscationMap.put("fish_b", "fish_swim");
+        deobfuscationMap.put("fox_a", "fox_defend_trusted");
+        deobfuscationMap.put("fox_b", "fox_faceplant");
+        deobfuscationMap.put("fox_e", "fox_breed");
+        deobfuscationMap.put("fox_f", "fox_eat_berries");
+        deobfuscationMap.put("fox_g", "fox_float");
+        deobfuscationMap.put("fox_h", "fox_follow_parent");
+        deobfuscationMap.put("fox_j", "fox_look_at_player");
+        deobfuscationMap.put("fox_l", "fox_melee_attack");
+        deobfuscationMap.put("fox_n", "fox_panic");
+        deobfuscationMap.put("fox_o", "fox_pounce");
+        deobfuscationMap.put("fox_p", "fox_search_for_items");
+        deobfuscationMap.put("fox_q", "fox_stroll_through_village");
+        deobfuscationMap.put("fox_r", "fox_perch_and_search");
+        deobfuscationMap.put("fox_s", "fox_seek_shelter");
+        deobfuscationMap.put("fox_t", "fox_sleep");
+        deobfuscationMap.put("fox_u", "fox_stalk_prey");
+        deobfuscationMap.put("illager_abstract_b", "raider_open_door");
+        deobfuscationMap.put("illager_illusioner_a", "illusioner_blindness_spell");
+        deobfuscationMap.put("illager_illusioner_b", "illusioner_mirror_spell");
+        deobfuscationMap.put("illager_wizard_b", "spellcaster_cast_spell");
+        deobfuscationMap.put("llama_a", "llama_attack_wolf");
+        deobfuscationMap.put("llama_c", "llama_hurt_by");
+        deobfuscationMap.put("llama_trader_a", "llamatrader_defended_wandering_trader");
+        deobfuscationMap.put("monster_patrolling_a", "long_distance_patrol");
+        deobfuscationMap.put("ocelot_a", "ocelot_avoid_entity");
+        deobfuscationMap.put("ocelot_b", "ocelot_tempt");
+        deobfuscationMap.put("panda_b", "panda_attack");
+        deobfuscationMap.put("panda_c", "panda_avoid");
+        deobfuscationMap.put("panda_d", "panda_breed");
+        deobfuscationMap.put("panda_e", "panda_hurt_by_target");
+        deobfuscationMap.put("panda_f", "panda_lie_on_back");
+        deobfuscationMap.put("panda_g", "panda_look_at_player");
+        deobfuscationMap.put("panda_i", "panda_panic");
+        deobfuscationMap.put("panda_j", "panda_roll");
+        deobfuscationMap.put("panda_k", "panda_sit");
+        deobfuscationMap.put("panda_l", "panda_sneeze");
+        deobfuscationMap.put("phantom_b", "phantom_attack_player");
+        deobfuscationMap.put("phantom_c", "phantom_attack_strategy");
+        deobfuscationMap.put("phantom_e", "phantom_circle_around_anchor");
+        deobfuscationMap.put("phantom_i", "phantom_sweep_attack");
+        deobfuscationMap.put("polar_bear_a", "polarbear_attack_players");
+        deobfuscationMap.put("polar_bear_b", "polarbear_hurt_by");
+        deobfuscationMap.put("polar_bear_c", "polarbear_melee");
+        deobfuscationMap.put("polar_bear_d", "polarbear_panic");
+        deobfuscationMap.put("puffer_fish_a", "pufferfish_puff");
+        deobfuscationMap.put("raider_a", "raider_hold_ground");
+        deobfuscationMap.put("raider_b", "raider_obtain_banner");
+        deobfuscationMap.put("raider_c", "raider_celebration");
+        deobfuscationMap.put("raider_d", "raider_move_through_village");
+        deobfuscationMap.put("ravager_a", "ravager_melee_attack");
+        deobfuscationMap.put("shulker_a", "shulker_attack");
+        deobfuscationMap.put("shulker_c", "shulker_defense");
+        deobfuscationMap.put("shulker_d", "shulker_nearest");
+        deobfuscationMap.put("shulker_e", "shulker_peek");
+        deobfuscationMap.put("squid_a", "squid_flee");
+        deobfuscationMap.put("skeleton_abstract_1", "skeleton_melee");
+        deobfuscationMap.put("strider_a", "strider_go_to_lava");
+        deobfuscationMap.put("turtle_a", "turtle_breed");
+        deobfuscationMap.put("turtle_b", "turtle_go_home");
+        deobfuscationMap.put("turtle_c", "turtle_goto_water");
+        deobfuscationMap.put("turtle_d", "turtle_lay_egg");
+        deobfuscationMap.put("turtle_f", "turtle_panic");
+        deobfuscationMap.put("turtle_h", "turtle_random_stroll");
+        deobfuscationMap.put("turtle_i", "turtle_tempt");
+        deobfuscationMap.put("turtle_j", "turtle_travel");
+        deobfuscationMap.put("vex_a", "vex_charge_attack");
+        deobfuscationMap.put("vex_b", "vex_copy_target_of_owner");
+        deobfuscationMap.put("vex_d", "vex_random_move");
+        deobfuscationMap.put("villager_trader_a", "villagertrader_wander_to_position");
+        deobfuscationMap.put("vindicator_a", "vindicator_break_door");
+        deobfuscationMap.put("vindicator_b", "vindicator_johnny_attack");
+        deobfuscationMap.put("vindicator_c", "vindicator_melee_attack");
+        deobfuscationMap.put("wither_a", "wither_do_nothing");
+        deobfuscationMap.put("wolf_a", "wolf_avoid_entity");
+        deobfuscationMap.put("zombie_a", "zombie_attack_turtle_egg");
+
+        ignored.add("selector_1");
+        ignored.add("selector_2");
+        ignored.add("wrapped");
+
+        bukkitMap.put(EntityInsentient.class, Mob.class);
+        bukkitMap.put(EntityAgeable.class, Ageable.class);
+        bukkitMap.put(EntityAmbient.class, Ambient.class);
+        bukkitMap.put(EntityAnimal.class, Animals.class);
+        bukkitMap.put(EntityBat.class, Bat.class);
+        bukkitMap.put(EntityBee.class, Bee.class);
+        bukkitMap.put(EntityBlaze.class, Blaze.class);
+        bukkitMap.put(EntityCat.class, Cat.class);
+        bukkitMap.put(EntityCaveSpider.class, CaveSpider.class);
+        bukkitMap.put(EntityChicken.class, Chicken.class);
+        bukkitMap.put(EntityCod.class, Cod.class);
+        bukkitMap.put(EntityCow.class, Cow.class);
+        bukkitMap.put(EntityCreature.class, Creature.class);
+        bukkitMap.put(EntityCreeper.class, Creeper.class);
+        bukkitMap.put(EntityDolphin.class, Dolphin.class);
+        bukkitMap.put(EntityDrowned.class, Drowned.class);
+        bukkitMap.put(EntityEnderDragon.class, EnderDragon.class);
+        bukkitMap.put(EntityEnderman.class, Enderman.class);
+        bukkitMap.put(EntityEndermite.class, Endermite.class);
+        bukkitMap.put(EntityEvoker.class, Evoker.class);
+        bukkitMap.put(EntityFish.class, Fish.class);
+        bukkitMap.put(EntityFishSchool.class, Fish.class); // close enough
+        bukkitMap.put(EntityFlying.class, Flying.class);
+        bukkitMap.put(EntityFox.class, Fox.class);
+        bukkitMap.put(EntityGhast.class, Ghast.class);
+        bukkitMap.put(EntityGiantZombie.class, Giant.class);
+        bukkitMap.put(EntityGolem.class, Golem.class);
+        bukkitMap.put(EntityGuardian.class, Guardian.class);
+        bukkitMap.put(EntityGuardianElder.class, ElderGuardian.class);
+        bukkitMap.put(EntityHorse.class, Horse.class);
+        bukkitMap.put(EntityHorseAbstract.class, AbstractHorse.class);
+        bukkitMap.put(EntityHorseChestedAbstract.class, ChestedHorse.class);
+        bukkitMap.put(EntityHorseDonkey.class, Donkey.class);
+        bukkitMap.put(EntityHorseMule.class, Mule.class);
+        bukkitMap.put(EntityHorseSkeleton.class, SkeletonHorse.class);
+        bukkitMap.put(EntityHorseZombie.class, ZombieHorse.class);
+        bukkitMap.put(EntityIllagerAbstract.class, Illager.class);
+        bukkitMap.put(EntityIllagerIllusioner.class, Illusioner.class);
+        bukkitMap.put(EntityIllagerWizard.class, Spellcaster.class);
+        bukkitMap.put(EntityIronGolem.class, IronGolem.class);
+        bukkitMap.put(EntityLlama.class, Llama.class);
+        bukkitMap.put(EntityLlamaTrader.class, TraderLlama.class);
+        bukkitMap.put(EntityMagmaCube.class, MagmaCube.class);
+        bukkitMap.put(EntityMonster.class, Monster.class);
+        bukkitMap.put(EntityMonsterPatrolling.class, Monster.class); // close enough
+        bukkitMap.put(EntityMushroomCow.class, MushroomCow.class);
+        bukkitMap.put(EntityOcelot.class, Ocelot.class);
+        bukkitMap.put(EntityPanda.class, Panda.class);
+        bukkitMap.put(EntityParrot.class, Parrot.class);
+        bukkitMap.put(EntityPerchable.class, Parrot.class); // close enough
+        bukkitMap.put(EntityPhantom.class, Phantom.class);
+        bukkitMap.put(EntityPig.class, Pig.class);
+        bukkitMap.put(EntityPigZombie.class, PigZombie.class);
+        bukkitMap.put(EntityPillager.class, Pillager.class);
+        bukkitMap.put(EntityPolarBear.class, PolarBear.class);
+        bukkitMap.put(EntityPufferFish.class, PufferFish.class);
+        bukkitMap.put(EntityRabbit.class, Rabbit.class);
+        bukkitMap.put(EntityRaider.class, Raider.class);
+        bukkitMap.put(EntityRavager.class, Ravager.class);
+        bukkitMap.put(EntitySalmon.class, Salmon.class);
+        bukkitMap.put(EntitySheep.class, Sheep.class);
+        bukkitMap.put(EntityShulker.class, Shulker.class);
+        bukkitMap.put(EntitySilverfish.class, Silverfish.class);
+        bukkitMap.put(EntitySkeleton.class, Skeleton.class);
+        bukkitMap.put(EntitySkeletonAbstract.class, Skeleton.class);
+        bukkitMap.put(EntitySkeletonStray.class, Stray.class);
+        bukkitMap.put(EntitySkeletonWither.class, WitherSkeleton.class);
+        bukkitMap.put(EntitySlime.class, Slime.class);
+        bukkitMap.put(EntitySnowman.class, Snowman.class);
+        bukkitMap.put(EntitySpider.class, Spider.class);
+        bukkitMap.put(EntitySquid.class, Squid.class);
+        bukkitMap.put(EntityTameableAnimal.class, Tameable.class);
+        bukkitMap.put(EntityTropicalFish.class, TropicalFish.class);
+        bukkitMap.put(EntityTurtle.class, Turtle.class);
+        bukkitMap.put(EntityVex.class, Vex.class);
+        bukkitMap.put(EntityVillager.class, Villager.class);
+        bukkitMap.put(EntityVillagerAbstract.class, AbstractVillager.class);
+        bukkitMap.put(EntityVillagerTrader.class, WanderingTrader.class);
+        bukkitMap.put(EntityVindicator.class, Vindicator.class);
+        bukkitMap.put(EntityWaterAnimal.class, WaterMob.class);
+        bukkitMap.put(EntityWitch.class, Witch.class);
+        bukkitMap.put(EntityWither.class, Wither.class);
+        bukkitMap.put(EntityWolf.class, Wolf.class);
+        bukkitMap.put(EntityZombie.class, Zombie.class);
+        bukkitMap.put(EntityZombieHusk.class, Husk.class);
+        bukkitMap.put(EntityZombieVillager.class, ZombieVillager.class);
+        bukkitMap.put(EntityHoglin.class, Hoglin.class);
+        bukkitMap.put(EntityPiglin.class, Piglin.class);
+        bukkitMap.put(EntityPiglinAbstract.class, PiglinAbstract.class);
+        bukkitMap.put(EntityPiglinBrute.class, PiglinBrute.class);
+        bukkitMap.put(EntityStrider.class, Strider.class);
+        bukkitMap.put(EntityZoglin.class, Zoglin.class);
+    }
+
+    public static String getUsableName(Class<?> clazz) {
+        String name = clazz.getName();
+        name = name.substring(name.lastIndexOf(".") + 1);
+        boolean flag = false;
+        // inner classes
+        if (name.contains("$")) {
+            String cut = name.substring(name.indexOf("$") + 1);
+            if (cut.length() <= 2) {
+                name = name.replace("Entity", "");
+                name = name.replace("$", "_");
+                flag = true;
+            } else {
+                // mapped, wooo
+                name = cut;
+            }
+        }
+        name = name.replace("PathfinderGoal", "");
+        StringBuilder sb = new StringBuilder();
+        for (char c : name.toCharArray()) {
+            if (c >= 'A' && c <= 'Z') {
+                sb.append("_");
+                sb.append(Character.toLowerCase(c));
+            } else {
+                sb.append(c);
+            }
+        }
+        name = sb.toString();
+        name = name.replaceFirst("_", "");
+
+        if (flag && !deobfuscationMap.containsKey(name.toLowerCase()) && !ignored.contains(name)) {
+            System.out.println("need to map " + clazz.getName() + " (" + name.toLowerCase() + ")");
+        }
+
+        // did we rename this key?
+        return deobfuscationMap.getOrDefault(name, name);
+    }
+
+    public static EnumSet<GoalType> vanillaToPaper(OptimizedSmallEnumSet<PathfinderGoal.Type> types) {
+        EnumSet<GoalType> goals = EnumSet.noneOf(GoalType.class);
+        for (GoalType type : GoalType.values()) {
+            if (types.hasElement(paperToVanilla(type))) {
+                goals.add(type);
+            }
+        }
+        return goals;
+    }
+
+    public static GoalType vanillaToPaper(PathfinderGoal.Type type) {
+        switch (type) {
+            case MOVE:
+                return GoalType.MOVE;
+            case LOOK:
+                return GoalType.LOOK;
+            case JUMP:
+                return GoalType.JUMP;
+            case UNKNOWN_BEHAVIOR:
+                return GoalType.UNKNOWN_BEHAVIOR;
+            case TARGET:
+                return GoalType.TARGET;
+            default:
+                throw new IllegalArgumentException("Unknown vanilla mob goal type " + type.name());
+        }
+    }
+
+    public static EnumSet<PathfinderGoal.Type> paperToVanilla(EnumSet<GoalType> types) {
+        EnumSet<PathfinderGoal.Type> goals = EnumSet.noneOf(PathfinderGoal.Type.class);
+        for (GoalType type : types) {
+            goals.add(paperToVanilla(type));
+        }
+        return goals;
+    }
+
+    public static PathfinderGoal.Type paperToVanilla(GoalType type) {
+        switch (type) {
+            case MOVE:
+                return PathfinderGoal.Type.MOVE;
+            case LOOK:
+                return PathfinderGoal.Type.LOOK;
+            case JUMP:
+                return PathfinderGoal.Type.JUMP;
+            case UNKNOWN_BEHAVIOR:
+                return PathfinderGoal.Type.UNKNOWN_BEHAVIOR;
+            case TARGET:
+                return PathfinderGoal.Type.TARGET;
+            default:
+                throw new IllegalArgumentException("Unknown paper mob goal type " + type.name());
+        }
+    }
+
+    public static <T extends Mob> GoalKey<T> getKey(Class<? extends PathfinderGoal> goalClass) {
+        String name = getUsableName(goalClass);
+        if (ignored.contains(name)) {
+            //noinspection unchecked
+            return (GoalKey<T>) GoalKey.of(Mob.class, NamespacedKey.minecraft(name));
+        }
+        return GoalKey.of(getEntity(goalClass), NamespacedKey.minecraft(name));
+    }
+
+    public static <T extends Mob> Class<T> getEntity(Class<? extends PathfinderGoal> goalClass) {
+        //noinspection unchecked
+        return (Class<T>) entityClassCache.computeIfAbsent(goalClass, key -> {
+            for (Constructor<?> ctor : key.getDeclaredConstructors()) {
+                for (int i = 0; i < ctor.getParameterCount(); i++) {
+                    Class<?> param = ctor.getParameterTypes()[i];
+                    if (EntityInsentient.class.isAssignableFrom(param)) {
+                        //noinspection unchecked
+                        return toBukkitClass((Class<? extends EntityInsentient>) param);
+                    } else if (IRangedEntity.class.isAssignableFrom(param)) {
+                        return RangedEntity.class;
+                    }
+                }
+            }
+            throw new RuntimeException("Can't figure out applicable entity for mob goal " + goalClass); // maybe just return EntityInsentient?
+        });
+    }
+
+    public static Class<? extends Mob> toBukkitClass(Class<? extends EntityInsentient> nmsClass) {
+        Class<? extends Mob> bukkitClass = bukkitMap.get(nmsClass);
+        if (bukkitClass == null) {
+            throw new RuntimeException("Can't figure out applicable bukkit entity for nms entity " + nmsClass); // maybe just return Mob?
+        }
+        return bukkitClass;
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/entity/ai/PaperCustomGoal.java b/src/main/java/com/destroystokyo/paper/entity/ai/PaperCustomGoal.java
new file mode 100644
index 0000000000000000000000000000000000000000..5720feaaf92fc8b6a70e7f6e2d25163c42d231c1
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/entity/ai/PaperCustomGoal.java
@@ -0,0 +1,55 @@
+package com.destroystokyo.paper.entity.ai;
+
+import net.minecraft.server.PathfinderGoal;
+
+import org.bukkit.entity.Mob;
+
+/**
+ * Wraps api in vanilla
+ */
+public class PaperCustomGoal<T extends Mob> extends PathfinderGoal {
+
+    private final Goal<T> handle;
+
+    public PaperCustomGoal(Goal<T> handle) {
+        this.handle = handle;
+
+        this.setTypes(MobGoalHelper.paperToVanilla(handle.getTypes()));
+        if (this.getGoalTypes().size() == 0) {
+            this.getGoalTypes().addUnchecked(Type.UNKNOWN_BEHAVIOR);
+        }
+    }
+
+    @Override
+    public boolean shouldActivate() {
+        return handle.shouldActivate();
+    }
+
+    @Override
+    public boolean shouldStayActive() {
+        return handle.shouldStayActive();
+    }
+
+    @Override
+    public void start() {
+        handle.start();
+    }
+
+    @Override
+    public void onTaskReset() {
+        handle.stop();
+    }
+
+    @Override
+    public void tick() {
+        handle.tick();
+    }
+
+    public Goal<T> getHandle() {
+        return handle;
+    }
+
+    public GoalKey<T> getKey() {
+        return handle.getKey();
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/entity/ai/PaperMobGoals.java b/src/main/java/com/destroystokyo/paper/entity/ai/PaperMobGoals.java
new file mode 100644
index 0000000000000000000000000000000000000000..45b383f4d118d076e41c42b5b158b2a3713341a8
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/entity/ai/PaperMobGoals.java
@@ -0,0 +1,224 @@
+package com.destroystokyo.paper.entity.ai;
+
+import net.minecraft.server.PathfinderGoal;
+import net.minecraft.server.PathfinderGoalSelector;
+import net.minecraft.server.PathfinderGoalWrapped;
+
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.bukkit.craftbukkit.entity.CraftMob;
+import org.bukkit.entity.Mob;
+
+public class PaperMobGoals implements MobGoals {
+
+    private final Map<PathfinderGoal, PaperVanillaGoal<?>> instanceCache = new HashMap<>();
+
+    @Override
+    public <T extends Mob> void addGoal(T mob, int priority, Goal<T> goal) {
+        CraftMob craftMob = (CraftMob) mob;
+        getHandle(craftMob, goal.getTypes()).addGoal(priority, new PaperCustomGoal<>(goal));
+    }
+
+    @Override
+    public <T extends Mob> void removeGoal(T mob, Goal<T> goal) {
+        CraftMob craftMob = (CraftMob) mob;
+        if (goal instanceof PaperCustomGoal) {
+            getHandle(craftMob, goal.getTypes()).removeGoal((PathfinderGoal) goal);
+        } else if (goal instanceof PaperVanillaGoal) {
+            getHandle(craftMob, goal.getTypes()).removeGoal(((PaperVanillaGoal<?>) goal).getHandle());
+        } else {
+            List<PathfinderGoal> toRemove = new LinkedList<>();
+            for (PathfinderGoalWrapped item : getHandle(craftMob, goal.getTypes()).getTasks()) {
+                if (item.getGoal() instanceof PaperCustomGoal) {
+                    //noinspection unchecked
+                    if (((PaperCustomGoal<T>) item.getGoal()).getHandle() == goal) {
+                        toRemove.add(item.getGoal());
+                    }
+                }
+            }
+
+            for (PathfinderGoal g : toRemove) {
+                getHandle(craftMob, goal.getTypes()).removeGoal(g);
+            }
+        }
+    }
+
+    @Override
+    public <T extends Mob> void removeAllGoals(T mob) {
+        for (GoalType type : GoalType.values()) {
+            removeAllGoals(mob, type);
+        }
+    }
+
+    @Override
+    public <T extends Mob> void removeAllGoals(T mob, GoalType type) {
+        for (Goal<T> goal : getAllGoals(mob, type)) {
+            removeGoal(mob, goal);
+        }
+    }
+
+    @Override
+    public <T extends Mob> void removeGoal(T mob, GoalKey<T> key) {
+        for (Goal<T> goal : getGoals(mob, key)) {
+            removeGoal(mob, goal);
+        }
+    }
+
+    @Override
+    public <T extends Mob> boolean hasGoal(T mob, GoalKey<T> key) {
+        for (Goal<T> g : getAllGoals(mob)) {
+            if (g.getKey().equals(key)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public <T extends Mob> Goal<T> getGoal(T mob, GoalKey<T> key) {
+        for (Goal<T> g : getAllGoals(mob)) {
+            if (g.getKey().equals(key)) {
+                return g;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public <T extends Mob> Collection<Goal<T>> getGoals(T mob, GoalKey<T> key) {
+        Set<Goal<T>> goals = new HashSet<>();
+        for (Goal<T> g : getAllGoals(mob)) {
+            if (g.getKey().equals(key)) {
+                goals.add(g);
+            }
+        }
+        return goals;
+    }
+
+    @Override
+    public <T extends Mob> Collection<Goal<T>> getAllGoals(T mob) {
+        Set<Goal<T>> goals = new HashSet<>();
+        for (GoalType type : GoalType.values()) {
+            goals.addAll(getAllGoals(mob, type));
+        }
+        return goals;
+    }
+
+    @Override
+    public <T extends Mob> Collection<Goal<T>> getAllGoals(T mob, GoalType type) {
+        CraftMob craftMob = (CraftMob) mob;
+        Set<Goal<T>> goals = new HashSet<>();
+        for (PathfinderGoalWrapped item : getHandle(craftMob, type).getTasks()) {
+            if (!item.getGoal().getGoalTypes().hasElement(MobGoalHelper.paperToVanilla(type))) {
+                continue;
+            }
+
+            if (item.getGoal() instanceof PaperCustomGoal) {
+                //noinspection unchecked
+                goals.add(((PaperCustomGoal<T>) item.getGoal()).getHandle());
+            } else {
+                //noinspection unchecked
+                goals.add((Goal<T>) instanceCache.computeIfAbsent(item.getGoal(), PaperVanillaGoal::new));
+            }
+        }
+        return goals;
+    }
+
+    @Override
+    public <T extends Mob> Collection<Goal<T>> getAllGoalsWithout(T mob, GoalType type) {
+        CraftMob craftMob = (CraftMob) mob;
+        Set<Goal<T>> goals = new HashSet<>();
+        for (GoalType internalType : GoalType.values()) {
+            if(internalType == type) {
+                continue;
+            }
+            for (PathfinderGoalWrapped item : getHandle(craftMob, internalType).getTasks()) {
+                if (item.getGoal().getGoalTypes().hasElement(MobGoalHelper.paperToVanilla(type))) {
+                    continue;
+                }
+
+                if (item.getGoal() instanceof PaperCustomGoal) {
+                    //noinspection unchecked
+                    goals.add(((PaperCustomGoal<T>) item.getGoal()).getHandle());
+                } else {
+                    //noinspection unchecked
+                    goals.add((Goal<T>) instanceCache.computeIfAbsent(item.getGoal(), PaperVanillaGoal::new));
+                }
+            }
+        }
+        return goals;
+    }
+
+    @Override
+    public <T extends Mob> Collection<Goal<T>> getRunningGoals(T mob) {
+        Set<Goal<T>> goals = new HashSet<>();
+        for (GoalType type : GoalType.values()) {
+            goals.addAll(getRunningGoals(mob, type));
+        }
+        return goals;
+    }
+
+    @Override
+    public <T extends Mob> Collection<Goal<T>> getRunningGoals(T mob, GoalType type) {
+        CraftMob craftMob = (CraftMob) mob;
+        Set<Goal<T>> goals = new HashSet<>();
+        getHandle(craftMob, type).getExecutingGoals()
+            .filter(item -> item.getGoal().getGoalTypes().hasElement(MobGoalHelper.paperToVanilla(type)))
+            .forEach(item -> {
+                if (item.getGoal() instanceof PaperCustomGoal) {
+                    //noinspection unchecked
+                    goals.add(((PaperCustomGoal<T>) item.getGoal()).getHandle());
+                } else {
+                    //noinspection unchecked
+                    goals.add((Goal<T>) instanceCache.computeIfAbsent(item.getGoal(), PaperVanillaGoal::new));
+                }
+            });
+        return goals;
+    }
+
+    @Override
+    public <T extends Mob> Collection<Goal<T>> getRunningGoalsWithout(T mob, GoalType type) {
+        CraftMob craftMob = (CraftMob) mob;
+        Set<Goal<T>> goals = new HashSet<>();
+        for (GoalType internalType : GoalType.values()) {
+            if (internalType == type) {
+                continue;
+            }
+            getHandle(craftMob, internalType).getExecutingGoals()
+                .filter(item -> !item.getGoal().getGoalTypes().hasElement(MobGoalHelper.paperToVanilla(type)))
+                .forEach(item -> {
+                    if (item.getGoal() instanceof PaperCustomGoal) {
+                        //noinspection unchecked
+                        goals.add(((PaperCustomGoal<T>) item.getGoal()).getHandle());
+                    } else {
+                        //noinspection unchecked
+                        goals.add((Goal<T>) instanceCache.computeIfAbsent(item.getGoal(), PaperVanillaGoal::new));
+                    }
+                });
+        }
+        return goals;
+    }
+
+    private PathfinderGoalSelector getHandle(CraftMob mob, EnumSet<GoalType> types) {
+        if (types.contains(GoalType.TARGET)) {
+            return mob.getHandle().targetSelector;
+        } else {
+            return mob.getHandle().goalSelector;
+        }
+    }
+
+    private PathfinderGoalSelector getHandle(CraftMob mob, GoalType type) {
+        if (type == GoalType.TARGET) {
+            return mob.getHandle().targetSelector;
+        } else {
+            return mob.getHandle().goalSelector;
+        }
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/entity/ai/PaperVanillaGoal.java b/src/main/java/com/destroystokyo/paper/entity/ai/PaperVanillaGoal.java
new file mode 100644
index 0000000000000000000000000000000000000000..2988e3d1b37d73b6d1ef514acd237e88a5cc079e
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/entity/ai/PaperVanillaGoal.java
@@ -0,0 +1,63 @@
+package com.destroystokyo.paper.entity.ai;
+
+import net.minecraft.server.PathfinderGoal;
+
+import java.util.EnumSet;
+
+import org.bukkit.entity.Mob;
+
+/**
+ * Wraps vanilla in api
+ */
+public class PaperVanillaGoal<T extends Mob> implements VanillaGoal<T> {
+
+    private final PathfinderGoal handle;
+    private final GoalKey<T> key;
+
+    private final EnumSet<GoalType> types;
+
+    public PaperVanillaGoal(PathfinderGoal handle) {
+        this.handle = handle;
+        this.key = MobGoalHelper.getKey(handle.getClass());
+        this.types = MobGoalHelper.vanillaToPaper(handle.getGoalTypes());
+    }
+
+    public PathfinderGoal getHandle() {
+        return handle;
+    }
+
+    @Override
+    public boolean shouldActivate() {
+        return handle.shouldActivate2();
+    }
+
+    @Override
+    public boolean shouldStayActive() {
+        return handle.shouldStayActive2();
+    }
+
+    @Override
+    public void start() {
+        handle.start();
+    }
+
+    @Override
+    public void stop() {
+        handle.onTaskReset();
+    }
+
+    @Override
+    public void tick() {
+        handle.tick();
+    }
+
+    @Override
+    public GoalKey<T> getKey() {
+        return key;
+    }
+
+    @Override
+    public EnumSet<GoalType> getTypes() {
+        return types;
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java
index 9df0006c1a283f77c4d01d9fce9062fc1c9bbb1f..b3329c6fcd6758a781a51f5ba8f5052ac1c77b49 100644
--- a/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java
+++ b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java
@@ -64,4 +64,8 @@ public final class OptimizedSmallEnumSet<E extends Enum<E>> {
     public boolean hasCommonElements(final OptimizedSmallEnumSet<E> other) {
         return (other.backingSet & this.backingSet) != 0;
     }
+
+    public boolean hasElement(final E element) {
+        return (this.backingSet & (1L << element.ordinal())) != 0;
+    }
 }
diff --git a/src/main/java/net/minecraft/server/PathfinderGoal.java b/src/main/java/net/minecraft/server/PathfinderGoal.java
index 42b690b9215d82346990d93a5cead727b1fa10ab..f103b6630e3d00a4e1a514495c2724111e06f752 100644
--- a/src/main/java/net/minecraft/server/PathfinderGoal.java
+++ b/src/main/java/net/minecraft/server/PathfinderGoal.java
@@ -8,11 +8,17 @@ public abstract class PathfinderGoal {
     private final EnumSet<PathfinderGoal.Type> a = EnumSet.noneOf(PathfinderGoal.Type.class); // Paper unused, but dummy to prevent plugins from crashing as hard. Theyll need to support paper in a special case if this is super important, but really doesn't seem like it would be.
     private final OptimizedSmallEnumSet<Type> goalTypes = new OptimizedSmallEnumSet<>(PathfinderGoal.Type.class); // Paper - remove streams from pathfindergoalselector
 
-    public PathfinderGoal() {}
+    // Paper start make sure goaltypes is never empty
+    public PathfinderGoal() {
+        if (this.goalTypes.size() == 0) {
+            this.goalTypes.addUnchecked(Type.UNKNOWN_BEHAVIOR);
+        }
+    }
+    // paper end
 
-    public abstract boolean a();
+    public boolean a() { return this.shouldActivate(); } public boolean shouldActivate() { return false;} public boolean shouldActivate2() { return a(); } // Paper - OBFHELPER, for both directions...
 
-    public boolean b() {
+    public boolean b() { return this.shouldStayActive(); } public boolean shouldStayActive2() { return b(); } public boolean shouldStayActive() { // Paper - OBFHELPER, for both directions...
         return this.a();
     }
 
@@ -20,19 +26,23 @@ public abstract class PathfinderGoal {
         return true;
     }
 
-    public void c() {}
+    public void c() { this.start(); } public void start() {} // Paper - OBFHELPER
 
     public void d() {
         onTaskReset(); // Paper
     }
     public void onTaskReset() {} // Paper
 
-    public void e() {}
+    public void e() { this.tick(); } public void tick() {} // Paper OBFHELPER
 
-    public void a(EnumSet<PathfinderGoal.Type> enumset) {
+    public void a(EnumSet<PathfinderGoal.Type> enumset) { this.setTypes(enumset); } public void setTypes(EnumSet<PathfinderGoal.Type> enumset) { // Paper - OBFHELPER
         // Paper start - remove streams from pathfindergoalselector
         this.goalTypes.clear();
         this.goalTypes.addAllUnchecked(enumset);
+        // make sure its never empty
+        if (this.goalTypes.size() == 0) {
+            this.goalTypes.addUnchecked(Type.UNKNOWN_BEHAVIOR);
+        }
         // Paper end - remove streams from pathfindergoalselector
     }
 
@@ -48,7 +58,7 @@ public abstract class PathfinderGoal {
 
     public static enum Type {
 
-        MOVE, LOOK, JUMP, TARGET;
+        MOVE, LOOK, JUMP, TARGET, UNKNOWN_BEHAVIOR; // Paper - add unknown
 
         private Type() {}
     }
diff --git a/src/main/java/net/minecraft/server/PathfinderGoalSelector.java b/src/main/java/net/minecraft/server/PathfinderGoalSelector.java
index 22f4fec58fbaab24673dd418700c51671248c510..d3f0327a2a7cdedf3fe8d10df981a9f1cb378d26 100644
--- a/src/main/java/net/minecraft/server/PathfinderGoalSelector.java
+++ b/src/main/java/net/minecraft/server/PathfinderGoalSelector.java
@@ -27,7 +27,7 @@ public class PathfinderGoalSelector {
         }
     };
     private final Map<PathfinderGoal.Type, PathfinderGoalWrapped> c = new EnumMap(PathfinderGoal.Type.class);
-    private final Set<PathfinderGoalWrapped> d = Sets.newLinkedHashSet(); private Set<PathfinderGoalWrapped> getTasks() { return d; }// Paper - OBFHELPER
+    private final Set<PathfinderGoalWrapped> d = Sets.newLinkedHashSet(); public final Set<PathfinderGoalWrapped> getTasks() { return d; }// Paper - OBFHELPER // Paper - private -> public
     private final Supplier<GameProfilerFiller> e;
     private final EnumSet<PathfinderGoal.Type> f = EnumSet.noneOf(PathfinderGoal.Type.class); // Paper unused, but dummy to prevent plugins from crashing as hard. Theyll need to support paper in a special case if this is super important, but really doesn't seem like it would be.
     private final OptimizedSmallEnumSet<PathfinderGoal.Type> goalTypes = new OptimizedSmallEnumSet<>(PathfinderGoal.Type.class); // Paper - remove streams from pathfindergoalselector
@@ -38,7 +38,7 @@ public class PathfinderGoalSelector {
         this.e = supplier;
     }
 
-    public void a(int i, PathfinderGoal pathfindergoal) {
+    public void addGoal(int priority, PathfinderGoal goal) {a(priority, goal);} public void a(int i, PathfinderGoal pathfindergoal) { // Paper - OBFHELPER
         this.d.add(new PathfinderGoalWrapped(i, pathfindergoal));
     }
 
@@ -61,7 +61,7 @@ public class PathfinderGoalSelector {
     }
     // Paper end
 
-    public void a(PathfinderGoal pathfindergoal) {
+    public void removeGoal(PathfinderGoal goal) {a(goal);} public void a(PathfinderGoal pathfindergoal) { // Paper - OBFHELPER
         // Paper start - remove streams from pathfindergoalselector
         for (Iterator<PathfinderGoalWrapped> iterator = this.d.iterator(); iterator.hasNext();) {
             PathfinderGoalWrapped goalWrapped = iterator.next();
@@ -157,6 +157,7 @@ public class PathfinderGoalSelector {
         gameprofilerfiller.exit();
     }
 
+    public final Stream<PathfinderGoalWrapped> getExecutingGoals() { return d(); } // Paper - OBFHELPER
     public Stream<PathfinderGoalWrapped> d() {
         return this.d.stream().filter(PathfinderGoalWrapped::g);
     }
diff --git a/src/main/java/net/minecraft/server/PathfinderGoalWrapped.java b/src/main/java/net/minecraft/server/PathfinderGoalWrapped.java
index 22773eca7561d39cd838338efe6d40ab9421feba..2e56fcc32f914d2b5f4bb9d368f504f4d3e8795c 100644
--- a/src/main/java/net/minecraft/server/PathfinderGoalWrapped.java
+++ b/src/main/java/net/minecraft/server/PathfinderGoalWrapped.java
@@ -5,8 +5,8 @@ import javax.annotation.Nullable;
 
 public class PathfinderGoalWrapped extends PathfinderGoal {
 
-    private final PathfinderGoal a;
-    private final int b;
+    private final PathfinderGoal a; public PathfinderGoal getGoal() {return a;} // Paper - OBFHELPER
+    private final int b; public int getPriority() {return b;} // Paper - OBFHELPER
     private boolean c;
 
     public PathfinderGoalWrapped(int i, PathfinderGoal pathfindergoal) {
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 0acaa3d463995a1863f97a6951fcbdfd61a8ef7e..2b2d292fdd13d86159a791acfd1e70217401e795 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -2321,5 +2321,11 @@ public final class CraftServer implements Server {
     public boolean isStopping() {
         return net.minecraft.server.MinecraftServer.getServer().hasStopped();
     }
+
+    private com.destroystokyo.paper.entity.ai.MobGoals mobGoals = new com.destroystokyo.paper.entity.ai.PaperMobGoals();
+    @Override
+    public com.destroystokyo.paper.entity.ai.MobGoals getMobGoals() {
+        return mobGoals;
+    }
     // Paper end
 }
diff --git a/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java b/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..232c382b586b0812c9f7161565c0d382177adf7d
--- /dev/null
+++ b/src/test/java/com/destroystokyo/paper/entity/ai/VanillaMobGoalTest.java
@@ -0,0 +1,103 @@
+package com.destroystokyo.paper.entity.ai;
+
+import net.minecraft.server.EntityInsentient;
+import net.minecraft.server.PathfinderGoal;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.bukkit.entity.Mob;
+
+import io.github.classgraph.ClassGraph;
+import io.github.classgraph.ScanResult;
+
+public class VanillaMobGoalTest {
+
+    @Test
+    public void testKeys() {
+        List<GoalKey<?>> deprecated = new ArrayList<>();
+        List<GoalKey<?>> keys = new ArrayList<>();
+        for (Field field : VanillaGoal.class.getFields()) {
+            if (field.getType().equals(GoalKey.class)) {
+                try {
+                    GoalKey<?> goalKey = (GoalKey<?>) field.get(null);
+                    if (field.getAnnotation(Deprecated.class) != null) {
+                        deprecated.add(goalKey);
+                    } else {
+                        keys.add(goalKey);
+                    }
+                } catch (IllegalAccessException e) {
+                    System.out.println("Skipping " + field.getName() + ": " + e.getMessage());
+                }
+            }
+        }
+
+        List<Class<?>> classes;
+        try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages("net.minecraft.server").scan()) {
+            classes = scanResult.getSubclasses("net.minecraft.server.PathfinderGoal").loadClasses();
+        }
+
+        List<GoalKey<?>> vanillaNames = classes.stream()
+            .filter(VanillaMobGoalTest::hasNoEnclosingClass)
+            .filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
+            .map(goalClass -> MobGoalHelper.getKey((Class<? extends PathfinderGoal>) goalClass))
+            .collect(Collectors.toList());
+
+        List<GoalKey<?>> missingFromAPI = new ArrayList<>(vanillaNames);
+        missingFromAPI.removeAll(keys);
+        missingFromAPI.removeIf(k -> MobGoalHelper.ignored.contains(k.getNamespacedKey().getKey()));
+        List<GoalKey<?>> missingFromVanilla = new ArrayList<>(keys);
+        missingFromVanilla.removeAll(vanillaNames);
+
+        boolean shouldFail = false;
+        if (missingFromAPI.size() != 0) {
+            System.out.println("Missing from API: ");
+            for (GoalKey<?> key : missingFromAPI) {
+                System.out.println("GoalKey<" + key.getEntityClass().getSimpleName() + "> " + key.getNamespacedKey().getKey().toUpperCase() +
+                                   " = GoalKey.of(" + key.getEntityClass().getSimpleName() + ".class, NamespacedKey.minecraft(\"" + key.getNamespacedKey().getKey() + "\"));");
+            }
+            shouldFail = true;
+        }
+        if (missingFromVanilla.size() != 0) {
+            System.out.println("Missing from vanilla: ");
+            missingFromVanilla.forEach(System.out::println);
+            shouldFail = true;
+        }
+
+        if (deprecated.size() != 0) {
+            System.out.println("Deprecated (might want to remove them at some point): ");
+            deprecated.forEach(System.out::println);
+        }
+
+        if (shouldFail) Assert.fail("See above");
+    }
+
+    private static boolean hasNoEnclosingClass(Class<?> clazz) {
+        return clazz.getEnclosingClass() == null || hasNoEnclosingClass(clazz.getSuperclass());
+    }
+
+    @Test
+    public void testBukkitMap() {
+        List<Class<?>> classes;
+        try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages("net.minecraft.server").scan()) {
+            classes = scanResult.getSubclasses("net.minecraft.server.EntityInsentient").loadClasses();
+        }
+
+        boolean shouldFail = false;
+        for (Class<?> nmsClass : classes) {
+            Class<? extends Mob> bukkitClass = MobGoalHelper.toBukkitClass((Class<? extends EntityInsentient>) nmsClass);
+            if (bukkitClass == null) {
+                shouldFail = true;
+                System.out.println("Missing bukkitMap.put(" + nmsClass.getSimpleName() + ".class, " + nmsClass.getSimpleName().replace("Entity", "") + ".class);");
+            }
+        }
+
+        if (shouldFail) Assert.fail("See above");
+    }
+}