478 lines
21 KiB
Plaintext
478 lines
21 KiB
Plaintext
chapter 11 "Complex Data Types"
|
|
Intermediate LPC
|
|
Descartes of Borg
|
|
November 1993
|
|
|
|
Chapter 3: Complex Data Types
|
|
|
|
3.1 Simple Data Types
|
|
In the textbook LPC Basics, you learned about the common, basic LPC
|
|
data types: int, string, object, void. Most important you learned that
|
|
many operations and functions behave differently based on the data type
|
|
of the variables upon which they are operating. Some operators and
|
|
functions will even give errors if you use them with the wrong data
|
|
types. For example, "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.
|
|
|
|
3.2 The Values NULL and 0
|
|
Before getting fully into arrays, there first should be a full understanding
|
|
of the concept of NULL versus the concept of 0. In LPC, a null value is
|
|
represented by the integer 0. Although the integer 0 and NULL are often
|
|
freely interchangeable, this interchangeability often leads to some great
|
|
confusion when you get into the realm of complex data types. You may
|
|
have even encountered such confusion while using strings.
|
|
|
|
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.
|
|
|
|
3.3 Arrays in LPC
|
|
An array is a powerful complex data type of LPC which allows you to
|
|
access multiple values through a single variable. For instance,
|
|
Nightmare has an indefinite number of currencies in which players may
|
|
do business. Only five of those currencies, however, can be considered
|
|
hard currencies. A hard currency for the sake of this example is a
|
|
currency which is readily exchangeable for any other hard currency,
|
|
whereas a soft currency may only be bought, but not sold. In the bank,
|
|
there is a list of hard currencies to allow bank keepers to know which
|
|
currencies are in fact hard currencies. With simple data types, we would
|
|
have to perform the following nasty operation for every exchange
|
|
transaction:
|
|
|
|
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.
|
|
|
|
3.4 Using Arrays
|
|
You now should understand how to declare and recognize an array in
|
|
code. In order to understand how they work in code, let's review the
|
|
bank code, this time using arrays:
|
|
|
|
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.
|
|
|
|
The efun 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:
|
|
|
|
Whole arrays:
|
|
variable: arr
|
|
constant: ({ "platinum", "gold", "electrum", "silver", "copper" })
|
|
|
|
Individual members of arrays:
|
|
variable: arr[2]
|
|
constant: "electrum"
|
|
|
|
You can use these means of reference to do all the things you are used to
|
|
doing with other data types. You can assign values, use the values in
|
|
operations, pass the values as parameters to functions, and use the
|
|
values as return types. It is important to remember that when you are
|
|
treating an element alone as an individual, the individual element is not
|
|
itself an array (unless you are dealing with an array of arrays). In the
|
|
example above, the individual elements are strings. So that:
|
|
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.
|
|
|
|
3.5 Mappings
|
|
One of the major advances made in LPMuds since they were created is
|
|
the mapping data type. People alternately refer to them as associative
|
|
arrays. Practically speaking, a mapping allows you freedom from the
|
|
association of a numerical index to a value which arrays require.
|
|
Instead, mappings allow you to associate values with indices which
|
|
actually have meaning to you, much like a relational database.
|
|
|
|
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:
|
|
|
|
constant values:
|
|
whole: ([ index:value, index:value ]) Ex: ([ "gold":10, "silver":20 ])
|
|
element: 10
|
|
|
|
variable values:
|
|
whole: map (where map is the name of a mapping variable)
|
|
element: map["gold"]
|
|
|
|
So now my monetary functions would look like:
|
|
|
|
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.
|
|
|
|
3.6 Summary
|
|
Mappings and arrays can be built as complex as you need them to be.
|
|
You can have an array of mappings of arrays. Such a thing would be
|
|
declared like this:
|
|
|
|
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:
|
|
Indexing on illegal type.
|
|
Illegal index.
|
|
Bad argument 1 to (+ += - -=) /* insert your favourite operator */
|
|
Error messages 1 and 3 are darn near almost always caused by a failure
|
|
to initialize the array or mapping in question. Error message 2 is caused
|
|
generally when you are trying to use an index in an initialized array
|
|
which does not exist. Also, for arrays, often people new to arrays will
|
|
get error message 3 because they try to add a single element to an array
|
|
by adding the initial array to the single element value instead of adding
|
|
an array of the single element to the initial array. Remember, add only
|
|
arrays to arrays.
|
|
|
|
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 SetExits() (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.
|
|
|
|
Copyright (c) George Reese 1993
|