Jump to content

Photo

A Brief Introduction To ZScript For Newbies.


  • Please log in to reply
7 replies to this topic

#1 ShadowTiger

ShadowTiger

    The Doctor Is In

  • Members

Posted 03 February 2007 - 09:21 PM

Introduction:



I'm writing this guide as a general introduction to ZScript without going too far in depth. I'm not going to write any long and elaborate scripts for people to try to figure out, then try to write it on their own and have no idea how. Believe you me, I tried that method, without much success. I do believe that the road to success is paved with trial and error, and working with your own hands in a safe environment where you can see what works and what doesn't, but a lot of that requires at least a moderate amount of knowledge to build on beforehand, as well as a proper guide to show you why what you're doing either does not work, or does. You can't understand one without the other.


This guide is going to be mostly me sitting here writing an essay and hoping that you'll have the patience to read it. I know it's not easy to read it all, but I'm really hoping that what I write here will be of great assistance to the community. It's nothing pleasant to see people around you scripting incredible custom bosses and inconceivable creations while you're still creating your masterpieces the hard way. This guide won't even try to tell you how to do such things, but it will at least take you a step further into scripting, which can put you closer to the right path to learning it on your own.




Synopsis:

During the course of the tutorial, I will be presenting several key concepts that I hope you'll take the time to acknowledge and memorize on your own. They will more than likely be absolutely crucial to your eventual understanding of ZScript. Of course, it's all important, but without these side notes, the overall concept might be a bit lacking in the mortar which solidifies everything into an actual concept rather than a series of bewildering stand-alone facts.


I will be introducing a few more guide-related concepts during the course of the tutorial. Let's get started, shall we?



 

Chapter 1
The basic concept of a script:




These ZScripts that you see can have any number of interpretations to them. You see things like PlaySFXand think "Oh, this must be playing a sound effect!" and you can also see cx=4xy+=1;and have absolutely no idea whatsoever about what it's doing. That's perfectly understandable, particularly if you haven't actually written that code. Still, it is a language all its own, with its own unique intricacies which have their own meaning. This is one of the parts about scripting where the more you read the scripts, the more you begin to grasp their functions. Wherever you see a script, try to stop a moment and see what it's trying to do. Nobody is expecting you to understand precisely what it's doing or how it's doing it, nor what each individual entity within the script is doing, but it's a good idea to at least devote a minute or two studying the script and seeing what it's attempting to do. Even if you don't understand it, you might catch a glimpse of the concept of a loop or an alteration to Link within the script. It's a good step for beginners to get used to.




The different types of variables:


I'll be honest with you all. When I first started scripting, (Which, again, honestly, I haven't mastered yet.) I thought I understood the concept of a "Variable" quite well. I thought it was a place to store a value for reference. I wasn't wrong either. It's just that there were more variable types than I had thought existed. There are two types of variables (Possibly more, but I don't think we'll be dealing with them in ZScript.)in ZScript:
  • User-Defined Variables
  • Hard Coded Variables
The Hard Coded Variables are the sort of variables whose values can change, which are directly related to ZC. Link's health is a hard coded variable not in the sense that its value remains constant, but that it's already in existence, constantly referring to whatever Link's health is at the moment.

The User-Defined Variable is something you can define to hold whatever value you would like. You can use it to store the value equivalent to Link's Hit Points, for example. While Link's hit points may change, the value within that variable won't change along with Link's Hit Points, because the variable is separate from Link's HP. It will just contain whatever you had last assigned it to. The variable that's keeping track of Link's Hit Points is far more dynamic than the variable you created yourself for your own purposes. That said, within this tutorial, any variable that you create yourself, I'll write in Red. Any hard-coded variable I'll write in Green. Even though it's the user-defined variables that are more important for us to notice and to contrast against the hard-coded variables, I'll still highlight them for the sake of completeness.



 

Chapter 2
Beginning a script:



Even if you don't understand why, memorize the following. It's what got me through my darkest coding hours.


CODE
import "std.zh"
ffc script your_name_for_this_script_goes_here
{
  void run()
  {
      (YOUR SCRIPT WILL GO HERE)
      (YOUR SCRIPT WILL GO HERE) \
      (YOUR SCRIPT WILL GO HERE)  } (etc)
      (YOUR SCRIPT WILL GO HERE) /
      (YOUR SCRIPT WILL GO HERE)
  }
}



Note: Even though we're not going to go into the descriptions of these all that much, I want to point out that the first line, "import "std.zh"" is used to make reference to the std.zh file that should be in your ZC folder. It contains a reference to things used in ZC, conecting ZScript with what you script. It's probably a good idea to put it in there, even though I've seen many scripts without it, starting with ffc script (Etc).


The void run()is another necessary component, stated exactly like that. However, you are able to place certain things within the parentheses before the semicolon. This tutorial doesn't cover that though.

The proper format for a ZScript header, aside from the italicized note above, is ffc, then script, then the_lowercase_name_of_your_script. This will appear as you have written it within the script area when your script compiles, assuming it does. Again, note how I wrote the last bit in red. It denotates a something that you name yourself. It doesn't have to refer to anything hard-coded within ZC. You can even name it jwe4jtaw4 if you wanted to. The other parts need to remain constant though, exactly how you see them.




 

Chapter 3
Analyzing some key components:




Introduction:

First of all, the easiest concept of any form of scripting to understand is a Comment. A Comment is something that the script reader can see, but the script itself cannot. The script simply ignores it as if it weren't there. It makes no impact on the script whatsoever. It's a good habit to Comment up your code so it's easier to read for others when they read your script. A helpful comment it something like this:
// Here, If our variable Xis less than our variable Y, We subtract six Rupees.(Do note of course that this is only an example. Your comments can be as helpful to yourself as you'd like.)
Clearly, it's very helpful to comment up your code, since there is no cost in doing so. It makes it very easy for you to understand it as well, if you need to go back and change something later. It's also useful to make comments as you write your code so you don't end up confused in the middle of writing.




The Semicolon:

The Semicolon ( ;) is another crucial component of a script. It is placed at the end of a statement, very similar to a how a period at the end of a sentence operates. You would put one at the end of a sentence stating something, but not at the end of a sentence that leads into another. For example:

int xyz= 3;

Here, a semicolon is seen at the end of the line. The line it's ending is fairly exact, as all it does is set the variable xyzequal to 3. Let's take a look at another situation:

if(xyz< 5)
{
__Link->HP+=16;
}


In that example, note the lack of a semicolon at the end of either curly braces, as well as the lack of a semicolon after the if statement. It simply ends with the closing parenthesis after the 5. There is no semicolon there because it's leading into another set of braces. It's not a standalone sentence, capable of existing on its own. It's like ending a sentence with "However," and just walking away.



(Pardon the double post; I went over the character limit.)

The Curly Braces:

The curly braces {and }are used for a number of reasons. I hope nobody minds me going into a bit more detail here than I would have wanted, but I think it would be useful to explain these so you can get a grasp of how capable ZScript (And C, its parent language.) really are.

"Scope" is a term used to define where a Variableis acknowledged by the script. First, I'll use an English example, then I'll use a script-based example. All humans know the language of the body, right? We shake our heads horizontally for no, and shake them vertically for yes. We smile to show joy or approval, and frown to show dislike or disapproval. It's pretty universal. (Pretend for the sake of the example that it is.) Thus, the Scope of an expression in this universal human language is the entire human race. Then picture the English language. A lot of people know it, but not everyone in the world. Thus, the Scope of an expression or phrase used within the language is understood by (Hopefully) everyone who knows English. Then you have an expression only used within a particular state. Suppose the phrase "I'll cook yer daughter and roaster 'er over a pine tree" is used in [A fictional state] to mean something friendly. Chances are, nobody else is going to know what that means. Its Scope is limited to that state. Nobody outside of that state will know what it means, and may refuse to acknowledge its validity. However, the people in that state still speak English, and will certainly understand the universal body language. Thus, the Scope of the body language extends into the areas of those who speak English, and to those in that fictional state, but not vice versa.

So Scope, in terms of ZScript or C in general, the Scope of a Variableis something contained within a set of curly brackets. I will now present a mock script of my own design (So don't bother trying to put this script into a Freeform combo, as it's fake.) and comment it appropriately in terms of Scope.


ffc script scopeexample// The standard opening line.
{ // The first brace, signifying the opening of the code.
__void main() // A necessary statement to the code.
__{ // The second opening bracket.
____int janeway= 5; // Setting variable janewayequal to 5.
____int scoobydoo= 3; // Setting variable scoobydooequal to 3.
____if(scoobydoo> 2) // Checks to see if scoobydoois greater than 2.
____{ // The third opening bracket.
______int janeway= 14; // Creates and sets the integer variable janewayto 14.
____} // The closing of the third opening bracket.
____scoobydoo= janeway; // Sets the new value of scoobydooto be the value of janeway.
__} // The closing of the second opening bracket.
} // The closing of the final brace, and the end of the script.



(Ignore the elements you didn't recognize for now.)

Note how it creates a variable(scoobydoo) within the second tier of curly braces, and creates another variable(janeway*) within the third tier of curly braces, whereas the scoobydoowas made within the second tier of curly braces. The third tier is within the second tier.

* = (Note that these names were chosen to be the most unlikely names of variables to be hard coded short of utter gibberish.)



Can you guys please critique this for me as much as possible? I feel as if I could write more. Also, the post automatically goes above and beyond the 10000 character limit because I double posted to add the rest, and it automatically merged the two, instantly bypassing said limit. Heh.

#2 Beefster

Beefster

    Human Being

  • Members
  • Real Name:Justin
  • Location:Colorado

Posted 04 February 2007 - 03:40 PM

You can't forget while, if, else and elsif (If they even exist) statements.You briefly go over them, but you need go go more in-depth. Going into bools and floats is also quite important. &&(ands) ||(ors) and !(nots) are also essential. But other than that, you covered the basics. I would like to add on.

While Loops
A while loop is used to:
  • A.) Make something repeat forever
  • B.) Make something repeat until a certain condition is false
  • C.) Make something repeat until a certain condition is true
Type A
Type A is accomplished in the following way:
CODE
while(true){
foo += 1;
}


Type B
Type B is a little different. You just have to change what's inside the parentheses.
CODE
while(foo == 5){
bar += 1;
}


Type C
Well technically it's the same as Type B, but you just add a !(not) to the beginning of the statement.
There is one way to do it with bools and another with ints/floats.
CODE
while(!(foo == 5)){
bar += 1;
}

CODE
while(!foo){
bar += 1;
}


If and Else statements
It follows a very similar format of while loops, but it only does what is inside the brackets once if and only if the condition is met.
CODE
if(foo){
bar += 1;
}


You can also include &&(and) and ||(or) logic bits in your statement.
CODE
if(foo && bar || foo && !(foobar == 7)){
bar = false;
}


EDIT: oops, neglected an extra parenthese

Edited by beefster, 04 February 2007 - 03:42 PM.


#3 Fire Wizzrobe

Fire Wizzrobe

    Master

  • Members
  • Real Name:T

Posted 04 February 2007 - 04:11 PM

I'd like to explain the "for" structure too. These repeat a commands for a certain amount of time or in other terms, are a temporary loop. The command lies between the two {}s.

CODE

for (n=0; n<=90; n++){
this->X += 1;
}


Ok, this makes an FFC move left for 90 tics, or 1 1/2 seconds.

The middle statement, n<=90, is the condition to be met for the loop to end. N must be greater than or equal to 90. The leftmost statement, n=0, is the value that n starts with. The rightmost statement, n++, increases n by 1 everytime Zquest goes though the loop. (++ does the same thing as n + 1). For example, if you made it n + 2;, then the FFC would move for 45 tics.

Edited by Fire Wizzrobe, 04 February 2007 - 06:24 PM.


#4 Stungun

Stungun

    Warrior of Light

  • Members
  • Location:Where dreams are born

Posted 04 February 2007 - 06:04 PM

It's better to explain in depth how these branches work; They're used universally. C++, Game Maker, ZC... We need to go over some basic things like expressions.

An expression is how the program determines logic. It simply asks itself a question and analyzes the answer, mathematically most often. These are used whenever you want something to happen only under certain circumstances.
When an expression is seen, the program simplifies it like an equation.
An example expression: (5 > 6)
If the program comes across this expression, it will ask itself 'Is five greater than six?' Then it says 'No...no it's not', and turns that into 'false', which is it's way of saying 'no'. If the expression was (6 > 5), it would ask 'Is six greater than five?' and say 'Yes', turning it into 'true', which is it's way of saying 'yes'.
Certain commands in the scripting language, in fact many of them, rely on an expression being resolved to 'true' or 'false'.

Expressions can be made complicated as well, because it goes over them multiple times until it's simplified to a 'true' or 'false' answer, like an algebraic equation. Don't panic - it's really simple, you may remember in mathematics Order of Operations, anything within ( and ) are resolved before anything else. To be more specific, () are done in layers. You may be familiar with something like this:
5 * (5 / (6 * 2) ) = ?
Yes, it's a simple equation by the Order of Operations; 6 * 2 is first calculated, to resolve itself to 12, making:
5 * (5 / 12) = ?
Then 5 / 12 is resolved, ~0.416 is what you get from it, making:
5 * 0.416 = ?
From there, it's a simple mathematical equation that you can solve in Calculator with just a few button presses; And that's just the way expressions are resolved, from the inside out. So you can imagine how this would be resolved:
(6 > (25 / (3 + 2) ) )
First, 3 + 2 is resolved to 5, leaving:
(6 > (25 / 5) )
Then 25 / 5 is resolved to another 5, leaving:
(6 > 5)
So, since six is greater than five, this is resolved to true, leaving:
true
Thus whatever was asking the question gets answered, 'Yes'.

Simple mathematics, isn't it?

This is where variables come into play. You may remember them from algebra, but if you don't, do NOT fret! It's simple.
Variables are like a zip-loc bag; You can see whatever's been put in them, and you can also open them up and change the contents. These contents are changed by 'assignments', which will be explained later.
Variables are usually represented by words such as 'janeway' or 'scoobydoo' as ShadowTiger mentioned above, or letters like 'x' and 'i'. They hold a single number (sometimes strings, but that comes later!) within them, like 6 or 29. When the program comes across a variable in an expression, it sees through the variable (like we see through a zip-loc bag) and looks at the number contained (like looking at a filled bag and saying 'It's salad'). So in other words it simplifies the variable in the equation to it's container number!
So, if x = 25, in an expression like the following:
( (x + 2) > 18 + (2 * 2) )
The program will resolve x as 25, making:
( (25 + 2) > 18 + (2 * 2) )
Then, it will resolve the innermost layer on both sides:
(27 > 18 + 4)
Finally, it resolves the remaining calculation:
(27 > 22)
Then it resolves it to:
true
Simple enough, isn't it? Since variables can be changed throughout play, they are an excellent way of tracking certain needed values, like for example the number of times Link has slashed against a wooden log.

Assignments are like expressions, but they are changing the variable on the left side of the equation, to whatever's on the right side; The right side can be complicated too. An expression can be simple as this:
x = 39;
-----
Or, it can be a bit more complicated:
x = 12 * 4;
In this case, 12*4 is resolved to 48, making:
x = 48;
-----
You can also put variables on the right side of the equation, if you've done assignments on them earlier. For example, if y = 3:
x = 12 / y;
y is resolved to 3, making:
x = 12 / 3;
That's then resolved to 4, making:
x = 4;
-----
You can also use the order of operations to make complicated assignments:
Given that y = 55, and z = 33:
x = ( 4 + (3 * y) / (z * y) );
Don't bother trying to figure that one out, I'm sure you get the point.
-----
You don't have to use the equals sign ALONE for assignments; There are a few shortcuts you can use.
+= causes the equation on the right to be ADDED to whatever the variable on the left is.
-= is the same, but for subtraction.
There's others but these are the most basic.

Note that before your variable can come into play, you have to define it; This is done by making an assignment and prefixing it with "int ". For example:
int x = 4
Note that Zelda Classic predefines some variables for it's own use, such as Link's HP, as ShadowTiger explained. Feel free to take advantage of these predefined variables; By assigning them values, you can change things going on in the game. You DO NOT need to define these types of variables, the game did it for you already.

Now, let's take a look at how we can USE these assignments and expressions in code.

IF
Example:
CODE
if (swords > 0)
{
hippies -= 1;
}

IF branches are used when you want to execute a code, but only under certain circumstances. When your program comes across the IF, it checks the expression ahead of it; If the expression is resolved to TRUE, it moves on and executes the code enclosed in the { and } below. If it's resolved to FALSE, it skips past that block of code.
In the example above, it's simple enough; If the player has more than zero (at least one) swords, a hippie is removed. (You can imagine this example was inspired by Hunter P Brown)
This branch is the staple of just about any form of programming; I can't imagine a single program that DOESN'T use an if statement somewhere in their programming. It's valuable, simple and flexible.

ELSE
Example:
CODE
if (swords > 0)
{
hippies -= 1;
}
else
{
swords += 1;
}

This, when used immediatly after an IF statement (more specifically, after the } of the code block), causes the code block AFTER it only to execute if the IF statement's expression resolved as false. Put simply, it happens if the previous stuff didn't; In the above example, if the player doesn't have any swords, one is added for him.

Please note that you can bunch up the { } with other parts of the code, and you can prefix any line with as many spaces as you want. So it would look a lot better like this, and still run:
CODE
if (swords > 0) {
hippies -= 1;
} else {
swords += 1;
}

See, if you indent the blocks of code, it looks a lot nicer.

-post continued below-

WHILE
Example:
CODE
while (swords > 0)
{
hippies -= 5;
swords -= 1;
}

This is like an if branch, however, repeated. It checks the expression to see if it's TRUE or FALSE; If it's true, it performs the code below; However, instead of continuing with the rest of the code, it checks the expression again, and if it's still true, does it again. It will keep repeating this until the expression is finally false, in which case the program continues on with the rest of the code.
In the example above, the while loop is set so that as long as the player has swords, five hippies are removed; each time this happens, a sword is removed, however. So if the player had five swords, 25 hippies would be removed, at which point the player would be out of swords.
Please note that code executes faster than your eye can see, however while code is running, nothing else in ZC will happen! This means if you set up a while loop that never finds an end, the program will freeze.
If you want to make it so that the code is running simultaneously to ZC's other actions (say, you want to make it so that several times a second, something happens while you're walking) be sure to put the "Waitframe();" command somewhere in the loop, like this:
CODE
while (1 == 1)
{
Link->HP += 1;
Waitframe();
}

Note that Link->HP, divided by 16, shows how many hearts Link has. This script, while running, will continually recover Link's HP (at a fairly absurd rate, to boot).
Waitframe() causes the script to halt, ZC runs normally for a split second, and the script then picks up where it left off. A simple script like this shouldn't cause any noticeable interruptions, because your computer is much faster than your eyes.

FOR
Example:
CODE
for (countdown = 6000; countdown > 0; countdown -= 1)
{
Waitframe();
}
Link->HP = 0;

For loops are fairly complicated and intimidating for new programmers, especially if not explained properly. They're actually simple though. What they do, as Fire Wizzrobe explained, but as I feel I can explain a tad better:
A FOR loop is like the WHILE loop, but a bit more sophisticated. They are usually used to perform things a certain number of times; Although a while loop can do the same thing, people seem to like this a lot.
First, the command at the left side of the () is executed. This is usually an assignment. Following that, the commands in the {} are executed, followed by the command at the right side of the (), which tends to alter the value defined in the left side command by a small amount. Then it checks the expression in the middle of the ()... if it returns true, it continues the loop by executing the {} command, then the right-side command, checking the expression again, and so on.
The example shown above causes a countdown to occur. Due to the waitframe() command, it will count down as you play the game if the countdown reaches 0, Link dies. This could be useful in a Metroid quest (e.g. escaping the Tourian shaft after Mother Brain), or anything that involves an escape.
To be more specific for the example, the countdown is set at 6000, and after every frame, it reduces by 1; When the countdown reaches 0, the loop ends, and Link dies.
If you still don't understand the for loop, don't fret, because the same can be accomplished as follows:
CODE
int countdown = 6000;
while (countdown > 0) {
countdown -= 1;
Waitframe();
}
Link->HP = 0;

Remember that like WHILE loops, you will need the Waitframe() command in a FOR loop, if you want the script to occur simultaneous to everything else. If you forgot the waitframe() command, the machine would just count down to 0, while you couldn't do anything; and Link would die.

I hope this makes Zscript easy to understand. And plus, if you figured this out and didn't know anything about programming beforehand, congratulations, because you now understand some basic programming concepts! (Expressions, assignments, variables, if/else, while, for)

Edited by Stungun, 04 February 2007 - 06:04 PM.


#5 CastChaos

CastChaos

    Deified

  • Members

Posted 05 February 2007 - 01:16 PM

Great, we have some basics, however I'd like to see that mastermind who could make something like the Cane of Somaria script just after reading these. My suggestion is that somebody come, put here a complicated (or not so complicated, but not so simple) script and explain every line why is it there, what other could come there, what would that modify and what shouldn't go there.

CODE

ffc script helping newbs
{
void run (whatever goes here)
{
IF ( somebody's want = helping)
  {
  put here = that explanation
  }
  else (at least explain 'position' things)
}
}


(So, the subscreen editor crashes in b142 because the developers forgot a 'waitframe' command?)

EDIT: Very important! What do I do after writing... erm, importing a script? When I click Compile!!! I'm just given a lot of empty... things. Plus what's the difference between FFC, Global FFC and Item FFC?

Edited by CastChaos, 05 February 2007 - 01:30 PM.


#6 Nimono

Nimono

    Ultra Miyoa Extraordinaire!

  • Members
  • Real Name:Matthew
  • Location:Static Void Kingdom

Posted 05 February 2007 - 02:30 PM

But what of Item Scripts? Item scripts are special scripts that activate only once, and that's when you use them. They are basically like FFC scripts, but must use item in place of ffc at the opening. Here's an example:

CODE

import "std.zh"
item script Link Mirage
{
     void run()
     {
          int LinkM = Screen->LoadFFC (1);
          LinkM = Link->X + 16;
     }
}


That right there would make a certain FFC appear 1 tile to the right of Link every time the Item is used. But what if you wanted to make up to 4 Mirages, have only one of them get modified at a time, have each be another tile away from the last one, and have it modify the first mirage if you try to make a mirage after you make a fourth one? Do this:

CODE

import "std.zh"
item script Link Mirage
{
     int AddingFFC = 1;
     int AddingPosition = 16;

     void run()
     {
          int LinkM = Screen->LoadFFC (AddingFFC);
          LinkM = Link->X + AddingPosition;
          if (AddingFFC < 4)
          {
               AddingFFC + 1;
          }
          else AddingFFC = 1;
          if (AddingPosition < 64)
          {
               AddingPostion + 16;
          }
          else AddingPosition = 16;
     }
}


Note the two varialbles put before void run(). These, since they are before Run, are declared as Global Variables. They never reset, meaning you can keep modifying them even after a script quits. However, this means that they won't even get reset if you change screens. You'd have to do a screen check for that. I can tell you how to do that, but.... That's a bit more advanced. For now, just know that for variables that you need modified for Item Scripts even after the script ends, USE GLOBAL VARIABLES. They only run their int parts ONCE, which makes it very easy to work with them. It might even be best to declare ALL your variables as Global, but.... Don't push it. Don't declare a variable as Global if it's going to get modified in the script before any checks of it or anything happen. If you have a variable that gets set to Link's current HP, THEN checked for, it won't do you any good to declare it as Global, unless another script needs it....

#7 AgentLym

AgentLym

    • Zelda and Pokémon Master •

  • Members
  • Location:Skyloft

Posted 24 April 2008 - 09:40 PM

Wow... I think I get some of this stuff now! I can not wait to make a script!

Stungun, your tutorial was the best (and most understandable) beginners tutorial for scripting I've seen yet! Make more! icon_biggrin.gif

And if someone could explain both of Matthew's scripts, line by line; that would help me a bunches! icon_razz.gif

Thanks! I think I'll make a script right now. (it won't be too complicated, though)

Note: When and if you guys make tutorials, make sure the readers know what expressions, variables, and such are. I was lost until I found Stungun's tutorial, but now I sorta know what and how scripts work!
Just make sure that if you use something that is not intuitive ("int"? I thought it meant "initiative" the first time I saw it... I know it's "integer" now)

Thanks! icon_biggrin.gif

Edited by AgentLym, 24 April 2008 - 09:41 PM.


#8 Christian

Christian

    Summoner

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

Posted 24 November 2008 - 06:58 PM

wow! agentlym took the words out of my mouth! stungun your tutorial was the best one of all. please write more icon_smile.gif its all really simple! its just common math and expressions variables and equations hehe! oh yes, im am defitnetly going to try and make a script myself. if anyone can expalin what other symbols in zscript are ..it can be so much help... thanks for your sharing of knowledge stungun. icon_wink.gif


0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users