Skip to content

Add Mob Goal API - #2619

Merged
aikar merged 1 commit into
PaperMC:masterfrom
MiniDigger:feature/mob-ai-goals
May 5, 2020
Merged

Add Mob Goal API#2619
aikar merged 1 commit into
PaperMC:masterfrom
MiniDigger:feature/mob-ai-goals

Conversation

@MiniDigger

@MiniDigger MiniDigger commented Oct 6, 2019

Copy link
Copy Markdown
Member

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:

Zombie zombie = (Zombie) me.getWorld().spawnEntity(me.getLocation(), EntityType.ZOMBIE);
Bukkit.getMobGoals().removeGoal(zombie, VanillaGoal.LOOK_AT_PLAYER);

@MiniDigger

MiniDigger commented Oct 6, 2019

Copy link
Copy Markdown
Member Author

(am gonna fix the tests tomorrow/the next time I do changes)

@aikar

aikar commented Oct 6, 2019

Copy link
Copy Markdown
Member

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.
A goals API could cover more than the selection of a goal.

I think just dropping the 'Selector' and making it entity.getGoals().add/removeGoal/getGoals is a clean and unlimiting API.

@MiniDigger

Copy link
Copy Markdown
Member Author

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?
I just looked at MCP and mojang mappings, they both actually call it GoalSelector too (and the fields goalSelector and targetSelector)

Comment thread Spigot-Server-Patches/0446-Implement-Mob-Goal-API.patch Outdated
Comment thread Spigot-Server-Patches/0446-Implement-Mob-Goal-API.patch Outdated
Comment thread Spigot-Server-Patches/0446-Implement-Mob-Goal-API.patch Outdated
@aikar

aikar commented Oct 6, 2019

Copy link
Copy Markdown
Member

Implementation wise I can understand the use of the word Selector. but API's should not be tightly coupled to implementation design.
for pathfinder, i went with a simple
./src/main/java/com/destroystokyo/paper/entity/Pathfinder.java

this could be 'Goals', EntityGoals (probably best) or even GoalManager. though I like the cleanness of skipping the word manager.

so maybe entity.getEntityGoals()?

@aikar

aikar commented Oct 6, 2019

Copy link
Copy Markdown
Member

or for correctness, since this only applies to mobs, .getMobGoals() and interface MobGoals

@MiniDigger

Copy link
Copy Markdown
Member Author

ok, so getMobGoals and getMobTargets, which both return MobGoals?
This is already kinda inconsistent again :/

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.

@aikar

aikar commented Oct 6, 2019

Copy link
Copy Markdown
Member

Could have a single interface for managing goals as MobGoals.
goals consist of 2 types of goals, a targeting goal, and behavioral goals

I would say go with 1 MobGoals interface that has .addTargetGoal and .addBehaviorGoal
This lines up with how the implementation is, but doesn't lock us to that.

@yannicklamprecht

Copy link
Copy Markdown
Contributor

I remember that the differentiation of behaviour and target goals got dropped in 1.14.x. Am I right?

@BillyGalbreath

Copy link
Copy Markdown
Contributor

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

@aikar

aikar commented Oct 6, 2019

Copy link
Copy Markdown
Member

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)
.getGoals(type)
.getGoal(type, key)

@MiniDigger

Copy link
Copy Markdown
Member Author

Changed the API to what aikar suggested in his last comment

@MiniDigger
MiniDigger requested a review from aikar October 12, 2019 09:39
@MiniDigger

Copy link
Copy Markdown
Member Author

Since aikar seems busy: anybody else has feedback on this?

@yannicklamprecht

Copy link
Copy Markdown
Contributor

I'm fine with that but I'm prejudiced.

@stefvanschie

Copy link
Copy Markdown
Contributor

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?)

@MiniDigger

Copy link
Copy Markdown
Member Author

@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.
Addressed the other two things.

@MiniDigger

Copy link
Copy Markdown
Member Author

@MiniDigger
MiniDigger force-pushed the feature/mob-ai-goals branch 2 times, most recently from bf387f6 to 06593b1 Compare January 3, 2020 16:00
@MiniDigger

MiniDigger commented Jan 3, 2020

Copy link
Copy Markdown
Member Author

Updated to 1.15 ---> ready for review

Example:
https://streamable.com/xqtg8
https://github.com/MiniDigger/MobGoalTest/blob/master/src/main/java/me/minidigger/mobgoaltest/MobGoalTest.java

@Phoenix616 Phoenix616 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread Spigot-API-Patches/0189-Add-Mob-Goal-API.patch Outdated
Comment thread Spigot-API-Patches/0189-Add-Mob-Goal-API.patch Outdated
@MiniDigger

Copy link
Copy Markdown
Member Author

exposed the sub type and converted type into an enum

Comment thread Spigot-API-Patches/0189-Add-Mob-Goal-API.patch Outdated
@MiniDigger

Copy link
Copy Markdown
Member Author

https://i.imgur.com/wfn2kt1.png

@aikar

aikar commented Apr 26, 2020

Copy link
Copy Markdown
Member

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"))
GoalKey FOO_BEHAVIOR = GoalKey.of(GoalType.BEHAVIOR, NamespacedKey.of(plugin, "foo"));

or
GoalKey FOO = GoalKey.of(GoalType.BOTH, NamespacedKey.of(plugin, "foo"))

Thoughts?

@BillyGalbreath

BillyGalbreath commented Apr 26, 2020

Copy link
Copy Markdown
Contributor

The type really isn't a "type". It's a flag. The goals run in order of index, and if a goal with a certain flag is running then all goals after it with the same flag are skipped and won't run. This is why cows won't follow you if you hold wheat while they are in panic mode because you hit them, etc.

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 ^_^

@DarkArc

DarkArc commented Apr 26, 2020

Copy link
Copy Markdown
Contributor

@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 Monster for hostile mobs, turned out to not be so...

https://github.com/packet-loss-gaming/Grindstone/blob/45828a4567bf12478a048a96bf5a253f17c94b34/src/main/java/gg/packetloss/grindstone/util/EntityUtil.java#L62

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 Fox might be ForestCreature in the future, LOOK_AT might have originally been keyed for a Zombie, changed to Monster, changed to Headeded, changed to SingleHeaded, and ThreeHeadedDragonCow). There may be further pain as well as some authors may wish to author goals that would work for A & B in the hierarchy but A & B are incompatible types (e.g. Monster and Slime).


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.

@aikar

aikar commented Apr 26, 2020

Copy link
Copy Markdown
Member

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.
Yes valid concern, but we can fix those ;)

public interface Slime extends Monster { // Paper

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

GoalKey<Fox> 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.

GoalKey<Creature> DANCE_IN_CIRCLES = ...;

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.

@Proximyst

Copy link
Copy Markdown
Contributor

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 Fox part of it; javac will infer a cast to Fox when getting the stored Object for this, resulting in sadness. I'm not familiar with the new comments here, just wanted to note this as I saw it in the log channel :p

@aikar

aikar commented Apr 26, 2020

Copy link
Copy Markdown
Member

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.

@MiniDigger MiniDigger mentioned this pull request Apr 28, 2020
12 tasks
@MiniDigger

MiniDigger commented Apr 28, 2020

Copy link
Copy Markdown
Member Author

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.
Creation isnt part of this API anyways (and will not be part until mojang does major refactors), so I fail to see the benefit.

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

@yannicklamprecht

Copy link
Copy Markdown
Contributor

I'll try it with the other Generics keyword super. Will comment if this works.
Possibly that works. Until today I never used super in this context. thumbs_pressed
https://stackoverflow.com/questions/4343202/difference-between-super-t-and-extends-t-in-java

@MiniDigger

Copy link
Copy Markdown
Member Author

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)

@yannicklamprecht

yannicklamprecht commented Apr 28, 2020

Copy link
Copy Markdown
Contributor

Your sample goals are compiling with my changes.
The only change I did was:
GoalKey<? super T> key

L30: addGoal(GoalType type, int priority, Goal<? super T> goal)
L36: removeGoal(GoalType type, Goal<? super T> goal)
L74: removeGoal(GoalType type, GoalKey<? super T> key)
L96: hasGoal(GoalType type, GoalKey<? super T> key)

EDIT: Oh you solved it another way.

@MiniDigger

MiniDigger commented Apr 28, 2020

Copy link
Copy Markdown
Member Author

Goalflag is goaltype now and the old goaltype is gone

this is done and ready to review/merge

@MiniDigger

MiniDigger commented Apr 28, 2020

Copy link
Copy Markdown
Member Author

Ok now its done actually, it was missing nms -> bukkit mappings, just look at that beatiful vanilla goal class now <3
https://github.com/PaperMC/Paper/pull/2619/files#diff-226fdc53c87dec44700458d36a0b75f7R292

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.

@MiniDigger MiniDigger closed this Apr 29, 2020
@MiniDigger MiniDigger reopened this Apr 29, 2020
@MiniDigger

Copy link
Copy Markdown
Member Author

needs rebase, this is on 1.15.1, lmao.

also needs to add the inactiveTick method from EAR 2.0

@MiniDigger

Copy link
Copy Markdown
Member Author

Oh, btw, I did this

-public interface Tameable extends Entity {
+public interface Tameable extends Animals { // Paper

It should be fine but that needs double checking by someone

@yannicklamprecht

Copy link
Copy Markdown
Contributor

Hope Animals is just a typo here. Plural > singular ?

@aikar

aikar commented Apr 29, 2020

Copy link
Copy Markdown
Member

That's the bukkit api, can't do anything about it.

But yes this is correct as you have it:
public abstract class EntityTameableAnimal extends EntityAnimal { }

@MiniDigger

Copy link
Copy Markdown
Member Author

@MiniDigger
MiniDigger force-pushed the feature/mob-ai-goals branch from 49663d6 to 60e9a14 Compare April 30, 2020 15:18
@MiniDigger

MiniDigger commented Apr 30, 2020

Copy link
Copy Markdown
Member Author

rebased to master and squashed, this is ready to merge now

@MiniDigger
MiniDigger requested a review from aikar April 30, 2020 15:24
@MiniDigger
MiniDigger force-pushed the feature/mob-ai-goals branch from 60e9a14 to 787242e Compare May 5, 2020 15:57
@MiniDigger
MiniDigger force-pushed the feature/mob-ai-goals branch from 787242e to 75e1e3b Compare May 5, 2020 16:05
@aikar
aikar merged commit 75e1e3b into PaperMC:master May 5, 2020
@MiniDigger
MiniDigger deleted the feature/mob-ai-goals branch May 6, 2020 00:31
@MiniDigger MiniDigger mentioned this pull request Aug 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.