From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: MiniDigger Date: Fri, 3 Jan 2020 16:26:19 +0100 Subject: [PATCH] Implement Mob Goal API diff --git a/pom.xml b/pom.xml index 6cde6f7dcbb3ff2fda0189a06573640777548d27..ef8ee637a8a0e5e703922b2991c58f4f116b23fb 100644 --- a/pom.xml +++ b/pom.xml @@ -133,6 +133,13 @@ 1.3 test + + + io.github.classgraph + classgraph + 4.8.47 + test + 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..7b12014db0abd9c4b89f58ceaaa7d9dd032b027a --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/entity/ai/MobGoalHelper.java @@ -0,0 +1,353 @@ +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 deobfuscationMap = HashBiMap.create(); + private static final Map, Class> entityClassCache = new HashMap<>(); + private static final Map, Class> bukkitMap = new HashMap<>(); + + static final Set 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("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(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 vanillaToPaper(OptimizedSmallEnumSet types) { + EnumSet 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 paperToVanilla(EnumSet types) { + EnumSet 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 GoalKey getKey(Class goalClass) { + String name = getUsableName(goalClass); + if (ignored.contains(name)) { + //noinspection unchecked + return (GoalKey) GoalKey.of(Mob.class, NamespacedKey.minecraft(name)); + } + return GoalKey.of(getEntity(goalClass), NamespacedKey.minecraft(name)); + } + + public static Class getEntity(Class goalClass) { + //noinspection unchecked + return (Class) 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) 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 toBukkitClass(Class nmsClass) { + Class 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 extends PathfinderGoal { + + private final Goal handle; + + public PaperCustomGoal(Goal 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 getHandle() { + return handle; + } + + public GoalKey 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> instanceCache = new HashMap<>(); + + @Override + public void addGoal(T mob, int priority, Goal goal) { + CraftMob craftMob = (CraftMob) mob; + getHandle(craftMob, goal.getTypes()).addGoal(priority, new PaperCustomGoal<>(goal)); + } + + @Override + public void removeGoal(T mob, Goal 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 toRemove = new LinkedList<>(); + for (PathfinderGoalWrapped item : getHandle(craftMob, goal.getTypes()).getTasks()) { + if (item.getGoal() instanceof PaperCustomGoal) { + //noinspection unchecked + if (((PaperCustomGoal) item.getGoal()).getHandle() == goal) { + toRemove.add(item.getGoal()); + } + } + } + + for (PathfinderGoal g : toRemove) { + getHandle(craftMob, goal.getTypes()).removeGoal(g); + } + } + } + + @Override + public void removeAllGoals(T mob) { + for (GoalType type : GoalType.values()) { + removeAllGoals(mob, type); + } + } + + @Override + public void removeAllGoals(T mob, GoalType type) { + for (Goal goal : getAllGoals(mob, type)) { + removeGoal(mob, goal); + } + } + + @Override + public void removeGoal(T mob, GoalKey key) { + for (Goal goal : getGoals(mob, key)) { + removeGoal(mob, goal); + } + } + + @Override + public boolean hasGoal(T mob, GoalKey key) { + for (Goal g : getAllGoals(mob)) { + if (g.getKey().equals(key)) { + return true; + } + } + return false; + } + + @Override + public Goal getGoal(T mob, GoalKey key) { + for (Goal g : getAllGoals(mob)) { + if (g.getKey().equals(key)) { + return g; + } + } + return null; + } + + @Override + public Collection> getGoals(T mob, GoalKey key) { + Set> goals = new HashSet<>(); + for (Goal g : getAllGoals(mob)) { + if (g.getKey().equals(key)) { + goals.add(g); + } + } + return goals; + } + + @Override + public Collection> getAllGoals(T mob) { + Set> goals = new HashSet<>(); + for (GoalType type : GoalType.values()) { + goals.addAll(getAllGoals(mob, type)); + } + return goals; + } + + @Override + public Collection> getAllGoals(T mob, GoalType type) { + CraftMob craftMob = (CraftMob) mob; + Set> 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) item.getGoal()).getHandle()); + } else { + //noinspection unchecked + goals.add((Goal) instanceCache.computeIfAbsent(item.getGoal(), PaperVanillaGoal::new)); + } + } + return goals; + } + + @Override + public Collection> getAllGoalsWithout(T mob, GoalType type) { + CraftMob craftMob = (CraftMob) mob; + Set> 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) item.getGoal()).getHandle()); + } else { + //noinspection unchecked + goals.add((Goal) instanceCache.computeIfAbsent(item.getGoal(), PaperVanillaGoal::new)); + } + } + } + return goals; + } + + @Override + public Collection> getRunningGoals(T mob) { + Set> goals = new HashSet<>(); + for (GoalType type : GoalType.values()) { + goals.addAll(getRunningGoals(mob, type)); + } + return goals; + } + + @Override + public Collection> getRunningGoals(T mob, GoalType type) { + CraftMob craftMob = (CraftMob) mob; + Set> 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) item.getGoal()).getHandle()); + } else { + //noinspection unchecked + goals.add((Goal) instanceCache.computeIfAbsent(item.getGoal(), PaperVanillaGoal::new)); + } + }); + return goals; + } + + @Override + public Collection> getRunningGoalsWithout(T mob, GoalType type) { + CraftMob craftMob = (CraftMob) mob; + Set> 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) item.getGoal()).getHandle()); + } else { + //noinspection unchecked + goals.add((Goal) instanceCache.computeIfAbsent(item.getGoal(), PaperVanillaGoal::new)); + } + }); + } + return goals; + } + + private PathfinderGoalSelector getHandle(CraftMob mob, EnumSet 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 implements VanillaGoal { + + private final PathfinderGoal handle; + private final GoalKey key; + + private final EnumSet 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 getKey() { + return key; + } + + @Override + public EnumSet 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> { public boolean hasCommonElements(final OptimizedSmallEnumSet 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 a85d81186ae92dec72f2cc3a1dcd8c4b7e7ede62..8f26cb20b8c63f2809838c97528cef62ec39bcdb 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 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 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 enumset) { + public void a(EnumSet enumset) { this.setTypes(enumset); } public void setTypes(EnumSet 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 c = new EnumMap(PathfinderGoal.Type.class); - private final Set d = Sets.newLinkedHashSet(); private Set getTasks() { return d; }// Paper - OBFHELPER + private final Set d = Sets.newLinkedHashSet(); public final Set getTasks() { return d; }// Paper - OBFHELPER // Paper - private -> public private final Supplier e; private final EnumSet 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 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 iterator = this.d.iterator(); iterator.hasNext();) { PathfinderGoalWrapped goalWrapped = iterator.next(); @@ -157,6 +157,7 @@ public class PathfinderGoalSelector { gameprofilerfiller.exit(); } + public final Stream getExecutingGoals() { return d(); } // Paper - OBFHELPER public Stream 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 96f4401044cacf88e8e00b5b18821c105e634fba..112d8bab65bf41263a477c5faa717687fe8a2bc9 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 04164a0749fdef5a570cd37781090cddef90aeb1..9e4a67efff99301cceb796573a2cd1f3e0ec06cb 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -2305,5 +2305,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> deprecated = new ArrayList<>(); + List> 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> classes; + try (ScanResult scanResult = new ClassGraph().enableAllInfo().whitelistPackages("net.minecraft.server").scan()) { + classes = scanResult.getSubclasses("net.minecraft.server.PathfinderGoal").loadClasses(); + } + + List> vanillaNames = classes.stream() + .filter(VanillaMobGoalTest::hasNoEnclosingClass) + .filter(clazz -> !Modifier.isAbstract(clazz.getModifiers())) + .map(goalClass -> MobGoalHelper.getKey((Class) goalClass)) + .collect(Collectors.toList()); + + List> missingFromAPI = new ArrayList<>(vanillaNames); + missingFromAPI.removeAll(keys); + missingFromAPI.removeIf(k -> MobGoalHelper.ignored.contains(k.getNamespacedKey().getKey())); + List> 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> 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 bukkitClass = MobGoalHelper.toBukkitClass((Class) 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"); + } +}