Enmipho's Introduction to DUC

DUC, or Direct Unit Control, is a module of the Aoe2 AI scripting language introduced by UserPatch. It brings a number of extremely powerful capabilities to AIs, in particular the ability to manipulate individual units and groups of units like a human player would by clicking. This guide aims to introduce scripters to the capabilities of DUC. It assumes that the reader is already familiar with the basics of the language, and with making rules using non-UserPatch facts and actions. If this is not the case for you, you might want to check out this guide first.

This guide makes use of UserPatch facts and actions. If you aren't familiar with UserPatch facts/actions, you can identify them because they all start with up- and are documented here. To use them, you must have the constants in this file defined somewhere in you AI before the first UserPatch fact or action is used.

IDs and Lists

The first thing to understand about DUC is that every unit on the map has a unique ID. To select a unit using DUC, you need to tell the game the ID of the unit you want. So, how do we find that ID? UserPatch makes two lists available to us. We can tell the game to look for certain units, and when it finds them it will put their IDs in the lists. The lists are called the local list and the remote list. The local list holds IDs belonging to our own units. It can hold up to 240 IDs. The remote list can hold the IDs of any player's units - our own, our allies and enemies', and even Gaia's. The remote list can hold at most 40 IDs at one time.

We use the local list when we want to find out information about our units or give them an order. We use the remote list when want to look up information about any unit, or to make it the target of an order we're giving to our own units. So for example, if we're playing a game where we want to get all of our trebs to attack an enemy's wonder, we would need to get the IDs of our trebs in the local list (since those are the units we want to give the order to) and the enemy's wonder's ID in our remote list (since it's the target of our order.) If we're the player defending the wonder and want to repair it, we would get our villagers' IDs in the local list and our wonder's ID in the remote list. In this case even though the wonder is our own unit, we use the remote list since it's the target of the order we are giving.

First Example: Local search and target-point

Our task for this first example is simple: we want to select our scout and move it to the middle of the map. The first thing we want to do is find our scout's ID. Since the scout is our own unit and we are searching for it to give it an order, we want to put its ID in the local list rather than the remote list.

The first thing we need to do is understand exactly what kind of unit we're looking for. Depending on whether we're a civ from the Americas or a civ from elsewhere in the world, the scout unit we start with in a standard game could be either a scout cavalry or an eagle scout. I would recommend defining a constant telling you what kind of scout unit your civ has.

1#load-if-defined AZTEC-CIV
2 (defconst scout-type -267);Eagle warrior line
3#else
4 #load-if-defined INCAN-CIV
5 (defconst scout-type -267)
6 #else
7 #load-if-defined MAYAN-CIV
8 (defconst scout-type -267)
9 #else
10 (defconst scout-type -286);Scout cavalry line
11 #end-if
12 #end-if
13#end-if

If you aren't familiar with the above constants, they're line constants, representing the unit's entire line. So if you have 3 militia that you upgrade to men-at-arms, searching for militia won't find your units anymore, since they're not militia anymore. Searching for the militiaman-line, -296, though, will still find your units both before and after upgrading to man-at-arms, and even after champion has been researched. You can see the line constants for each unit here.

So, now that we know what unit to look for, we can tell the game to look for our scout unit and put its ID in the local list.

1(defconst gl-state-1 100)
2(defconst gl-state-2 101)
3(defconst gl-state-3 102)
4(defconst gl-state-4 103)
5(defconst gl-point-x 104)
6(defconst gl-point-y 105)
7(defrule
8 (true)
9 =>
10 (up-full-reset-search);This removes any previous IDs from the lists and clears any filters on the search. We'll talk about filters later, for now we'll just clear them before each search.
11 (up-find-local c: scout-type c: 1);Here we're telling the game to find at most 1 unit of our civ's scout line and put its ID in the local list.
12 (up-get-search-state gl-state-1);This tells the game to put information about whether any IDs were found by previous searches and are currently in our lists in the 4 goals starting at gl-state-1.
13 (disable-self)
14)

We can verify that we did indeed find our scout using the data provided by up-get-search-state. The first goal it writes tells us the total number of IDs in the local list. The second goal gives us the number of IDs found by the most recent local search. Since we removed all IDs from the list with up-full-reset-search, we can expect these goals to be the same after running our search. The third and fourth goals are the same as the first and second, but contain information about the remote list instead of the local one. So the third goal is the total number of IDs currently in the remote list and the fourth is the number of IDs found by the most recent remote search.

So if our search found our scout, the first and second goals in the search state should both be set to 1 after up-get-search-state. Assuming we found our scout, we can tell it to move to the center of the map using up-target-point.

1(defrule
2 (up-compare-goal gl-state-1 c:== 1)
3 =>
4 (up-get-point position-center gl-point-x);Get the (x,y) coordinates of the center of the map. This will write the x-coordinate to gl-point-x and the y-coordinate to the next goal after gl-point-x (for us, gl-point-y). Remember that when you store points in goals, you have to use a goal higher than 40.
5 (up-target-point gl-point-x action-move -1 -1);This tells all units whose IDs are in our local search results (which should only be our scout) to move to the point stored in gl-point-x and gl-point-y
6 (disable-self)
7)

So putting it all together, we have:

1#load-if-defined AZTEC-CIV
2 (defconst scout-type -267)
3#else
4 #load-if-defined INCAN-CIV
5 (defconst scout-type -267)
6 #else
7 #load-if-defined MAYAN-CIV
8 (defconst scout-type -267)
9 #else
10 (defconst scout-type -286)
11 #end-if
12 #end-if
13#end-if
14
15(defconst gl-state-1 100)
16(defconst gl-state-2 101)
17(defconst gl-state-3 102)
18(defconst gl-state-4 103)
19(defconst gl-point-x 104)
20(defconst gl-point-y 105)
21(defrule
22 (true)
23 =>
24 (up-full-reset-search)
25 (up-find-local c: scout-type c: 1)
26 (up-get-search-state gl-state-1)
27 (disable-self)
28)
29
30(defrule
31 (up-compare-goal gl-state-1 c:== 1)
32 =>
33 (up-get-point position-center gl-point-x)
34 (up-target-point gl-point-x action-move -1 -1)
35 (disable-self)
36)

This should move our scout to the middle of the map.

Targeting units is much the same as targeting points. The only difference is, after making sure the units we want to give the order to are in our local list, we then have to populate the remote list with the units we want to target and call up-target-objects.

Performance considerations

Along with pathing calls like up-path-distance and up-get-path-distance, searches (the commands starting with up-find-) are some of the most computationally expensive facts and actions you have access to. With this in mind, you should be aware of the performance implications of repeatedly searching for units. For searches that you plan to execute regularly, it is recommended that you use a timer to ensure that they don't execute more frequent than once every 2 seconds. Searching over and over again every time your rules are executed can lag the game. Commands like up-target-point also cause cause the game to do a pathing call (even if we don't see it directly in our code) which can also impact performance if done over and over again.

Our above code is fine however, since the (disable-self) prevents our code from being executed more than once.

Second Example: Remote search and targeting objects

Our second task is a bit more realistic than the first one: we want to check if our opponent in a 1v1 has loom or not.

First, let's define how we want to do this. As a human playing the game, we can check an enemy villager for loom once it enters our line of sight by clicking on it and checking either its armour or its hitpoints. An unloomed vill has 25 hitpoints and 0/0 armour, while a vill with loom has 40 hitpoints and 0+1/0+2 armour, so if we click one of our enemy's vills and see it has 40 hp or the added armor we know our enemy has loom. For this exercise, we'll use the villager's hitpoints to determine whether or not they have loom. The approach for an AI is very similar to how a human would check. We want to find an enemy villager, check its stats, and determine whether or not our enemy has loom based on the stats we know villagers to have.

The first step is to find a villager belonging to our opponent. Since we are looking for another player's unit, we need to use the remote list. There's one big difference with using the remote list instead of the local list. The game game can only search for one player's units at a time. We tell the game which player's units we want it to search by setting the value of sn-focus-player-number. Since for the sake of this example we can assume we're playing a 1v1, we can use up-find-player to find any enemy, and that will be our only opponent.

1;Goals
2(defconst gl-state-1 100)
3(defconst gl-state-2 101)
4(defconst gl-state-3 102)
5(defconst gl-state-4 103)
6(defconst gl-enemy 106)
7(defconst gl-enemy-has-loom 107);0 until we scout our enemy having loom, then 1
8(defconst gl-temp 108);A temporary goal we can use to store the maximum hp of our opponent's villager
9
10;Timers
11(defconst t-3-sec 1)
12
13;Misc
14(defconst vil-loom-hp 40)
15
16(defrule
17 (true)
18 =>
19 (up-find-player enemy find-closest gl-enemy)
20 (set-goal gl-enemy-has-loom 0)
21 (enable-timer t-3-sec 3)
22 (disable-self)
23)
24
25;DUC goes here ----------------------------------
26
27;------------------------------------------------
28
29(defrule
30 (timer-triggered t-3-sec)
31 =>
32 (disable-timer t-3-sec)
33 (enable-timer t-3-sec 3)
34)
35
36(defrule
37 (up-compare-goal gl-enemy-has-loom c:== 1)
38 =>
39 (chat-to-all "Our opponent has loom!")
40 (disable-self)
41)

Since we want this check to happen repeatedly until we see our enemy has loom, we are going to use a timer to keep this code from executing too often.

The remote search call is very similar to the local search call, except as we discussed we have to set the sn-focus-player-number strategic number beforehand.

1;DUC goes here ----------------------------------
2
3(defrule
4 (timer-triggered t-3-sec);Execute this rule no more frequently than once every 3 seconds
5 (up-compare-goal gl-enemy-has-loom c:== 0)
6 =>
7 (up-modify-sn sn-focus-player-number g:= gl-enemy);Tell the game the player number of the player whose units we want to search through
8 (up-full-reset-search);904 = Villager group
9 (up-find-remote c: 904 c: 1)
10 (up-get-search-state gl-state-1)
11)
12
13(defrule
14 (timer-triggered t-3-sec)
15 (up-compare-goal gl-enemy-has-loom c:== 0)
16 (up-compare-goal gl-state-3 c:== 1)
17 =>
18 (up-set-target-object search-remote c: 0);Tell the game that we want to look up information about the 0th unit in the remote list (both the local and remote lists are 0-indexed)
19 (up-get-object-data object-data-maxhp gl-temp);Store the maxhp attribute of the unit in gl-temp. Maxhp is the number of hitpoints the unit will have if it's fully healed.
20)
21
22(defrule
23 (timer-triggered t-3-sec)
24 (up-compare-goal gl-enemy-has-loom c:== 0)
25 (up-compare-goal gl-state-3 c:== 1)
26 (up-compare-goal gl-temp c:>= vil-loom-hp)
27 =>
28 (set-goal gl-enemy-has-loom 1)
29)

A note on player numbers

Many people new to scripting get confused by the difference between player number and player color. Players in the game lobby can choose a colour between 1 and 8. Often in multiplayer games, players will refer to other players by the number of their color. For example, in a teamgame, when someone scouts the red player's TC, they might flare it and say "p2", since in the lobby, red is color 2. However, the player number used by AIs is completely different from the player's color. For AIs, the players are numbered 1-8 by their position in the lobby (highest player is player 1, second-highest is player 2 and so on), regardless of what colors they pick. These are the numbers provided by the up-find-player and up-find-next-player commands, and the numbers you will use to set sn-focus-player-number.

Third Example: Filtering on distance

A common frustration among scripters goes like this: your AI sends a vill to build a lumber camp. The vill finishes building the lumber camp and immediately walks away to take berries on the other side of your base. Sound familiar? Our goal for this task is to make sure this doesn't happen after we build our first lumber camp.

This is our final and most difficult task. To accomplish it we'll need to

  1. Detect when our first lumber camp is built
  2. See if there's a villager within a 1-tile radius of the camp who isn't a lumberjack
  3. If so, find a nearby tree and tell the vill to chop it.

Detecting when our first lumber camp was built is trivial - we can just check when (building-type-count lumber-camp > 0). Finding the lumber camp is also probably pretty easy for you by now.

1;Goals
2(defconst gl-state-1 100)
3(defconst gl-state-2 101)
4(defconst gl-state-3 102)
5(defconst gl-state-4 103)
6(defconst gl-point-x 104)
7(defconst gl-point-y 105)
8
9(defrule
10 (true)
11 =>
12 (set-goal gl-state-1 0)
13 (set-goal gl-state-2 0)
14 (set-goal gl-state-3 0)
15)
16
17(defrule
18 (building-type-count lumber-camp > 0)
19 =>
20 (up-full-reset-search)
21 (up-find-local c: lumber-camp c: 1)
22 (up-get-search-state gl-state-1)
23 (disable-self)
24)
25
26(defrule
27 (up-compare-goal gl-state-1 c:> 0)
28 =>
29 ;Now what?
30)

Great! we've found our lumber camp. The next thing we want to do is determine its exact location, so we know where to look for villagers. We can finish our rule as below.

1(defrule
2 (up-compare-goal gl-state-1 c:> 0)
3 =>
4 (up-set-target-object search-local c: 0)
5 (up-get-point position-object gl-point-x);Read the x and y coordinates of the target object into gl-point-x and gl-point-y
6; We could achieve the same thing by reading the coordinates directly as object data.
7; (up-get-object-data object-data-point-x gl-point-x)
8; (up-get-object-data object-data-point-y gl-point y)
9 (up-reset-search 1 1 0 0);Now that we have the point we need saved in a goal we don't need the ID of our lumber camp anymore, so we remove it from the local list.
10 (up-set-target-point gl-point-x);Set the lumber camp's location as the target point. This will let us look for villagers only in the area of the lumber camp.
11 (up-filter-distance c: 0 c: 2);This tells the game that when we search for units we only want it to look within two tiles of the target point.
12 (up-find-local c: 904 c: 1);904 = Villager group
13
14;Ok, so we've searched for a villager within a 1-tile radius of our lumber camp.
15;However, what if that villager is already a lumberjack?
16;In that case, retasking it would be pointless and possibly just cause more idle time.
17;We can make sure we haven't selected a lumberjack by removing all lumberjacks from the local list.
18
19 (up-remove-objects search-local object-data-type c: 218);218 = Female lumberjack
20 (up-remove-objects search-local object-data-type c: 123);123 = Male lumberjack
21 (up-get-search-state gl-state-1)

Something to keep in mind about filtering is that after we apply a filter to our search, that filter will affect all of our searches until we remove it. So if we were to do another search right now, it would only find units within two tiles of our first lumber camp. We can remove filters from our searches by using up-reset-filters or up-full-reset-search.

So, now we've searched for a non-lumberjack villager within one tile of our lumber camp. If the contents of our local list are empty, there is no such vill and our work is done. But if there is such a vill, we'll want to find a tree for him to chop.

1(defrule
2 (up-compare-goal gl-state-2 c:> 0);The target point is still the lumber camp, so this will restrict the search to a 3-tile radius around it.
3 =>
4 (up-filter-distance c: 1 c: 3)
5 (up-find-resource c: wood c: 1);up-find-resource is a special type of search that only looks for resources. Like up-find-remote, it stores any IDs it finds in the remote list.
6 (up-get-search-state gl-state-1)
7)
8
9(defrule
10 (up-compare-goal gl-state-1 c:> 0)
11 (up-compare-goal gl-state-3 c:> 0)
12 =>
13 (up-target-objects 0 action-default -1 -1);The action our last few rules have been building up to, this tasks the villager to the tree.
14)

And there we go! Even with sn-food-gatherer-percentage set to 100, our vill should start chopping wood. Note that this isn't a perfect solution - there's a chance another villager other than the one who built the lumber camp could be passing by the lumber camp at the exact time it was completed, in which case that vill might be the one who gets targeted to wood.

A note on status

The code in the previous example has an interesting property: it will only ever find and target living trees, and will ignore trees that have been chopped down. This is because chopped down trees have a different status than living ones. A status is a property every unit has. It will be one of the numbers in the UserPatch constants below.

1;--------------------------------------
2; Define ObjectStatus Constants
3;--------------------------------------
4(defconst status-pending 0)
5(defconst status-ready 2)
6(defconst status-resource 3)
7(defconst status-down 4)
8(defconst status-gather 5)

Many search actions ignore status completely, but up-find-status-local, up-find-status-remote, and up-find-resource, which we used to find our tree, do take it into account. By default, searches will look for objects that have status-ready. This can be modified with up-filter-status. For more on which types of units have which statuses, see ObjectStatus.