From 8b44e8adada72d6734e5b4268064dd422eb0f206 Mon Sep 17 00:00:00 2001 From: Janet Blackquill Date: Fri, 26 Apr 2024 23:59:25 -0400 Subject: [PATCH] Preliminary API for 'new' brains AI system for mobs Would fix #10443 if done. This is a rough proposal for abstracting over the brains AI system for mobs for plugins to make use of, similar to the Goals API. I don't think it's feasible to attempt to make the new AI conform to the old interfaces. At the moment I understand the rough skeleton and arteries of the system, though I'm still trying to piece together what does what in user-facing terms. 'get the mob to walk somewhere' has been unsuccessful so far. As far as API stability concerns, as long as the exposed set of tasks isn't too 1:1 with what Mojang has, then we should be fine (until the next Big Rewrite:tm: whenever that is) considering the general stability of the system across versions since its introductions. There are still some unanswered questions in the API design: - do events make sense to have here? I'd say no considering the amount of lambdas in the Minecraft implementation that make any real introspection API unfeasible. - do we care to expose sensors/make them modifiable? It could probably be done in a followup PR to make custom tasks & sensors a thing, as I'm mostly concerned about exposing what exists in Vanilla at the moment. - is the arrangement of the entrypoints to the new API sensical? Test plugin is included in PR to ease testing of the new API until it's ready to ship. --- .../paper/registry/keys/ActivityKeys.java | 217 ++++++++++++ .../java/io/papermc/generator/Generators.java | 2 + ...API-for-working-with-new-mob-AI-syst.patch | 202 +++++++++++ ...inary-API-for-working-with-new-mob-A.patch | 323 ++++++++++++++++++ .../io/papermc/testplugin/TestPlugin.java | 63 ++++ 5 files changed, 807 insertions(+) create mode 100644 paper-api-generator/generated/io/papermc/paper/registry/keys/ActivityKeys.java create mode 100644 patches/api/0474-Add-preliminary-API-for-working-with-new-mob-AI-syst.patch create mode 100644 patches/server/1060-Implement-preliminary-API-for-working-with-new-mob-A.patch diff --git a/paper-api-generator/generated/io/papermc/paper/registry/keys/ActivityKeys.java b/paper-api-generator/generated/io/papermc/paper/registry/keys/ActivityKeys.java new file mode 100644 index 0000000000..b5bad6e77d --- /dev/null +++ b/paper-api-generator/generated/io/papermc/paper/registry/keys/ActivityKeys.java @@ -0,0 +1,217 @@ +package io.papermc.paper.registry.keys; + +import static net.kyori.adventure.key.Key.key; + +import io.papermc.paper.entity.ai.Activity; +import io.papermc.paper.generated.GeneratedFrom; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.TypedKey; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Vanilla keys for {@link RegistryKey#ACTIVITY}. + * + * @apiNote The fields provided here are a direct representation of + * what is available from the vanilla game source. They may be + * changed (including removals) on any Minecraft version + * bump, so cross-version compatibility is not provided on the + * same level as it is on most of the other API. + */ +@SuppressWarnings({ + "unused", + "SpellCheckingInspection" +}) +@GeneratedFrom("1.20.4") +@ApiStatus.Experimental +public final class ActivityKeys { + /** + * {@code minecraft:core} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey CORE = create(key("core")); + + /** + * {@code minecraft:idle} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey IDLE = create(key("idle")); + + /** + * {@code minecraft:work} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey WORK = create(key("work")); + + /** + * {@code minecraft:play} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey PLAY = create(key("play")); + + /** + * {@code minecraft:rest} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey REST = create(key("rest")); + + /** + * {@code minecraft:meet} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey MEET = create(key("meet")); + + /** + * {@code minecraft:panic} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey PANIC = create(key("panic")); + + /** + * {@code minecraft:raid} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey RAID = create(key("raid")); + + /** + * {@code minecraft:pre_raid} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey PRE_RAID = create(key("pre_raid")); + + /** + * {@code minecraft:hide} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey HIDE = create(key("hide")); + + /** + * {@code minecraft:fight} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey FIGHT = create(key("fight")); + + /** + * {@code minecraft:celebrate} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey CELEBRATE = create(key("celebrate")); + + /** + * {@code minecraft:admire_item} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey ADMIRE_ITEM = create(key("admire_item")); + + /** + * {@code minecraft:avoid} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey AVOID = create(key("avoid")); + + /** + * {@code minecraft:ride} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey RIDE = create(key("ride")); + + /** + * {@code minecraft:play_dead} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey PLAY_DEAD = create(key("play_dead")); + + /** + * {@code minecraft:long_jump} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey LONG_JUMP = create(key("long_jump")); + + /** + * {@code minecraft:ram} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey RAM = create(key("ram")); + + /** + * {@code minecraft:tongue} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey TONGUE = create(key("tongue")); + + /** + * {@code minecraft:swim} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey SWIM = create(key("swim")); + + /** + * {@code minecraft:lay_spawn} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey LAY_SPAWN = create(key("lay_spawn")); + + /** + * {@code minecraft:sniff} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey SNIFF = create(key("sniff")); + + /** + * {@code minecraft:investigate} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey INVESTIGATE = create(key("investigate")); + + /** + * {@code minecraft:roar} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey ROAR = create(key("roar")); + + /** + * {@code minecraft:emerge} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey EMERGE = create(key("emerge")); + + /** + * {@code minecraft:dig} + * + * @apiNote This field is version-dependant and may be removed in future Minecraft versions + */ + public static final TypedKey DIG = create(key("dig")); + + private ActivityKeys() { + } + + private static @NotNull TypedKey create(final @NotNull Key key) { + return TypedKey.create(RegistryKey.ACTIVITY, key); + } +} diff --git a/paper-api-generator/src/main/java/io/papermc/generator/Generators.java b/paper-api-generator/src/main/java/io/papermc/generator/Generators.java index ac62e26e93..294b3bb555 100644 --- a/paper-api-generator/src/main/java/io/papermc/generator/Generators.java +++ b/paper-api-generator/src/main/java/io/papermc/generator/Generators.java @@ -13,6 +13,7 @@ import org.bukkit.generator.structure.Structure; import org.bukkit.generator.structure.StructureType; import org.bukkit.inventory.meta.trim.TrimMaterial; import org.bukkit.inventory.meta.trim.TrimPattern; +import io.papermc.paper.entity.ai.Activity; public interface Generators { @@ -23,6 +24,7 @@ public interface Generators { simpleKey("TrimPatternKeys", TrimPattern.class, Registries.TRIM_PATTERN, RegistryKey.TRIM_PATTERN, true), simpleKey("StructureKeys", Structure.class, Registries.STRUCTURE, RegistryKey.STRUCTURE, true), simpleKey("StructureTypeKeys", StructureType.class, Registries.STRUCTURE_TYPE, RegistryKey.STRUCTURE_TYPE, false), + simpleKey("ActivityKeys", Activity.class, Registries.ACTIVITY, RegistryKey.ACTIVITY, false), new MobGoalGenerator("VanillaGoal", "com.destroystokyo.paper.entity.ai") }; diff --git a/patches/api/0474-Add-preliminary-API-for-working-with-new-mob-AI-syst.patch b/patches/api/0474-Add-preliminary-API-for-working-with-new-mob-AI-syst.patch new file mode 100644 index 0000000000..437d4f759b --- /dev/null +++ b/patches/api/0474-Add-preliminary-API-for-working-with-new-mob-AI-syst.patch @@ -0,0 +1,202 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Janet Blackquill +Date: Fri, 26 Apr 2024 23:45:11 -0400 +Subject: [PATCH] Add preliminary API for working with 'new' mob AI system + + +diff --git a/src/main/java/io/papermc/paper/entity/ai/Activity.java b/src/main/java/io/papermc/paper/entity/ai/Activity.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0dff7ba2e36c1185453832e28466c82ccb2cd621 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/entity/ai/Activity.java +@@ -0,0 +1,9 @@ ++package io.papermc.paper.entity.ai; ++ ++import org.bukkit.Keyed; ++ ++/** ++ * An Activity is a key to a list of tasks in a Brain. ++ */ ++public interface Activity extends Keyed { ++} +diff --git a/src/main/java/io/papermc/paper/entity/ai/Brain.java b/src/main/java/io/papermc/paper/entity/ai/Brain.java +new file mode 100644 +index 0000000000000000000000000000000000000000..b359ece13fece6d89b1a1cbf55a78af8152a93de +--- /dev/null ++++ b/src/main/java/io/papermc/paper/entity/ai/Brain.java +@@ -0,0 +1,49 @@ ++package io.papermc.paper.entity.ai; ++ ++import java.util.List; ++import org.bukkit.entity.LivingEntity; ++import org.bukkit.entity.Entity; ++import org.bukkit.Location; ++import org.jetbrains.annotations.NotNull; ++ ++/** ++ * A brain is the central AI for some modern Minecraft entities such as ++ * villagers and sniffers. ++ */ ++public interface Brain { ++ /** ++ * If the tasks in the given `activity` can be run, the brain will switch to ++ * the given `activity`. ++ */ ++ public void useActivityIfPossible(@NotNull Activity activity); ++ /** ++ * Makes the brain switch to the default activity. ++ */ ++ public void useDefaultActivity(); ++ /** ++ * Sets the tasks for the given `activity` to the provided list of `tasks`. ++ */ ++ ////// TODO: what is that int parameter doing ++ public void setTasksForActivity(@NotNull Activity activity, int begin, @NotNull List> tasks); ++ /** ++ * Clears all tasks for activities associated with this brain. ++ */ ++ public void clearActivities(); ++ /** ++ * Sets the default activity for this brain. ++ */ ++ public void setDefaultActivity(@NotNull Activity activity); ++ /** ++ * Checks whether the given activity is active. ++ */ ++ public boolean isActive(@NotNull Activity activity); ++ /** ++ * Sets the entity's current walk target to the given location. ++ */ ++ public void setWalkTarget(@NotNull Location location, float speed, int completeWithinDistance); ++ /** ++ * Sets the entity's current walk target to the given entity. ++ */ ++ public void setWalkTarget(@NotNull Entity entity, float speed, int completeWithinDistance); ++} ++ +diff --git a/src/main/java/io/papermc/paper/entity/ai/Task.java b/src/main/java/io/papermc/paper/entity/ai/Task.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8ee58cbb3a520a473e28e48e378c8a9fcb7d24eb +--- /dev/null ++++ b/src/main/java/io/papermc/paper/entity/ai/Task.java +@@ -0,0 +1,10 @@ ++package io.papermc.paper.entity.ai; ++ ++import org.bukkit.entity.LivingEntity; ++ ++/** ++ * A task can be associated with an Activity in a Brain in order ++ * to instruct the entity to do something. ++ */ ++public interface Task { ++} +\ No newline at end of file +diff --git a/src/main/java/io/papermc/paper/entity/ai/Tasks.java b/src/main/java/io/papermc/paper/entity/ai/Tasks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..955dec447a3b93016b62b161b5018fd75251f962 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/entity/ai/Tasks.java +@@ -0,0 +1,45 @@ ++package io.papermc.paper.entity.ai; ++ ++import org.bukkit.Location; ++import org.bukkit.entity.Mob; ++import org.bukkit.entity.memory.MemoryKey; ++import org.jetbrains.annotations.NotNull; ++import java.util.function.Predicate; ++import java.util.function.Function; ++import org.bukkit.entity.LivingEntity; ++import java.util.Map; ++ ++public interface Tasks { ++ /** ++ * Instructs the entity to get within N blocks of the block location. ++ */ ++ public @NotNull Task walkToWalkTarget(int minRunTime, int maxRunTime); ++ /** ++ * Instructs the entity to swim if it is in water. ++ */ ++ public @NotNull Task swimIfInWater(float chance); ++ /** ++ * Instructs the entity to panic if it is hit, freezing, or on fire. ++ */ ++ public @NotNull Task panicOnDamage(float speed); ++ /** ++ * Instructs the entity to change its look target to the closest target ++ * for which the predicate returns true. ++ */ ++ public @NotNull Task setLookTarget(@NotNull Predicate predicate, float maximumDistance); ++ /** ++ * Instructs the entity to set its walk target to the look target if it matches the given predicate. ++ * ++ * @param speed a function defining the speed at which the entity will walk to the given target ++ * @param completionRange the distance in blocks that the entity will attempt to get within ++ */ ++ public @NotNull Task setWalkTargetToLookTarget(@NotNull Predicate predicate, @NotNull Function speed, int completionRange); ++ /** ++ * Instructs the entity to look at the look target. ++ */ ++ public @NotNull Task lookAtLookTarget(int minRunTime, int maxRunTime); ++ /** ++ * Instructs the entity to run one of the given tasks with weighted probability. ++ */ ++ public @NotNull Task runOneOf(@NotNull Map, Integer> tasks); ++} +diff --git a/src/main/java/io/papermc/paper/registry/RegistryKey.java b/src/main/java/io/papermc/paper/registry/RegistryKey.java +index c4b30b16ce4db754b958c493ad86d0863592c263..05f96057ffdd488877517625ce82dc83712150c5 100644 +--- a/src/main/java/io/papermc/paper/registry/RegistryKey.java ++++ b/src/main/java/io/papermc/paper/registry/RegistryKey.java +@@ -8,6 +8,7 @@ import org.bukkit.generator.structure.StructureType; + import org.bukkit.inventory.meta.trim.TrimMaterial; + import org.bukkit.inventory.meta.trim.TrimPattern; + import org.jetbrains.annotations.ApiStatus; ++import io.papermc.paper.entity.ai.Activity; + + import static io.papermc.paper.registry.RegistryKeyImpl.create; + +@@ -35,6 +36,11 @@ public sealed interface RegistryKey extends Keyed permits RegistryKeyImpl { + * @see io.papermc.paper.registry.keys.GameEventKeys + */ + RegistryKey GAME_EVENT = create("game_event"); ++ /** ++ * Built-in registry for activities ++ * @see io.papermc.paper.registry.keys.ActivityKeys ++ */ ++ RegistryKey ACTIVITY = create("activity"); + /** + * Built-in registry for structure types. + * @see io.papermc.paper.registry.keys.StructureTypeKeys +diff --git a/src/main/java/org/bukkit/Registry.java b/src/main/java/org/bukkit/Registry.java +index e1fb4d8cca6a9c59047b1396f5c40bea957d777a..7e1f769db2a0df7e687a7af2c9329d93ecf00db1 100644 +--- a/src/main/java/org/bukkit/Registry.java ++++ b/src/main/java/org/bukkit/Registry.java +@@ -270,6 +270,13 @@ public interface Registry extends Iterable { + Registry GAME_EVENT = Objects.requireNonNull(Bukkit.getRegistry(GameEvent.class), "No registry present for GameEvent. This is a bug."); + + // Paper start ++ /** ++ * Activities. ++ * ++ * @see io.papermc.paper.entity.ai.Activity ++ */ ++ Registry ACTIVITY = Bukkit.getRegistry(io.papermc.paper.entity.ai.Activity.class); ++ + /** + * Configured structures. + * @see io.papermc.paper.world.structure.ConfiguredStructure +diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java +index 4ff1b38eb65f97344257204cf018f176f247ed36..4f57f4e0d65a99da3d5ce1ccc5b1534d608c3d36 100644 +--- a/src/main/java/org/bukkit/Server.java ++++ b/src/main/java/org/bukkit/Server.java +@@ -2518,4 +2518,11 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + */ + boolean isOwnedByCurrentRegion(@NotNull Entity entity); + // Paper end - Folia region threading API ++ // Paper start - activities and tasks API ++ @NotNull ++ io.papermc.paper.entity.ai.Tasks getTasks(); ++ ++ @NotNull ++ io.papermc.paper.entity.ai.Brain getBrain(@NotNull E entity); ++ // Paper end + } diff --git a/patches/server/1060-Implement-preliminary-API-for-working-with-new-mob-A.patch b/patches/server/1060-Implement-preliminary-API-for-working-with-new-mob-A.patch new file mode 100644 index 0000000000..97697c164a --- /dev/null +++ b/patches/server/1060-Implement-preliminary-API-for-working-with-new-mob-A.patch @@ -0,0 +1,323 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Janet Blackquill +Date: Fri, 26 Apr 2024 23:46:35 -0400 +Subject: [PATCH] Implement preliminary API for working with 'new' mob AI + system + + +diff --git a/src/main/java/io/papermc/paper/entity/ai/PaperActivity.java b/src/main/java/io/papermc/paper/entity/ai/PaperActivity.java +new file mode 100644 +index 0000000000000000000000000000000000000000..8724ac343b62f29020aa06093fc01787619d64fb +--- /dev/null ++++ b/src/main/java/io/papermc/paper/entity/ai/PaperActivity.java +@@ -0,0 +1,36 @@ ++package io.papermc.paper.entity.ai; ++ ++import net.minecraft.world.entity.schedule.Activity; ++import net.minecraft.core.registries.Registries; ++import org.bukkit.NamespacedKey; ++import org.bukkit.Registry; ++import org.bukkit.craftbukkit.CraftRegistry; ++import org.bukkit.craftbukkit.util.Handleable; ++ ++public class PaperActivity implements io.papermc.paper.entity.ai.Activity, Handleable { ++ final private NamespacedKey key; ++ final private Activity handle; ++ ++ public static io.papermc.paper.entity.ai.Activity minecraftToBukkit(Activity minecraft) { ++ return CraftRegistry.minecraftToBukkit(minecraft, Registries.ACTIVITY, Registry.ACTIVITY); ++ } ++ public static Activity bukkitToMinecraft(io.papermc.paper.entity.ai.Activity bukkit) { ++ return CraftRegistry.bukkitToMinecraft(bukkit); ++ } ++ ++ public PaperActivity(NamespacedKey key, Activity handle) { ++ this.key = key; ++ this.handle = handle; ++ } ++ ++ @Override ++ public Activity getHandle() { ++ return this.handle; ++ } ++ ++ @Override ++ public NamespacedKey getKey() { ++ if (true) return java.util.Objects.requireNonNull(org.bukkit.Registry.ACTIVITY.getKey(this), () -> this + " doesn't have a key"); ++ return this.key; ++ } ++} +diff --git a/src/main/java/io/papermc/paper/entity/ai/PaperBrain.java b/src/main/java/io/papermc/paper/entity/ai/PaperBrain.java +new file mode 100644 +index 0000000000000000000000000000000000000000..dfc84db49790e65bae01623b232b322e319d3d38 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/entity/ai/PaperBrain.java +@@ -0,0 +1,71 @@ ++package io.papermc.paper.entity.ai; ++ ++import java.util.List; ++import org.bukkit.entity.LivingEntity; ++import net.minecraft.world.entity.ai.Brain; ++import net.minecraft.world.entity.ai.memory.MemoryModuleType; ++import net.minecraft.world.entity.ai.memory.MemoryStatus; ++import com.google.common.collect.ImmutableList; ++import net.minecraft.world.entity.ai.behavior.BehaviorControl; ++import org.jetbrains.annotations.NotNull; ++import org.bukkit.entity.Entity; ++import org.bukkit.craftbukkit.entity.CraftEntity; ++import org.bukkit.Location; ++import net.minecraft.world.entity.ai.memory.WalkTarget; ++import org.bukkit.craftbukkit.util.CraftLocation; ++ ++public class PaperBrain implements io.papermc.paper.entity.ai.Brain { ++ public final Brain handle; ++ ++ public PaperBrain(Brain handle) { ++ this.handle = handle; ++ } ++ public Brain getHandle() { ++ return this.handle; ++ } ++ ++ @Override ++ public void useDefaultActivity() { ++ this.getHandle().useDefaultActivity(); ++ } ++ ++ @Override ++ public void useActivityIfPossible(@NotNull Activity activity) { ++ this.getHandle().setActiveActivityIfPossible(((PaperActivity)activity).getHandle()); ++ } ++ ++ @Override ++ public void setTasksForActivity(@NotNull Activity activity, int begin, @NotNull List> tasks) { ++ this.getHandle().addActivity( ++ ((PaperActivity)activity).getHandle(), ++ begin, ++ ImmutableList.>copyOf(tasks.stream().map(task -> ((PaperTask)task).getHandle()).iterator()) ++ ); ++ } ++ ++ @Override ++ public void clearActivities() { ++ this.getHandle().removeAllBehaviors(); ++ } ++ ++ @Override ++ public void setDefaultActivity(@NotNull Activity activity) { ++ this.getHandle().setDefaultActivity(((PaperActivity)activity).getHandle()); ++ } ++ ++ @Override ++ public boolean isActive(@NotNull Activity activity) { ++ return this.getHandle().isActive(((PaperActivity)activity).getHandle()); ++ } ++ ++ @Override ++ public void setWalkTarget(@NotNull Location location, float speed, int completeWithinDistance) { ++ this.getHandle().setMemory(MemoryModuleType.WALK_TARGET, new WalkTarget(CraftLocation.toBlockPosition(location), speed, completeWithinDistance)); ++ } ++ ++ @Override ++ public void setWalkTarget(@NotNull Entity entity, float speed, int completeWithinDistance) { ++ this.getHandle().setMemory(MemoryModuleType.WALK_TARGET, new WalkTarget(((CraftEntity)entity).getHandle(), speed, completeWithinDistance)); ++ } ++} ++ +diff --git a/src/main/java/io/papermc/paper/entity/ai/PaperTask.java b/src/main/java/io/papermc/paper/entity/ai/PaperTask.java +new file mode 100644 +index 0000000000000000000000000000000000000000..870ff6a179b62302c1b6dbf603fd5621396dd964 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/entity/ai/PaperTask.java +@@ -0,0 +1,16 @@ ++package io.papermc.paper.entity.ai; ++ ++import org.bukkit.entity.LivingEntity; ++ ++import net.minecraft.world.entity.ai.behavior.BehaviorControl; ++ ++public class PaperTask implements io.papermc.paper.entity.ai.Task { ++ final private BehaviorControl handle; ++ ++ public PaperTask(BehaviorControl handle) { ++ this.handle = handle; ++ } ++ public BehaviorControl getHandle() { ++ return this.handle; ++ } ++} +\ No newline at end of file +diff --git a/src/main/java/io/papermc/paper/entity/ai/PaperTasks.java b/src/main/java/io/papermc/paper/entity/ai/PaperTasks.java +new file mode 100644 +index 0000000000000000000000000000000000000000..433fcfdbb0b0112f2e80e71fc9bea19ad62d32b5 +--- /dev/null ++++ b/src/main/java/io/papermc/paper/entity/ai/PaperTasks.java +@@ -0,0 +1,54 @@ ++package io.papermc.paper.entity.ai; ++ ++import org.bukkit.Location; ++import org.bukkit.entity.Mob; ++import org.bukkit.entity.LivingEntity; ++import org.bukkit.entity.memory.MemoryKey; ++import net.minecraft.world.entity.ai.behavior.BehaviorControl; ++import net.minecraft.world.entity.ai.behavior.MoveToTargetSink; ++import net.minecraft.world.entity.ai.behavior.LookAtTargetSink; ++import net.minecraft.world.entity.ai.behavior.Swim; ++import net.minecraft.world.entity.ai.behavior.AnimalPanic; ++import net.minecraft.world.entity.ai.behavior.SetEntityLookTarget; ++import net.minecraft.world.entity.ai.behavior.SetWalkTargetFromLookTarget; ++import net.minecraft.world.entity.ai.behavior.RunOne; ++import org.bukkit.craftbukkit.entity.memory.CraftMemoryKey; ++import org.jetbrains.annotations.NotNull; ++import java.util.function.Predicate; ++import java.util.function.Function; ++import java.util.Map; ++import java.util.List; ++import java.util.ArrayList; ++import com.mojang.datafixers.util.Pair; ++ ++public class PaperTasks implements io.papermc.paper.entity.ai.Tasks { ++ public @NotNull Task walkToWalkTarget(int minRunTime, int maxRunTime) { ++ return new PaperTask(new MoveToTargetSink(minRunTime, maxRunTime)); ++ } ++ public @NotNull Task swimIfInWater(float chance) { ++ return new PaperTask(new Swim(chance)); ++ } ++ public @NotNull Task panicOnDamage(float speed) { ++ return new PaperTask(new AnimalPanic(speed)); ++ } ++ public @NotNull Task setLookTarget(Predicate predicate, float maximumDistance) { ++ return new PaperTask(SetEntityLookTarget.create(nmsEntity -> predicate.test(nmsEntity.getBukkitLivingEntity()), maximumDistance)); ++ } ++ public @NotNull Task setWalkTargetToLookTarget(Predicate predicate, Function speed, int completionRange) { ++ return new PaperTask(SetWalkTargetFromLookTarget.create( ++ nmsEntity -> predicate.test(nmsEntity.getBukkitLivingEntity()), ++ nmsEntity -> speed.apply(nmsEntity.getBukkitLivingEntity()), ++ completionRange ++ )); ++ } ++ public @NotNull Task lookAtLookTarget(int minRunTime, int maxRunTime) { ++ return new PaperTask(new LookAtTargetSink(minRunTime, maxRunTime)); ++ } ++ public @NotNull Task runOneOf(Map, Integer> tasks) { ++ List, Integer>> mcTasks = new ArrayList<>(tasks.size()); ++ for (Map.Entry, Integer> entry : tasks.entrySet()) { ++ mcTasks.add(new Pair(((PaperTask)entry.getKey()).getHandle(), entry.getValue())); ++ } ++ return new PaperTask(new RunOne(mcTasks)); ++ } ++} +\ No newline at end of file +diff --git a/src/main/java/net/minecraft/world/entity/ai/Brain.java b/src/main/java/net/minecraft/world/entity/ai/Brain.java +index afbb027021acfbe25d534a84f1750e420bbde6e0..8ca8152a98710ee256fc1a11be6cd932bc6695fb 100644 +--- a/src/main/java/net/minecraft/world/entity/ai/Brain.java ++++ b/src/main/java/net/minecraft/world/entity/ai/Brain.java +@@ -79,7 +79,7 @@ public class Brain { + } + + public DataResult> decode(DynamicOps dynamicOps, MapLike mapLike) { +- MutableObject>>> mutableObject = new MutableObject<>( ++ MutableObject>>> dataResultMutableObject = new MutableObject<>( // Paper - fix decompilation name collision + DataResult.success(ImmutableList.builder()) + ); + mapLike.entries() +@@ -91,10 +91,10 @@ public class Brain { + DataResult> dataResult2 = dataResult.flatMap( + memoryType -> this.captureRead((MemoryModuleType)memoryType, dynamicOps, (T)pair.getSecond()) + ); +- mutableObject.setValue(mutableObject.getValue().apply2(Builder::add, dataResult2)); ++ dataResultMutableObject.setValue(dataResultMutableObject.getValue().apply2(Builder::add, dataResult2)); // Paper - fix decompilation name collision + } + ); +- ImmutableList> immutableList = mutableObject.getValue() ++ ImmutableList> immutableList = dataResultMutableObject.getValue() // Paper - fix decompilation name collision + .resultOrPartial(Brain.LOGGER::error) + .map(Builder::build) + .orElseGet(ImmutableList::of); +@@ -180,28 +180,30 @@ public class Brain { + } + + void setMemoryInternal(MemoryModuleType type, Optional> memory) { +- if (this.memories.containsKey(type)) { ++ // Paper start - allow custom memories to be set on any brain ++ // if (this.memories.containsKey(type)) { + if (memory.isPresent() && this.isEmptyCollection(memory.get().getValue())) { + this.eraseMemory(type); + } else { + this.memories.put(type, memory); + } +- } ++ // } ++ // Paper end - allow custom memories to be set on any brain + } + + public Optional getMemory(MemoryModuleType type) { +- Optional> optional = this.memories.get(type); ++ Optional> optional = (Optional>)this.memories.get(type); // Paper - decompilation fix + if (optional == null) { + throw new IllegalStateException("Unregistered memory fetched: " + type); + } else { +- return optional.map(ExpirableValue::getValue); ++ return optional.map(x -> x.getValue()); // Paper - decompilation fix + } + } + + @Nullable + public Optional getMemoryInternal(MemoryModuleType type) { +- Optional> optional = this.memories.get(type); +- return optional == null ? null : optional.map(ExpirableValue::getValue); ++ Optional> optional = (Optional>)this.memories.get(type); // Paper - decompilation fix ++ return optional == null ? null : optional.map(x -> x.getValue()); // Paper - decompilation fix + } + + public long getTimeUntilExpiry(MemoryModuleType type) { +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java +index 4fc02698a9312496e7f9bce1c64f317374d2a42f..05e3feb65dc9288c8288c1ecef7426bed018c57e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftRegistry.java +@@ -125,6 +125,11 @@ public class CraftRegistry implements Registry { + return new io.papermc.paper.world.structure.PaperConfiguredStructure.LegacyRegistry(registryHolder.registryOrThrow(Registries.STRUCTURE)); + } + // Paper end ++ // Paper start - activity registry ++ if (bukkitClass == io.papermc.paper.entity.ai.Activity.class) { ++ return new CraftRegistry<>(io.papermc.paper.entity.ai.Activity.class, registryHolder.registryOrThrow(Registries.ACTIVITY), io.papermc.paper.entity.ai.PaperActivity::new); ++ } ++ // Paper end - activity registry + + return null; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index c490a29bcf7410bc54959ee71375605964379ed5..b54752d076d187f23aafa90514b62c4301dca8ab 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -3259,4 +3259,17 @@ public final class CraftServer implements Server { + } + + // Paper end ++ // Paper begin - activities and tasks AI API ++ private final io.papermc.paper.entity.ai.Tasks tasks = new io.papermc.paper.entity.ai.PaperTasks(); ++ ++ @Override ++ public io.papermc.paper.entity.ai.Tasks getTasks() { ++ return this.tasks; ++ } ++ ++ @Override ++ public io.papermc.paper.entity.ai.Brain getBrain(E entity) { ++ return new io.papermc.paper.entity.ai.PaperBrain(((org.bukkit.craftbukkit.entity.CraftLivingEntity)entity).getHandle().getBrain()); ++ } ++ // Paper end - activities and tasks AI API + } +diff --git a/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java b/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java +index 167cd003a52b722772708a528d724db14c89c35a..c4ee058f4ea6f320b945056f9848afdeb2ae1fc2 100644 +--- a/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java ++++ b/src/test/java/org/bukkit/support/provider/RegistriesArgumentProvider.java +@@ -43,6 +43,7 @@ public class RegistriesArgumentProvider implements ArgumentsProvider { + DATA.add(Arguments.of(TrimMaterial.class, Registries.TRIM_MATERIAL, CraftTrimMaterial.class, net.minecraft.world.item.armortrim.TrimMaterial.class)); + DATA.add(Arguments.of(TrimPattern.class, Registries.TRIM_PATTERN, CraftTrimPattern.class, net.minecraft.world.item.armortrim.TrimPattern.class)); + DATA.add(Arguments.of(DamageType.class, Registries.DAMAGE_TYPE, CraftDamageType.class, net.minecraft.world.damagesource.DamageType.class)); ++ DATA.add(Arguments.of(io.papermc.paper.entity.ai.Activity.class, Registries.ACTIVITY, io.papermc.paper.entity.ai.PaperActivity.class, net.minecraft.world.entity.schedule.Activity.class)); // Paper - add activity registry + } + + @Override diff --git a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java index 4e68423bb7..07d686abce 100644 --- a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java +++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java @@ -2,11 +2,74 @@ package io.papermc.testplugin; import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Sniffer; +import java.util.List; +import org.bukkit.Bukkit; +import org.bukkit.entity.memory.MemoryKey; +import java.util.Collections; +import io.papermc.paper.registry.keys.ActivityKeys; +import org.bukkit.Registry; +import org.bukkit.NamespacedKey; +import java.util.Map; public final class TestPlugin extends JavaPlugin implements Listener { + public class SnifferCommand extends org.bukkit.command.Command { + public SnifferCommand() { + super("sniffer"); + } + public boolean execute(CommandSender sender, String label, String []params) { + var player = (Player)sender; + var sniffer = (Sniffer)player.getWorld() + .spawnEntity(player.getLocation(), EntityType.SNIFFER); + + var target = player.getLocation(); + target.add(10, 0, 0); + + var server = Bukkit.getServer(); + var core = Registry.ACTIVITY.get(new NamespacedKey(ActivityKeys.CORE.key().namespace(), ActivityKeys.CORE.key().value())); + var idle = Registry.ACTIVITY.get(new NamespacedKey(ActivityKeys.IDLE.key().namespace(), ActivityKeys.IDLE.key().value())); + var brain = Bukkit.getServer().getBrain(sniffer); + var tasks = server.getTasks(); + brain.setWalkTarget(target, 1.25F, 0); + brain.clearActivities(); + brain.setTasksForActivity(core, 0, List.of( + tasks.swimIfInWater(0.8f), + tasks.panicOnDamage(9.0f), + tasks.runOneOf( + Map.of( + tasks.setLookTarget(entity -> entity instanceof Sniffer, 30f), + 6, + tasks.setLookTarget(entity -> entity instanceof Player, 30f), + 12 + ) + ), + tasks.runOneOf( + Map.of( + tasks.setWalkTargetToLookTarget(entity -> true, entity -> 9.0f, 0), + 6, + tasks.setWalkTargetToLookTarget(entity -> true, entity -> 2.0f, 0), + 12 + ) + ) + )); + brain.setTasksForActivity(idle, 0, List.of( + tasks.lookAtLookTarget(150, 250), + tasks.walkToWalkTarget(150, 250) + )); + brain.useActivityIfPossible(idle); + + sender.sendMessage(brain.isActive(idle) ? "Yes!" : "no no no"); + + return true; + } + } @Override public void onEnable() { + this.getServer().getCommandMap().register("sniffer", new SnifferCommand()); this.getServer().getPluginManager().registerEvents(this, this); } }