Yscript 2023-03-19
Referenzen
Grundlagen
Yscript ist eine flexible, halb-kompilierende Skriptsprache für sämtliche ''YDK''-Projekte.Zum Aufwärmen das klassische "Hello World!" in Yscript:
class Demo
{
function onLoad()
{
deb( 'Hello World!' );
}
}
Include-Direktive
Zur Vereinfachung kann der Quelltext in mehrere Dateien ausgelagert werden, die mit der "#include" Direktive ''vor'' dem Kompilieren automatisch wieder eingefügt werden:#include "sample.txt"
Da die Includes vom Präprozessor ersetzt werden, kann man ''jeden'' Teil des Quelltextes auslagern, seien es ganze Funktionen, Textabschnitte oder gar einzelne Buchstaben, wobei Letzteres wohl kaum praktischen Nutzen finden wird. Hier ein Beispiel für einen ausgelagerten String:
Inhalt von "meldung.txt"
"Hallo!"
Das Hauptskript:
class Demo
{
function onLoad()
{
deb( #include "meldung.txt" );
}
}
Entsprechend ist allerdings kein Einbinden von Quelltext während der Laufzeit möglich. Einmal kompiliert kann das Skript nicht mehr verändert werden.
Sprungmarken
Mittels "mark" und "jump" kann der Skriptverlauf verändert werden:function Test()
{
mark start;
deb( "Dies ist ein Test!" );
if( din( "Nochmal testen? (y/n)" ) == "y" )
{
jump start;
}
}
Hier wird die Sprungmarke "start" definiert und genutzt. Alternativ kann der Sprungmarkenname auch in Klammern angegeben werden:
mark( endlos );
jump( endlos );
Konstanten
Intern
bool false; // Falsch (0)
bool true; // Wahr (-1)
var cmp; // Liefert den aktuellen Wert aus "switch"
object this; // Liefert das aktuelle Skriptobjekt
Mathematisch
float Pi = 3.141593; // Kreiszahl (Pi)
float Pi2 = 6.283186; // Pi * 2
float Pi14; // Werte von 1/4 Pi bis 8/4 Pi.
float Pi24; // Entspricht dem vollen Kreis
... // in 45 Grad Schritten, zur
float Pi84; // besseren Übersicht im Code.
float e = 2.718282; // Eulersche Zahl
float Sqr2 = 1.414214; // Wurzel aus 2
float DTR = 0.017453; // Grad zu Rad
float RTD = 57.29578; // Rad zu Grad
Keys
int keyUp; // Pfeiltaste oben
int keyRight; // Pfeiltaste rechts
int keyDown; // Pfeiltaste unten
int keyLeft; // Pfeiltaste links
Variablen
Variablen zuweisen
Variablen werden in Yscript mit dem Dollarzeichen "$" angesprochen. Dadurch unterscheiden sie sich von den Konstanten, die ohne Vorzeichen notiert werden. Vorsicht: Auch Variablen sind in Yscript case-sensitive, "$test" und "$Test" sind also unterschiedlich.$a = "Test"; // Der Variable a einen Wert zuweisen
deb( $a ); // Den Wert der Variable "a" ausgeben ("test")
deb( Pi ); // Den Wert der Konstante "PI" ausgeben (3.141593)
unset( $a ); // Variable "a" aus dem Speicher entfernen
deb( $a ); // Fehler, da $a nicht mehr existiert
Im Beispiel wurde $a ein String zugewiesen und dadurch gleichzeitig der Datentyp von $a festgelegt. Andere Datentypen werden folgendermassen initialisiert:
$a = true; // Bool: "true" oder "false"
$b = 0b0; // Byte: "0b" + Zahl
$c = 0; // Int: Nur Zahlen (bzw. Minuszeichen)
$d = 0x0; // Int: "0x" + Hexzahl
$e = 0.0; // Float: Zahlen mit Dezimalpunkt (bzw. Minuszeichen)
$f = ""; // String: in '' oder "" gefasst
$g = []; // Array: eckige Klammern
Alternativ kann man dafür auch die entsprechenden Casting-Funktionen verwenden:
$a = cbool(); // Als Boolean initialisieren Wert: false
$b = cbyte(); // Als Byte initialisieren Wert: 0
$c = cint(); // Als Integer initialisieren Wert: 0
$d = cfloat(); // Als Float initialisieren Wert: 0.0
$e = cstr(); // Als String initialisieren Wert: ""
$v = cvar(); // Als Variant initialisieren Wert: ""
Typecasting
Dieselben Funktionen können auch für Typecasting (ändern des Datentyps) verwendet werden:$a = "412.5"; // Variable $a als String erstellen
$a = cfloat( $a ); // Wert von $a in Float umwandeln
deb( $a ); // Ergebnis: 412.5
$a = cint( $a ); // Wert von $a in Integer umwandeln
deb( $a ); // Ergebnis: 412
$a = cbyte( $a ); // Wert von $a in Byte umwandeln
// -> Fehler, weil Byte nur von 0 bis 255 reicht
Datentypen
Name | Min | Max |
bool | 0 | -1 |
byte | 0 | 255 |
int | -2^31 | 2^31 |
float | -3.402823E+38 | 3.402823E+38 |
str | 0 Zeichen | 2^16 Zeichen |
Name | Beschreibung |
array | Ein Array beliebiger Grösse |
object | Ein beliebiges Objekt |
var | Variabler Datentyp (nicht empfohlen) |
Arrays
Arrays werden in Yscript als Variablen des Typs "array" gehandhabt. Einzelne Elemente des Arrays werden mit eckigen Klammern "[" und "]" angesprochen, wobei der Index entweder eine Zahl (byte, int) oder ein Text (string) sein darf. Andere Datentypen im Index werden nach Möglichkeit umgewandelt (float zu int).Zu beachten: Arrays in Yscript sind 1-based, d.h. das erste Element eines Arrays findet man auf [1] statt auf [0].
$a = [1,2,3]; // Array "a" mit den Werten 1, 2 und 3 erstellen
$a[5] = 250; // Eine Zahl im 5. Feld des Arrays "a" speichern
$a[10] = "foo"; // Einen String im 10. Feld des Arrays speichern
$a["test"] = 12; // Wert im Feld "test" des Arrays speichern
deb( $a[2] ); // Wert des 2. Feldes ausgeben (liefert 2)
deb( $a[10] ); // Wert des 10. Feldes ausgeben (liefert "foo")
deb( $a["test"] ); // Wert des Feldes "test" ausgeben (liefert 12)
Mehrdimensionale Arrays
Bei multidimensionalen Arrays werden die Indexe durch Kommata getrennt:$map[5,8] = 100; // Wert im 8. Feld des 5. Feldes speichern
$row[8] = [6,12,18]; // Hier wird ein mehrdimensionaler Array konstruiert,
$map[1] = $row; // indem ein Array in einem Array gespeichert wird.
deb( $map[1,8,2] ); // Liefert 12
// Oder als verschachtelte Listen
$temp = [ "eins", [ "foo", "bar" ], "zwei" ];
Arrays durchgehen
Eine häufige Anwendung ist das simple durchlaufen eines Arrays. In Yscript funktioniert das so:$list = [ "Hallo", "wie", "gehts?" ];
// Durchlaufen des Arrays mittels For-Schleife
// Man beachte dass $a von 1 bis und mit count() läuft:
for( $a = 1; $a <= count( $list ); $a++ )
{
deb( $list[$a] ); // Texte ausgeben
}
Alternativ kann man mit $a auch von 0 bis count() gehen und stattdessen den Index um +1 anpassen:
for( $a = 0; $a < count( $list ); $a++ )
{
deb( $list[$a+1] ); // Texte ausgeben
}
Element vorhanden?
Um herauszufinden, ob ein bestimmtes Element im Array vorhanden ist, kann man auch den Find-Operator (?=) verwenden. Er liefert den Index des gefundenen Feldes oder 0, wenn das Element nicht gefunden wurde:$list = [ "Test", "Haus", "Welt" ];
$x = ( "Test" ?= $list ); // $x wird zu 1
$x = ( "Welt" ?= $list ); // $x wird zu 3
$x = ( "Hund" ?= $list ); // $x wird zu 0
Soll nur abgefragt werden, ''ob'' das Element vorhanden ist, kann man den Operator direkt in ein if() packen:
if( "Test" ?= $list )
{
// Wenn Test in der Liste ist...
}
Arrays verändern
Neue Elemente werden mit der "push" Funktion in einen Array eingefügt. Soll einfach nur ein Element am Ende des Arrays angehängt werden, kann eine leere eckige Klammer verwendet werden:$temp = []; // Leeren Array erstellen
$temp[] = "foo"; // Element hinzufügen
push( $temp, "bar" ); // Weiteres Element hinzufügen
push( $temp, "x", 2 ); // Element an 2. Stelle einfügen
dump( $temp ); // Ergebnis: array( "foo", "x", "bar" );
Elemente werden mit "pop" wieder aus dem Array entfernt. Wird dabei kein Index angegeben, schneidet pop() einfach das letzte Element des Arrays ab.
$temp = [ 10, 20, 30 ]; // Array mit drei Zahlen erstellen
$x = pop( $temp, 2 ); // Element an 2. Stelle entnehmen
deb( $x ); // Ergebnis: 20
dump( $temp ); // Ergebnis: array( 10, 30 );
Strings
Strings werden in Yscript in Anführungszeichen " oder Apostrophe ' gefasst. Der Unterschied ist, dass Strings in Apostrophen nicht ausgewertet werden, also Steuerzeichen wie "\n" tatsächlich als "\n" ausgegeben werden (und nicht eine neue Zeile verursachen).deb( 'Hallo\nWelt!' ); // Ergebnis: Hallo\nWelt!
deb( "Hallo\nWelt!" ); // Ergebnis: Hallo
// Welt!
- Die maximale Länge beträgt 65'536 Zeichen (2^16).
- Alle Strings werden im Format ISO-8859-1 gehandhabt.
Steuerzeichen
<pre>\n Neue Zeile (Charcode: 10)
\t Tabulator (Charcode: 9)
\" Anf.zeichen (Charcode: 34)
\\ Backslash (Charcode: 92)
</pre>
Strings verketten
$text1 = "Bei"
$text2 = "text";
deb( $text1 + $text2 ); // Ergebnis: "Beitext"
deb( $text1 + "spiel" + $text2 ); // Ergebnis: "Beispieltext"
Strings vergleichen
if( "Hallo" == "Hallo" ) // Trifft zu
if( "Hallo" == "HAllo" ) // Trifft nicht zu
if( "Blubb" != "Bloed" ) // Trifft zu
if( "Lampe" != "Lampe" ) // Trifft nicht zu
Mustervergleich
if( "Test" % "T*" ) // Trifft zu
if( "Test" !% "T*" ) // Trifft nicht zu
if( "Test" % "Te?t" ) // Trifft zu
if( "Test" % "T??st" ) // Trifft nicht zu
if( "Test" % "T[aei]st" ) // Trifft zu
if( "Test" % "Te[aei]t" ) // Trifft nicht zu
Strings umwandeln
$text = "Hello World!";
deb( !$text ); // Ergebnis: "!dlroW olleH" (Reversion)
deb( $text - "l" ); // Ergebnis: "Heo Word!" (Extraktion)
Verzweigungen
Yscript unterstützt die grundlegenden if/else -Verzweigungen wie jede andere Programmiersprache.if( $x == 0 )
{
deb( "X ist null." );
}
else if( $x == 1 )
{
deb( "X ist eins." );
}
else
{
deb( "X ist unbekannt" );
}
Switches
Switches in Yscript dürfen maximal 256 Cases beinhalten. Yscript erlaubt aber die Verwendung von mathematischen Operatoren innerhalb einer "case" Anweisung, wobei "case" wie eine Variable behandelt wird, die mit "==" abgefragt werden kann. Wird kein Operator angegeben, so fügt der Compiler automatisch das "==" ein:switch( $a )
{
case 5: // Wenn $a gleich 5
case == 5: // Dasselbe mit explizitem Operator
case $x: // Wenn $a gleich $x
case > 7: // Wenn $a grösser 7
case < 4: // Wenn $a kleiner 4
case >= 9: // Wenn $a grösser gleich 9
case <= 2: // Wenn $a kleiner gleich 2
default: // Alle anderen Werte für $a
}
Entsprechend ist es auch möglich, komplexere Cases zu schreiben oder Funktionen aufzurufen, diese "Hacks" werden allerdings nicht empfohlen - stattdessen sollte man mit "if" arbeiten und ggf. die Werte zwischenspeichern.
switch( $a )
{
case foo(): // Wenn $a mit der Rückgabe von foo() übereinstimmt
case $x + 2: // Wenn $a gleich $x + 2
case % 2 == 0: // Wenn $a modulo 2 gleich 0
case == 3 && $x == 8: // Wenn $a gleich 3 und $x gleich 8
case * 8 == 24: // Wenn $a * 8 gleich 24 (wobei 3 die einzige Lösung ist)
}
Dies wird ermöglicht, indem der Wert innerhalb von "switch" in der Konstante "cmp" zwischengespeichert wird. Obwohl keine gängige Praxis lässt sich das einsetzen, um in einem Case den Wert mehrfach zu prüfen:
switch( $a )
{
case > 3 && cmp < 7: // Wenn $a zwischen 3 und 7
}
Der Find-Operator
Der Find-Operator geht eine Liste (Array) von Werten durch und prüft, ob der gesuchte Wert mit einem davon übereinstimmt. Falls ja wird der Index zurückgegeben, andernfalls 0. Der Vorgang wird abgebrochen, sobald ein passender Wert gefunden wurde, die Liste sollte daher nach (geschätzter) Häufigkeit sortiert werden.// Mehrere mögliche Werte
if( $a == 10 || $a == 15 || $a == 20 )
// Dasselbe mit Find-Operator
if( $a ?= [10, 15, 20] )
// Anwendung mit Strings
if( $text ?= ["hi", "hallo", "hoi"] )
// Anwendung mit case
switch( $a )
{
case ?= [1, 2, 3]: // Wenn $a gleich 1, 2 oder 3
}
// Mit komplexen Elementen
if( $x ?= [$test, foo(), $y / 10] )
Da der Find-Operator den Index des gefundenen Elements zurückgibt, kann man ihn auch benutzen, um Elemente eines Arrays zu finden:
// Um Elemente zu finden
$list = [20, 42, 10, 8];
$pos = ( 42 ?= $list );
deb( $pos ); // Gibt 2 aus
Sollte das Element nicht gefunden werden, wird 0 zurückgegeben.
Klassen
Klassen sind ein wichtiger Bestandteil von Yscript, allerdings sind sie nur bedingt mit den Klassen aus anderen Programmiersprachen vergleichbar. Die Klassen in Yscript nehmen hauptsächlich eine Rahmenfunktion ein und ermöglichen ausserdem die Wiederverwertung und Zentralisierung von Funktionen.Beispielklasse:
class Torch
{
$state = 0;
function LightOn()
{
$state = 1;
}
function LightOff()
{
$state = 0;
}
function getState()
{
return $state;
}
}
Diese einfache Beispielklasse "Torch" (Fackel) enthält eine Variable $state und zwei Funktionen, um diese zu verändern. Die dritte Funktion dient dazu, die $state Variable von aussen abzufragen. Die Funktionen können deshalb auf die Variable zugreifen, weil sie innerhalb der Klasse selbst definiert wurde, sie wird daher auch eine Klassenvariable genannt. Das heisst aber nicht, dass alle Torches auf dieselbe $state-Variable zugreifen - für jede Instanz werden eigene Variablen generiert.
Eigenheiten
Preisliste
Eigene Funktionen aufzurufen sollte man in Yscript möglichst vermeiden, da der Overhead beim Übertragen der Parameter relativ gross ist. Interne Funktionen hingegen stellen keine Bremse dar und auch Schlüsselwörter wie "if", "switch" oder "for" bremsen das Skript nicht aus. Am schnellsten ist der Umgang mit Variablen, wobei Arrays naturgemäss ein wenig unterliegen.Dabei darf allerdings nicht vergessen werden, dass Skripts generell ca. 11x langsamer ausgeführt werden als native Programmteile.
Gültigkeitsbereiche
Yscript unterscheidet drei Gültigkeitsbereiche:<table width="400" style="border:1px solid #A0A0A0; background:#F8F8F8; color:black; margin-bottom:20px;">
<tr class="head" style="background:#F0F0F0;">
<td style="border-bottom:1px solid #A0A0A0; color:#808080;">Name</td>
<td style="border-bottom:1px solid #A0A0A0; color:#808080;">Beschreibung</td>
</tr>
<tr>
<td valign="top">Global</td><td>Im gesamtem Skript gültig</td>
</tr>
<tr>
<td valign="top">Lokal</td><td>Auf das dazugehörige Objekt beschränkt</td>
</tr>
<tr>
<td valign="top">Temp</td><td>Auf die Funktion bzw. Parameter beschränkt</td>
</tr>
</table>
Es gibt allerdings keinen seperaten Bereich für Schleifen und dergleichen:
function test()
{
for( $a = 0; $a < 10; $a++ );
deb( $a ); // Ergebnis: 10
}
Allgemein sollte man wissen, dass die Scopes in Yscript (noch) nicht ausgereift sind und es möglicherweise Probleme geben kann, wenn Variablen über mehrere Klassenfunktionen hinweg weitergegeben werden. Bislang konnte dies allerdings nicht bestätigt werden.
Klassen-Konstruktor
In Yscript gibt es keinen seperaten Konstruktur, stattdessen wird der Code im Klassenkörper dazu verwendet. Alle Codezeilen, die nicht in einer Funktion verpackt sind, werden also vom Compiler aussortiert und in eine interne Konstruktorfunktion verschoben. Folgendes Beispiel soll dies verdeutlichen:class Level
{
if( $nacht )
{
// Licht auf Mondschein
setLight( 0, 20, 80 );
} else {
// Licht auf Sonnenschein
setLight( 255, 255, 255 );
}
// Umgebung einrichten
createLights();
importEnemies();
}
Dieser Code wird ausgeführt, sobald die Klasse mit new() instanziert wird.
Was wird nicht kompiliert?
- Namensauflösungen und alle Funktionsauflösungen werden bei Bedarf zur Laufzeit ausgelesen.
- Alle mathematischen Operationen werden zur Laufzeit berechnet. Ein "1+2+3" im Quelltext sollte daher bereits vom Entwickler durch "6" ersetzt werden, um Rechenleistung zu sparen. Möglicherweise wird ein späterer Compiler dies berücksichtigen.
- Die Konstante "cmp" ist keine Konstante, sondern wird zur Laufzeit jeweils dem Switch-Wert zugewiesen.
- Alle Funktionsaufrufe werden zur Laufzeit generiert, d.h. es wird beim Kompilieren nicht geprüft, ob die Anzahl Parameter oder ihr Datentyp zu der Funktion passen. Überschüssige Parameter werden übrigens ignoriert, während fehlende Parameter durch leere Strings ersetzt werden. Dieses Verhalten wird evtl. noch geändert.
Bugs & Probleme
Variablen verschwinden nach Funktionsaufruf
Status- Gelöst - der Bug wurde behoben.
Problem
Werden bei einem fremden Funktionsaufruf Variablen eines fremden Objekts übergeben, so wird die Funktion im falschen Scope ausgeführt (im Scope der aufrufenden Funktion) und kann daher nicht auf die Klassenvariablen ihres Objekts zugreifen.
Das Problem liegt in der unsauberen Organisation der Gültigkeitsbereiche, eine Korrektur ist nicht zu erwarten. Ein Workaround existiert. Möglicherweise existieren noch weitere Bugs, die auf fehlerhaften Scopes basieren.
Symptome
- Variablen, die als Parameter übergeben wurden, verschwinden nach einem Funktionsaufruf wieder, innerhalb der Funktion funktionieren sie jedoch korrekt.
- Eine Funktion kann nicht auf Klassenvariablen zugreifen bzw. sie sind nicht definiert.
Lösung
Die fremde Variable temporär zuweisen und diese Variable als Parameter verwenden.
Syntax-Adapter
In der Grundausstattung hat Yscript grosse Ähnlichkeit mit ''PHP''. So werden Codeblöcke in geschweifte Klammern verpackt und Variablen mit einem Dollarzeichen gekennzeichnet. Diese sehr technische Form eines Quelltextes ist aber oft unpraktisch, z.B. wenn man eine NPC-Diskussion skripten will. Für solche Fälle gibt es ''Syntax-Adapter'', welche die Eingabe des Codes vereinfachen.Discussion
Der Diskussionsadapter vereinfacht die Übersetzung von verzweigten Gesprächen in Yscript.Als Adapter-Tag wird "talk" verwendet. Die Verzweigungen werden durch Tabs (aka. Einrücken) realisiert und die Texte werden durch den Zeilenumbruch abgeschlossen. Innerhalb eines Textes kann einen Zeilenumbruch mittelns "\n" erzielt werden. Die Struktur sieht folgendermassen aus:
[talk]
Gesprächstext 1
Gesprächstext 2
Gesprächstext 3
Frage
Antwort 1
Gesprächstext 4
Gesprächstext 5
Gesprächstext 6
Antwort 2
Gesprächstext 7
Antwort 3
Gesprächstext 8
Gesprächstext 9
[/talk]
Hier werden zuerst die Texte 1-3 nacheinander eingeblendet, dann die Frage mit 3 Antwortmöglichkeiten. Die Antwort-Zeilen sollen dabei nicht die kompletten Antworttexte sondern nur eine kurze Beschreibung dessen enthalten. So kann der Spieler seine Antwort schnell aussuchen (z.B. "Zustimmen" oder "Ablehnnen") und damit einen Dialog auslösen ("Zustimmen" => "Also gut, ich nehms. Aber nur, weil du es bist.").
Antworten ausblenden
Soll eine Antwort ausgeblendet werden, kann in Klammern ein Skript-Ausdruck davorgestellt werden:[talk]
Gespräch
Frage
Antwort 1
if( $answer ) Antwort 2
if( $x == 2 ) Antwort 3
[/talk]
Die jeweilige Antwort wird angezeigt, wenn der Wert in Klammern "true" ergibt.
Skripts im Gespräch
Innerhalb der Diskussion kann auch normaler Skript-Code aufgerufen werden. Dazu muss am Anfang der Zeile (also direkt nach den Tabs) eine Raute "#" notiert werden, dann der Code.Gespräch 1
Gespräch 2
# doSomething();
Gespräch 3
Wichtige Hinweise
Verzweigungen werden in Yscript mittels Callback-Funktionen gelöst. Entsprechend wird jedes Gespräch vom Diskussionsadapter in mehrere Callback-Funktionen verteilt. Diese Funktionen werden "d_cb_###" genannt und direkt in der Klasse platziert.Daraus entstehen zwei wesentliche Einschränkungen:
- Sprungmarken können grundsätzlich nicht verwendet werden. Ausnahme: Wenn ''mark'' und ''jump'' sich auf derselben Gesprächsebene befinden.
- Innerhalb von Gesprächen kann nur auf die Klassenvariablen des jeweiligen Objekts zugegriffen werden, da es nicht in der Funktion, in der es notiert wurde, ausgeführt wird, sondern in eigenen Callback-Funktionen.
Konkretes Beispiel
Hier ein konkretes Gespräch zwischen Leon und Tom. Die Namen werden im Gespräch jeweils am Anfang der Zeile notiert. Zeilen, die mit # beginnen, werden nicht umgewandelt sondern als Skriptbefehle interpretiert.class Abenteurer()
{
function onTalk()
{
$versagt = $player.flagged( "lost_item" );
[talk]
<Leon> Hallo.
<Tom> Hi Leon, wie gehts?
Geht so.
<Leon> Ganz gut.
<Tom> Freut mich, bis dann.
<Leon> Du gehst schon?
<Tom> Klar.
if($versagt) Lass mich in Ruhe...
<Leon> Ach lass mich...
<Tom> Was ist denn los?
<Leon> Naja...
<Tom> Sag?
Das "Goblinproblem"
<Leon> Die Goblins haben mich fertiggemacht.
<Tom> Sei doch froh, dass du noch lebst!
"Schatz nicht gefunden"
<Leon> Ich konnte den Schatz nicht finden.
# flash();
<Tom> Welchen Schatz?
Ehrlich antworten
<Leon> Den verfluchten Schatz der Piraten.
<Tom> Oh. Ich -äh- muss weg, machs gut!
"Hoppla"
<Leon> Sagte ich Schatz? Ich meinte Spatz.
<Tom> Natürlich, und jetzt zieh Leine.
Keine Zeit!
<Leon> Muss weiter, hab zu tun!
<Tom> Na dann...
[/talk]
}
}