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™️ 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.
This commit is contained in:
Janet Blackquill 2024-04-26 23:59:25 -04:00
parent f4c7d373e4
commit 8b44e8adad
5 changed files with 807 additions and 0 deletions

View File

@ -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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> 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<Activity> DIG = create(key("dig"));
private ActivityKeys() {
}
private static @NotNull TypedKey<Activity> create(final @NotNull Key key) {
return TypedKey.create(RegistryKey.ACTIVITY, key);
}
}

View File

@ -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")
};

View File

@ -0,0 +1,202 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Janet Blackquill <uhhadd@gmail.com>
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<E extends LivingEntity> {
+ /**
+ * 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<Task<E>> 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<E extends LivingEntity> {
+}
\ 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 <Entity extends Mob> @NotNull Task<Entity> walkToWalkTarget(int minRunTime, int maxRunTime);
+ /**
+ * Instructs the entity to swim if it is in water.
+ */
+ public <Entity extends Mob> @NotNull Task<Entity> swimIfInWater(float chance);
+ /**
+ * Instructs the entity to panic if it is hit, freezing, or on fire.
+ */
+ public <Entity extends Mob> @NotNull Task<Entity> panicOnDamage(float speed);
+ /**
+ * Instructs the entity to change its look target to the closest target
+ * for which the predicate returns true.
+ */
+ public <Entity extends LivingEntity> @NotNull Task<Entity> setLookTarget(@NotNull Predicate<LivingEntity> 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 <Entity extends LivingEntity> @NotNull Task<Entity> setWalkTargetToLookTarget(@NotNull Predicate<LivingEntity> predicate, @NotNull Function<LivingEntity, Float> speed, int completionRange);
+ /**
+ * Instructs the entity to look at the look target.
+ */
+ public <Entity extends Mob> @NotNull Task<Entity> lookAtLookTarget(int minRunTime, int maxRunTime);
+ /**
+ * Instructs the entity to run one of the given tasks with weighted probability.
+ */
+ public <Entity extends LivingEntity> @NotNull Task<Entity> runOneOf(@NotNull Map<Task<? super Entity>, 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<T> extends Keyed permits RegistryKeyImpl {
* @see io.papermc.paper.registry.keys.GameEventKeys
*/
RegistryKey<GameEvent> GAME_EVENT = create("game_event");
+ /**
+ * Built-in registry for activities
+ * @see io.papermc.paper.registry.keys.ActivityKeys
+ */
+ RegistryKey<Activity> 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<T extends Keyed> extends Iterable<T> {
Registry<GameEvent> 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<io.papermc.paper.entity.ai.Activity> 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
+ <E extends org.bukkit.entity.LivingEntity> io.papermc.paper.entity.ai.Brain<E> getBrain(@NotNull E entity);
+ // Paper end
}

View File

@ -0,0 +1,323 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Janet Blackquill <uhhadd@gmail.com>
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<Activity> {
+ 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<Paper extends LivingEntity, Minecraft extends net.minecraft.world.entity.LivingEntity> implements io.papermc.paper.entity.ai.Brain<Paper> {
+ public final Brain<Minecraft> handle;
+
+ public PaperBrain(Brain<Minecraft> handle) {
+ this.handle = handle;
+ }
+ public Brain<Minecraft> 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<Task<Paper>> tasks) {
+ this.getHandle().addActivity(
+ ((PaperActivity)activity).getHandle(),
+ begin,
+ ImmutableList.<BehaviorControl<Minecraft>>copyOf(tasks.stream().map(task -> ((PaperTask<Paper, Minecraft>)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<Paper extends LivingEntity, Minecraft extends net.minecraft.world.entity.LivingEntity> implements io.papermc.paper.entity.ai.Task {
+ final private BehaviorControl<Minecraft> handle;
+
+ public PaperTask(BehaviorControl<Minecraft> handle) {
+ this.handle = handle;
+ }
+ public BehaviorControl<Minecraft> 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 <Entity extends Mob> @NotNull Task<Entity> walkToWalkTarget(int minRunTime, int maxRunTime) {
+ return new PaperTask(new MoveToTargetSink(minRunTime, maxRunTime));
+ }
+ public <Entity extends Mob> @NotNull Task<Entity> swimIfInWater(float chance) {
+ return new PaperTask(new Swim(chance));
+ }
+ public <Entity extends Mob> @NotNull Task<Entity> panicOnDamage(float speed) {
+ return new PaperTask(new AnimalPanic(speed));
+ }
+ public <Entity extends LivingEntity> @NotNull Task<Entity> setLookTarget(Predicate<LivingEntity> predicate, float maximumDistance) {
+ return new PaperTask(SetEntityLookTarget.create(nmsEntity -> predicate.test(nmsEntity.getBukkitLivingEntity()), maximumDistance));
+ }
+ public <Entity extends LivingEntity> @NotNull Task<Entity> setWalkTargetToLookTarget(Predicate<LivingEntity> predicate, Function<LivingEntity, Float> speed, int completionRange) {
+ return new PaperTask(SetWalkTargetFromLookTarget.create(
+ nmsEntity -> predicate.test(nmsEntity.getBukkitLivingEntity()),
+ nmsEntity -> speed.apply(nmsEntity.getBukkitLivingEntity()),
+ completionRange
+ ));
+ }
+ public <Entity extends Mob> @NotNull Task<Entity> lookAtLookTarget(int minRunTime, int maxRunTime) {
+ return new PaperTask(new LookAtTargetSink(minRunTime, maxRunTime));
+ }
+ public <Bukkit extends LivingEntity> @NotNull Task<Bukkit> runOneOf(Map<Task<? super Bukkit>, Integer> tasks) {
+ List<Pair<BehaviorControl<? super net.minecraft.world.entity.LivingEntity>, Integer>> mcTasks = new ArrayList<>(tasks.size());
+ for (Map.Entry<Task<? super Bukkit>, 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<E extends LivingEntity> {
}
public <T> DataResult<Brain<E>> decode(DynamicOps<T> dynamicOps, MapLike<T> mapLike) {
- MutableObject<DataResult<Builder<Brain.MemoryValue<?>>>> mutableObject = new MutableObject<>(
+ MutableObject<DataResult<Builder<Brain.MemoryValue<?>>>> dataResultMutableObject = new MutableObject<>( // Paper - fix decompilation name collision
DataResult.success(ImmutableList.builder())
);
mapLike.entries()
@@ -91,10 +91,10 @@ public class Brain<E extends LivingEntity> {
DataResult<? extends Brain.MemoryValue<?>> dataResult2 = dataResult.flatMap(
memoryType -> this.captureRead((MemoryModuleType<T>)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<Brain.MemoryValue<?>> immutableList = mutableObject.getValue()
+ ImmutableList<Brain.MemoryValue<?>> 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<E extends LivingEntity> {
}
<U> void setMemoryInternal(MemoryModuleType<U> type, Optional<? extends ExpirableValue<?>> 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 <U> Optional<U> getMemory(MemoryModuleType<U> type) {
- Optional<? extends ExpirableValue<?>> optional = this.memories.get(type);
+ Optional<? extends ExpirableValue<U>> optional = (Optional<? extends ExpirableValue<U>>)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 <U> Optional<U> getMemoryInternal(MemoryModuleType<U> type) {
- Optional<? extends ExpirableValue<?>> optional = this.memories.get(type);
- return optional == null ? null : optional.map(ExpirableValue::getValue);
+ Optional<? extends ExpirableValue<U>> optional = (Optional<? extends ExpirableValue<U>>)this.memories.get(type); // Paper - decompilation fix
+ return optional == null ? null : optional.map(x -> x.getValue()); // Paper - decompilation fix
}
public <U> long getTimeUntilExpiry(MemoryModuleType<U> 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<B extends Keyed, M> implements Registry<B> {
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 <E extends org.bukkit.entity.LivingEntity> io.papermc.paper.entity.ai.Brain<E> 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

View File

@ -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);
}
}