Support OpenKore:
Learn about
the Fund Pool

AI subsystem and task framework overview

English | 正體中文

The AI, or "artificial intelligence", is responsible for automatically initiating tasks.

A task is a set of actions to be performed. "task" is loosely defined: any set of actions that cannot be performed instaneously can be a task. For example, attacking a monster is a task, and routing (walking to a specific destination) is a task. But determining whether to attack a monster, and determining which monsters to attack, is not a task: it is the responsibility of the AI. In short: tasks do things, while the AI decides what to do.

The AI watches the environment. The environment may be actors currently on screen, items in the inventory, character status, etc. Basically anything can be part of "the environment". Some information about the environment (stored in global variables in OpenKore) is recorded by the network subsystem.

Based on the curent state of the environment, the AI makes a decision and initiates appropriate tasks. For example, if the AI notices that a monster has appeared, it can initiate an "attack" task. Of course, the AI can also stop a running task, if necessary. The following diagram illustrates the idea:


Figure 1: AI subsystem overview


Contents

Task framework overview

The task framework is independent from the AI subsystem. The AI depends on the task framework, but not vice-versa. The central parts of the task framework are the Task class and the TaskManager class.


Figure 2: Task framework overview

The Task class

The task framework follows the Unix philosophy: do one thing, but do it well. Every Task class should be specialized in only one thing, but do it as well as possible. Each task is implemented inside its own Task class. A task can be started, canceled, interrupted and resumed. A Task class contains code for handling all that.

As can be seen in the following class diagram, there is specialized Task subclass for each task:


Figure 3: Task class hierarchy

The problem of conflicts

Some tasks can conflict with each other. For example, suppose we have a "use skill" task and a "walk" task. They cannot be run simultaneously, as you can't use a skill while walking and you can't walk while using a skill, i.e. the two tasks conflict with each other.

To solve this problem, we introduce the concept of a mutex (short for mutual exclusion object). A task has a set of mutexes, and those mutexes define what kind of shared resources that task uses. When two tasks share a common mutex, then it means that those tasks cannot be run simultaneously. In the previous example with the "use skill" and "walk" tasks, the shared resource is the character's ability to walk. Let us call the mutex for this resource "walk". Then both tasks have this mutex in their mutex set.

There is another problem. Suppose we have the following hypothetical situation:

  • The "attack monster" task is running right now. This task has the "walk" mutex, because to attack a monster we have to walk to it.
  • We have another task called "run away from a monster". This task also has the "walk" mutex.
  • We are attacking a Poring, but an Argiope suddenly appears, and the AI knows that the character cannot defeat it. It initiates a "run away from monster" task so that the character will run away from both the Poring and the Argiope.

But then what should OpenKore do? Should it keep the "attack monster" task running, or should it interrupt the "attack monster" task and run the "run away" task? To us humans, the answer is obviously the latter. But to a computer program, the answer is not so clear.

In order to solve this problem, we introduce the concept of a priority. If task A has a higher priority than task B, and both tasks conflict with eachother, then task B should be interrupted and task A should be run first.

The TaskManager class: resolving conflicts

With mutexes and priorities, we now have a way to define conflicts among tasks. Now we must solve them, and to do that we need a task manager.

The AI first creates a task, then passes it to the TaskManager. The TaskManager will decide when to run a task, and when to interrupt or resume a task, depending on conflict definitions provided by the tasks. It will make sure that conflicting tasks do not run simultanenously, while non-conflicting tasks do.

Subtasks and encapsulation

Tasks can run their own subtasks in order to fulfill their goal, as illustrated in figure 2. For example, suppose we have a "walk" task and an "attack monster" class. The "attack monster" class can internally use the "walk" task to walk to a monster. But as far as the outside world (task manager and AI) is concerned, there is only one task: the "attack" task. They do not know, nor should they care, whether a task has subtasks. Thus, implementation details of a task are hidden, i.e. encapsulation.

By reusing other tasks, more complex tasks can be implemented more easily. A task has full control over its subtasks, as the subtasks are not managed by the task manager. The following following dependency diagram illustrates an example:

  • The Task::MapRoute task is responsible for walking between different maps. Internally, it uses Task::CalcRouteMap to calculate a path between maps.
  • But we have another class - Task::Route. This task is responsible for walking inside a single map. Task::MapRoute uses Task::Route to walk between different portals inside one map.
  • Task::Route, in turn, uses the specialized Task::Move task, which is responsible for moving over short distances. In Ragnarok Online, you cannot send a "I want to move to spot (x,y)" message to the server if (x,y) is over 15 blocks away. Task::Move is specialized in moving distances of at most 15 blocks, and handles all the details like standing up, dealing with lag, handling timeouts, etc. Task::Route is responsible for planning the entire, longer route.
  • But one cannot walk without standing up. So Task::Move, in turn, uses the specialized Task::SitStand task for standing up. Task::SitStand handles all the details like dealing with lag, timeouts, etc. The Task::SitStand task is particularly useful because many AI functionality requires the character to first stand up.

Note that the dependencies only go one way. Task::Move knows about Task::SitStand, but not vice-versa. And note that the structure is layered: Task::MapRoute only knows about Task::Route and Task::CalcMapRoute. It doesn't know, nor does it care, that Task::Route uses Task::Move internally.

It should also be noted that a subtask only has exactly one "parent" task. A Task is a class, and one can create infinitely many instances. So if two tasks both need sit/stand functionality, then they each create their own instance of Task::SitStand.

Notes

  • The priority for a task is assigned during creation, and must not change during a task's life time. Thus, the AI determines a task's priority.

Summary

  • Each task is specialized in one thing.
  • Tasks can be run in parallel as long as they don't conflict with each other.
  • Conflicts are defined with mutexes and priorities. Each task says "I have these mutexes" and "I have this priority".
  • The task manager is responsible for deciding which tasks may run, using the mutex and priority information provided by the tasks.


Implementing a Task

The Task class has the following queries:

  • String getName()
  • getStatus()
  • getError()
  • Array* getMutexes()
  • int getPriority()

The following commands are available:

  • void interrupt()
  • void resume()
  • void stop()
  • void iterate()

See the Task class API documentation for the full specification.

The most important method is iterate(). Most methods are already implemented for you in the Task class, but you must implement iterate() yourself, to program your task's behavior. This method is continuously called by the task manager (or by a parent task), until the task is completed. iterate() then performs a step that uses as little time as possible. If you do not (for example, if you call sleep 10) then the entire application will freeze. When your task is completed, call either setDone() or setError() to mark the task as completed. As a rule of thumb, iterate() should not take more time than 10 miliseconds.

The process of calling iterate() is illustrated in the following diagram:


Appendix A: What is wrong with the old Kore AI design?

The old Kore AI design has served us for a long time, but it's showing its limitations. For instance, flee-from-target is hard to implement because it can conflict in so many ways with everything else. Moving-to-a-monster-before-attacking has many bugs which are hard to fix, related to attack-routing AI interaction. Let us analyze some of the problems.

Moving-to-a-monster-before-attacking

It's one of the most basic things. You have to move to a monster before you can attack it. But even something simple like this doesn't always go well. The attack AI triggers a route AI, and the attack AI is suspended until the route AI is done. When the monster moves while you're moving to the monster's old location, you have to wait until the route AI is finished, so you cannot instantanously respond to the monster's movement. This is probably what causes the "dancing around the monster" problem.

OK, that is not entirely true. OpenKore currently cancels the route AI if it detects that the monster moved. But it's ugly:

# Monster has moved; stop moving and let the attack AI re-adjust route
AI::dequeue;
AI::dequeue if (AI::action eq "route");

Notice the two AI::dequeue() calls. That's because route triggers move internally. We poke into other AIs' internals. Not good, especially if their internals one day change.

OK, the above example isn't that bad. But it quickly gets worse:

} elsif (((AI::action eq "route" && AI::action(1) eq "attack") || (AI::action eq "move" && AI::action(2) eq "attack"))

This is the code for detecting whether a route AI is triggered by an attack AI.

Using skills when AI is disabled

When we type the 'ss' command to use a skill on ourselves, Kore will trigger a "skill_use" AI sequence, which will then perform the necessary actions in order to use a skill. However, this did not work if AI is disabled. This problem was recently fixed, but in a pretty ugly way: the "skill_use" AI block was modified to ignore the 'AI enabled' configuration option.


Appendix B: Design rationale for the new AI subsystem

Mutexes and priorities

In the old AI design, we see that each AI block specifically checks what AI sequence is currently active. Based on that, it decides whether to activate itself. An example of such a check is:

##### AUTO-SKILL USE #####
if (AI::isIdle || AI::is(qw(route mapRoute follow sitAuto take items_gather items_take attack))
       || (AI::action eq "skill_use" && AI::args->{tag} eq "attackSkill")) {

This prevents the auto-skill use AI block from activating itself when, for example, the "" AI sequence is active.

The problems with this approach are:

  • Wasted CPU cycles, because in every main loop iteration, the check is performed again, regardless of whether it's necessary.
  • It requires that every AI block has knowledge about other AI blocks. It must know exactly which other AI blocks conflict with the current one.

But what the old design actually wants is to prevent conflicts. Mutexes and priorities allow you to define conflicts without explicit knowledge about all the other tasks. The task manager can reschedule tasks only when necessary.

Object orientation and encapsulation

Some tasks not only check which AI sequence is active, but also what kind of AI sequence it is. For example, the auto-attack AI will activate an "attack" AI sequence if we're currently walking, but not when the walk is triggered by a user command. The code is not shown here, but it's extremely ugly. The code to decide whether a route AI is triggered by the attack AI (as shown in appendix A) is also extremely ugly. The old Kore AI is entirely non-object-oriented.

This all is trivially solved if we use an object oriented design and encapsulation. No longer will the attack task have to check whether the currently active task is a "(route task AND triggered by attack task) OR (move task AND triggered by route task AND route task is triggered by attack task)".

  • The 'route' task becomes a subtask of the 'attack' task, and the 'attack' task has full control over that 'route' task.
  • The 'route' task, in turn, may have a 'move' subtask.
  • The parent 'attack' task does not know, nor care, that its 'route' subtask has its own subtasks - the 'attack' task only cares that the 'route' task does what it is supposed to do.

Distinction between AI and tasks

The reason why I made a distinction between AI and tasks is in order to solve the "Using skills when AI is disabled" problem in appendix A. If we turn the "skill_use" AI into a task, then we can initiate that task independently from whether the AI is enabled or not. If the AI is disabled then the AI will not automatically initiate skill tasks, but the user can still do it manually.


Appendix C: FAQ

I still don't understand what a Task is.

A task is simply a class with code in it for performing complex actions. With "complex actions" I mean an action that cannot be performed by just sending one message to the server - for example, things that you have to retry a few times, or things that require some time to complete. A good example is using skills: sometimes the server doesn't immediately let you use the skill (because of lag, or because you're attacking, or because you're moving, or whatever). So a task will attempt to retry it a few times, until the skill has been used.

Does a subtask have multiple parent tasks?

No, a subtask only has exactly one "parent" task. A Task is a class, and one can create infinitely many instances. So if two tasks both need sit/stand functionality, then they each create their own instance of Task::SitStand.

What's the difference between a Task and a class?

A Task is a class, but not vice versa. Just like a duck is a bird, but a bird is not always a duck.

I don't understand object oriented programming (OOP), how do I learn it?

Object Oriented programming is a style of programming which breaks up tasks into manageable chunks (objects). Object Oriented programming makes coding in projects much more efficient and less buggy. Here are some good tutorials to learn about object oriented style:

(Note: this last one is a comparison of programming styles and tailored to C++)

How do I write OO code in Perl?

Since the task framework relies heavily on object oriented concepts, it's important that one masters it. Here are some tutorials: