Chapter 10

Lists

The dark and chilling silence was broken by the blood curdling scream of a modem. One witness later identified it as 28.8 kbaud from the final strangulated burst of static.

A list is a data object which contains a number of values. The items in the list are numbered 1, 2, 3, to the last in the list. This number is called the index of the item and is used to access it. This makes the list datum in DM similar to an array in the C language.

1. Declaring a List

There are several ways to declare a list variable. One is to use the list object type as you would with any other type of variable. The others use a special syntax designed specifically for lists.

  1. var/list/mylist
  2. var/mylist[]
  3. var/mylist = new /list(size)
  4. var/mylist[size]

The first and second methods are identical. They define a list variable. Note however, that they do not actually create a list object. It is just the same as when you define a variable of any other type (like a mob). Only the type of the variable has been defined. The contents of the variable are still initialized to null.

The third and fourth methods for defining a list create a new list object in addition to defining the variable. The specified size indicates how many items to allocate room for in the list. Since the size can be changed dynamically (that is whenever you need to), an initial size of 0 may even be specified.

2. Accessing List Items

An individual item in the list may be accessed by putting the item's index inside braces [ ]. This is called indexing the item.

The following (useless) example creates some objects and stores them in a list.

var/dwarves[7]

world/New()
   var/i
   for(i=1,i<=7,i++)
      dwarves[i] = new/mob/dwarf()

The individual dwarves can then be accessed at any time by using their indices: dwarves[1], dwarves[2] to get the first dwarf, second dwarf, and so on.

It is an error to specify an index that is beyond the end of the list. Doing so will cause the procedure to crash. That means you should be careful to stay within the bounds of the list, which brings us to the next topic.

3. len variable

The length of a list is stored in its len variable. This can be changed to resize the list. More commonly it is used to find out how many items are in the list.

The previous example could be improved so that the number of dwarves is only specified in one place.

var/dwarves[7]

world/New()
   var/i
   for(i=1,i<=dwarves.len,i++)
      dwarves[i] = new/mob/dwarf()

This is a very common use of the for loop. Note the difference between this where we are looping over the indices of a list and the other type of for loop, for(i in dwarves), that iterates over the items in the list. The indices are necessary, as in this example, when you need to assign the item to a different value.

4. List Procs and Operators

The list data object provides a number of procedures and operators for manipulating the items in a list. These are described in the following sections.

4.1 + and += operators

The + operator creates a new list containing the items from the first list followed by those from the second list. If the second argument is not a list, it is added as an individual item.

The += operator behaves the same except a new list is not created. Instead the left-hand list is directly altered. In cases where this is the desired effect, it is more efficient to use this operator than to create an entirely new list.

list1 + list2
list1 += list2

This leads to yet another way of initializing the seven dwarves.

var/dwarves[0]

world/New()
   var/i
   for(i=1,i<=7,i++)
      dwarves += new/mob/dwarf()

In that example, individual items are added one at a time. In the next example, the ability to operate on an entire list is exercised to make an annoying little trap:

turf/naked_maker
   Enter(mob/M)
      contents += M.contents
      return 1

Do you see what it does? One thing you must know to understand what happens here is that contents lists are handled specially. When an object enters one contents list, it is automatically removed from any other. So when the user's contents are added to the turf's contents, all the objects in the user's inventory drop to the ground. Beware of the naked maker!

4.2 - and -= operators

The - operator does the reverse of the + operator. It creates a new list containing the items from the first list with those from the second list removed. If the second argument is not a list, only that individual item is removed. If the same value exists twice, the one towards the end of the list will be removed first.

Similarly, the -= operator behaves exactly like - except it directly modifies the left-hand list.

list1 - list2
list1 -= list2

4.3 Add and Remove procs

The Add and Remove procs behave respectively like the += and -= operators. The only difference is that Remove returns 1 if any values were actually removed and 0 if not.

Add (Item1,Item2,...)
Remove (Item1,Item2,...)

4.4 Find proc

The Find proc searches for an item in the list and returns the index if it is found. By default the entire list is searched, but a smaller section can be specified.

Find (Item,Start=1,End=0)
Item is the value to search for.
Start is the first index to search.
End is the first index not to search.
Returns the index of the item or 0 if none.

This could be used, for example, to determine which of the seven dwarves a given dwarf is.

mob/dwarf
   verb/which()
      set src in view()
      var/n = dwarves.Find(src)
      usr << "I am the [n]\th dwarf.  Where is Snow White?"

This way, you can happily wander around asking each dwarf which one they are and they will tell you. Notice the use of the \th text macro. That produces the correct ordinal ending: 1st, 2nd, 3rd, and so on.

4.4.1 in operator

The in operator can be used to tell you if an item is in a list. The Find proc could be used for that purpose, but if you don't care about the position of the item in the list, in is more convenient and efficient. The expression is 1 if the item exists in the list and 0 if not.

item in list

Using this, you could check if a given dwarf is one of the seven with the statement if(D in dwarves) ....

A more complex example using this would be a list of super-users.

var/SuperUsers[0]

client/New()
   if(key in SuperUsers)
      usr = new/mob/DM()
      usr.name = key
      usr.key = key //connect the player
   return ..()

Anyone in the special SuperUser list gets a mob of type /mob/DM. Everyone else gets the default world.mob. Of course, you would need to add some keys to the SuperUser list for this to have any effect.

4.5 Copy proc

The Copy proc creates a new list from the contents of all or part of the source list.

Copy (Start=1,End=0)
Start is the first index to copy.
End is the first index not to copy.
Returns a new list.

4.6 Cut proc

The Cut proc removes a section (or all) of a list.

Cut (Start=1,End=0)
Start is the first index to copy.
End is the first index not to copy.

5. Creating Lists

There are several instructions in DM for creating lists. Each one initializes the contents of the list in a different way. These are described in the following sections.

5.1 list instruction

The list instruction creates a list from its arguments.

list (Item1,Item2,...)
Returns a list of the specified items.

The most common place where one uses this is in the argument definition of a verb.

mob/verb/set_gender(G in list("male","female","neuter"))
   gender = G

This is a more user-friendly version of the gender changing verb. Instead of having the user type into an open-ended text field, this verb restricts the choice of gender to three allowed values.

5.2 newlist instruction

The newlist instruction creates a list of objects from the specified types. It is called newlist because it is equivalent to list() with new called on each argument.

newlist (Type1,Type2,...)
Returns a new list.

This instruction can be used to initialize the contents of an object. In the following example, new players will be created with two objects already in their inventory.

mob/player
   contents = newlist(/obj/scroll/introduction,
                      /obj/food/fortune_cookie)

Notice that DM permits arguments to be placed on successive lines. That applies to any procedure--not just newlist. Any amount of space, newline, and indentation may follow each comma. When argument lists get very long (as they may with list and newlist), it is sometimes nice to format them in a column rather than a single line.

5.3 typesof instruction

The typesof instruction creates a list of all the object types derived from the specified type of object.

typesof(Type1,Type2,...)

This instruction often saves one from having to code the same list manually (and then maybe forgetting to update it when adding a new object type). As an example, suppose you wanted the super-user to be able to create any object at will.

mob/DM
   verb/create(TypeOfObject in typesof(/obj,/mob))
      new TypeOfObject(usr) //Presto!

This has the user enter a raw type name (like /obj or /obj/scroll). That's ok for the DM, but one could make it more friendly by having the user select from a list of objects rather than types.

var/objects[0]
world/New()
   var/T
   for(T in typesof(/obj,/mob))
      objects += new T
mob/DM
   verb/create(O in objects)
      new O:type(usr)

The global list of objects is initialized in world/New(). Of course to be used in this way, we are assuming that all the objects have unique names. All sorts of variations are possible. For example, you could have an object variable indicating if certain object types should be excluded from the list or if an alternative name should be used, etc. The typesof instruction is a useful tool when writing generalized code such as this.

6. Pre-Defined Lists

So far, you have seen several object variables which are also lists. This section will review these and add a new list to your bag of tricks.

It is worth noting that the pre-defined lists are each used so frequently that they are implemented specially by DM for improved efficiency. For the most part, they behave just like lists that you might create yourself, but in some cases there are subtle differences. These will be mentioned in the sections that follow.

6.1 contents list

All the game objects (and the world object too) have a contents variable. In the case of the world, this contains all mobs, objs, turfs, and areas. Areas contain turfs, mobs, and objs. In all other cases, the contents are mobs and objs. No other object types may be added to these lists. Each game object exists in the world contents list and possibly the contents of one other object, depending on whether the object's loc is null or not.

Order of objects in the world contents list is not meaningful. Newer objects may exist before or after older objects because of the way the list is managed internally. The order in all other contents lists is from oldest arrivals to newest, with objs first and mobs following. This is the same order in which the objects are displayed on the map.

Another peculiarity about contents lists is that every object has its own fixed list. It is not possible, for example, to make the contents variable of an object refer to another list. If you try to assign it, the list will be copied rather than shared. This is different from the normal behavior in which assignments merely copy references to objects rather than duplicating the object data.

The following example takes advantage of the order of contents to make the center key on the keypad pick up the top most object.

client/Center()
   var/obj/O
   var/obj/LastO

   for(O in usr.loc) LastO = O
   if(LastO) //top most obj
      LastO.Move(usr)

This works by looping through the turf contents and saving a reference to the last one. Another way to accomplish the same thing would be to manually loop through the contents list in reverse. In either case, the reason for using a loop rather than just taking the last item in the contents list is to filter out mobs.

6.1.1 Area Contents

The contents of areas are handled a little differently from other objects. Normally, only those objects directly inside another are listed in its contents list. For example, if a player is carrying around a bag object, the contents of the bag are not listed in the player's contents.

In the case of areas on the map, however, this would result in only turfs being listed in the contents of the area. As a convenience, the contents of turfs are also included in areas.

Using this fact, you could make a sniff command which informs the user about other creatures in the area.

mob/wolfman
   verb/sniff()
      var/A = usr.loc

      //loop through usr's containers
      //until we hit an area
      while(A && !istype(A,/area))
         A = A:loc

      usr << "You smell:"
      var/mob/M
      for(M in A)
         usr << "\a [M]"

First a reference to the user's area is obtained. We could have just set it equal to usr.loc:loc, that is the area containing the user's turf. However, the method used is more general. It works even if the user is inside an object other than a turf. If that is not possible in your game, then you can just assume the player is always located on a turf.

The \a macro used in the output text generates the correct indefinite article for the following object. For example it would produce "a cat" in one case and "an elephant" in another. With a proper noun like "Dan," it produces nothing at all.

6.2 verb lists

The real objects and the client object all have verb lists. These contain a list of all the verbs attached to the object. By manipulating the verb lists, commands can be added and removed at run-time.

A reference to a procedure is obtained by specifying the path to its definition. It is this value that exists in the verb list. Note that there is no difference between proc and verb references. You can add either one to the list of verbs.

The next example allows a player to memorize spells from a book and thus permanently add a command to their repertoire.

obj/spellbook
   verb/read_stunray()
      usr.verbs += /obj/spellbook/proc/stunray
   proc/stunray(mob/trg)
      set src=usr
      view() << "[usr] immobilizes [trg] with a mysterious gesture."

To add another spell to the book, one would create a verb for reading it and a corresponding proc that will be added to the player's list of verbs.

Another way to add to an object's verb list is by using new. This method also provides a way to assign a new name or description to the verb.

new Proc(Obj,Name,Desc)
Proc is a reference to an existing procedure.
Obj is the object to add the verb to.
Name is the optional new name for the verb.
Desc is the optional new description of the verb.

The read_stunray verb can be rewritten as follows:

obj/spellbook
   verb/read_stunray()
      new/obj/spellbook/proc/stunray(usr)

6.3 args list

The args list is a proc variable that contains each of the parameters passed to the procedure. This can be used to create procedures that take an arbitrary number of arguments.

An example using this is a procedure like typesof, except it only includes final types--that is, object types with no children.

proc/final_typesof()
   var/BaseType
   var/DerivedType
   var/FinalList[0]
   for(BaseType in args)
      for(DerivedType in typesof(BaseType))
         var/children[] = typesof(DerivedType)
         if(children.len == 1) FinalList += DerivedType
   return FinalList

By using the args list, we allow for multiple arguments being specified to final_typesof() just like the typesof() procedure.

7. Multi-Dimensional Lists

The lists described so far have been single-dimensional, meaning that a single index is specified to retrieve a value. Multi-dimensional lists may also be created. The most common case would be a two- or three-dimensional list used to hold some information about the map.

7.1 Declaring the List

The following are two ways to declare a multi-dimensional list.

  1. var/MyList[5][10]
  2. var/MyList[][] = new/list(5,10)

These two examples each create a 2-dimensional list. Such a list is really a list of lists. In this case, MyList is a list of five lists. Those five lists each have a length of ten.

Whether you use new to create the list or specify the size directly in the brackets depends on whether you know the size of the dimensions at the time when the variable is defined.

7.2 Using the List

The elements in a multi-dimensional list are accessed by specifying an index for each dimension in brackets. If fewer dimensions are specified, then the result is itself a list containing all the values for the missing dimensions.

The following example creates a copy of the map in a three-dimensional list. This could be used to restore the map at a later time.

var/initmap[][][]

proc/CopyMap()
   var
      x; y; z

   initmap = new/list(world.maxx,world.maxy,world.maxz)
   for(x=1, x<=world.maxx, x++)
      for(y=1, y<=world.maxy, y++)
         for(z=1, z<=world.maxz, z++)
            var/turf/T = locate(x,y,z)
            initmap[x][y][z] = T.type

To get a reference to the turf at a given coordinate, the locate instruction is used. The procedure simply loops over all coordinates on the map and stores the type of turf found there.

8. Associative Lists

An associative list is one in which unique items in the list are paired with other values. Associated values are accessed by using the list item as the index into the list. The item is still accessed by using its numerical index.

Figure 10.13: An Associative List

Index Item Value
1 "wall" /turf/wall
2 "floor" /turf/floor
3 "door" /turf/door

The following code demonstrates how to construct a list such as the one in figure 10.13.

var/materials[0]
proc/InitMaterials()
   materials["wall"] = /turf/wall
   materials["floor"] = /turf/floor
   materials["door"] = /turf/door

Notice that the items "wall", "floor", and "door" are implicitly added to the list, merely by assigning their associated values. We could have begun by doing materials.Add("wall","floor","door"), but that would have been redundant.

Having constructed the list of building materials, you can now make a verb that puts it to good use.

mob/verb
   build(m in materials)
      var/mtype = materials[m]
      new mtype(usr.loc)

This provides a nicer looking list of building materials than the raw type paths. Players see "door" instead of /turf/door.

This example emphasizes the fact that materials behaves in every way like a list of text strings, not a list of object types. Only when indexed by one of the text strings does it reveal the associated value, which in this case is an object type.

8.1 Looping

The same methods for looping through lists work with associative lists. In fact, whether a list contains associated values or not really has no bearing on the matter. However, it is instructive to see how the associated value is accessed in the process.

Displaying the contents of the materials list can be done as follows.

mob/verb
   list_materials()
      var/m
      for(m in materials)
         usr << "[m] = [materials[m]]"

That method loops through the items in the list. It can also be done by looping through the numerical indices.

mob/verb
   list_materials()
      var/i
      for(i=1,i<=materials.len,i++)
         var/m = materials[i]
         usr << "[m] = [materials[m]]"

Notice the difference between materials[i] and materials[m]. The first is a text string and the second is the associated object type.

8.2 Specifics

From the preceding discussion, you can gather several things. One is that numerical list items may not have associated values. The reason is that a numerical index is treated differently from an object or text string index. The former is referred to as an array index and the latter is referred to as an associative index. There is no way to specify an associative index for a number. (However, you could store the number in an object, or convert it to a text string.)

Another thing to note is that items in an associative list should generally be unique. If two list entries refer to the same item, they cannot each have an independent association. Instead, they share the associated value.

As long as you only add items implicitly by using them as an index in the assignment of an associated value, there is no need to worry about uniqueness. New entries will only be formed for items which do not already exist. Only if you explicitly add items (using list.Add() or +=) is there any possibility of duplicate items.

Another related question is what happens if you try to get the associated value for an item which does not exist in the list (like materials["window"]). The result will be null, but the index item is not added to the list. In this way, you can be sure that the list is never modified when reading from it, only when writing to it.

As a final note on the implementation, be warned that the special lists (such as contents) do not support associative values. Of course, if anyone can come up with a good reason why they should, that may change.

9. Parameter Lists

One very common use for associative lists is to contain a list of parameters. By that, I mean a list of unique names and associated values.

9.1 world.params

The world.params list, mentioned in section 8.1, is an example. It allows a world to receive arbitrary parameters from the person who starts it up. While that isn't something you would want to do very often, it does allow a DM program to adapt to various conditions or to perform different functions.

The -params command-line option to Dream Daemon is used to initialize the parameter list. Multiple parameters may be assigned in a single argument or several -params may be specified.

-params name1=value1&name2=value2&...

This produces a world.params list with items "name1", "name2", and so on, with the corresponding associated values. A semicolon ; may be used in place of & to separate fields. There are a few other special characters that will be described in the following section.

9.2 params2list

Another time when parameter lists may be useful is in topic links. You can pack several pieces of information into a single text string using the same format as for -params. The result may be tagged onto a URL as additional "topic" information. In fact, the format for parameter text strings is borrowed from HTML forms, which use it for a similar purpose.

The params2list instruction handles conversion from a parameter text string to an associative list. It is automatically used to generate world.params but you may invoke it yourself in other situations.

params2list (ParamText)
ParamText is the parameter text string.
Returns an associative parameter list.

As a simple example, the expression

params2list("DM=James+Byond&SaveFile=myworld.sav")

would produce the following parameter list:

Index Item Value
1 "DM" "James Byond"
2 "SaveFile" "myworld.sav"

Notice that + in the parameter text is translated into a space. In this case, we could have simply used a space instead, but the official format encodes all characters other than letters, numbers, periods, and dashes specially. (The standard MIME type being used here is application/x-www-form-urlencoded for you CGI programmers out there.) That allows the parameter text to be used on the command-line or in other contexts where special characters might be interpreted strangely.

All special characters other than spaces are encoded using an ASCII value. The format is %xx where xx are two hexadecimal digits representing the ASCII value. A few common cases are listed in figure 10.15.

(Hexadecimal is a base 16 number system. Digits 10 through 15 are written in either upper or lowercase as A through F.)

Figure 10.15: Special Characters in Parameter Text

Character Hex-code
= %3d
& %26
; %3b
+ %2b
\n %0d%0a

9.3 list2params

The list2params instruction does the reverse of params2list. It takes an associative list and returns the parameter text corresponding to it. Special characters are automatically encoded in the process.

list2params (List)
List is the list of parameters and associated values.
Returns corresponding parameter text.

You could use list2params to pack several parameters into a topic link. Then when the link is executed, you could use params2list to extract the information.

The following example does that to form an inter-world communication channel.

mob/verb/broadcast(msg as text,address as text)
   var/plist[0]
   plist["function"] = "broadcast"
   plist["speaker"] = usr.key
   plist["message"] = msg

   //broadcast message to world at given address
   world.Export("[address]#[list2params(plist)]")

//here is where we receive messages from other worlds
world/Topic(T)
   var
      plist = params2list(T)
      function = plist["function"]

   if(function == "broadcast")
      var
         speaker = plist["speaker"]
         message = plist["message"]

      world << "[speaker] broadcasts, '[message]'"
   else return ..()