"a"+"b"
is handled much differently than
1+1
. When you ass "a"+"b"
, you are adding
"b"
onto the end of "a"
to get "ab"
.
On the other hand, when you add 1+1
, you do not get
11
, you get 2
as you would expect.I refer to these data types as simple data types, because they atomic in that they cannot be broken down into smaller component data types. The object data type is a sort of exception, but you really cannot refer individually to the components which make it up, so I refer to it as a simple data type.
This chapter introduces the concept of the complex data type, a data type which is made up of units of simple data types. LPC has two common complex data types, both kinds of arrays. First, there is the traditional array which stores values in consecutive elements accessed by a number representing which element they are stored in. Second is an associative array called a mapping. A mapping associates to values together to allow a more natural access to data.
0 represents a value which for integers means the value you add to another value yet still retain the value added. This for any addition operation on any data type, the ZERO value for that data type is the value that you can add to any other value and get the original value. Thus: A plus ZERO equals A where A is some value of a given data type and ZERO is the ZERO value for that data type. This is not any sort of official mathematical definition. There exists one, but I am not a mathematician, so I have no idea what the term is. Thus for integers, 0 is the ZERO value since 1 + 0 equals 1.
NULL, on the other hand, is the absence of any value or meaning. The LPC driver will interpret NULL as an integer 0 if it can make sense of it in that context. In any context besides integer addition, A plus NULL causes an error. NULL causes an error because adding valueless fields in other data types to those data types makes no sense.
Looking at this from another point of view, we can get the ZERO
value for strings by knowing what added to "a"
will give us
"a"
as a result. The answer is not 0
, but instead
""
. With integers, interchanging NULL and 0 was
acceptable since 0 represents no value with respect to the integer data type.
This interchangeability is not true for other data types, since their
ZERO values do not represent no value. Namely, ""
represents a string of no length and is very different from 0
.
When you first declare any variable of any type, it has no value. Any
data type except integers therefore must be initialized somehow before
you perform any operation on it. Generally, initialization is done in the
create()
function for global variables, or at the top of the
local function for local variables by assigning them some value, often the
ZERO value for that data type. For example, in the following code
I want to build a string with random words:
string build_nonsense() { string str; int i; str = ""; /* Here str is initialized to the string ZERO value */ for(i=0; i<6; i++) { switch(random(3)+1) { case 1: str += "bing"; break; case 2: str += "borg"; break; case 3: str += "foo"; break; } if(i==5) str += ".\n"; else str += " "; } return capitalize(str); }If we had not initialized the variable str, an error would have resulted from trying to add a string to a NULL value. Instead, this code first initializes str to the ZERO value for strings,
""
. After that, it enters a loop which makes 6 cycles, each time
randomly adding one of three possible words to the string. For all words
except the last, an additional blank character is added. For the last word, a
period and a return character are added. The function then exits the loop,
capitalizes the nonsense string, then exits.
int exchange(string str) { string from, to; int amt; if(!str) return 0; if(sscanf(str, "%d %s for %s", amt, from, to) != 3) return 0; if(from != "platinum" && from != "gold" && from != "silver" && from != "electrum" && from != "copper") { notify_fail("We do not buy soft currencies!\n"); return 0; } ... }With five hard currencies, we have a rather simple example. After all it took only two lines of code to represent the if statement which filtered out bad currencies. But what if you had to check against all the names which cannot be used to make characters in the game? There might be 100 of those; would you want to write a 100 part if statement? What if you wanted to add a currency to the list of hard currencies? That means you would have to change every check in the game for hard currencies to add one more part to the if clauses. Arrays allow you simple access to groups of related data so that you do not have to deal with each individual value every time you want to perform a group operation.
As a constant, an array might look like this:
({ "platinum", "gold", "silver", "electrum", "copper" })which is an array of type string. Individual data values in arrays are called elements, or sometimes members. In code, just as constant strings are represented by surrounding them with
""
, constant
arrays are represented by being surrounded by ({ })
, with
individual elements of the array being separated by a ,
.You may have arrays of any LPC data type, simple or complex. Arrays made up of mixes of values are called arrays of mixed type. In most LPC drivers, you declare an array using a throw-back to C language syntax for arrays. This syntax is often confusing for LPC coders because the syntax has a meaning in C that simply does not translate into LPC. Nevertheless, if we wanted an array of type string, we would declare it in the following manner:
string *arr;In other words, the data type of the elements it will contain followed by a space and an asterisk. Remember, however, that this newly declared string array has a NULL value in it at the time of declaration.
string *hard_currencies; int exchange(string str) { string from, to; int amt; if(!str) return 0; if(sscanf(str, "%d %s for %s", amt, from, to) != 3) return 0; if(member_array(from, hard_currencies) == -1) { notify_fail("We do not buy soft currencies!\n"); return 0; } ... }This code assumes hard_currencies is a global variable and is initialized in
create()
as:
hard_currencies = ({ "platinum", "gold", "electrum", "silver", "copper" });Ideally, you would have hard currencies as a
#define
in a header
file for all objects to use, but #define
is a topic for a later
chapter.
Once you know what the member_array()
efun does, this method
certainly is much easier to read as well as is much more efficient and
easier to code. In fact, you can probably guess what the
member_array()
efun does: It tells you if a given value is a
member of the array in question. Specifically here, we want to know if the
currency the player is trying to sell is an element in the
hard_curencies
array. What might be confusing to you is, not only
does member_array()
tell us if the value is an element in the
array, but it in fact tells us which element of the array the value is.
How does it tell you which element? It is easier to understand arrays if
you think of the array variable as holding a number. In the value above,
for the sake of argument, we will say that hard_currencies
holds
the value 179000
. This value tells the driver where to look for
the array hard_currencies
represents. Thus,
hard_currencies
points to a place where the array values may be
found. When someone is talking about the first element of the array, they
want the element located at 179000
.
When the object needs the value of the second element of the array, it
looks at 179000
+ one value, then 179000
plus two
values for the third, and so on. We can therefore access individual elements
of an array by their index, which is the number of values beyond the starting
point of the array we need to look to find the value. For the array
hard_currencies
array:
"platinum"
has an index of 0
.
"gold"
has an index of 1
.
"electrum"
has an index of 2
.
"silver"
has an index of 3
.
"copper"
has an index of 4
.
member_array()
thus returns the index of the element
being tested if it is in the array, or -1
if it is not in the
array. In order to reference an individual element in an array, you use its
index number in the following manner:
array_name[index_no]Example:
hard_currencies[3]where
hard_currencies[3]
would refer to "silver"
.
So, you now should now several ways in which arrays appear either as
a whole or as individual elements. As a whole, you refer to an array
variable by its name and an array constant by enclosing the array in ({
})
and separating elements by ,
. Individually, you refer
to array variables by the array name followed by the element's index number
enclosed in []
, and to array constants in the same way you would
refer to simple data types of the same type as the constant. Examples:
arr
({ "platinum", "gold", "electrum", "silver",
"copper" })
arr[2]
"electrum"
str = arr[3] + " and " + arr[1];will create
str
to equal "silver and gold"
.
Although this seems simple enough, many people new to arrays start to run into
trouble when trying to add elements to an array. When you are treating an
array as a whole and you wish to add a new element to it, you must do it by
adding another array.Note the following example:
string str1, str2; string *arr; str1 = "hi"; str2 = "bye"; /* str1 + str2 equals "hibye" */ arr = ({ str1 }) + ({ str2 }); /* arr is equal to ({ str1, str2 }) */Before going any further, I have to note that this example gives an extremely horrible way of building an array. You should set it:
arr =
({ str1, str2 })
. The point of the example, however, is that you must
add like types together. If you try adding an element to an array as the data
type it is, you will get an error. Instead you have to treat it as an array
of a single element.
In an array of 5 elements, you access those values solely by their integer indices which cover the range 0 to 4. Imagine going back to the example of money again. Players have money of different amounts and different types. In the player object, you need a way to store the types of money that exist as well as relate them to the amount of that currency type the player has. The best way to do this with arrays would have been to store an array of strings representing money types and an array of integers representing values in the player object. This would result in CPU-eating ugly code like this:
int query_money(string type) { int i; i = member_array(type, currencies); if(i>-1 && i < sizeof(amounts)) /* sizeof efun returns # of elements */ return amounts[i]; else return 0; }And that is a simple query function. Look at an add function:
void add_money(string type, int amt) { string *tmp1; int * tmp2; int i, x, j, maxj; i = member_array(type, currencies); if(i >= sizeof(amounts)) /* corrupt data, we are in a bad way */ return; else if(i== -1) { currencies += ({ type }); amounts += ({ amt }); return; } else { amounts[i] += amt; if(amounts[i] < 1) { tmp1 = allocate(sizeof(currencies)-1); tmp2 = allocate(sizeof(amounts)-1); for(j=0, x =0, maxj=sizeof(tmp1); j < maxj; j++) { if(j==i) x = 1; tmp1[j] = currencies[j+x]; tmp2[j] = amounts[j+x]; } currencies = tmp1; amounts = tmp2; } } }That is really some nasty code to perform the rather simple concept of adding some money. First, we figure out if the player has any of that kind of money, and if so, which element of the currencies array it is. After that, we have to check to see that the integrity of the currency data has been maintained. If the index of the type in the currencies array is greater than the highest index of the amounts array, then we have a problem since the indices are our only way of relating the two arrays. Once we know our data is in tact, if the currency type is not currently held by the player, we simply tack on the type as a new element to the currencies array and the amount as a new element to the amounts array. Finally, if it is a currency the player currently has, we just add the amount to the corresponding index in the amounts array. If the money gets below 1, meaning having no money of that type, we want to clear the currency out of memory.
Subtracting an element from an array is no simple matter. Take, for example, the result of the following:
string *arr; arr = ({ "a", "b", "a" }); arr -= ({ arr[2] });What do you think the final value of
arr
is? Well, it is:
({ "b", "a" })Subtracting
arr[2]
from the original array does not remove the
third element from the array. Instead, it subtracts the value of the third
element of the array from the array. And array subtraction removes the first
instance of the value from the array. Since we do not want to be forced on
counting on the elements of the array as being unique, we are forced to go
through some somersaults to remove the correct element from both arrays in
order to maintain the correspondence of the indices in the two arrays.Mappings provide a better way. They allow you to directly associate the money type with its value. Some people think of mappings as arrays where you are not restricted to integers as indices. Truth is, mappings are an entirely different concept in storing aggregate information. Arrays force you to choose an index which is meaningful to the machine for locating the appropriate data. The indices tell the machine how many elements beyond the first value the value you desire can be found. With mappings, you choose indices which are meaningful to you without worrying about how that machine locates and stores it.
You may recognize mappings in the following forms:
([ index:value, index:value ])
Ex: ([ "gold":10, "silver":20 ])
10
map
(where map
is the name of a
mapping variable)
map["gold"]
int query_money(string type) { return money[type]; } void add_money(string type, int amt) { if(!money[type]) money[type] = amt; else money[type] += amt; if(money[type] < 1) map_delete(money, type); /* this is for MudOS */ ...OR... money = m_delete(money, type) /* for some LPMud 3.* varieties */ ... OR... m_delete(money, type); /* for other LPMud 3.* varieties */ }Please notice first that the efuns for clearing a mapping element from the mapping vary from driver to driver. Check with your driver's documentation for the exact name an syntax of the relevant efun.
As you can see immediately, you do not need to check the integrity of your data since the values which interest you are inextricably bound to one another in the mapping. Secondly, getting rid of useless values is a simple efun call rather than a tricky, CPU-eating loop. Finally, the query function is made up solely of a return instruction.
You must declare and initialize any mapping before using it. Declarations look like:
mapping map;Whereas common initializations look like:
map = ([]); map = allocate_mapping(10) ...OR... map = m_allocate(10); map = ([ "gold": 20, "silver": 15 ]);As with other data types, there are rules defining how they work in common operations like addition and subtraction:
([ "gold":20, "silver":30 ]) + ([ "electrum":5 ])gives:
(["gold":20, "silver":30, "electrum":5])Although my demonstration shows a continuity of order, there is in fact no guarantee of the order in which elements of mappings will stored. Equivalence tests among mappings are therefore not a good thing.
mapping *map_of_arrs;which might look like:
({ ([ ind1: ({ valA1, valA2}), ind2: ({valB1, valB2}) ]), ([ indX: ({valX1,valX2}) ]) })Mappings may use any data type as an index, including objects. Mapping indices are often referred to as keys as well, a term from databases. Always keep in mind that with any non-integer data type, you must first initialize a variable before making use of it in common operations such as addition and subtraction. In spite of the ease and dynamics added to LPC coding by mappings and arrays, errors caused by failing to initialize their values can be the most maddening experience for people new to these data types. I would venture that a very high percentage of all errors people experimenting with mappings and arrays for the first time encounter are one of three error messages:
+ += - -=
) /* insert your favourite
operator */
At this point, you should feel comfortable enough with mappings and
arrays to play with them. Expect to encounter the above error messages
a lot when first playing with these. The key to success with mappings is
in debugging all of these errors and seeing exactly what causes wholes
in your programming which allow you to try to work with uninitialized
mappings and arrays. Finally, go back through the basic room code and
look at things like the set_exits()
(or the equivalent on your
mudlib) function. Chances are it makes use of mappings. In some instances,
it will use arrays as well for compatibility with mudlib.n.