Creation/Dev/GS1 To GS2

From Graal Bible

Introducing the new scripting engine

This document is describing some problems and solutions when switching from the old Graal scripting engine (GS1) to the new engine (GS2). The new scripting engine has been designed to on the one hand be compatible to the old engine, and on the other hand provide new functionality to make it easier to write Graal scripts and do more interesting things with it. Those new things include a windowing system (GUI), a particle engine, multi-dimensional arrays, function parameter passing, true object orientation.

Since the old engine has been designed 7 years ago, a lot of functionality has been added to that engine while the basic design has not been changed, so a lot of modifications can be more seen as a "hack" and are not very easy to use. For example numeric variables and string variables must be accessed in a different way, even more complex operations need to be used to access numeric arrays and string lists. The new scripting engine tries to stay compatible with the old functions, but also add easier ways to access the variables. Numeric and string variables can now be accessed the same way, as well as numeric arrays and string lists.

To make the new functionality easy to use, some old "hacky" ways of accessing variables are not supported anymore. If scripts are written correctly without using those methods, then they will work fine in the new engine. If not, then they need to be fixed manually. In generally there are not a lot of scripts using those problematic ways of accessing variables, but sometime scripters just use copy & paste and so also copy the error to a lot of scripts. Then the only solution will be to edit those scripts manually, copying the fixed code part to the other affected scripts, or directly use classes and the join function.

Using the correct syntax

The new scripting engine finally reports syntax errors, when modifying scripts you see the error output on RC chat (RC is short for RemoteControl, an admin tool for Graal). If there are errors while the npcserver is starting up, then the errors are written in to logs/syntaxerrors.txt. The new engine also reports errors while running the scripts, including endless loops (loops are limited to 10000 cycles) and call of non-existing functions. On clientside those errors are output into the F2 window, on serverside they are displayed on RC chat, but can also be redirected to file using the server option logscripterrorstofile=true, the errors will then appear in logs/scripterrors.txt.

While some people might get headache when seeing a lot of script errors, that error reporting is actually giving you the possibility to track errors easily. While the new engine is not accepting as many syntax hacks as the old engine, it is actually easier now to fix them.

Closing brackets

An error that often happens is a missing opening bracket or missing closing bracket. The best way to not accidently do that error is to follow a constant and ordered scripting style, indenting scripting lines, using spaces between operators. Each time you open a { bracket you indent the following lines by 2 spaces. You can also use 4 spaces, that doesn't matter as long as you keep the same number of spaces in the whole script. Tabulators are not recommended since special characters are automatically removed by the engine. When you afterwards use a }, then you stop the indenting. That way you can check the correct number of opening and closing brackets by just watching if the last closing bracket appears at the very beginning of a line. The Graal script compiler doesn't require that indenting, but it is still recommended.

Semicolon

A thing that is quite simple to keep care of but is often done wrong is to add semicolons behind commands. That must be done to separate the commands from each other. In the old scripting engine it was possible to forget the semicolon if the next character was a closing bracket }. The bracket was automatically finishing the last command. In the new engine that it possible too as long as the last command is a function call, it's not working if the last command is an assignment:

Bad:  if (playerenters) {x=3}
Good: if (playerenters) {x=3;}

Scripters also often think that a closing bracket } never needs a semicolon behind it, although the semicolon is required if the brackets are used for defining an array or subscript:

Bad:  setshape2 2,2,{0,0,0,0}
Good: setshape2 2,2,{0,0,0,0};

That mainly affects the function calls setshape2, showpoly and putnpc2. Those always require a semicolon at the end, no matter if the function parameters are finishing with a closing bracket }.

Syntax incompatibilities

While most old scripts compile fine when having no syntax error, there is one exception when a correct old script is not working with the new scripting engine: the in-operator is different:

Works in GS1:  i = (playerx,playery in <20,40>);
Works in both: i = (playerx in <20,40> && playery in <20,40>);
Works in GS2:  i = ({playerx,playery} in <20,40>);

Variable lookup

As described in chapter one, the variable lookup has been changed in the new scripting engine. Lookup means the behaviour how variable names are translated into actual object attributes, or to be more close to the hardware, lookup is the translation into memory addresses. That means that a variable that was formerly connected to one value, is now connected to another value, and eventually having different behaviour when modifying it, and is so changing the logic of the script or even mess it up.

Example

Several scripts were using the variable this.x to store temporary data, or just as for-loop variable. While in the old engine this.x was just a numeric scripting variable, it is now redirecting to the x value of the current script object (this). So if you modify it, then the npc is magically starting to move, which was probably not the intention of the scripter. In that case the variable name must be modified to something like i or temp.i.

Variable types in the new scripting engine

The new scripting engine (GS2) has basicly 3 types of variables:

  • Object variables: objectname.variablename
    Object can be this for the current object, or player or an NPC name like DB_Items. The variable will then access the attributes of the object if there is a built-in attribute with that name (e.g. this.x), or a script variable belonging to that object (e.g. this.i)
  • Attributes of the active object: attributename
    If there is no leading, then the attributes of the current script object (for which the script is executed) are accessed, if there is an attribute with that name, e.g. x.
  • Global variables: variablename
    If the variable name has no leading and is not matching a built-in attribute of the current script object, then it is taken from the global variable pool. This is different to most other scripting languages where variables without leading are seen as local variables, but this behaviour is required to be compatible with the old scripting engine. Also it is a simple way to share data between scripts - you can set a flag in one script and read the flag in another script without needing to know where it is stored. Global variables are not saved, so they are not persistent and disappear after a server restart.

There are a few exceptions to this lookup scheme, e.g. function parameters can be accessed without a leading in front of the variable name. Also the with operator can temporary modify the current script object (this). In generally you don't need to care about those exceptions, to be sure that everything is working fine you should prefer to add a leading in front of variable names though.

Change of variable lookup

Attributes of the current object

As described in chapter 3.1., the built-in attributes of the current object can be accessed by this.x, this.y etc. now. In the old scripting engine that was not possible, those variable names were used for normal variables. That means if your scripts are using such variables, make sure that it is used for the correct thing. That includes numeric variables (this.x = 3;) as well as string variables (setstring this.x,3;);

Most common variables that need to be changed:

  • this.x, this.y, this.dir, this.hp, this.ap
  • this.hp on serverside
  • this.width, this.height, this.image

Global variables

Variables without leading are global now. While on clientside that was already the case in the old scripting engine, on serverside those variables were formerly only accessible in the current script. If you are using such variables, don't trust that other scripts are not modifying it. Eventually rename those variables, or use this.variables which are only working for the current object, or temp.variables which are only working in the current function body.

With-Operator

The with operator is temporary changing the current script object (this): all this. variables inside the opening and closing bracket are redirected to the with-object instead of the executing script object. Example:

this.myvar = 3;
with (findplayer("Max")) {
  this.myvar = 5;
}

After executing that code, myvar of the npc has the value 3, while myvar of the player "Max" has 5. The player "Max" is the with-object in this case. While both variables are accessed using the same variable name, they are directing to different variables. In the old scripting engine this was only modified when the with-object is another npc. In the new engine any script object can overwrite this.

Another thing that you need to keep care of is that you can also access the built-in variables of the with-object without using the this leading:

x = 20;
with (findnpc("Turret")) {
  x = 30;
}

In this case the x position of the current npc will be changed to 20, while the x position of the Turret will be changed to 30. The only exceptions for this behaviour are the variables x, y, dir and ap when using a player as with-object: an x without leading will never direct to the player.x variable, it will always take the npc for which the script is executed, or the npc that is positioned highest in the with-stack when using several with-operations inside each other.

Numeric variables, string variables and array using the same name

One major improvement in the new scripting engine is that you can easier access string variables and arrays. One drawback is that if you have used numeric variables, arrays or string variables with the same name, those are now directing to the same variable. So you will need to modify the variable names if they are supposed to hold different data:

Bad:
  this.myvar = 1;
  setstring this.myvar,2;
  this.myvar = {1,2,3};

Good:
  this.mynumericvar = 1;
  this.mystringvar = "2";
  this.myarrayvar = {1,2,3};

Easier access to string variables

You only need to read this chapter if you want to use the new way of accessing string variables, it is not required to make old scripts compatible.

String variables can be accessed easier now:
Writing string variables:

Old: setstring this.myvar,text;
New: this.myvar = "text";

Reading string variables:

Old: #s(this.mvyar)
New: this.myvar

You can see that string variables can now be accessed similar to numeric variables. Sometimes the variable name must have a different leading though to access the same variable:

Old: setstring varwithoutleading,text;
New: player.varwithoutleading = "text";

Also constructing dynamic variable names for string variables is different now, you need to use the new syntax. This documentation is not for explaining the complete syntax, but it might help to lead you to the correct path:

Old: setstring this.item#v(itemnumber),apple;
New: this.("item" @ itemnumber) = "apple";

You need to keep care on following things:

  • you concat strings using @
  • if you want to concat a string to another variable, don't forget to put "" around it to mark it as string
  • never put a dot inside the dynamic variable name:
Bad:  ("this.item" @ itemnumber)
Good: this.("item" @ itemnumber)
Good: ("Party" @ partynumber).name
  • never use array brackets in the dynamic variable name:
Bad:  this.("item[" @ index @ "]")
Good: this.item[index]
Good: this.("itemstats_" @ itemnumber)[index]

Server-side and client-side

The new scripting engine is reporting the calls of non-existing functions. As described in chapter 2, you can redirect those error reports to file using server option logscripterrorstofile=true, or you set the variable this.scriptlogmissingfunctions to false in case a script wants to call functions of unknown objects and doesn't want to flood the RC chat. Still it is not good to call non-existing functions, most of the time that function call actually was meant to have a purpose and is causing problems if it's not executed. In some cases the object might have disappeared or was accessed using a bad name. It might also be possible that the function actually doesn't exist: on many servers scripts are executed on server-side which are meant for client-side, and vice-versa. There are lists of available scripting functions, check those if you are not sure if the function is existing (Script Functions: NPC Server, Script Functions: Client).

You can add a line //#CLIENTSIDE in front of a script if it's meant for clientside execution, or remove that line if its meant for serverside executing. If you call the non-existing function on the bad side but rely on executing that function, then you will need to send a trigger to the other side or modify a variable, which can be read on the other side and reacted probably.

Common functions that are client-side only:

  • play(soundfile);
  • hurt(hurtpower);
  • addtiledef(image,levelstart,tilestype)
  • addtiledef2(image,levelstart,x,y)
  • showtext(index,x,y,font,style,text);
    also showani, showpoly etc.
    those might be added soon to serverside though, currently only showimg and changeimgcolors etc. are available on serverside

Common functions that are server-side only:

  • setlevel2(levelname,newx,newy);
  • putnpc2(newx,newy,script);

Miscellaneous

  • setmap/setminimap: The parameter order has been changed to match the server options bigmap= and minimap=, first parameter is now the text file with the level names, and second parameter is the map image (which is not used for the big map anymore though, it is always using auto-mapping). Note: these commands will not work soon anymore, use gmaps instead.