| 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 |
| <* NOTE Highlander@MorgenGrauen 11.2.94: |
| WRONG in MorgenGrauen (at least). The result is actually ({ "b" }). Array |
| subtraction removes ALL instances of the subtracted value from the array. |
| This holds true for all Amylaar-driver LPMuds. |
| *> |
| 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 = m_allocate(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 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. |
| |
| Copyright (c) George Reese 1993 |