|
CTP2 Bureau The Modding Knowledgebase |
|
Apolyton CTP2 forums Apolyton CTP2 archives Source Code Project SVN |
![]() |
Search the site: |
Locutus' Ultimate Guide To SLICNot quite as 'ultimate' as the title suggests, because it's unfinished. It's an elaborate manual that intends to explain to programming newbies and veterans alike (but with a focus on newbies) the all the details of the SLIC programming language. The most important part that's missing is the part that deals with messages, other than that it's reasonably complete. However, I do hope to get around to doing a complete makeover of that guide, as a lot of new info, examples and other useful stuff could be added to all chapters. Still a very important resource for beginning SLIC programmers though.by Wouter Snijders aka Locutus1. Introduction/Elementary Information/Before you get started1.1 What is SLIC? 2. Arithmatic2.1 Large calculatorThe term 'computer' comes from the verb 'to compute' which means as much as 'to calculate'. (This may seem trivial to English readers but for some people who's first language isn't English it's not. But even most people who's first language *is* English might take the term 'computer' so much for granted that they don't even realize this.) But that would suggest that a computer is nothing more than a large calculator! Well, that is absolutely true. Proof of this is the fact that a large part of all instructions that the average CPU (or computer chip, which is the core of every PC) gets, consists of basic arithmetic instructions such as addition, substraction or multiplication. The majority of the rest of the instructions takes care of loading and storing information to/from memory and 'moving around' in the program code (for which control structures are responsible, see the next chaptor for that). More complicated mathematics is generally achieved by combining these basic arithmetic instructions in a smart way (a simple exampe: 3 squared is the same as 3 * 3 (which is the computer version of 3 x 3) or, even more simplified, 3 + 3 + 3). 2.2 Arithmatic in SLICBecause of this importance of simple arithmetic, just about every programming or scripting language (every language that I know of anyway) is capable of performing at least basic arithmatics and SLIC is no exception. The basic arithmetic operations that SLIC can handle are the following (they are all explained below): Numerical: Relational: Logical/Boolean: Miscellaneous: (For the (Java/C-)programmers out there: no, there is no increment (++) or decrement (--) and there's no power operation (^) either) Though most people will know a lot of these operations, I'll give short expanations and examples of each one of them. All these examples can be tested in SLIC by using the following code: int_t x; HandleEvent(BeginTurn) 'LOC_MathTest' post { Messagebox 'LOC_MathTest_M' {
The textfiles should contain something like this: LOC_MATHTEXT "The result of the first expression is {x} and the result of the second expression is {y}"
With this code you can calculate two expressions (an expression is a combination of operations, so either one operation or several operations combined) at once. To calculate only one expression remove all references to y (in the code as well as in the textfile) and to calculate three expressions add references to e.g. z. <expression1> and <expression2> can be any of the examples that I give below. 2.3 Numerical operationsNumerical operations in SLIC can only be applied to whole numbers, fractions or decimal numbers aren't supported. (You might want to mention someplace, not necessarily here, that in the Databases, floating point values are multiplied by 100 and converted to ints.) I still haven't figured out the exact range of numbers, but it's large enough for anything you'll ever need: I've been able to use numbers larger than 100 million and smaller and -100 million without losing accuracy in calculations (but if you actually ever need to use such large numbers double-check accuracy anyway, I didn't check extremely thoroughly and it's not impossible that accuracy is lost with such high numbers). Addition 4 + 2 The result will be... Oh, come on, do I really have to say this? Substraction 4 - 12 Which will result in -8. Multiplication 4 * 3 This is the same as 4 + 4 + 4 and hence results in 12. Note that you don't use the 'x' symbol for this, like you may have learnt in primary school, but '*'. This is because if you use variables (which will be introduced further in this chapter) you could get in trouble. A computer doens't know what to do with something like "x x x" (hell, I don't think I would either), whereas "x * x" doesn't cause any problems. Division 15 / 3 The result for this is 5. A more complicated example, which illustrates exactly why this operation is so complicated for computers: 25 / 2 or: 25 / -2 What would the results for these expressions be? We humans would immediately say 12.5 and -12.5 but as I already mentioned earier, SLIC works with integers (a type of numbers that is often used in programming and scripting languages) and integers have to be whole numbers. How this problem is solved differs per programming/scripting language, but SLIC's solution is to cut off the decimal part of the result of any calculation that isn't an integer. This means 12.5 becomes 12 and -12.5 becomes -12. Partial results of expressions are also cut off, so: 3 /4 + 3 / 4 is 0 + 0 = 0 and not 0.75 + 0.75 = 1.5 and thus 1, as you might expect. 12 / 0 should be 'undefined', but a computer doesn't know what that is or how to deal with it. In most other languages a division by zero will result in an error (a division-by-zero error), but SLIC is in this aspect different, the result of a division by zero in SLIC results in 0. The conclusion of all this is that you *can* use divisions in SLIC but that you will have to be very careful with them because it's very easy to get inaccurate or non-sense results if you make any mistakes with them or work carelessly. So if you ever get any unexpected results in your calculations, double-checking your divisions is probably one of the first things to do. 2.4 BracketsBrackets aren't really arithmatic operations but they can be used in combining simple arithmatic combinations into more complex ones. For example, without brackets it may not be clear to you (or a computer) what the result of 3 * 2 + 5 would be. It could be 11 (6 + 5) or it could be 21 (3 * 7). There is a order of precedence that the computer applies and it is basicly the same as the one most people will have learnt in school (see the end of this chapter), but in complicated expressions it's wise to rely on brackets rather then on the order of precedence because it will prevent a lot of errors and make the code much more readable. An example of how the use of brackets (and divisions) can make a diffence: 2 / 1 * 6 + 4 / 2 * 2 - 5 + 4 / 2 * 3 + 1 This would be so much clearer if it was written like this: (2 / 1) * 6 + (4 / 2) * 2 - 5 + (4 / 2) * 3 + 1 But it would be quite different if it was written like this: 2 / (1 * 6) + 4 / (2 * 2) - (5 + (4 / 2) * 3 + 1) The result of the the first two expressions is (2 * 6) + (2 * 2) - 5 + (2 * 3) + 1 = 18 whereas the last expression equals (2/ 6) + (4 / 4) - (5 + (2 * 3) + 1) = -10 2/3, or -10 for SLIC and approximately -10.667 for humans. 2.5 Boolean logicBut primary school math is not all the arithmetic SLIC can handle. There's also the logical, or Boolean, arithmatic. Many people will be less familiar with this than with the arithmetic discussed in the previous section, but it is in fact much more often used and hence more important then the 'regular' arithmetic, while being just as easy to understand, if not easier. Boolean logic all revolves around two values, true and false. Usually false is represented by a 0 and true by 1 (or, more accurately, any non-zero value). The result of these calculations is often used to make decisions: if an expression is true, then do this, if it is false then do that (or do nothing at all). Boolean values are, at least in SLIC, stored in the same way as integers so you can use the same code to test these expressions as the code used for numerical expressions, only will the result now always be either 1 or 0. 2.5.1 Relational operatorsOne subgroup of Boolean logic is the group of relational expressions. The values that they get are normal integers but the result of these functions is a Boolean value. Though I presume everyone will know them, I'll give a explanations and examples anyway. Greater than (3 > 1) returns true. (Strictly speaking the brackets aren't necessary but if inserted in the code, they make the code more readable IMHO) Smaller than (3 < 1) returns false. Greater than or equal to (-34 >= 3) returns false. Smaller than or equal to (3 <= 3) returns true. Equal to (4 == 4) returns true. Unequal to: (4 != 4) returns false. 2.5.2 Boolean operatorsWhen calculating with integers we can simply make complex expressions out of simple ones by using the same operations in the correct manner (so not "3 3 + * 4" but "3 + 3 * 4"). In Boolean logic this works slightly different. We can't just say "6 < 3 >= 8". SLIC does give back a result for this, but it is something that we can't be satisfied with. In this specific example the result will be false return false (6 < 3 = false, hence 0 and 0 >= 8 is also false). This problem is caused by the fact that relational expressions have integers as 'input' but Booleans as 'output' while booleans are really just normal integers in SLIC. So what we need is a few operations that have Booleans as both 'input' and 'output'. These are the Boolean operations and there are only three of them, simply because more are not needed, with these three any logical expression (with two arguments) can be formed. AND (4 < 3) && (2 > -8) equals "false AND true" and thus returns false. OR (1 < 5) || (9 > 3) equals "true OR true" and thus returns true. NOT !(1 < 5) equals "NOT true" and thus returns false. So let's say you want to make sure that 8 is a number larger than or equal to 5 or smaller than 2, but not 9 (in this case a trivial exercise but it will prove very useful when variables are used instead of numbers, see the next paragraphs). Such a check would look like this: 8 >= 5 || 8 < 2 && !8 == 9 Normally this is not sufficient though. SLIC can handle this and, at least in this case, will give the correct result. But in other cases and for humans who try to understand the code, it is necessary to place brackets where there is a difference in the 'type' of operation (relational or Boolean). This would result in the following: (8 >= 5) || (8 < 2) && !(8 == 9) But even now it may not be not good enough. We could still be talking about two situations here (although SLIC wil apply order of precedence here and not have a problem): (8 >= 5) || ((8 < 2) && !(8 == 9)) or ((8 >= 5) || (8 < 2)) && !(8 == 9) Right now there is no difference in the result but look what happens if you replace all 8s with a 9. Now the first situation says TRUE || (FALSE && NOT TRUE) and hence returns true, but the second situation says (TRUE || FALSE) && NOT TRUE and hence returns false. The second situation would be the desired one in that case. Without brackets NOT has the highest precedence and it is followed by AND and OR, which are evaluated from left to right because they are equal in precedence, so without the extra brackets things would have been fine as well, no matter what you replace 8 with. But don't take any risks with these things and place brackets whenever appropriate. 2.6 Order of precedenceIn conclusion of this chapter I will give you the order of precedence of arithmetic operations in SLIC. I didn't give this earlier because only now have you seen all the operations that are present in SLIC. All operations that are on the same level of precedence are evaluated from left to right. If at any time you are uncertain if brackets are necessesary , place them anyway, just to be on the safe side. >From highest to lowest: = (assignment - not really an operation, see next chapter) 3. Variables and Datatypes3.1 VariablesLike the end of the previous chapter already showed, you don't always work with the same values (that would be rather silly: instead of 5 + 3 you might as well use 8 right away). The power of a computer lies, among other things, in the fact that you don't have to work with fixed values, like with a calculator. In SLIC, as well as most other programming languages, you can use variables. ((x >= 5) || (x < 2)) && !(x == 9) Assigning values to variables is very simple. Simply state the name of the variable followed by a '=' (now you see why '==' is used for expressing equality: one symbol for two different things would be confusing) and follow that by a value or an expression that gives a value, either Boolean or numerical. Make sure the assignment is followed by a ';' sign. Example (similar to what you saw in the previous chapter): HandleEvent(BeginTurn) 'AssignValue' post {
This will assign the value 8 to x and the value 4 * x - (1/2) = 32 - 1/2 = 31 1/2, or 31 to y. As you can see, you can use variables to assign values to other variables. Even better, you can even use a variable itself to change it's own value, like this: HandleEvent(BeginTurn) 'AssignValue' post {
Here the last known value of x (or y) replaces the x (or y) right of the '='-sign and the value of the whole expression is assigned to the x (or y) left of the '='-sign. So if x was declared 3 earlier in the program then the expression x = x + 1 will become x = 3 + 1 and hence x = 4. So x will get the value 4. In the same fashion if y has the value 6 then it will get the new value 18. Not that this only works when the variable already has a value earlier on in the program. If it doesn't, the game will either give an error or assume the previous value to be 0 (I should test this). Boolean values can be replaced by variables as well, where any non-zero value is regarded true: HandleEvent(MoveUnits) 'BooleanValue' pre {
This will assign the value 8 to x and true to y (3 < 5 is true and x = 8 is not 0, so true, and true && true assigns true to y). 3.2 Declaration of variablesBut you can't say 'x = 8;' just like that in the middle of your SLIC script. SLIC will not know how to deal with it. For a good administration of all these variables (there can be many in a large script) it is necessary to declare a variable first before using it. Though it means slightly more work for the programmer, you will find that it is very beneficial both for man and machine: the primary reason for this is that it makes debugging much, much easier, especially if you're as sloppy and chaotic in your working methods as I am: a typo is so easy to make and can be so difficult to detect. Declarations help prevent this as a typo in a variable name will result in an undeclared variable and hence an error. int_t <variablename>; Where <variablename> can be any legal name for a variable. It doesn't matter where you declare variables, you can do this at any point in the program, but make sure you declare it before you use it (so higher up in the file(s)). For good readability it is advisable to declare your variables as early as possible, so at the beginning of the file for global variables and at the beginning of a trigger for local variables (for the concepts of global and local variables: see further). Examples: int_t x; HandleEvent(UnitsMove) 'DeclareVariables' post { HandleEvent(Battle) 'DeclareVariables2' post { Messagebox 'LOC_VarTest_M' {
LOC_VARTEXT "The result of y is {y}, the value of SLIC_IS_COOL is {SLIC_IS_COOL}, the value of x is {x}, and the value of LocutusOfBorg is {LocutusOfBorg}." --> TEST TO SEE WHAT HAPPENS WITH UNDECLARED VARIABLES <-- Note: if a variable is declared and then used before it has been assigned a value, the value will be 0. (The value is 'initialized' to 0). So inserting the line "x = y;" right after the declaration of y in the previous example will cause x to become 0. However, if you need a variable to be 0, it is for clarity reasons usually better to manually assign the value 0 to this variable anyway. 3.2 Scope of variablesIn the above example you could see that some variables are declared within the eventhandler and some are declared outside it. This has to do with the scope of variables: with the area in which they can be used, in which they are valid, to which they apply. Variables that are defined outside any eventhandler, trigger, messagebox, alertbox or whatever are defined globally and variables inside these are defined locally. Global variables are variables that can be used anywhere in the script. In the previous exampe I defined x as a global variable so I could use it in the 'DeclareVariables' eventhandler and in the 'DeclareVariables2' eventhandler. LocutusOfBorg is a local variable and can hence only be used in the 'DeclareVariables' eventhandler. Assigning a value to it or using it in any way to assign a value to some other variable in 'DeclareVariables2' would be incorrect and result in an error. The advantage of this difference in scope is that everytime you use a global variable like x, it will always have the value it had the last time you used it, even if that is hours ago and/or in a totally different eventhandler. If you use a local variable like y, it will only have it's value within that particular trigger only for the time in which a particular instance of that eventhandler (or messagebox or whatever) is used. int_t x; HandleEvent(BeginTurn) 'IntruigingSituation' post {
One interesting question the above example provides is: what happens if we declare a variable both locally and globally? The answer is that this will work without problems. The result of this will be that for as long as the local variable exists, the local variable is used and the global version is temporarily stored 'elsewhere' and is unavailable for use. As soon as the eventhandler or messagebox or whatever it is in which the local variable is declared is 'finished', the game will 'destroy' the local variable and 'restore' the global variable. This isn't an accurate description of how things really work internally, but the general idea should be clear. --> HOW ABOUT DECLARATIONS IN FOR-LUSSES? ETC <-- 3.3 DatatypesThis is all very nice of course, but it's all a bit academic. This manipulating and storing of numbers won't help you much if you need to manipulate or store a unit or a city or whatever. Yes, theorically it would have been possible to represent cities and units by numbers somehow, Activision could have chosen to do that, but in practice it would be very inconvenient. Fortunately the Actigrammers were smarter than that and implemented a more convenient system. They provided SLIC with a couple of other 'datatypes', in other words, they added several different types of variables, variables that store other things than numbers. These other 'basic' types are city-, unit-, location-, player- and army-variables. Here are a couple of examples on how to declare them: int_t x; // an integer (whole number) As you can see it all speaks for itself. One important note though: players are stored as integers and are usually interchangable with integers. Unfortunately this can sometimes cause problems and I haven't completely figured out yet when exactly it goes wrong and how to prevent it. If you encounter a problem with this though, you can solve it by using built-in variables (see further) instead of user-declared variables (the variables decribed until now). unit_t v; HandleEvent(BeginTurn) 'StructTest' post { Messagebox 'StructTest_M' { // display mesage
STRUCT_TEST "Variable d contains the city of {d.name} and variable v contains a {v.name}. The expression (d == c) results in {boolean} and the expression (v != u) results in {bool2}." Because these variables are abstract, they can't be displayed in a message either. So to refer to them in a message, you must use a description of them as I showed in the example by referring to them by their name. 3.4 ArraysSometimes it's useful to store more than one variable. For example, if you want to store a list of all cities that have a certain building or all units of a certain type, it is difficult to do this with regular variables. You'd need to declare these variables one by one, assign the right value (or unit or city or location or whatever) to the right variable and you'd need to keep in mind that the number of elements you'd like to keep track of may vary. Example (no need to understand this, just an example of how complicated it might get): city_t c1; HandleEvent(CreateCity) 'NoArrayExample' post { HandleEvent(CreateUnit) 'NoArrayExample2' {
As you can see, this would be a very complicated and tedious job, if possible at all (in this case it is, but this is just a very simple example, more complex tasks may prove impossible to perform). For this reason something exists to make this process a lot easier. It's called a array and it is, as the name suggests, a whole row (array) of variables instead of just one. Arrays can be declared in a single line and you can make them contain as many variables as you need or you can even not specify a number at all and make the number of elements vary in the game, depending on how many you need. Using control structures (see next chapter) you can 'automate' the assigning and usage of values as well and hence make these operations very compact and simple as well in many cases. int_t values[10]; // max 10 integers/players can be stored in this array Reading and writing values from and to an array happens by making the name of an array followed by []-brackets with a index-number between the brackets. The index starts with 0 and the highest index is the length of an array minus one (if the length of an array is 3 then the indices are 0, 1 and 2). If no length was specified then there is no max to this. Reading a index that doesn't have a value yet is the same as reading a value from a regular variable: it will either default to 0 or give an error. Reading a value that is higher than the highest index allowed (or lower than the lowest index allowed (0)), the result will be an error (array index out of bounds). Examples: int_t i; HandleEvent(CreateCity) 'TestArray' post { // every time a city is foundedvalues[0] = 3; cities[i] = city[0]; // city[0] is city that was just created, see further Message(1, 'TestArray_M'); Messagebox 'TestArray_M' { // message
ARRAY_TEST "value[0]: {value[0]}, value[1]: {value[1]}, value[9]: {value[9]}, cities[0]: {cities[0]}, cities[1000]: {cities[1000]}, test: {test}." To find out the length of an array you can use the name of the array followed by ".#", so "values.#" in the previous example would return 10. The length of variable-sized arrays is determined by the last element that was assigned, namely that last element plus one (the one comes from the fact that 0 is a valid index as well). In the previous example the variable test would get the value 1001 (1000 + 1), even if there are actually only five spots that actually contain a city. The maximum number of cities this array could currently contain is 1001(indices 0 to 1000) and that's what the # refers to. (I'm assuming of course that there are less than 1001 cities founded, if there are more, variable i and hence the length of the array will get larger than 1001). 3.5 Built-in variablesWhat I've discussed up to now are user-defined variables: they are declared/defined by users (i.e. programmers). However, there is also a whole collection of other variables in the game, which the game engine itself uses to temporarily store all kinds of important variables (when exactly what values are assigned to what built-in variables is explained in chapter 7 and the appendices). This is extremely useful for us as programmers, because we can use these variables for our own purposes. These built-in variables, as they are called, are stored in the form of arrays and have fixed names. This means that you can't use these names elsewhere in the program, they're reserved especially for the built-in variables. You can however store values in these built-in variables like you would in any other array, but when you do this you risk overwriting existing values that may be important later on and you also risk having your values overwritten if the game engine needs the same variable you used. So generally it's not too good an idea to use these variables for storage yourself, unless you have little choice or are confident that there won't be any problems. --> LENGTH OF BUILT-IN ARRAYS, WHAT DOES THAT DO? <-- The names of all built-in variable arrays are: unit, city, player, army, location, advance, action, value, building, wonder, unitrecord, gold and good. Remember, these are arrays, so to access them, use unit[0], city[1], building[3], etc. The only non-array built-in variable is g (it stands for global and stores some info that is important at all times under all conditions, see appendices for details). These names are important to remember because they cannot be used as names for your own user-defined variables. If you need a unit variable (or array), call it aUnit or tmpUnit, but never just unit. 3.6 StructuresYou may have guessed that the built-in array unit contains units and that city contains cities and that value contains integers, but building and advance FE are new. These are standard datatypes in SLIC, just like city and unit but the main difference is that you can't declare user defined variables of this type. This may seem rather odd but it isn't really, since they can and are only used in very specific situations. What makes these variables so interesting is not that you can (or rather can't) use them as regular variables to store buildings or advances in. The reason why these extra datatypes exist is that all datatypes in SLIC (every built-in variable class represents one datatype in SLIC and every datatype in SLIC is represented by one built-in variable class (unit, city, advance, etc)) are so-called structures. They are more than just variables, every datatype has certain properties. A city has an owner, a size, a name, a happinesslevel, etc and every player has a certain number of units, a certain number of cities, a pollutionlevel, a capital, a leader, etc and a building has a name and a type. All these properties and many more are stored 'inside' the variables and can be accessed by the programmer by following the variable that stores some datatype with a dot and the name of the 'holder' of the property. FE city.population returns the size of a city, unit.owner returns the owner of a unit, building.name returns the building name, etc. Most of these properties are integers but some are booleans, cities or strings (see chapter 6). See the appendices for details on what all possibilities are for all unit types. Here is an example of how built-in variables can be used inside the game: HandleEvent(BattleAftermath) 'TestStructures' post {tmpArmy = army[0]; // an army from context One important note here is that players and integers are messed up. You can't put players in their own user-defined variable, like you can with cities or units, but since they are normally interchangeable with integers, you can treat them as such. So an integer variable can store players as well. However, a number isn't a structure, so you can't say tmpPlayer.cities if you want to get the number of cities a player has when that player is stored in the integer variable tmpPlayer. To access these properties of players, you must store them in a built-in variable first. So if tmpPlayer contains a player and you want the number of cities, do this: (I suspect the same principle goes for advances, buildings, etc). player[2] = tmpPlayer; Note that I used player[2] since player[2] isn't used very often by the game engine so it's not likely to overwrite any important value and it's not likely it will be overwritten itself anytime soon either. player[0] and player[1] are used much more often by the game engine so you have to be take that into account when you use those. --> WHAT HAPPENS IF player.capital.location(.x) IS USED? <-- --> CONSTANTS? <-- 4. Control Structures4.1 Non-linear executionAll the SLIC code you've seen so far is pretty straight-forward: within an eventhandler, lines are executed one by one, from top to bottom (of the eventhandler). This is called linear execution and it can be rather limiting: this way all instructions are always executed exactly one time (per time the eventhandler is called). Under some circumstances however, you may want to skip certain commands and execute others instead (FE when a player builds a city near water, create a unit; when a player builds a city away from water, create a building). Or maybe you want the execute the same (or very similar) commands 2, 3, 10 or even 100 times (FE create 4 barbarian archers). Maybe you want to execute the same piece of code over and over again until a certain condition is met (give a player random advances until the player obtains Masonry). With linear execution most of these things are impossible or at least very difficult to do. But as you may have guessed by now, something was created to take care of this: control structures. Control structures allow you to make your code non-linear: you can use if- and if/else-conditions to execute code only under certain conditions, use for-loops to execute code a certain number of times and while-loops to execute code until a certain condition is met. These are powerful structures that can be found back in almost every programming or scripting language including SLIC, so I will describe them here. Control structures are one of the few types of code in SLIC that require the usage of {}-brackets instead of ';', just like EventHandlers and Message/Alertboxes. 4.2 If-clausesAs mentioned above, the if-clause can be used to execute certain code only under certain conditions. Here is the complete syntax of the SLIC command: if (<condition>) {
The parts between []-brackets are all optional. The <code(_x)> parts can be any kind of code, either a single line or 2 or 10 or even 100 lines, as many you need. Just like 'regular' code that's outside any if-block, every line needs to be ended with ';' (I put the ';' behind <code> to stress this, so don't think it's necessary to end the last line of the code-block with two ';'s or anything). The <condition(_x)> parts represent boolean expressions, they can contain any kind of expression that returns either true (1) or false (0). Here are a bunch of examples of the simplest form of the if-clause, the form with the the necessary if-part and no else- or elseif-parts. This executes the code in the <code> block only if the <condition> is met and skips the <code> if the <condition> is not met. int_t i; HandleEvent(BeginTurn) 'TestIf' post { Messagebox 'TestIf_M' { // message
TEST_IF "i = {i} and j = {j}." These examples should be pretty much self-explanatory. What exactly the values of i and j are at any point doesn't really matter of course, the important thing is to see how the if-clauses work. A slightly more complex form of the if-clause is the if/else-clause: if the <condition> is true, the <code> block is executed, if it is false however, the <code_n> block gets executed. One of the two <code> blocks is always executed, but never both. This is useful because now if you need two if-clauses with one saying 'x > 4' and the other saying 'x <= 4', you can replace this with one if/else-clause. Examples: int_t i; HandleEvent(BeginTurn) 'TestIfElse' post {
The third form of the if-clause is the most complicated one: it involves elseifs. This means that there's not one condition but several. It works by evaluating the <condition> and if the result is true, executing <code> and moving on to the code after the whole if-structure. If the result is false, the <condition_1> is evaluated. If that's true, <code_1> is executed and if it's false, the <condtion_2> is evaluated and if needed the next <condition_x> and after that the next, etc. There can be as many elseifs plus conditions as you need and whether you end this structure with a general, unconditional, else or not (to take care of the situations where none of the conditions are true), is up to you. Remember that in this structure, only one of the code blocks is executed, never more. This means that if several of the given conditions are true, only the code of the one that is evaluated first (the topmost) will be executed and the other code blocks will be skipped. If this isn't desirable, you should use two seperate if-clauses in a row instead of one if/elseif-clause. Also, If there's no unconditional else, it's possible that none of the code blocks is executed, but that depends on what exactly the conditions are. Examples: int_t i; HandleEvent(BeginTurn) 'TestIfElseIf' post {
One last thing to keep in mind is that <code> blocks can contain any kind of code, so if-clauses within if-clauses are allowed as well (this is called nesting, the inner if-clauses are nested ifs). This can be done as often as you need it (meaning that an if within an if within an if within an if within an if is also allowed if you want/need to use it): int_t i; HandleEvent(BeginTurn) 'TestNestedIf' post {
4.3 While-loopThe while-loop is basicly one step up from an if-clause: instead of executing a code-block *if* a condition is true, a while-loop executes a code-block *while* a condition is true. So it repeats the same code over and over again as long as a certain condition is still true. This does mean that you'll have to make sure that the condition will actually become false at some point or the game will enter a so called infinite loop: the loop will never end and be executed endlessly until the game runs out of available resources and crashes. Let me first give the complete syntax of the while-loop, you'll be pleased to see that it's much simpler than the if-clause: while (<condition>) {
As you can see, there are no elses or elsewhiles to worry about, just plain old simple while. As you'll understand, the <condition> part represents any boolean expression and the <code> part represents any kind of code-block. Here too, every line must end with ';', just like regular code, and here too it's possible to create nested while-loops if needed. To end a loop you will normally put some kind of variable in the condition part of the while-loop and at some point change that variable in the code-block to such a value that the condition becomes false. You can do this directly FE by putting '(x > 4)' in the condition and changing x from 8 to 2 somewhere in the code-block (either immediately or incrementally - FE decrease it with one every time you execute the code) or you can do this indirectly FE by putting (player[0].units < 12) in the condition and creating new units in the code block (how this can be achieved, you can read in the next chapter and in the appendices). Examples: int_t i; HandleEvent(BeginTurn) 'TestWhile' post {
HandleEvent(BeginTurn) 'TestInfiniteLoop' post {
4.4 For-loopThe final control stucture that SLIC knows is actually redundant. The for-loop, although present in almost every programming and scripting lanuage, can easily be replaced by a while-loop plus a variable (though not all while-loops can be replaced with for-loops). There's no real logical reason for including it in SLIC, it's more of a sentimental thing: many programmers (including yours truly) simply prefer to use for over while when it's possible. The only solid argument for this preference I could think of is that everything that affects the condition-part of the for-loop is in a single line, so you can instantly see how often the code-block of the for-loop will be executed without have to look at the rest of the code. At least, if you program it properly. It is possible to affect the condition-part outside the regular line (the variable that forms the core of a for-loop is just a regular variable and can be changed like any variable), but this is not a smart thing to do since this would basicly cancel out the only advantage the for-loop has over the while-loop and because it would also make the code less readible (since the reader expects to find all related code on a single line). Generally the while-loop is used for decisionmaking (should this code-block be executed once more or should I continue?) and the for-loop for iterations (executing the code a certain number of times, where the number is determined in advance). Here is the complete syntax of the for loop: --> WHAT HAPPENS IF VARIABLE IS CHANGED OUTSIDE FOR LOOP <-- int_t <var>; and here is how you can do the same thing with while: int_t <var>; This may look more complicated then it is. <var> is the variable that ties the whole thing together, it is just a regular variable, just like any other. <val> is the starting value (or optionally a integer-variable containing a value), it determines what value <var> will have when the loop starts. <condition> is the same as the <condition> in if-clauses and while-loops and it uses <var> in some way (FE '<var> < 12' or '<var> >= player[0].units'). <var> = <expression> determines how <var> changes after every loop through the <code> block (FE '<var> = <var> + 1'). <code> is of course the same <code> block as the one in if and while. A few examples might clarify a few things: int_t i; HandleEvent(BeginTurn) 'TestFor' post {
As you can see in this example, the code that affects the condition part of the for-loop is on a single line (apart from the 'int_t i;') while the code that affects the condition of the while loop is spread out all over the code and takes up 4 lines. This example also shows how easy it is to repeat a certain piece of code x times: '(i = 0; i < x; i = i + 1)' is all you need. This kind of thing is exactly what for-loops do best: plain and simple looping through the same code a number of times. If it's more convenient to have i start at a non-zero value or to decrease it with 1 or to increase it with 5 or whatever, then that's no problem and you can replace various values with variables as well, but generally it's not a good idea to change any of these variables in the code-block itself, either directly or indirectly. In such a situation I would advise to use a while-loop, because in that case you're making decisions rather than iterations. It's of course also possible to use nested for-loops but keep in mind that the variable that ties the inner for-loop(s) together should be a different one from the outer for-loop(s). Here's a couple more examples: int_t i; HandleEvent(BeginTurn) 'TestFor' post {
4.5 Code layoutIf you write large parts of code with all kinds of these control structures with a lot of functions (see next chapter), declarations, expressions and who knows what more before, after and between them and in all kinds of combinations and nests, your code can become quite complex and unreadable. To prevent this most programmers in languages other similar to SLIC add spaces, tabs and endline characters (i.e. 'enters' or 'returns') to have a better overview. Some of these same habits can, in slightly adapted form, be used in SLIC as well. Note that none of these habits are rules that you *have* to follow, but they are much more guidelines that make the code a lot more readable and easier to understand, for yourself and others. The most important 'rule' when it comes to layout is to do use a consistent layout: no matter how you layout your code, do it in the same fashion all the time. Nothing is more distracting and confusing than to mix different styles of layout for the same piece of code. A very standardized form of layout in any programming language is the usage of tabs. Normally a tab (or a fixed number of spaces) is placed before each line after a line containing a '{'-sign. The line that contains the accompanying '}'-sign and all following lines don't have a tab preceding them anymore. If within this structure another '{'-sign occurs each line after that until the '}' is preceded by two tabs (or two times a certain number of spaces), etc. An exception to this rule (at least in SLIC) is that declarations of variables are usually placed on the previous 'tab-level' and a tab is by some people placed between the type and the name. So instead of HandleEvent(BeginTurn) 'Layout' post {
you'll often see HandleEvent(BeginTurn) 'Layout' post {
or HandleEvent(BeginTurn) 'Layout' post {
Practically this means that #include lines and variable declarations as well as the headers of eventhandlers, message- and alertboxes, triggers, etc. all have no preceding tabs and all the code inside these eventhandlers, triggers and messages have 1 tab (and more if there are conditions or loops in this code). Example: Not recommended: location_t myLocation; // give these a value elsewhere Recommended: location_t myLocation; // give these a value elsewhere HandleEvent(WakeUnit) 'TestTabs' post {
Another layout issue that's fairly standardized is where to use endline characters ('enters' or 'returns'). Generally every ';' is followed by a endline character and often '}' as well, though some programmers prefer to put else or elseif on the same line as the '}'. Note that all this is of course only if the line has no comments, if a line ends with a comment, the '//' that marks the start of the comment should be regarded as the endline character. This may seem fairly trivial but some programmers like to stuff several commands on a single line, esp. in small code blocks or if-clauses. When you have a large block of code that consists of very similar but not quite identical code (in such a way that loops can't shorten the code significantly), this could be a good idea (it would actually be more clearer than putting all code in seperate lines), but otherwise it's not. Example: Not recommended: location_t myLocation;location_t myLocation2; int_t y; HandleEvent(WakeUnit) 'TestTabs' post {
Recommended: (see above example of tab-usage) or int_t activePlayer; HandleEvent(BeginTurn) 'TestEnters' post {
Other layout issues to keep in mind are less standardized, so you'll have to decide for yourself which style you like best. One of these issues is when to use spaces: should it be 'x=x+1', 'x = x+1' or 'x = x + 1' or some other method? In theory you could even use tabs instead of spaces or endline characters (though I don't think that would make the code any clearer). Another example is whether is should be 'int_t x' (space) or 'int_t x' (tab). The use of regular or capitol letters is another example (is it 'eventHandler', 'EventHandler', 'Eventhandler' or 'eventhandler'?). Yet another example is the usage of tabs and brackets: should it be 'if(...' (no space) or 'if (...' (space), 'Message(' or 'Message (...', 'EventHandler(...' or 'EventHandler (...'. Personally, I use the option with the space-option for control structures and the non-space options for functions (like Message) and eventhandlers. One could argue that it might be better to always use the non-space option but I don't do that, that's just not my style... One last example I'd like to mention is that there are two conventional ways of using the '{' sign. Some people put it on the same line as the structure they belong to, others place it right under that line (without putting a tab in front of it). Both methods are conventional styles and which one you use doesn't matter, as long as you use it consistently. The first method has as main advantage that more code fits on a page/screen (and IMHO it looks better) and the second example has as main advantage that it makes it easier to spot missing '{' or '}' (you won't believe how often these are accidentally ommited). Example: if (x == 3) {if (x == 3) 5. Events and Built-in Functions5.1 The engine of SLICThis chapter is really the keychapter of this book. The subjects of this chapter are absolutely key to SLIC. If SLIC was a car, then built-in functions and events (together with built-in variables) form the engine of SLIC. So why didn't I start with explaining them in chapter 1? Well, just like with the car: an engine without a car is absolutely useless, but a car without an engine still has some uses (FE as a storage place, a shelter or a museum piece). You may not be able to from A to B with a car without an engine but you can't do anything with an engine without a car, so before you can understand how an engine works, you must know something about the rest of the car: the wheels, the steering wheel, the fuel (in case it wasn't obvious already: no, I don't know much about cars). The chairs and radio are less vital so you can explain those later (next few chapter will deal with the SLIC equivilants of those), but you can't do much without understanding some basic things first. SLIC without fucntions is fairly pointless. The best evidence of this is that in the previous chapters I tried to use as little functions in my examples as possible, but doing without functions altogether made some of the examples so trivial or difficult that I was forced to use some functions after all. However, you can't use functions or events in SLIC without first understanding variables and control structures (though details of messageboxes and strings can wait until later). 5.1 FunctionsThe truth is however, there isn't all that much to tell about functions. They're extremely important for SLIC but their principle is also so simple I don't know exactly what to tell about them. Built-in functions are pieces of code in the game-engine that you can call from a SLIC script by referring to them by their name and that will perform some action for you on the gamestate. They will read a certain value (just like built-in variables), check if certain conditions are true (as sort of a built-in if-clause) or actually change the gamestate for you. They can create units, kill players, give or take gold, resize cities, etc. etc. Some of the perform more 'domestic' functions: call message- or alertboxes, check validity of certain variables, 'force' a certain type of variable into another (FE obtain a city from a location, presuming there is one at that location), exit the game, etc. To refine your calls to these built-in blocks of code, you can (or rather must) pass on some arguments: a number, a unit, a location, etc, any kind of variable and in some cases even a (quoted or unquoted) word, like the name of a unit-type or a message. In one case this argument represents the unit that needs to be killed, in another case it's the type of the unit that needs to be created or the location where the presence of a trade good should be checked. The appendices contain elaborate lists with all the functions that are available, what they do and what arguments they require. A function should be called by using its name and adding the arguments after the name in brackets, just like the Message function you already saw a couple of dozen times by now. Examples (all functions have been commented): HandleEvent(BeginTurn) 'TestFunctions' post {i = Cities(tmpPlayer); // give the number of cities of the current player TEST_FUNCTIONS "At this location, ({location[2].x}, { location[2].y}) a unit will be created if an city exists here; i = {value[3]} and j = {value[3]}. Active player is {player[0].civ_name_singular}." 5.1.1 Return typesAs I already explained and as you can see in the examples, some functions perform an action (like CreateUnit), some check a condition (like CityIsValid) and some return a value (like Cities). Usually an indication of what type they are is given by their type. There are three types or functions: void, int and bool functions (although the official Activision documentation treats bool and int as identical, which they basicly are since a boolean in SLIC is really just an integer). Functions that perform a function (/an action) usually don't return a value, which makes them void. Functions that check conditions return a boolean value (true or false) and are thus bool and functions that return a value are usually int, since they return a integer. Exceptions are that some functions perform a certain action but aren't void bool. These functions return a boolean value that indicates if the operation they should have performed was successful or not. GetNeighbor is an example of this. Another exception are functions that return a unit or location or whatever as variable: since SLIC can't make functions return values other than integers, these functions are either void or bool and return their value in the form of an argument: one of the arguments isn't needed to perform the function but is used to put a value in after the execution the function. FE GetCityByIndex needs 3 variables: a player, an index and a city variable. The city variable isn't used in executing the function but after the execution this variable will contain the result of the operation: the city of the given player with the given index. And it's a rarity, but there are even those functions that return an integer and perform an action at the same time as well, like GetNearestWater, which puts the nearest ocean tile in a variable but also returns the distance to this variable. --> DOES GETNEARESTWATER STILL EXIST IN SLIC2? <-- One last remark on functions: some functions only work when used in messages, which is explained in detail in the next chapter. However, if you start experimenting with functions yourself, you should keep an eye on this (they are marked in both my appendices and in the official Activision documentation) since it won't be easy to figure out why certain functions don't work when you try them out and you don't know this. 5.2 EventsEvents are more than just the engine of SLIC, events are even more important. Their use is twofold: (1) they can be generated which basicly means that they are used as if they were functions (and form the 'engine' of SLIC) and (2) they can be used as triggers for blocks of SLIC code: all SLIC code is divided in blocks (eventhandlers, triggers, message- and alertboxes) and these blocks are only executed when something 'triggers' them. In the case of eventhandlers, these triggers are events (hence the name eventhandler: they handle the correct processing of events by executing SLIC code). In this second usage events are even more important than functions: the options of executing SLIC code without eventhandlers and events are extremely limited and without this usage of events SLIC would be (almost) completely useless. The best example of this is that I've been able to give lots of examples of SLIC code in the previous chapters to demonstrate things like variables and arithmatics without using functions (or events used as functions), but I haven't been able to give even a single example that didn't use events to trigger the actual example code (it's possible to do so, but that would force me to use much more exotic methods of triggering code, which I didn't find very desirable either). 5.2.1 Generating eventsGenerating an event means that you more or less force an event to happen. So if you generate a CreateUnit event, a unit will be created and if you generate a GrantAdvance event, a advance will be given to a player (the details of what type of unit and what advance is given to what player are taken care of in the arguments). So basicly generating an event is very similar to calling a function. The syntax is very similar and the effects of events are identical to or comparable with the effects of most functions (though events are still different enough to add a whole new layer of functionality to the game). As I mentioned, the syntax for generating events is very similar to that of calling functions: Event:<eventname>(<arguments>); A complete list of all events in the game and the variables they require can be found in the appendices. Examples: HandleEvent(BeginTurn) 'TestEventGeneration' post { if (tmpPlayer > 1) { // usually player 0 is barbarian, player 1 is human
HandleEvent(SleepUnit) 'TestTiming' post { while (GetUnitsInCell(tmpUnit2.location)) > 0) { // while still units at this location
5.2.2 Restrictions & timingThere are a few things to keep in mind when generating events. First of all, not all events can be generated. Some events require special types of arguments that don't exist in SLIC and can thus not be given to events. Arguments like this are marked with GEA_ in front of their name. Example: the MoveOrder event requires (among others) as argument a move path. But a move path is a complex object that simply doesn't exist in SLIC, so this event can only be used to trigger code with and not to be generated. Another type of event that can't be generated are those events that FE are part of the turn sequence. When an AI player has his turn, a whole list of actions need to be performed: visibility needs to be updated, production points need to be added, unit orders need to be processed, etc. All these actions have events tied to them (like UnitBeginTurnVision or CityBeginTurnVision) that can be used to trigger certain events on. However, generating these events outside the regular turn-sequence will either not work at all or cause all kinds of weird results, the game is simply not designed to add production points to the things cities are building while the human player is giving his units orders, so this should normally not be done either. The third kind of event to keep an eye on are those events that are simply obsolete and don't do anything useful anymore. I don't know exactly which events there are but one should assume they exist, so if certain events have little effect when generated, you should keep in mind that this event might be obsolete or broken (the same goes for functions and variables and events as triggers BTW). Another very important thing to keep in mind when generating events is timing. When a function is called, it will be executed immediately, so it's effects on the game are immediately noticable and can be taken into account in the rest of the code. FE, you could have some code that performs a loop as long as there are less than x units in some location. This works perfectly if units at that location are created using the function CreateUnit: the units will be created instantly and the execution of the code will not continue before the unit has been created, so the next time you evaluate the number of units at that location it will be higher than before. With events this would go horribly wrong: events are not executed immediately when they are generated, but rather put in a queue and executed one-by-one. An event isn't executed until all other events that have been added to the queue before this event have been completely executed. In this case 'completely' includes the SLIC code: a event isn't 'completely' executed until the event has been processed by the game-engine AND all the SLIC code tied to this events (through eventhandlers) has finished executing. Events are executed 'asynchronously', which in practice means that you have no control over when they will execute: it might be right after the current event has been executed, but there may also be 20 other events that come first. Whenever you generate events in your code, you should be very well aware of this, since it is very, very easy to make mistakes in this area: at the time of writing this, only a couple of thousand lines of code have been written by fans (hopefully when you read this the amount of existing SLIC code will be a multitude of this), but already dozens of mistakes have been made due to not realizing this (and I have to admit many of these mistakes were made by me). |
|
| This site is currently maintained by: BureauBert (Webmaster) Maquiladora (Manager) Legal Notice: Legal Notice Statistics: 307170 unique users since 02.05.2004 (» Site Statistics) Hosted by: WebhostOne |