Jump to content

Photo

Advanced scripting tutorial (WIP)


  • Please log in to reply
44 replies to this topic

#1 Saffith

Saffith

    IPv7 user

  • ZC Developers
  • Gender:Male

Posted 28 March 2009 - 03:18 PM

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. This is also where the limitations and quirks of the language can become significant 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 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, but there are two important differences. The first is obvious: the data type is different.
CODE
npc enemy; // An npc (enemy) pointer
ffc anFFC; // An ffc pointer

The other difference is that, unlike numeric and Boolean variables, pointers cannot be global.
CODE
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 - Represents 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.

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.
CODE
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.
CODE
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.
CODE
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 point to the same object afterward, but its properties may have changed.
CODE
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 any Screen->LoadXXXX function, you need to make sure the object you want is really there.
CODE
item it = Screen->LoadItem(5); // Not safe - there may be no item #5

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

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.
CODE
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 Waitdraw() or Waitframe(),
// no matter what you do to it.

npc enemy;
if(Screen->NumNPCs() >= 1)
{
    enemy = Screen->LoadNPC(1);
}

enemy->HP = 0;
enemy->X = 80;  // This is OK; even though it has 0 HP, the enemy won't
enemy->Y = 128; // actually die until a Waitframe() is reached

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 if you know it's a concern, it may be worth checking.
CODE
eweapon wpn = Screen->CreateEWeapon(EW_SCRIPT1);
// If there are a ton of eweapons onscreen already, this may fail
if(!wpn->isValid())
{
    // Must be a bullet hell quest
    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. So how do you do that? Your first thought might be something like this.
CODE
if(!enemy->isValid() || enemy->HP <= 0)

That seems logical, but it's actually not safe. 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.
CODE
bool IsDead(npc en)
{
    if(!en->isValid())
        return true;
    if(en->HP <= 0)
        return true;
    return false;
}

// Elsewhere...
if(IsDead(enemy))

All this validation must look incredibly tedious, but it's really not all 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 the checking is still repetitious, 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 replacement function like this.
CODE
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 function correctly if you don't know when your pointers are valid.
(However, in older 2.50 builds, using invalid pointers could crash the game. It should be safe now, but do you really want to risk it?)

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.
CODE
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.
CODE
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.
CODE
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...
CODE
// 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);
        }
    }
}

CODE
// 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.
        if(!arrow->isValid())
        {
            Quit();
        }
        
        arrow->X = Link->X;
        arrow->Y = Link->Y;
        arrow->Dir = Link->Dir;
        arrow->Step = 500;
        arrow->Damage=2;
        
        // 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;
        }
    }
}

CODE
//This script makes the FFC follow Link around, offset by half a tile (it should be 2x2),
// and removes any enemy weapons that get close to him.
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.
CODE
item script RunFireArrow
{
    void run()
    {
        ffc scriptRunner = Screen->LoadFFC(1);
        scriptRunner->Data = 1;
        scriptRunner->Script = 1;
    }
}



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.
CODE
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[200000]; // 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.
CODE
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.
CODE
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

Now, here's the 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.
CODE
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, 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 errors to track down.

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.
CODE
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]);
        }
    }
}

Because the function header in the script gives no indication that an argument should be an array rather than a single variable, you need to be sure to state that, as well as any size requirements, in whatever documentation you write. It's also a good idea to make a note of it in a comment above the function header.

Related to that, here's another possible pitfall. What if you want a function to create an array and return a pointer to it?
CODE
ffc script Example
{
    void run()
    {
        int anArray = MakeArray();
        Trace(anArray[3]);
    }
    
    int MakeArray()
    {
        int array[] = { 1, 2, 3, 4, 5 };
        return array;
    }
}

What does that Trace() print? If you guessed 4, you're wrong:
CODE
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. Creating an array in a function and returning a pointer to it simply cannot be done.

You cannot compare arrays using == and !=. Like pointers, these will tell you if the two array pointers are pointing 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.
CODE
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're equal
}

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 of an array, you can still have a constant representing an array's size.

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. You should already know that modifying scripts can reassign global variables, rendering saved games invalid, but arrays take the problem even further. Unlike simple global variables, global arrays affect the size of the save data. Because of this, it used to be that modifying a quest's global arrays and reloading a saved game could crash ZC; I don't know that this has been fixed.

One last detail to be aware of: the term "array pointer" can have two different meanings. It may mean a pointer to an array, or it could mean a number that indicates the index within an array.

Some sample scripts...
CODE
// 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();
        }
    }
}

CODE
// This will make the FFC follow Link, staying about a tile and a half behind him.

const int FL_ARRAY_SIZE = 16;

ffc script FollowLink
{
    void run()
    {
        // xHistory and yHistory store Link's previous positions
        int xHistory[16]; // Be sure these match FL_ARRAY_SIZE
        int yHistory[16];
        int linkPosIndex;
        int ffcPosIndex;
        
        // The arrays are treated as "circular buffers," meaning that the index
        // will wrap around from 15 to 0. linkPosIndex is the index where Link's
        // current position is stored. ffcPosIndex is one greater, the index
        // where Link's position was stored the longest time ago.
        linkPosIndex = 0;
        ffcPosIndex = 1;
        
        for(int index = 0; index < FL_ARRAY_SIZE; index++)
        {
            // It's often more convenient to set up an array in a loop
            // rather than initialize it.
            xHistory[index] = Link->X;
            yHistory[index] = Link->Y;
        }
        
        this->X = Link->X;
        this->Y = Link->Y;
        
        while(true)
        {
            // Don't do anything unless Link moves. What happens if you remove this if?
            if(Link->X != xHistory[linkPosIndex] || Link->Y != yHistory[linkPosIndex])
            {
                ffcPosIndex = (ffcPosIndex + 1) % FL_ARRAY_SIZE; // Wrap around
                this->X = xHistory[ffcPosIndex];
                this->Y = yHistory[ffcPosIndex];
                
                // Increment the pointer and store Link's new position.
                // This will overwrite what was previously at index ffcPosIndex.
                linkPosIndex = (linkPosIndex + 1) % FL_ARRAY_SIZE;
                xHistory[linkPosIndex] = Link->X;
                yHistory[linkPosIndex] = Link->Y;
            }
            
            Waitframe();
        }
    }
}

And some practice exercises.
  • Find all instances of flag CF_SCRIPT1 on the screen. When the combo at each location with that flag changes (check Screen->ComboD[]), trigger secrets.
    Hint

    Spoiler
  • Create an array of npc pointers. Create some enemies to store in it and make them invincible (use npc->Defense[]). Have them circle around another enemy on the screen. Kill them all when the enemy they're circling dies.
    Hint

    Spoiler


#2 Christian

Christian

    Summoner

  • Members
  • Real Name:Chris
  • Gender:Male
  • Location:New Jersey
  • Past Nicks:drzchulo973

Posted 09 May 2009 - 06:30 PM

any advance on this saffith?

#3 Saffith

Saffith

    IPv7 user

  • ZC Developers
  • Gender:Male

Posted 08 October 2009 - 09:03 AM

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 initialized with text in quotation marks instead of values in curly braces.
CODE
int hello[10] = "Hello"; // It's common to make string arrays a bit larger than necessary
int str[] = "This is some text!"; // But it's more common to leave out the size

// 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.
CODE
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.
CODE
int logString[] = "Logging...";
TraceS(logString);
TraceNL();
// Afterward, you'll find a line in allegro.log that says "Logging..."

So that's all simple enough. But how about this?
CODE
int hello[5] = "Hello";
TraceS(hello);
TraceNL();

If you do that, here's what you'll see in allegro.log (assuming the error logging quest rule is enabled):
CODE
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.
CODE
int hello[] = "Hello";
// The size of the array is six; hello[5] is 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. They'll also read past the end of the array, causing an error to be reported in allegro.log, and possibly more serious problems.
Because of the terminator, the length of a string and the size of the array are not 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.
CODE
int abcStr[] = "ABCDEFG";
abcStr[3] = 0; // Replace D with 0
TraceS(abcStr);
TraceNL();
// allegro.log will contain the line "ABC"

Although you input a string as text, it is still a string of numbers. There's rarely if ever a reason to use them as such, but it's certainly possible.
CODE
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

These numbers are the ASCII values of the characters. Values above 127 should be considered invalid, and many characters in the range 0-127 are not useful.

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 a number of functions for converting between numbers and strings.
CODE
//atof(): ASCII to float
int numStr[] = "12.3456";
float num = atof(numStr);
// num is now 12.3456

What if you want to compare two strings? That's what strcmp() does. However, it doesn't work the way you might expect.
CODE
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 a 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.
CODE
// 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 0, 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(). Your output arrays must be filled
    // with 0 before using it; otherwise, it's likely to write past the end of
    // the buffer and may hang the game. When running in a loop, that means
    // you have to clear out the whole thing every time.
    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", is used for hexadecimal numbers. "\n" means to start a new line. "\n" doesn't work in all contxexts; for instance, Screen->DrawString() will not recognize it and will instead write "^".

That's it for strings. They can be hard to work with, but the good news is that you'll rarely if ever need to do anything difficult with them.

As usual, some samples to close with.
CODE
// This script changes the save file name to "THIEF".

item script Shoplift
{
    void run()
    {
        int thiefStr[] = "THIEF";
        Game->SetSaveName(thiefStr);
    }
}

CODE
// 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]);
    }
}

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


Working with bits


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. This is a fairly difficult concept, but it's not very often used in ZScript, so you shouldn't consider it vital to learn.

For the purposes of this section, you should think not in terms of numbers, but of bit strings - series of independent bits.

There are four bitwise operators:
  • & AND - 1100 & 1010 = 1000
  • | OR - 1100 | 1010 = 1110
  • ^ XOR - 1100 ^ 1010 = 0110
  • ~ NOT - ~1100 = 0011
And two bit shift operators:
  • << Shift left - 1111000111 << 2 = 1100011100
  • >> Shift right - 111100011 >> 3 = 000111100

A bit of terminology: 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.
When discussing bit strings, the bits are usually numbered; however, there's no standard way of doing this. Bit 0 may be either the most or least significant bit. I prefer to say 0 is the least significant bit; that way, bit n can be found using 1<<n.

In the examples below, we'll assume the length of a bit string is 8. This is 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, and 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. Bitwise NOT is sometimes also called "complement."
~01101001 = 10010110

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 (greatest) bits "fall off" the end and are lost, and least significant (smallest) 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 usually filled with zeroes, but sometimes ones)

In ZScript, a bit string is simply a number. Wherever the operation uses a bit string, you simply give it a number instead; the bit string comes from the computer's internal binary representation of that number. A number has 18 bits that can be used for bit operations.
However, the unusual way in which ZScript represents numbers internally occasionally produces some bizarre quirks, particularly when using the right bit shift operator. The details are far too complex and technical to discuss here. The main thing you need to know is that setting the most significant bit can be problematic in some cases, so unless you're pretty comfortable with this stuff, you should pretend a number has only 17 bits.

This is a good time to point out that you can represent binary numbers directly in ZScript.
CODE
int bitstring = 01001001b;
// The b at the end indicates this is binary; the number is equal to 73 in decimal


Bitwise operators are used the same way as arithmetic operators.
CODE
int bits = 0;
bits = 10010110b | 11110000b; // The result is 11110110

// You can also combine the operators with =
bits ^= 00001111b; // The result is 11111001

// 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 01010001

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.
CODE
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

One of the most common uses 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.
CODE
// 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

// 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


CODE
// 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
}

It is possible to use the same variable as both a bit string and regular number, but you shouldn't do that until you're very comfortable with the concept. When you do, however, note that only the integer part of the number is used, and the fractional part is truncated. 0.9999 is the same as 0 to the bitwise operators.

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.
CODE
// 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.
CODE
int var1;
float var1; // ERROR: A variable named var1 already exists in 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 enemy pointer has been declared, it'll be used instead
    }
}
Trace(enemy->HP); // ERROR: enemy doesn't exist outside of the loop

You can see how this might get confusing. That's why it's best to avoid dealing with the matter as much as possible by using distinct names and, at least until you're comfortable with the matter, declaring any variables and pointers at the start of a function.

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: an array's scope is limited to the block in which it was created.

A related subject is function overloading. Two functions can have the same name as long as they take different arguments.
CODE
// These two functions do not conflict. The compiler can figure out which one
// to use based on the arguments used.

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.
CODE
// 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)
{
    // ...
}

ZScript isn't smart about function scope. A global function and a script function can conflict.
CODE
void SomeFunction()
{
}

ffc script Demo
{
    void run()
    {
        SomeFunction(); // ERROR: The compiler doesn't know which SomeFunction to use
    }
    
    void SomeFunction()
    {
    }
}




That's the end of this section, and the end of the ZScript tutorial. Congratulations and thanks for getting through it all. Now get to work, 'cause some kid's making the awesomest quest ever, and he needs twenty-three custom bosses. He'll totally give you credit.

#4 Saffith

Saffith

    IPv7 user

  • ZC Developers
  • Gender:Male

Posted 14 October 2009 - 01:39 PM

Double posting to get attention. icon_razz.gif

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.

Edited by Saffith, 14 October 2009 - 07:15 PM.


#5 Joe123

Joe123

    Retired

  • Members
  • Gender:Male

Posted 15 October 2009 - 05: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.
  • Gender:Male
  • Location:Redmond Subspace (I had a Potion)

Posted 13 January 2010 - 08: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

  • ZC Developers
  • Gender:Male

Posted 13 January 2010 - 09: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.
  • Gender:Male
  • Location:Redmond Subspace (I had a Potion)

Posted 18 January 2010 - 05: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

  • ZC Developers
  • Gender:Male

Posted 18 January 2010 - 05: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 - 09:38 AM.


#10 Schwa

Schwa

    Enjoy the Moment more. This strengthens Imagination.

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

Posted 19 January 2010 - 10: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

  • ZC Developers
  • Gender:Male

Posted 19 January 2010 - 11:49 PM

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.
  • Gender:Male
  • Location:Redmond Subspace (I had a Potion)

Posted 20 January 2010 - 10: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, 20 January 2010 - 11:00 PM.


#13 lucas92

lucas92

    Defender

  • Members
  • Gender:Unspecified

Posted 20 January 2010 - 11:00 PM

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
  • Gender:Male

Posted 21 January 2010 - 04: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.
  • Gender:Male
  • Location:Redmond Subspace (I had a Potion)

Posted 21 January 2010 - 08: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