Jump to content

Photo

Advanced scripting tutorial (WIP)


  • Please log in to reply
44 replies to this topic

#1 Saffith

Saffith

    IPv7 user

  • Members

Posted 28 March 2009 - 04:18 PM

This has been completed and added to the tutorial section. This version may be out of date. The latest version is here.


This tutorial covers the last few major features of ZScript. These are the most difficult aspects of the language to deal with; the concepts are harder, and there are more opportunities to make mistakes. The limitations and quirks of the language will also become greater obstacles.

If you aren't using it already, you should enable the quest rule "Log Script Errors To Allegro.log" and check for errors after running scripts. There are a lot of mistakes you can make here, and you may not even notice you have without seeing these error messages.


 

User-defined pointers


To interact with most types of objects in the game, you'll need to define your own pointers. Declaring a pointer is much like declaring any other variable, except, of course, that the data type is different.
 
npc enemy; // An npc (enemy) pointer
ffc anFFC; // An ffc pointer
There is an important difference from numeric and Boolean variables, however: pointers cannot be global.
 
lweapon globalWpn; // ERROR

ffc script Blah
{
    void run()
    {
        lweapon localWpn; // OK
    }
}
Here are the different pointer types and what they represent:
  • link - Represents Link (Link is the only pointer of this type)
  • screen - Represents the current screen (Screen is the only pointer of this type)
  • game - Represents the game (Game is the only pointer of this type)
  • ffc - Freeform combos
  • npc - Enemies, guys, and fairies
  • lweapon - Any of Link's weapons on the screen (including things like explosions and sparkles)
  • eweapon - Any enemy weapons on the screen
  • item - Any items on the screen
  • itemdata - The definition of an item
Note the capitalization in the first three. Link (capitalized) is a pointer whose type is link (uncapitalized). You cannot declare pointers of type link, screen, or game, nor can you take them as function arguments.

Pointers are declared the same way as numeric and Boolean variables, but the options for initializing them are more limited. To set a number, you can simply enter the number, like int x = 5;. With pointers, you can't do that; there's no way to enter an item directly in text. Instead, you must either use a function that returns the correct pointer type or another pointer of the same type.
 
npc enemy = Screen->LoadNPC(1); // Load enemy #1
npc enemy2 = enemy; // enemy and enemy2 point to the same npc
enemy = Screen->LoadNPC(5); // Now enemy points to npc #5; enemy2 still points to #1
By now, you may have gotten used to counting from 0, but pointer types don't work that way. If there are three items on the screen, they're numbered 1 to 3, not 0 to 2.
 
item it = Screen->LoadItem(0); // This will never load a valid item

if(Screen->NumItems() == 5)
{
    it = Screen->LoadItem(5); // This is fine; there are 5 items, so 5 is valid
}
Keep in mind that pointers refer to objects in the game. All pointers to a given object will see changes made to it.
 
npc en = Screen->LoadNPC(3);
npc en2 = en;
en->HP = 10; // Now en2->HP is also 10, because it's the same enemy
Similarly, when passing pointers as arguments to functions, the pointer will refer to the same object afterward, but its properties may have changed.
 
ffc script EnemyExample
{
    void run()
    {
        npc en = Screen->LoadNPC(3);
        ModifyEnemy(en);
        // After that function call, en still points to npc #3, but it now has 10 HP.
    }
    
    void ModifyEnemy(npc en)
    {
        en->HP = 10;
        en = Screen->LoadNPC(5);
    }
}
So far, this is all pretty straightforward. Here's where it gets tricky: in most cases, the objects these pointers represent are transient. There aren't always any enemies, items, or weapons present, and when there are, they eventually go away. What happens if you try to load item #5, but there is no item #5? If you've got a pointer to an enemy, what happens when the enemy dies? When using a pointer, you need to be sure it's valid - that is, that it refers to an object that actually exists.

Before using a Screen->Load* function, you need to make sure the object you want is really there.
 
item it = Screen->LoadItem(5); // If there aren't 5 items onscreen, the pointer won't be valid

npc enemy;
if(Screen->NumNPCs() >= 3)
{
    enemy = Screen->LoadNPC(3); // This is fine - there are at least 3 npcs, so npc #3 exists
}

// It's also okay to load first and validate later
lweapon lw = Screen->LoadLWeapon(10);
if(!lw->isValid())
    Quit(); // No such weapon
If a pointer is valid, it is guaranteed to remain valid until the next Waitframe() or Waitdraw(). After that, the object might be deleted, so you have to check its isValid() method to make sure it's still there.
 
item it;
if(Screen->NumItems() >= 5)
{
    it = Screen->LoadItem(5); // OK - there is an item #5
}

it->X = 80; // Still OK
Waitframe();
it->Y = 32; // Not safe - Link may have just picked up the item, making the pointer invalid

if(it->isValid())
{
    it->Y = 32; // OK
}

// A valid pointer will stay valid at least until the next Waitframe() or Waitdraw(),
// no matter what you do to it.

eweapon ew;
if(Screen->NumEWeapons() >= 1)
{
    ew = Screen->LoadEWeapon(1);
}

ew->DeadState = WDS_DEAD; // This tells the weapon to be deleted...
ew->X = 80;  // but it won't actually happen until Waitframe() is reached,
ew->Y = 128; // so you can safely use the pointer until then.
You don't just have to load existing objects; you can also create new ones. There is a limit to how many of each type of object can exist, however, which means it's possible the creation function will fail and return an invalid pointer. The limits are high enough that you can safely ignore them most of the time, but it may be worth checking in extreme cases.
 
// If there are 256 eweapons onscreen already, this will fail.
eweapon wpn = Screen->CreateEWeapon(EW_SCRIPT1);

if(!wpn->isValid())
{
    // Must be a bullet hell boss
    Quit();
}
One last, non-obvious validation issue. Let's say you've got an npc pointer, and you want to know when the enemy dies. You'll consider the enemy dead as soon as its HP hits 0, but the pointer will still be valid for several frames as it blinks out of existence. However, it's also possible that another script will kill the enemy by moving it far offscreen, in which case the pointer will suddenly become invalid without the enemy's HP changing. In order to determine if the enemy has died, then, you have to check both its HP and whether the pointer is valid. How do you do that? Your first thought might be something like this:
 
if(!enemy->isValid() || enemy->HP <= 0)
That seems logical, but there's a problem with it. ZC will always evaluate the whole condition. If isValid() returns false, the game will still check enemy->HP, making use of the invalid pointer.

The best way to do it is with a function that checks each possibility separately.
 
bool IsDead(npc en)
{
    if(!en->isValid())
        return true;
    if(en->HP <= 0)
        return true;
    return false;
}

// Elsewhere...
if(IsDead(enemy))
All this validation may look incredibly tedious, but it's really not that bad. It's only needed when loading or creating objects and after Waitframe() or Waitdraw(). Even in large scripts, this is only a few checks.

In the cases where a lot of repetitious validation is needed, you can often use functions to minimize the burden. For instance, if you're writing a script that uses Waitframe() in several places, you might make a function like this:
 
void MyWaitframe(ffc this, npc enemy)
{
    Waitframe();
    if(!enemy->isValid())
    {
        this->Data = 0;
        Quit();
    }
}
Use MyWaitframe() instead of Waitframe(), and you'll never have to worry about your pointer's validity again.

After all these warnings about validating your pointers, you're probably wondering just what happens if you use an invalid pointer. The answer is: nothing much. ZC will detect the problem, give you some meaningless number (probably -1) if you tried to read a variable, and report an error in allegro.log. The main concern is that your script probably won't work correctly if you don't know when your pointers are valid. And, of course, you don't want people using your scripts to find a lot of errors logged as a result.

ffc and itemdata pointers are exceptions to all of the above. There are always 32 FFCs on the screen, numbered 1-32. Even if they're blank, they're there. An itemdata pointer represents the definition of an item, which isn't even an object in the game. These two can be loaded without checking for their existence, and they don't even have isValid() functions.
 
ffc anFFC = Screen->LoadFFC(16); // OK - FFC #16 always exists
itemdata id = Game->LoadItemData(27); // OK - ItemData #27 always exists
With pointers that do represent onscreen objects, the numbers used to load them depend on the order in which they were added to the screen, and those numbers change when lower-numbered objects are removed.
 
eweapon wpn1;
eweapon wpn2;

wpn1 = Screen->LoadEWeapon(5);
Waitframe();
wpn2 = Screen->LoadEWeapon(5);
// wpn1 and wpn2 may not point to the same eweapon. If any of eweapons 1-5 were
// removed during the Waitframe(), a different eweapon will be in slot #5.
Pointers can be compared with the == and != operators. These check whether the two pointers refer to the same object.
 
lweapon wpn1 = Screen->LoadLWeapon(1);
lweapon wpn2 = Screen->LoadLWeapon(2);

if(wpn1 == wpn2) // This will evaluate to false, even if the weapons are identical
// ...

wpn2 = wpn1;
if(wpn1 == wpn2) // Now it will be true
That'll do it for this section. A few sample scripts...
// This script randomizes the HP of every enemy on the screen

import "std.zh"

item script EnemyHPRandomizer
{
    void run()
    {
        npc enemy;
        
        // This is a very common way of iterating over every enemy on the screen
        for(int counter = 1; counter <= Screen->NumNPCs(); counter++)
        {
            enemy = Screen->LoadNPC(counter);
            enemy->HP = Rand(1, 20);
        }
    }
}
// This item script fires a fast-moving arrow in the direction Link is facing.

import "std.zh"

item script Arrow
{
    void run()
    {
        lweapon arrow = Screen->CreateLWeapon(LW_ARROW);
        
        // Again, you can usually skip the validity check with CreateLWeapon(),
        // but it certainly doesn't hurt to make sure.
        if(!arrow->isValid())
        {
            Quit();
        }
        
        arrow->X = Link->X;
        arrow->Y = Link->Y;
        arrow->Dir = Link->Dir;
        arrow->Step = 500;
        arrow->Damage = 4;
        
        // Adjust the arrow's tile for the direction
        // Up is default, so it's skipped
        if(arrow->Dir == DIR_DOWN)
        {
            arrow->Flip = 2;
        }
        else if(arrow->Dir == DIR_LEFT)
        {
            arrow->Tile += 1;
            arrow->Flip = 1;
        }
        else if(arrow->Dir == DIR_RIGHT)
        {
            arrow->Tile += 1;
        }
    }
}
//This script makes the FFC follow Link around and removes any enemy weapons
// that get close to him. The FFC should be 2x2 tiles.

import "std.zh"

ffc script Barrier
{
    void run()
    {
        eweapon wpn;
        
        while(true)
        {
            this->X = Link->X - 8;
            this->Y = Link->Y - 8;
            
            // Cycle through every enemy weapon on the screen
            for(int counter = 1; counter <= Screen->NumEWeapons(); counter++)
            {
                wpn = Screen->LoadEWeapon(counter);
                
                if(Distance(Link->X, Link->Y, wpn->X, wpn->Y) < 20)
                {
                    wpn->DeadState = WDS_DEAD;
                }
            }
            
            Waitframe();
        }
    }
}
You can probably think of plenty of things to do for practice, but here are some suggestions anyway.
  • Write a script that spawns enemies at regular intervals if there aren't too many onscreen already.
    Spoiler
  • Modify the Barrier script above so that it protects an enemy instead of Link. Make sure it disappears after the enemy dies.
    Hint

    Spoiler
  • Write an FFC script that causes an arrow to leave a fire when it disappears.
    Hint

    Spoiler
How might that be useful? Try loading it into slot 1 and using this as the arrow item's action script.
 
item script RunFireArrow
{
    void run()
    {
        ffc scriptRunner = Screen->LoadFFC(1);
        scriptRunner->Data = 1; // Combo 1 should be invisible
        scriptRunner->Script = 1;
    }
}


#2 Christian

Christian

    Summoner

  • Members
  • Real Name:Chris
  • Location:New Jersey

Posted 09 May 2009 - 07:30 PM

any advance on this saffith?

#3 Saffith

Saffith

    IPv7 user

  • Members

Posted 08 October 2009 - 10:03 AM

User-defined arrays


If you need to keep a bunch of data in a form that can be handled in a loop, or if you just have a whole lot of data with the same basic meaning, you should store it in an array. Declaring an array differs from declaring a variable only in that you normally must specify the size.
 
int intArray[5]; // Creates an array of five ints, intArray[0] - intArray[4]
eweapon ewArray[3]; // An array of three eweapons, ewArray[0] - ewArray[2]

// An array can be huge. The limit is 214747 elements, the largest number
// ZScript can handle.
int bigArray[100000]; // This is perfectly fine

// The size must be a simple integer.
int anArray[x]; // ERROR: Variables can't be used
int anotherArray[ARRAY_SIZE]; // ERROR: Constants can't be used
int yetAnotherArray[5+5]; // ERROR: Arithmetic can't be used
Number and bool arrays can be global, but pointer arrays can't.
 
int intArrayGlobal[10]; // OK
lweapon lweaponArrayGlobal[5]; // ERROR

ffc script Example
{
    void run()
    {
        int intArrayLocal[5]; // OK
        npc npcArrayLocal[10]; // OK
    }
}
To initialize an array, put the initial values in braces, separated by commas.
 
int intArray[5] = { 1, 2, 3, 4, 5 }; // Creates intArray and sets array2[0]=1, array[1]=2, etc.

// This works for pointers, as well.
eweapon ewArray[3] = { Screen->LoadEWeapon(3), Screen->LoadEWeapon(4), Screen->LoadEWeapon(5) };

// You don't have to initialize every element.
int piArray[10] = { 3, 1, 4, 1, 5 }; // Elements 0-4 will be initialized to these numbers; 5-9 will default to 0

// But don't try to initialize more elements than are in the array.
// The compiler won't catch it, but it clearly doesn't make sense.
int badArray[3] = { 5, 4, 3, 2, 1 }; // There aren't five elements in the array

// Using an array initializer allows you to leave out the array size. The size
// will be the same as the number of elements it's initialized with.
int autoIntArray[] = { 0, 0, 0, 0 }; // This one will have four elements

// This is only valid for initializing the array. You can't do it later on.
int intArray2[5];
intArray2 = { 6, 7, 8, 9, 10 }; // ERROR
Here's the first tricky part. After declaring int myArray[10], you should think of myArray (with no brackets) as a pointer to an array rather than a regular variable. However, ZScript doesn't see it that way. To the compiler, there's no difference between an int[] and an int.
 
int myArray[10];

// As usual, you should assign data to the individual elements...
myArray[0] = 1;
myArray[1] = 5;

// Be careful - assigning a value to the array pointer is legal!
myArray = 20;
Assigning a new value to an array pointer changes what array it points to. If you're lucky, the pointer will become invalid, the game will hang, and you'll find a ton of errors in allegro.log. If you're not so lucky, the array pointer will refer to a different array, and your script will completely break with no indication as to why. Be very careful not to do this; it can be one of the hardest mistakes to find.

What if you want to pass an array as an argument to a function? The compiler sees an int array pointer as a regular int, so that's how you should handle it. Passing an array pointer is the same as passing a single variable.
 
ffc script ArrayArgs
{
    void run()
    {
        int idArray[5] = { 20, 21, 21, 32, 32 };
        npc enemyArray[5];
        
        CreateNPCs(idArray, enemyArray, 5);
    }
    
    void CreateNPCs(int ids, npc enemies, int arraySize)
    {
        for(int counter = 0; counter < arraySize; counter++)
        {
            enemies[counter] = Screen->CreateNPC(ids[counter]);
        }
    }
}
Notice that there's no indication in the function header that the arguments should be arrays. Be sure to make it clear in any documentation you write, as well as any size requirements. zscript.txt and std.txt identify array arguments by adding brackets after the argument name.

Related to that, here's another possible pitfall. What if you want a function to create an array and return a pointer to it?
 
ffc script Example
{
    void run()
    {
        int anArray = MakeArray();
        Trace(anArray[0]);
    }
    
    int MakeArray()
    {
        int array[] = { 5, 4, 3, 2, 1 };
        return array;
    }
}
What does that Trace() print? If you guessed 5, you're wrong:
 
Invalid pointer (1) passed to array (don't change the values of your array pointers)
-1.0000
Why? Because there's no array there anymore. If an array is declared in a function, it is deleted when that function ends, making any pointers to the array invalid. Declaring an array in a function and returning a pointer to it simply doesn't work.

You cannot compare arrays using == and !=. Like pointers, these will tell you if the two array pointers refer to the same array, not whether the arrays are identical. If you want to compare two arrays, you'll need a function that goes over each element individually.
 
bool ArraysEqual(int arr1, int arr2)
{
    int arraySize = SizeOfArray(arr1);
    int index;
    
    if(SizeOfArray(arr2) != arraySize)
    {
        return false;
    }
    
    for(index = 0; index < arraySize; index++)
    {
        if(arr1[index] != arr2[index])
        {
            return false;
        }
    }
    
    return true;
}

// Elsewhere...
if(ArraysEqual(anArray, anotherArray))
{
    // They match
}
If you want to know the size of an array, you can use the SizeOfArray() function, as seen above - but only if it's an array of numbers. The function isn't defined for arrays of bools or pointers. In those cases, you can pass the size of an array as an argument to any function that needs to know it. Alternatively, you could use a constant for that purpose. While you can't use a constant to set the size when declaring an array, it's often a good idea to use a matching constant for the size everywhere else.

Global arrays are stored in save data. The array pointers count toward the 255 global variable limit, but there is no limit on the size of the arrays. Changing the size of a global array invalidates existing saves.

One last, minor detail: the term "array pointer" is commonly used to mean two different things. It usually means a pointer to an array, but it can also mean a number that indicates the index within an array. This originates from C/C++, where the distinction isn't as clear.

Some sample scripts...
 
// This will create four fireballs that circle around Link. They will be
// replaced whenever they disappear.

import "std.zh"

const int FB_RADIUS = 24;
const float FB_SPEED = 2.5;
const int FB_SPRITE = 17;
const int FB_DAMAGE = 2;

ffc script FireballBarrier
{
    void run()
    {
        lweapon fireballs[4];
        float baseAngle;
        float angle;
        
        // Each fireball will be recreated any time its pointer isn't valid,
        // so there's no reason to set them up in advance.
        
        for(baseAngle = 0; true; baseAngle = (baseAngle + FB_SPEED) % 360)
        {
            for(int index = 0; index < 4; index++)
            {
                if(!fireballs[index]->isValid())
                {
                    fireballs[index] = Screen->CreateLWeapon(LW_SCRIPT1);
                    fireballs[index]->UseSprite(FB_SPRITE);
                    fireballs[index]->Damage = FB_DAMAGE;
                }
                
                // Each fireball is offset by 90 degrees from the last one
                angle = baseAngle + 90 * index;
                fireballs[index]->X = Link->X + (FB_RADIUS * Cos(angle));
                fireballs[index]->Y = Link->Y + (FB_RADIUS * Sin(angle));
            }
            
            Waitframe();
        }
    }
}
// This script creates a simple snow effect.

import "std.zh"

const int NUM_SNOWFLAKES = 80;
const int COLOR_WHITE = 1;

global script Snow
{
    void run()
    {
        int snowflakeX[80];  // X position of each snowflake
        int snowflakeY[80];  // Y position
        int snowflakeVx[80]; // X velocity
        int snowflakeVy[80]; // Y velocity
        
        // Randomize their positions and velocities
        for(int sf = 0; sf < NUM_SNOWFLAKES; sf++)
        {
            snowflakeX[sf] = Rand(256);
            snowflakeY[sf] = Rand(176);
            snowflakeVx[sf] = Randf(-0.2, 0.2);
            snowflakeVy[sf] = Randf(0.5, 1.0);
        }
        
        while(true)
        {
            // This is a very simple effect. Each snowflake moves in a straight line
            // and wraps around the edges of the screen.
            for(int sf = 0; sf < NUM_SNOWFLAKES; sf++)
            {
                snowflakeX[sf] += snowflakeVx[sf];
                snowflakeY[sf] += snowflakeVy[sf];
                
                if(snowflakeY[sf] > 175) // Fell off the bottom
                    snowflakeY[sf] = 0;
                
                if(snowflakeX[sf] < 0) // Off the left
                    snowflakeX[sf] = 255;
                else if(snowflakeX[sf] > 255) // Off the right
                    snowflakeX[sf] = 0;
                
                Screen->PutPixel(6, snowflakeX[sf], snowflakeY[sf], COLOR_WHITE, 0, 0, 0, OP_OPAQUE);
            }
            
            Waitframe();
        }
    }
}
And some practice exercises.
  • Create an FFC script that makes the first enemy invincible until all the others are defeated. Do this by setting all its defenses to NPCDT_BLOCK and restoring them later.
    Spoiler
  • Find all instances of flag CF_SCRIPT1 on the screen. When every combo with that flag changes to a different one (watch Screen->ComboD[]), trigger secrets.
    Hint

    Spoiler


Strings


If you want to do any sort of text processing, or if you want print text to the screen (other than with Screen->Message()) or allegro.log, you need to use strings. A string is a special int array used to represent text.

Strings are normally initialized with text in quotation marks instead of values in curly braces.
 
int hello[10] = "Hello"; // It's not uncommon to make string arrays a bit larger than necessary
int str[] = "This is some text!"; // But it's much more common to leave out the size entirely

// Just like initializing an array with braces, this can only be done as initialization;
// you can't set a string this way later.
int anotherString[20];
anotherString = "Words words words"; // ERROR
Just like a regular array, you can pass the string pointer into functions. Several functions use this to load a string into an array.
 
int saveName[9];
Game->GetSaveName(saveName);
// saveName now contains the name of the save file
You can pass the string pointer to TraceS() to log some text to allegro.log.
 
int logString[] = "Logging...";
TraceS(logString);
TraceNL();
// Afterward, you'll find a line in allegro.log that says "Logging..."
That's all simple enough. But how about this?
 
int hello[5] = "Hello";
TraceS(hello);
TraceNL();
If you do that, here's what you'll see in allegro.log:
 
Invalid index (5) to local array of size 5
Invalid index (5) to local array of size 5
Hello
Why? Because the array is too small. The string "Hello" actually requires six characters.

Strings in ZScript are null-terminated. That means that after all the text characters, there's a 0 marking the end of the string. This 0 is often called the null terminator. This is why Game->GetSaveName() requires an array of length 9, even though there are only 8 characters in the name.
 
int hello[] = "Hello";
Trace(SizeOfArray(hello)); // Traces 6
Trace(hello[5]); // Traces 0
Functions that read strings depend on the null terminator to know when they've reached the end of a string; if it's not there, they'll behave incorrectly. In most cases, they'll hang the game and log thousands of errors to allegro.log.

Because of the terminator, the length of a string and the size of the array are never the same. The length of the string is the number of characters before the terminator, which can be anywhere from 0 to one less than the array size. If there are multiple zeroes in an array, the string ends at the first one.
 
int abcStr[] = "ABCDEFG";
abcStr[3] = 0; // Replace D with 0, ending the string there
TraceS(abcStr);
TraceNL();
// allegro.log will contain the line "ABC"
Since a string is conceptually made up of text characters rather than numbers, it's worth knowing that you can signify a text character in code. Just put the character in single quotes.
 
int str[]="String!";
str[0] = 'a';
TraceS(str); // This will trace "atring!"

// This only works with a single character.
std[2] = 'bc'; // ERROR

// You can use this for comparisons, too.
if(str[1] == 'A') // It's case-sensitive - 'a' and 'A' are different

// You can initialize a string this way, if you like.
// Put the commas outside of the '' and be sure not to forget the terminator.
int str2[] = { 'L', 'i', 'n', 'k', 0 };

// You can even use thie for regular numbers.
int num = 'Z'; // That's 90
But an array really is just a string of numbers, and you're free to use it as such. There's rarely any reason to do so, however.
 
int hello[] = "Hello";
for(int i = 0; i < 6; i++)
{
    Trace(hello[i]);
}

// Allegro.log will contain this:
// 72.0000
// 101.0000
// 108.0000
// 108.0000
// 111.0000
// 0.0000

hello[0] += 2;
TraceS(hello); // This will trace "Jello"

// It works both ways:
int str[] = { 90, 101, 108, 100, 97, 0 };
TraceS(str); // This will trace "Zelda"
These numbers are the ASCII values of the characters. All numbers in the range 32-255 can be used as text characters, but characters above 126 vary depending on the font.

Working with strings often requires the use of string.zh, which defines many functions for manipulating and interpreting strings. These functions mostly come from C, so their names look quite different than those of most ZScript functions. A couple of functions in string.zh are worth singling out here.

There are several functions for converting between numbers and strings.
 
//atof(): ASCII to float
int numStr[] = "12.3456";
float num = atof(numStr);
// num is now 12.3456
strcmp() is used to compare two strings, but it doesn't work the way you might expect.


int string1[] = "A string";
int string2[] = "A string";

if(strcmp(string1, string2) == 0)
{
    // Strings match
}
strcmp() returns a positive number if string1 is greater, a negative number if string2 is greater, and 0 if they're equal. What does it mean for a one string to be greater than another? Compare the two strings one character at a time. At the first character that's different, whichever string has the lesser ASCII value in that position is the lesser string. For instance, "ABC" is less than "ABZ". "ABC" is also less than "ABCD", because the first differing character is the null terminator (0) in "ABC".

Finally, the most complex functions, but also perhaps the most important, are printf() and sprintf(). These are string formatting functions. printf() writes its output to allegro.log; sprintf() stores it into another array.


// Let's say you want to write some debug information to allegro.log.
// printf() is perfect for that.
int format[] = "Link's position: %d, %d\nLocation: DMap %d, screen %X\n";
printf(format, Link->X, Link->Y, Game->GetCurDMap(), Game->GetCurDMapScreen());

// This will print something like:
// Link's position: 80, 45
// Location: DMap 2, screen 0x2B

// If you want to write the same thing to the screen, you'd use sprintf().
int line1Format[] = "Link's position: %d, %d";
int line2Format[] = "Location: DMap %d, screen %X";
int line1Buffer[128]; // Make sure your output buffer is large enough for the final string!
int line2Buffer[128];

while(true)
{
    // WARNING: There is a bug in sprintf() in 2.50.0. The output array must
    // be filled with 0 before using it; otherwise, it's likely to write past
    // the end of the buffer and hang the game. When running in a loop,
    // that means you have to clear out the whole thing every time.
    // This bug was fixed in 2.50.1.
    for(int i = 0; i < 128; i++)
    {
        line1Buffer[i] = 0;
        line2Buffer[i] = 0;
    }
    
    sprintf(line1Buffer, line1Format, Link->X, Link->Y);
    sprintf(line2Buffer, line2Format, Game->GetCurDMap(), Game->GetCurDMapScreen());
    Screen->DrawString(6, 0, 0, FONT_Z1, 1, 0, TF_NORMAL, line1Buffer, OP_OPAQUE);
    Screen->DrawString(6, 0, 8, FONT_Z1, 1, 0, TF_NORMAL, line2Buffer, OP_OPAQUE);
    Waitframe();
}
printf() and sprintf() substitute special codes in their format strings with the values of the other arguments. In the example above, printf() sees "%d" in the format string and replaces it with the first number given, Link->X. The second %d is replaced with the second number, Link->Y, and so on. The fourth of these, "%X", prints the number in hexadecimal. "\n" means to start a new line. "\n" doesn't work in all contexts; for instance, Screen->DrawString() will not recognize it and will instead write "^".

That's it for strings. They're a bit unintuitive at times, but you'll rarely if ever need to do anything difficult with them.

As usual, some samples to end with.

// This script changes the save file name to "THIEF".

item script Shoplift
{
    void run()
    {
        int thiefStr[] = "THIEF";
        Game->SetSaveName(thiefStr);
    }
}
// This function will play the given DMap's music. It will try to play
// enhanced music first; if that fails, it will play a MIDI instead.

void PlayDMapMusic(int dmap)
{
    int filename[256];
    int track;
    bool success;
    
    Game->GetDMapMusicFilename(dmap, filename);
    track = Game->GetDMapMusicTrack(dmap);
    
    success = Game->PlayEnhancedMusic(filename, track);
    if(!success)
    {
        Game->PlayMIDI(Game->DMapMIDI[dmap]);
    }
}
// This script will set up an FFC to run the script named AnFFCScript.

item script RunScript
{
    void run()
    {
        int scriptName[] = "AnFFCScript";
        int scriptNum = Game->GetFFCScript(scriptName);
        ffc theFFC;
        
        if(scriptNum == -1) // No such script
        {
            Quit();
        }
        
        theFFC = Screen->LoadFFC(1);
        theFFC->Data = 1;
        theFFC->Script = scriptNum;
    }
}
And practice exercises:
  • Write a script that activate level 4 cheats if the save file's name is "Zelda". Make it case-insensitive.
    Hint

    Spoiler
  • Make a script that displays "You got <itemname>!" on the screen whenever Link is holding up an item.
    Hint

    Spoiler


#4 Saffith

Saffith

    IPv7 user

  • Members

Posted 14 October 2009 - 02:39 PM

Sorry to take so long with this (it's been a year already?!), but everything's finally covered. Still plenty of work to do, but at least it's gotten this far.


Hm. The array section seems too short, doesn't it? But there's only so much to say about them... Well, there'll be strings eventually. I guess that'll fill it out.



Working with bits


Unless you plan to do some very advanced stuff, you can consider this section mostly optional. The concepts are fairly difficult, and most of these operations are rarely used. The most common use, and therefore the most important to learn, is combining and reading flags with bitwise OR and AND.

As you're probably aware, computers represent data internally as binary digits, or bits. ZScript has several operators that let you work with bits directly rather than the numbers they collectively represent. For the purposes of this section, you should think not in terms of numbers, but of bit strings - series of independent bits.

A bit of terminology, first: significance refers to a bit's position in a string and the value it represents. The leftmost bit, which represents the greatest value, is the most significant bit. The rightmost and least is the least significant bit. Bits are generally counted from 0, with the least significant bit being bit 0. A bit may be said to be set when its value is 1 and unset or clear when its value is 0.

To illustrate, here are two examples of unsigned (i.e. non-negative) 8-bit numbers.

00000001
Bit 0, the least significant bit, is set. Bits 1-7 are unset. In decimal, this number is 1.

1000000
Bit 7, the most significant bit, is set. Bits 0-6 are unset. In decimal, this number is 128.

Moving on, ZScript has six bitwise operators:
  • & AND
  • | OR
  • ^ XOR
  • ~ NOT
  • << Left shift
  • >> Right shift
In the examples below, we'll assume the length of a bit string is 8. That's just for convenience; it's not actually correct in ZScript.

The AND operator & takes two bit strings. The result has a 1 in each position where both of the operands have a 1.
01101001 &
11001011
--------
01001001

Be sure not to confuse the bitwise AND (&) with the logical AND (&&).

The OR operator | takes two bit strings. The result has a 1 wherever at least one of the inputs has a 1.
01101001 |
11001011
--------
11101011

Again, be careful not to confuse bitwise OR (|) and logical OR (||).

"XOR" is short for "exclusive or." A XOR B means either A or B, but not both.
The XOR operator ^ takes two bit strings. The result has a 1 in any position where one operand has a 1 and the other has a 0. Where the operands are both 1 or both 0, the result has a 0.
01101001 ^
11001011
--------
10100010


The NOT operator ~ takes only one bit string. The result has all its bits flipped from the original bit string.
~01101001 = 10010110

Note the similarity between logical and bitwise AND, OR, and NOT. What the logical versions do with bools, the bitwise versions do with individual bits.

The bit shift operators << and >> take a bit string and a number. They simply move the bits over to the left or right a certain number of places. This does not change the size of the bit string. When shifting left, the most significant bits "fall off" the end and are lost, and least significant bits become zeroes. When shifting right, the reverse happens.

01101001 << 2 = 10100100 (the two most significant bits are cut off; the two least significant bits are filled with zeroes)
01101001 >> 2 = 00011010 (the two least significant bits are cut off; the two most significant bits are filled with zeroes or ones)

In ZScript, a bit string is simply a number. Wherever the operation uses a bit string, you simply give it a number; the bit string comes from the computer's internal binary representation of that number.

A number in ZScript has 18 bits that can be used for bit operations. Unless you're quite comfortable with this stuff, however, you should only use 17. The details are too complex to get into here, but the way ZScript represents numbers internally gives rise to some oddities when the most significant bit is set. It's best just to leave that bit be.

This is a good time to point out that you can represent binary numbers directly in ZScript.

// The b at the end indicates this is binary; the number is equal to 73 in decimal.
int bitString = 01001001b; // You don't have to write out all 18 bits.
Bitwise operators are used the same way as arithmetic operators.

int bits = 10010110b | 11110000b; // The result is 11110110

// You can also combine the operators with =
bits &= 00111100b; // The result is 00110100
bits <<= 2; // Now it's 11010000

// Don't forget the b at the end!
bits |= 1111; // That 1111 isn't given in binary; it's actually 10001010111, so the result is 10011010111
One potential pitfall when using bitwise operators is that their precedence is extremely low - that is, they come very late in the order of operations. This is important when using them with comparison operators.

if(x & 10b != 0) // ERROR: The compiler thinks you mean if(x & (10b != 0))

if((x & 10b) != 0) // This is the correct way to do it
The most common use of bitwise operations is to create flags. You can effectively put several related bools into a single variable. This is a basically the same as using a number as a bool array.

// Define flags
const int FLAG1 = 0001b;
const int FLAG2 = 0010b;
const int FLAG3 = 0100b;
const int FLAG4 = 1000b;

int flags = 0000b;

// Set flags with OR:
flags |= FLAG2; // Flags is now 0010
flags |= FLAG3; // Now it's 0110

// You'll often see them chained together.
int flags2 = FLAG1 | FLAG3 | FLAG4;

// To unset a flag, combine AND and NOT:
flags = flags & ~FLAG3; // 0110 & 1011 = 0010

// More succinctly:
flags &= ~FLAG3;

// To check if a flag is set, use AND:
if((flags & FLAG1) != 0)
{
    // Flag 1 is set
}
Bitwise operators are also useful if you want to use the combo solidity array Screen->ComboS[]. The numbers in that array use four bits, each representing the solidity of one fourth of the corresponding combo. Bit 0 represents the top-left quarter; bit 1, the bottom-left; bit 2, the top-right; and bit 3, the bottom-right.

0001 | 0100
-----------
0010 | 1000


// This checks whether the top half of combo 88 is at least partly solid
if((Screen->ComboS[88] & 0101b) != 0)
{
    // If either the top-left or top-right quadrant is solid, the condition is true
}

// This checks whether the entire top half of the combo is solid
if((Screen->ComboS[88] & 0101b) == 0101b)
{
    // Both bits have to match
}
Another use for bitwise operators is combining numbers. Occasionally, you may need to keep track of two numbers, but have only one variable with which to do so. This is entirely possible by combining the bit shift operators with OR and AND.

int x = 53; // 00110101
int y = 20; // 00010100

int combined;
combined = x << 8; // 0011010100000000
combined |= y;     // 0011010100010100

// These are spaced out for clarity; this would more likely be a single statement:
int combined2 = (x << 8) | y;

// When you want to get the original numbers back...
// 0xFF is the same as 11111111b
int x2 = combined >> 8;   // 00110101
int y2 = combined & 0xFF; // 00010100
The downside of this technique is that it limits the range of numbers available. Assuming you allot 8 bits to each number, as above, neither can be greater than 255.

That's the end of this section. Again, You'll probably need to understand the concept of using OR to combine flags and AND to read them at some point, but other uses of these operations are pretty rare.

Finally, the last set of practice exercises.
  • Write a script that disables Link's collision detection if the screen flag "General Use 1 (Scripts)" is set. std.zh provides a function to check a single screen flag, but don't use it.
    Hint

    Spoiler
  • Write a function to set a single bit in a number to 1 or 0, returning the new value of the number afterward. SetBit(bitStr, 7, true) should set bit 7 to 1; SetBit(bitStr, 4, false) should set bit 4 to 0. Remember that bits are counted from 0.
    Hint

    Spoiler


Scope and function overloading


The names of scripts, variables, and so on are formally known as identifiers. Each identifier has its own scope; that is, each has a certain region of code in which it's meaningful.

Curly braces are generally only useful to mark the bodies of scripts, functions, loops, and conditionals, but those aren't the only places they're allowed. In fact, you can stick them just about anywhere you like.

// This is all perfectly legal. Empty blocks simply do nothing.
while(true)
{
    Link->HP += 1;
    {
        Waitframe();
    }
    {
    }
}

{ } { { } }
Each set of braces defines a new scope. Variables declared within a block only exist until the closing brace.

It's actually not quite true that two variables can't have the same name. More precisely, two variables with the same scope cannot have the same name.

int var1;
float var1; // ERROR: A variable named var1 already exists at this scope

var1 = 10;
{ // This brace marks a new inner scope
    int var1; // This is fine - this makes a second var1 with a different scope
    var1 = 20; // The one with the innermost scope will always be used, so this sets the second var1
    Trace(var1); // This will write 20 to allegro.log
} // This brace ends the inner scope
Trace(var1); // This will write 10, because the second var1 doesn't exist here

for(int counter = 1; counter <= Screen->NumNPCs(); counter++)
{
    npc enemy = Screen->LoadNPC(counter);
    Trace(enemy->HP); // Simple enough
    {
        Trace(enemy->HP); // This does the same thing as before
        npc enemy = Screen->LoadNPC(1);
        Trace(enemy->ID); // Now that a second pointer named enemy has been declared, it'll be used instead
    }
}
Trace(enemy->HP); // ERROR: Neither enemy pointer exists in this scope
You can see how this can get confusing.

You may recall from the section on arrays that an array created in a function no longer exists after that function ends. That's the same thing: the array is deleted at the end of the scope in which it was declared. In practice, it works a bit differently. A function can't return a pointer to an array declared in that function, but the array can be accessed from outside that scope. When running multiple scripts together, it's possible for them to share local arrays.

int globalArrPtr;

ffc script ArrayExample1
{
    void run()
    {
        int arr[5] = { 10, 20, 30, 40, 50 };
        globalArrPtr = arr; // Give other scripts access to the local array
        
        while(!Link->PressEx1)
        {
            Waitframe();
        }
        
        // When the player presses Ex1, the script will end, and the array will be deleted.
    }
}

ffc script ArrayExample2
{
    void run()
    {
        // If ArrayExample1 is running, globalArrPtr points to its local array.
        // If the script isn't running, it may or may not point to another array.
        
        Trace(globalArrPtr[0]); // This could log 10, -1 and an error, or something else entirely.
    }
}
Logically, this should apply to functions as well, but they actually work a little differently. Although a function in the body of a script has a different scope than a global function, ZScript doesn't distinguish between them the same way. A global function and a script function can conflict.

void DoStuff()
{
}

ffc script Demo
{
    void run()
    {
        DoStuff(); // ERROR: The compiler doesn't know which DoStuff() to call
    }
    
    void DoStuff()
    {
    }
}
That brings us to one final topic: function overloading. Functions are distinguished not just by their names, but also by the arguments they take. Two functions can have the same name as long as they take different arguments.

// These two functions do not conflict. The compiler can figure out which one
// to use based on the arguments given.

void MoveToPosition(npc ptr, int x, int y)
{
    ptr->X = x;
    ptr->Y = y;
}

void MoveToPosition(ffc ptr, int x, int y)
{
    ptr->X = x;
    ptr->Y = y;
}
Function overloading isn't something you'll do often, but the MoveToPosition() function above is an example of when you might. If you want functions that do exactly the same thing but with different pointer types, that's the way to do it.

Only the number and types of arguments is considered; the compiler doesn't pay attention to their names.

// Putting these two functions together will cause an error. The compiler can't tell them apart.

void Move(int speed, int angle)
{
    // ...
}

void Move(int angle, int speed)
{
    // ...
}

// Also, remember that int and float are the same thing to the compiler.
// It can't tell this function apart from the other two.
void Move(float speed, float angle)
{
    // ...
}
That's about all that needs to be said on the subject, so that's the end of this section.


And, in fact, it's the end of the ZScript tutorial. Congratulations on getting through it all.

Edited by Saffith, 12 December 2014 - 02:12 AM.


#5 Joe123

Joe123

    Retired

  • Members

Posted 15 October 2009 - 06:07 PM

QUOTE
It's actually not quite true that two variables can't have the same name. Two variables can have the same name if they have different scopes.


CODE
int a;{int a;}

Perhaps I'm being pedantic, but the second one does have a different scope to the first, it's just included in the scope of the first.

Also you forgot the 'b' suffix to go with your '0x' prefix section.

EDIT:
QUOTE
Like with built-in functions, variables and pointers passed as arguments to a function will not be changed, but properties of pointers may be.

To someone who understands passing by value and passing by reference this makes perfect sense, but I would imagine that to someone who doesn't it might not do.

#6 Schwa

Schwa

    Enjoy the Moment more. This strengthens Imagination.

  • Members
  • Real Name:Hunter S.
  • Location:Redmond Subspace (I had a Potion)

Posted 13 January 2010 - 09:05 AM

*raises hand* I have a question, Saffith.

In your examples, you describe how you can initialize "array[5] = {1, 2, 3, 4, 5}" to set all the array's "slots" right off the bat. What confuses me, though, is that the ZC Wiki (at Shardstorm) explicitly states that all Arrays begin at 0, so shouldn't "array[5]" for example be "array[4]" for that to work?

I'm just wondering if they changed that in the later builds or something. X_x Thanks in advance.

#7 Saffith

Saffith

    IPv7 user

  • Members

Posted 13 January 2010 - 10:41 AM

QUOTE(Joe123 @ Oct 15 2009, 06:07 PM) View Post

CODE
int a;{int a;}

Perhaps I'm being pedantic, but the second one does have a different scope to the first, it's just included in the scope of the first.

Isn't that what I said? Maybe I wasn't clear enough.

QUOTE
Also you forgot the 'b' suffix to go with your '0x' prefix section.

That was back in the bitwise operator section. I could maybe mention it again, though, especially since I said that was optional.

QUOTE
To someone who understands passing by value and passing by reference this makes perfect sense, but I would imagine that to someone who doesn't it might not do.

All right. I'll see if I can't explain it better.

QUOTE(Schwa @ Jan 13 2010, 09:05 AM) View Post
*raises hand* I have a question, Saffith.

In your examples, you describe how you can initialize "array[5] = {1, 2, 3, 4, 5}" to set all the array's "slots" right off the bat. What confuses me, though, is that the ZC Wiki (at Shardstorm) explicitly states that all Arrays begin at 0, so shouldn't "array[5]" for example be "array[4]" for that to work?

I'm just wondering if they changed that in the later builds or something. X_x Thanks in advance.

The number in brackets when the array is declared is the total number of elements, and the indices are numbered from 0 to one less. array[5]={1,2,3,4,5}; sets array[0]=1 .. array[4]=5.

#8 Schwa

Schwa

    Enjoy the Moment more. This strengthens Imagination.

  • Members
  • Real Name:Hunter S.
  • Location:Redmond Subspace (I had a Potion)

Posted 18 January 2010 - 06:10 PM

Thanks for the help. I have another question.

How could you get a custom Array defined with negative values? The parser isn't making it easy on me. I tried to define "int gVAR_ar_LinkFrontX[7] = {0, 0, -16, 16, -11, 11, -11, 11};" as a global Array integer, but when I compile, I get an error saying it's expecting a number where "token -" is located.

Is this a bug in the engine? Or do we have a traditionally used method to get around this obelisk in mah code?

Oh, and in case you're curious as to what my Array does... I'm pairing it with a sister Array "gVAR_ar_LinkFrontY[7]", which when used together and referenced like "(Link->X + gVAR_ar_LinkFrontX[Link->Dir])", it provides a shortcut to always get the spot RIGHT in front of Link, wherever he's facing, without the tedious and choppy If-Then-Else-If-etc. method. Pretty cool, I'd say! I tribute the idea of mine to my experience in Super Mario World ROM hacking, where something like this would always be set up with a sort of Table like this in Assembly code. icon_kawaii.gif

#9 Saffith

Saffith

    IPv7 user

  • Members

Posted 18 January 2010 - 06:28 PM

Yeah, you just can't use negative numbers that way right now. Can't use them for constants, either. Hopefully, it'll be fixed before release. I think it's because the compiler sees the unary minus as an arithmetic operator, which means it would have to do math at compile time, and it doesn't do that yet.

You'll just have to work around it, I'm afraid. That's easy enough to do:
CODE
int gVAR_ar_LinkFrontX[8]; // 8 elements, not 7, even though the compiler doesn't complain
gVAR_ar_LinkFrontX[0]=0;
gVAR_ar_LinkFrontX[1]=0;
gVAR_ar_LinkFrontX[2]=-16;
gVAR_ar_LinkFrontX[3]=16;
gVAR_ar_LinkFrontX[4]=-11;
gVAR_ar_LinkFrontX[5]=11;
gVAR_ar_LinkFrontX[6]=-11;
gVAR_ar_LinkFrontX[7]=11;


Here's a shorter way, though I'd encourage you not to use it unless you understand it:
CODE
int gVAR_ar_LinkFrontX[8] = {0, 0, 16, 16, 11, 11, 11, 11};
for(int i=2; i<8; i+=2)
    gVAR_ar_LinkFrontX[i]*=-1;

Edited by Saffith, 19 January 2010 - 10:38 AM.


#10 Schwa

Schwa

    Enjoy the Moment more. This strengthens Imagination.

  • Members
  • Real Name:Hunter S.
  • Location:Redmond Subspace (I had a Potion)

Posted 19 January 2010 - 11:52 PM

Whoa, I LOVE that loop. That's awesome. And yes I understand it; I just really doubt I ever would have been able to come up with that on my own. When I do For Loops I always just think to decrement the Counter by 1 at a time! That really gives me some creative insight on a new way to do things there. icon_kawaii.gif

In the Loop example though, [8] should be [7], shouldn't it? icon_unsettled.gif 'Cause if the Array begins at zero (0 counts as "1"), then yeah...

I have to say, it bugs me a little though that all this setting up is necessary just to set up one only-somewhat-complex variable... icon_razz.gif

#11 Saffith

Saffith

    IPv7 user

  • Members

Posted 20 January 2010 - 12:49 AM

Well, note that the number in brackets actually has a different meaning when you're declaring an array than it does when you're accessing it. It's only in the latter case that you start counting at 0.

Maybe it'd be helpful to explain where that comes from. In C, when you declare an array, the array subscript indicates how much memory to allocate. If you declare int arr[10], say, contiguous memory is allocated for ten ints. arr, with no brackets, is then a pointer to the beginning of the array. When you access the array, the subscript represents an offset from that starting address. The first element being right at the start of the array, its offset is 0.

#12 Schwa

Schwa

    Enjoy the Moment more. This strengthens Imagination.

  • Members
  • Real Name:Hunter S.
  • Location:Redmond Subspace (I had a Potion)

Posted 20 January 2010 - 11:55 PM

...Jesus Christ that's confusing. I got it now, but... agh. Talk about needlessly complicated. @_@ Thank you though, that explanation was just what I needed.

Another question though, one that will hopefully end the headache I've been having for two hours with this routine I'm trying and failing at designing. Since we're on the subject of For Loops now... How come you write the Condition segment of your For Loops like "i < 8", in the example above? That reads "i is less than 8", doesn't it? Yet the Step segment is adding to 8 each time, not subtracting from it, so unless there's yet another crazy rule I'm missing (which I don't doubt at this point icon_xd.gif ), I would think "i" never reaches a point where the Condition is satisfied.

I looked, but couldn't find that detail explained above or in the Intermediate Tutorial, and the ZC Wiki is telling me counter-intuitive data about this. I want to cry again. I've been stoked on so much coffee trying to get this one to work and it's just so frustrating...

EPIC EDIT - HAHAHAHA. I FOUND IT. False != True in your tutorial. Hahahaha I am SO STUPID. icon_lol.gif icon_lol.gif icon_lol.gif God now I am so fired up it's like the headache was never there... Time for revenge against this script of mine!

Edited by Schwa, 21 January 2010 - 12:00 AM.


#13 lucas92

lucas92

    Defender

  • Members

Posted 21 January 2010 - 12:00 AM

I'm not sure what you don't understand here but let's say you understand the while loop.
This would be written like this in a while loop:

CODE
while(i < 8)
{
    gVAR_ar_LinkFrontX[i]*=-1;
    i+=2;
}




#14 Joe123

Joe123

    Retired

  • Members

Posted 21 January 2010 - 05:29 AM

QUOTE(Schwa @ Jan 21 2010, 04:55 AM) View Post
Another question though, one that will hopefully end the headache I've been having for two hours with this routine I'm trying and failing at designing. Since we're on the subject of For Loops now... How come you write the Condition segment of your For Loops like "i < 8", in the example above? That reads "i is less than 8", doesn't it? Yet the Step segment is adding to 8 each time, not subtracting from it, so unless there's yet another crazy rule I'm missing (which I don't doubt at this point icon_xd.gif ), I would think "i" never reaches a point where the Condition is satisfied.

CODE
for(initial condition; ending condition; increase)


The initial condition starts 'i' off at 0, then the loop increases it by two each time it runs until 'i' is greater than or equal to 8.

#15 Schwa

Schwa

    Enjoy the Moment more. This strengthens Imagination.

  • Members
  • Real Name:Hunter S.
  • Location:Redmond Subspace (I had a Potion)

Posted 21 January 2010 - 09:43 AM

No, I got it. I was confusing True and False. icon_razz.gif Last night I seemed to be stuck on thinking that For Loops break when the condition becomes False. It's the other way around. THAT was what was throwing me off.

Naturally, with that figured out, I got my Script to work only five minutes later. I was rewarded with an incredible, glowing feeling of satisfaction and victory during my entire bike ride home in the dark. icon_w00t.gif

Thanks for everything, guys! I'll let you know when/if I have moar questions about this stuff. Scripting is awesome! icon_love.gif


0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users