Add Mob Goal API - #2619
Conversation
|
(am gonna fix the tests tomorrow/the next time I do changes) |
|
the main thing I want to chime in is please avoid using too much bukkit internal naming. Goals is a valid name for the API, but I feel like 'Selector' does not fit. selection is an internal implementation detail. I think just dropping the 'Selector' and making it entity.getGoals().add/removeGoal/getGoals is a clean and unlimiting API. |
|
fair point, I also find the bukkit naming hella confusing, thats why I didn't bring the Pathfinder naming into this. I am fine with dropping the Selector in the getters to, but what would I call the GoalSelector class then? |
|
Implementation wise I can understand the use of the word Selector. but API's should not be tightly coupled to implementation design. this could be 'Goals', EntityGoals (probably best) or even GoalManager. though I like the cleanness of skipping the word manager. so maybe entity.getEntityGoals()? |
|
or for correctness, since this only applies to mobs, .getMobGoals() and interface MobGoals |
|
ok, so getMobGoals and getMobTargets, which both return MobGoals? to make that consistent we could have getGoalManager and getTargetManager and change the type to AIManager? not really happy about those suffixes, but getTargets might be confusing since there is already a getTarget method. |
|
Could have a single interface for managing goals as MobGoals. I would say go with 1 MobGoals interface that has .addTargetGoal and .addBehaviorGoal |
|
I remember that the differentiation of behaviour and target goals got dropped in 1.14.x. Am I right? |
Nope. Still very much a part of the system, even in 1.15 ;) |
|
Future proof way to handle it even if so, is to make a GoalType interface/enum (iface if want to support custom types?) and make target/behavior values, and in future if they merge, then the 2 values can be treated equally. then .addGoal(type, key, instance) |
|
Changed the API to what aikar suggested in his last comment |
|
Since aikar seems busy: anybody else has feedback on this? |
|
I'm fine with that but I'm prejudiced. |
|
I mentioned some things in the Discord guild yesterday after you said you wanted feedback, but I think it got lost in the stream of messages, so I'll post it here as well (if you already read it, then feel free to ignore this). In the javadoc of tick you have: "Called each this the goal is activated", should probably be "Called each tick the goal is active" or "Called each time the goal is activated" (not really sure which one you want, I'd assume the first?). Second of all in the javadoc for MobGoals: "Represents a part of the "brain" of an entity.". This might be confusing, since all mobs have a Brain NBT Tag and - please correct me if I'm wrong - these goals do not interact with that. I know this is pretty nitpicky and most people will probably have no problem udnerstanding this, so feel free to just ingore this, but I thought I'd mention it anyway. (Also, might want to change entity to mob in there, since it's for MobGoals, not EntityGoals?) |
|
@stefvanschie the brain part refers to memories, not "thoughts" like theses target and behavior goals. Not really sure if thats more confusing or if having the brain part in the javadoc actually clarifies stuff, I personally find it good, it helps me to figure out what the class does. |
|
not sure what happend to the history, fill fix/rebase on 1.15 when the PR has been reviewed. patch files are here: |
bf387f6 to
06593b1
Compare
|
Updated to 1.15 ---> ready for review Example: |
There was a problem hiding this comment.
Added some input regarding the API. I don't really know much of the internal stuff regarding pathfinders so I can't really give any input on that.
Also it looks like there were some internal changes in 1.14 that might be worth exposing (e.g. PathfinderGoal.Type).
|
exposed the sub type and converted type into an enum |
|
Is there any scenario where a GoalKey can be applied as both a target and a behavior? curious if we should move type into the GoalKey so its just .removeGoal(entity, key); where key holds the type. Though I guess even if that is possible, no harm in GoalKey FOO_TARGET = GoalKey.of(GoalType.TARGET, NamespacedKey.of(plugin, "foo")) or Thoughts? |
|
Scratch that, I'm getting mixed up bouncing between nms and api here. I see the "type" in this api isn't the same as nms "type". This api does use "flag" properly and this "type" is something new ^_^ |
|
@aikar I'm not sure tying this to the inheritance hierarchy is the right answer. There are discrepancies in the interfaces, that might not be present with the internal AI code. For instance, one that I assumed was fairly consistent I like what I presume is the goal of your request, compile time checks of this key based system, and the idea behind it -- big fan of getting errors before I run the code. However, imposing structure where there really isn't any -- especially tying to an inheritance hierarchy -- might cause more problems than it's worth. I could also foresee this decision causing a potentially excessive number of keys to be deprecated/renamed/changed as times goes on as Mojang both evolves their inheritance hierarchy, and their goals (e.g. what's used by Are we trying to design for future incompatibilities, or current incompatibilities -- i.e. do we have a concrete example of a horrible failure when Goal X is applied to Entity Y? If current incompatibilities concrete examples may be helpful. Respectfully, if we can't find/don't have a single "real" incompatibility where bad things ™️ happen, I think that's a strong case for going with what we have. @MiniDigger has clearly worked very hard on this -- ongoing for over 6 months -- and I think it would provide great value to the ecosystem. |
|
I agree it's a great PR, just want to evaluate enforcing compile time restrictions before it's a locked API. But comments about interface mismatches, that's simply a bug. But yes valid concern if we didn't fix it. The changing of interfaces later, I'm not sure is actually a concern here. Generics are compile time, lost at runtime. So something compiled on an older version of MC SHOULD still work on a newer version even if we change the generic of what its targetting. for example say only Foxes have DANCE_IN_CIRCLES
compile this on 1.15 Then in 1.16 we change this to Creature because mojang allowed it to be used on more.
It is my understanding the old 1.15 compile will still be valid on 1.16, and if a plugin updates to 1.16, they now can apply it to any creature. Unless I'm wrong about runtime ABI of generics, this should not be an issue. |
This is true assuming they do not use the |
|
We wouldn't be using the class that I can think of. it would be our code that uses it, not the implementing plugin. But valid concern to note that we wouldn't ever want to expose the class that the key is bound to for "access", just purely as a generic type hint. |
|
I tried to implement what was suggested above, but I ended up in generic hell and I am not sure I will be able to pull this off. Basically, since MobGoals is now bound to an entity, stuff like add and remove require you to use a GoalKey or Goal of that type (or a subclass), which doesnt work, since we want it to accept superclasses, not subclasses. Unless somebody else can pull this off, I would suggest we stop trying to force this. If that means this PR will not be accepted, thats fine too, ill just move the last working version to our fork then and ppl will have to life without a mob goal api. Test plugin code with compiler errors @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (command.getName().equalsIgnoreCase("testy")) {
if (!(sender instanceof Player)) {
return true;
}
Player player = (Player) sender;
Zombie zombie = (Zombie) player.getWorld().spawnEntity(player.getLocation(), EntityType.ZOMBIE);
String targetGoals = Bukkit.getMobGoals(zombie).getAllGoals(GoalType.TARGET).stream().map(goal -> goal.getKey().getNamespacedKey().getKey()).collect(Collectors.joining(", "));
String behaviorGoals = Bukkit.getMobGoals(zombie).getAllGoals(GoalType.BEHAVIOR).stream().map(goal -> goal.getKey().getNamespacedKey().getKey()).collect(Collectors.joining(", "));
player.sendMessage("Behavior goals: " + behaviorGoals);
player.sendMessage("Target goals: " + targetGoals);
// TODO FIX THIS - Cannot resolve method 'removeGoal(com.destroystokyo.paper.ai.GoalType, com.destroystokyo.paper.ai.GoalKey<org.bukkit.entity.Mob>)'
// GoalKey<Mob> NEAREST_ATTACKABLE_TARGET
Bukkit.getMobGoals(zombie).removeGoal(GoalType.TARGET, VanillaGoal.NEAREST_ATTACKABLE_TARGET);
Bukkit.getMobGoals(zombie).removeGoal(GoalType.BEHAVIOR, Bukkit.getMobGoals(zombie).getAllGoals(GoalType.BEHAVIOR).iterator().next());
String targetGoals2 = Bukkit.getMobGoals(zombie).getAllGoals(GoalType.TARGET).stream().map(goal -> goal.getKey().getNamespacedKey().getKey()).collect(Collectors.joining(", "));
player.sendMessage("Target goals2: " + targetGoals2);
Villager villager = (Villager) player.getWorld().spawnEntity(player.getLocation(), EntityType.VILLAGER);
// TODO FIX THIS Required type: Goal<org.bukkit.entity.Villager> Provided:Goal<Mob>
Bukkit.getMobGoals(villager).addGoal(GoalType.TARGET, 1, new Goal<Mob>() {
@Override
public boolean shouldActivate() {
return true;
}
@Override
public void tick() {
villager.playEffect(EntityEffect.HURT);
}
@Override
public GoalKey<Mob> getKey() {
return GoalKey.of(Mob.class, new NamespacedKey(Testy.this, "villager_test"));
}
@Override
public EnumSet<GoalFlag> getFlags() {
return EnumSet.of(GoalFlag.LOOK);
}
});
// remove test 1
Bukkit.getMobGoals(villager).addGoal(GoalType.TARGET, 2, new Goal<Villager>() {
@Override
public boolean shouldActivate() {
return true;
}
@Override
public void tick() {
villager.playEffect(EntityEffect.FIREWORK_EXPLODE);
}
@Override
public GoalKey<Villager> getKey() {
return GoalKey.of(Villager.class, new NamespacedKey(Testy.this, "test"));
}
@Override
public EnumSet<GoalFlag> getFlags() {
return EnumSet.of(GoalFlag.LOOK);
}
});
Bukkit.getMobGoals(villager).removeGoal(GoalType.TARGET, GoalKey.of(Villager.class, new NamespacedKey(Testy.this, "test")));
// remove test 2
Goal<Villager> goal = new Goal<Villager>() {
@Override
public boolean shouldActivate() {
return true;
}
@Override
public void tick() {
villager.playEffect(EntityEffect.FIREWORK_EXPLODE);
}
@Override
public GoalKey<Villager> getKey() {
return GoalKey.of(Villager.class, new NamespacedKey(Testy.this, "test2"));
}
@Override
public EnumSet<GoalFlag> getFlags() {
return EnumSet.of(GoalFlag.LOOK);
}
};
Bukkit.getMobGoals(villager).addGoal(GoalType.TARGET, 3, goal);
Bukkit.getMobGoals(villager).removeGoal(GoalType.TARGET, goal);
return true;
}
return false;
} |
|
I'll try it with the other Generics keyword |
|
Refactored the MobGoals manager to be a singleton, which solves the generic issues. Zombie zombie = (Zombie) me.getWorld().spawnEntity(me.getLocation(), EntityType.ZOMBIE);
Bukkit.getMobGoals().removeGoal(zombie, VanillaGoal.LOOK_AT_PLAYER);lets thing open is now trying to figure out if GoalType (TARGET, BEHAVIOR) and GoalFlag (MOVE, LOOK, JUMP, TARGET) are the same (aka, if flag == TARGET type = TARGET else type = BEHAVIOR) |
|
Your sample goals are compiling with my changes. EDIT: Oh you solved it another way. |
|
Goalflag is goaltype now and the old goaltype is gone this is done and ready to review/merge |
|
Ok now its done actually, it was missing nms -> bukkit mappings, just look at that beatiful vanilla goal class now <3 I know MobGoalHelper and VanillaGoal look scary, but that stuff is mostly auto generated and tested with unit tests, so updating to 1.16 shouldnt be bad. |
|
needs rebase, this is on 1.15.1, lmao. also needs to add the inactiveTick method from EAR 2.0 |
|
Oh, btw, I did this It should be fine but that needs double checking by someone |
|
Hope Animals is just a typo here. Plural > singular ? |
|
That's the bukkit api, can't do anything about it. But yes this is correct as you have it: |
|
I bribed Choco with tacos to upstream that change for me https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/pull-requests/492/diff#src/main/java/org/bukkit/entity/Wolf.java |
49663d6 to
60e9a14
Compare
|
rebased to master and squashed, this is ready to merge now |
60e9a14 to
787242e
Compare
787242e to
75e1e3b
Compare

Implements #2617
This first MVP-PR doesn't cover handling vanilla goals as good as I would want to: you can't create vanilla goals.
You can list and remove them tho (in addition to implementing your own goals), so this is already a step in the right direction.
The VanilllaGoal class has a list of constants with namespaced keys that vanilla uses. Its generated with the unit test (that also checks its completion)
This allows you to do stuff like this: