Scriptsystem

Aus wiki.3d-modell.design
Wechseln zu: Navigation, Suche

OMSI-Version: 1.0 - 2.0

Das Scriptsystem in OMSI ermöglicht es, dass Szenerieobjekte und vor allem Fahrzeuge mit individuellen Systemen ausgestattet werden können, die über Variablen und Funktionsaufrufe mit dem Hauptprogramm und der Grafik- und Soundengine kommunizieren.

Der hier beschriebene Stand des Scriptsystems entspricht OMSI Version 1.01.

Dateien des Scriptsystems

Das Scriptsystem umfasst folgende Dateien:

  • Scriptdateien mit ausführbarem Code (*.osc)
  • Varlist- und Stringvarlist-Dateien im OMSI\program-Verzeichnis zur Definition von Systemdateien und vordefinierten Dateien
  • Varlist- und Stringvarlist-Dateien zur Definition von Uservariablen
  • Constfile-Dateien zur Definition von Userkonstanten und Funktionstabellen

Abgesehen von den Dateien zur Definition von Systemvariablen müssen alle benötigten Dateien für jedes Fahrzeug/Szenerieobjekt (im folgenden kurz: Objekt) in dessen Konfigurationsdatei angemeldet werden.

Hierfür gibt es die Befehle [script], [varnamelist], [stringvarnamelist] und [constfile]. Der erste Eintrag jedes Befehls ist die Anzahl der Dateien, darauf folgen die entsprechenden Dateinamen (inkl. Pfad relativ zur Konfigurationsdatei. Die Namen der Dateien sind nicht relevant, solange sie korrekt in die Listen eingetragen werden.

Auf die Funktion der jeweiligen Dateien wird im Laufe der Beschreibung der Scriptsprache eingegangen.

Grundlagen der Scriptsprache

Das OMSI-Scriptsystem arbeitet mit der umgekehrten Polnischen Notation. Hierbei steht der Operator hinter den beiden Operanden; vereinfacht gesagt bedeutet es, dass die Operation " 1 + 2 " stattdessen folgendermaßen notiert wird: " 1 2 + ". Ein komplizierteres Beispiel: " (1 + 2) * (4 + 5) " entspricht " 1 2 + 4 5 + * ".

Ganz wichtig ist die Tatsache, dass die OMSI-Textdateien und insbesondere die Scriptsprache Case-Sensitive ist! D.h. es ist immer auf die Groß- und Kleinschreibung zu achten!

Stack und Register

Bei der Verarbeitung der Scripts verfügt OMSI über einen String-Stack und einen Stack für Gleitkommazahlen. Im folgenden ist mit "Stack" immer der Stack für Gleitkommazahlen gemeint, andernfalls wird von "String-Stack" gesprochen.

Beide Stacks enthält 8 Speicherplätze, welche von 0 bis 7 durchnumeriert sind. Jede Script-Operation kann nun einen oder mehrere neue Werte in einen der Stacks einfügen (push), wobei der neue Wert auf die Position 0 gesetzt wird und alle folgenden Werte "einen Platz aufrücken", oder sie kann einen oder mehrere Werte aus dem Stack herausziehen (pop/pull) oder lediglich auslesen (peek).

Fürs temporäre Ablegen von Zahlen gibt es zusätzlich acht indizierte Speicherplätze, welche direkt geschrieben oder gelesen werden können, die sich im sogenannten Register befinden - hier gibt es jedoch nur einen Register für die Gleitkommazahlen.

Beispiel-Operationen

Beispiel: Die Operation 1 + 4. Der Code hierfür lautet:

1 4 +

In der folgenden Tabelle wird demonstriert, wie sich hierbei der Stack verhält:

Operation:	Stack:	0	1	2	3	4 ...
-----------------------------------------------------------------------------
vorher:			0	0	0	0	0
"1"			1	0	0	0	0
"4"			4	1	0	0	0
"+"			5	0	0	0	0

Im Ausgangszustand ist der Stack nur mit Nullen (oder unbekannten/zufälligen Werten) gefüllt. Der Befehl "1" schiebt die Eins in den obersten Stackplatz und schiebt alle weiteren Werte (Nullen) nach hinten. Der Befehl "4" schiebt die Vier auf den 0. Platz und alle folgenden Werte, also insbesondere die Eins auf den jeweils nächsten Platz. Im nächsten Schritt "+" werden die obersten beiden Werte aus dem Stack herausgezogen - also 4 und 1 - und summiert. Das Ergebnis (5) wird wiederum in den Stack geschoben. Die beiden ursprünglichen Zahlen sind im Stack nicht mehr vorhanden.

Ein zweites Beispiel:

(1 + 2) * (4 + 5) muss wie bereits erwähnt folgendermaßen notiert werden:

1 2 + 4 5 + *

Der Stack verhält sich somit folgendermaßen:

Operation:	Stack:	0	1	2	3	4 ...
-----------------------------------------------------------------------------
vorher:			0	0	0	0	0
"1"			1	0	0	0	0
"2"			2	1	0	0	0
"+"			3	0	0	0	0
"4"			4	3	0	0	0
"5"			5	4	3	0	0
"+"			9	3	0	0	0
"*"			27	0	0	0	0

Am Ende befindet sich also das korrekte Ergebnis auf Stackplatz 0!

Gleitkommazahlen und Strings

Das OMSI-Skriptsystem arbeitet ausschließlich mit den Datentypen Gleitkommazahl (einfache Präzision mit Vorzeichen) und String. Beide Datentypen haben getrennte Stacks. Deshalb laufen die Verarbeitung von Gleitkommazahlen und Strings im Allgemeinen unabhängig nebeneinander. Jedoch kann es natürlich vorkommen, dass bestimmte Funktionen z.B. Zahlen in Strings umwandeln und somit gleichzeitig auf beide Teilbereiche zugreifen.

Auf boolische Variablen wurde verzichtet. Für boolische Werte werden im Allgemeinen die Zahlenwerte 0 und 1 verwendet.

Script-Schlüsselwörter

Kommentare

Kommentierungen sind nur möglich, indem in der auszukommentierenden Zeile ganz vorne ein Apostroph ['] gesetzt wird:

'Ich bin eine Kommentarzeile
   'Ich bin KEINE Kommentarzeile!

Einstiegs- und Ausstiegspunkte, Makros und Trigger

Alle Befehle müssen zwischen einem Einstiegs- und einem Ausstiegspunkt liegen.

Der Einstiegspunkt wird durch eins der folgenden Schlüsselwörter gekennzeichnet:

  • {frame} Im Laufe jedes Frames ruft OMSI über diesen Einstiegspunkt die frameweise Scriptverarbeitung auf, sofern er vorhanden ist.
  • {init} Bei der Initialisierung ruft OMSI über diesen Einstiegspunkt die Script-Initialisierung auf.
  • {frame_ai} Hierbei handelt es sich um eine Variante des {frame}-Einstiegspunktes, welcher bei Fahrzeugen dann aufgerufen wird, wenn diese nicht im Fokus des Benutzers stehen, z.B. wenn sie als KI-Fahrzeug unterwegs sind. Ist dieser Einstiegspunkt jedoch nicht vorhanden sondern nur {frame}, dann wird dieser alternativ aufgerufen. Bei Szenerieobjekten kommt dieser Eintrittspunkt nicht zur Anwendung.
  • {macro:name} Dieser Einstiegspunkt ruft ein Subsektion (Makro) auf, welche stets nach dem Aufruf definiert werden muss.
  • {trigger:name} Dieser Einstiegspunkt wird vom Hauptprogramm aufgerufen, wenn der Benutzer die Tastenkombination oder das Maus-Event mit der Bezeichnung name aufruft - es gibt auch bestimmte Trigger, die direkt vom Hauptprogramm aufgerufen werden, z.B. wenn OMSI bei KI-Bussen die Anzeige wechselt u.Ä.

{end} ist der universelle Ausstiegspunkt. Er muss stets den Block abschließen, der mit einem der obigen Befehle eröffnet wird.

Makroaufruf

Der Aufruf eines Makros erfolgt über (M.L.name). Der Aufruf des Makros muss immer vor dessen Definition erfolgen. Andernfalls meldet OMSI, dass das Makro nicht gefunden wurde.

Aufteilung des Scripts in mehrere Dateien

Bei einfachen Scripts (z.B. bei reinen KI-Fahrzeugen oder Szenerieobjekten) reicht es im Allgemeinen eine *.sco-Datei zu erstellen, welche über einen {frame}...{end}- und ggf. einen {init}...{end}-Block verfügt.

Handelt es sich jedoch um ein komplettes Fahrzeugscript, dann empfiehlt es sich für die verschiedenen Subsysteme getrennte Dateien anzulegen. Hierbei sollte folgendermaßen vorgegangen werden:

  • Es gibt ein Hauptscript und die nötigen Subsystemdateien. Die Hauptdatei enthält dabei die {frame}- und {init}-Blöcke, die jedoch nur Makros aufrufen, welche in den Subsystemdateien definiert sind.
  • Für jedes Subsystem gibt es eine *.sco-Datei und je nach Bedarf eine eigene Varlist und Constfile mit den zum System gehörenden Variablen. Außerdem werden auch hier die Trigger für die Tastenbefehle einsortiert.
  • Die Makros sollten sinnvollerweise folgendermaßen benannt werden: {macro:subsystem_frame} und {macro:subsystem_init}.
  • Damit die Makros vom Hauptscript aufgerufen werden, muss dieses in der Liste der Scripts in der Objekt-Konfigurationsdatei als erstes aufgerufen werden. Die Reihenfolge der Scripts untereinander muss ebenfalls so gewählt werden, dass alle dateiübergreifenden Makroaufrufe stets vor der jeweiligen Makrodefinition stehen.

Die Einschränkung, dass Makros immer hinter dem Aufruf definiert werden müssen, kann zwar bisweilen hinderlich sein, ist aber ein sehr wirksamer Schutz gegen Zirkelschlüsse und Endlosschleifen.

Trigger

Wie schon geschrieben wurde, kann ein {trigger:name}...{end}-Abschnitt durch verschiedene Möglichkeiten aus dem Hauptprogramm heraus aufgerufen werden. Hierzu zählen:

  • Auslösung per Tastatur. Wurde die Tastenkombination mit dem Namen tastkomb bezeichnet, wird {trigger:tastkomb} aufgerufen, wenn die Taste gedrückt wird und {trigger:tastkomb_off}, wenn die Taste losgelassen wird.
  • Auslösung per Maus. Wurde ein Mesh angeklickt mit der [mouseevent]-Bezeichnung mouse_ev, so wird {trigger:mouse_ev} aufgerufen, wenn die Maustaste gedrückt wird, {trigger:mouse_ev_drag}, während die Maustaste gehalten wird und {trigger:mouse_ev_off}, wenn die Maustaste losgelassen wird.
  • Außerdem gibt es noch eine Reihe von System-Triggern.

Operationen

Stack-Operationen

%stackdump% Gibt eine Dialogbox mit dem Gleitkomma-Stackinhalt aus. Sollte deshalb natürlich nur zu Debug-Zwecken eingesetzt werden.
s0, s1, ..., s7 Speichern des aktuellen Stackwertes im durch die Ziffer angegebenen Register. Der Wert verbleibt hierbei im Stack
l0, l1, ..., l7 Laden des entsprechenden Registerwertes und Verschiebung in den Stack. Registerwert bleibt erhalten.
d Dubliziert den obersten Stackwert; alle weiteren Stackwerte rücken nach hinten.
$msg Schreibt den obersten String-Stack-Wert in die Debug-Zeile von OMSI - egal, ob es sich um ein User-, KI-Fahrzeug oder Szenerieobjekt handelt.
$d Dubliziert den Stringstack-Wert analog zu "d".

Logische Operationen

Die logischen Operationen arbeiten nach dem Prinzip 0 = FALSE, alles andere ist TRUE.

&& UND, d.h. wenn einer der beiden obersten Stackwerte gleich 0 ist, ist das Ergbnis 0, sonst 1.
|| ODER
! Verneinung

Vergleichsoperationen

Die Vergleichsoperationen vergleichen die Werte in den beiden jeweils obersten Stackplätzen und fügen dann je nach Ergebnis eine "1" oder "0" im obersten Stackplatz ein. Die Größer-/Kleinerzeichen stehen dabei entsprechend für der Reihenfolge, in der die Werte zuvor angegeben wurden. Das bedeutet:

4 2 > ist wahr, weil die intern durchgeführte Operation in diesem Fall "4 > 2" ist.

= "1", falls die obersten Stackwerte identisch sind, sonst "0".
< "1", falls Stackwert 1 kleiner ist als Stackwert 0, sonst "0".
> ... größer ist ...
<= ... kleiner als oder gleich ist ...
>= ... größer als oder gleich ist ...
$= Wie "=" nur für die obersten beiden Stringstack-Plätze.
$< Kleiner als (String). Die Ungleich-Operationen bei Strings prüfen auf alphabetische Reihenfolge. "A" ist also kleiner als "B".
$> Größer als (String).
$<= Kleiner oder gleich (String).
$>= Größer oder gleich (String).

Mathematische Operationen

+ Plus
- Minus (Stackplatz 1 - Stackplatz 0, also vom Prinzip her wie bei den Größer/Kleiner-Operationen)
* Mal
/ geteilt (Stackplatz 1 / Stackplatz 0)
% Rest der Division (folgendermaßen erweitert für Gleitkommazahlen: Stack0 - trunc(Stack1 / Stack0) * Stack1 )
/-/ Vorzeichenwechsel
sin Sinus
arcsin Umkehrfunktion zum Sinus (ab OMSI2)
arctan Umkehrfunktion zum Tangens (ab OMSI2)
min Wahl des kleineren der beiden obersten Stackwerte
max Wahl des größeren der beiden obersten Stackwerte
exp Exponentialfunktion zur Basis e (e^Stack0)
sqrt Quadratwurzel
sqr Quadrat
sgn Rückgabe des Vorzeichens; je nachdem entweder -1, 0 oder 1
pi Kreiszahl pi
random ganzzahlige Zufallszahl 0 <= x < Stack0
abs Rückgabe des Absolutwertes
trunc Abrunden auf nächste ganze Zahl

String-Operationen

"bla" Einfügen des Strings bla auf dem obersten String-Stack-Platz
$+ Zusammenfügen zweier Strings. "Omnibus" "simulator" $+ ergibt "Omnibussimulator"
$* Der oberste Stack-String wird sooft wiederholt, bis die resultierende Zeichenlänger gerade noch kleiner oder gleich des obersten Stackwertes ist. Beispiel: "nu" 6 $* ergibt "nununu"
$length Gibt die Anzahl der Zeichen des obersten Stack-String zurück in den Stack.
$cutBegin Schneidet stack0 Zeichen vorne vom obersten Stack-String ab.
$cutEnd Schneidet stack0 Zeichen hinten vom obersten Stack-String ab.
$SetLengthR Passt die Länge des obersten Stack-Strings auf stack0 rechtsbündig an, indem am Anfang Zeichen entfernt oder Leerzeichen ergänzt werden.

Achtung: Der Befehl entfernt den Float-Operator nicht vom Stack. Der Wert, auf dessen Länge der String gekürzt wurde, verbleibt also auf Stack0.

$SetLengthC Passt die Länge des obersten Stack-Strings auf stack0 zentriert an, indem am Anfang und am Ende Zeichen entfernt oder Leerzeichen ergänzt werden (ab OMSI2).

Achtung: Der Befehl entfernt den Float-Operator nicht vom Stack. Der Wert, auf dessen Länge der String gekürzt wurde, verbleibt also auf Stack0.

$SetLengthL Passt die Länge des obersten Stack-Strings auf stack0 linksbündig an, indem am Ende Zeichen entfernt oder Leerzeichen ergänzt werden.

Achtung: Der Befehl entfernt den Float-Operator nicht vom Stack. Der Wert, auf dessen Länge der String gekürzt wurde, verbleibt also auf Stack0.

$IntToStr Rundet stack0 ab und wandelt die resultierende Ganzzahl um in einen String.
$IntToStrEnh Die erweiterte Version von IntToStr. Hierbei wird der oberste Stack-String verwendet, um das Format der String-Umwandlung zu bestimmen: Das erste Zeichen im String wird zum Auffüllen der fehlenden Zeichen verwendet, die folgenden Zeichen müssen eine Zahl ergeben, die die Stellen angibt. Beispiel: 35 " 5" $IntToStrEnh führt zu " 35" und 123456789 "011" $IntToStrEnh führt zu "00123456789". Falls ein Fehler vorliegt, wird "ERROR" ausgegeben.
$StrToFloat Wandelt den obersten Stack-String in eine Gleitkommazahl um, falls möglich. Andernfalls wird eine -1 geschrieben.
$RemoveSpaces Entfernt sämtliche Leerzeichen vor und nach dem eigentlichen String. Aus " Spandau Freudstr " wird "Spandau Freudstr" (ab OMSI2).

Variablenzugriff

Es wird unterschieden in Systemvariablen und in lokale Variablen. Die Systemvariablen gelten OMSI-weit, die lokalen Variablen werden zum Fahrzeug/Objekt zugehörig gespeichert.

Bei den lokalen Variablen gibt es stets einen Grundstamm an Variablen, welcher von OMSI vordefiniert werden. Darüber hinaus können beliebig viele Variablen mit Hilfe der Varlist-Dateien ergänzt werden. Außerdem gibt es seit OMSI2 die Variante, dass man die Variable zwar selbst definieren muss, sie dann aber trotzdem mit einem OMSI-Internen Zustand verbunden wird ("on-demand-vordefinierte Variablen").

(L.S.varname) Lädt die Systemvariable varname in den obersten Stackplatz
(L.L.varname) Lädt die lokale Variable varname in den obersten Stackplatz
(L.$.varname) Lädt die lokale String-Variable varname in den obersten String-Stackplatz
(S.L.varname) Speichert den obersten Stackplatz in die lokale Variable varname
(S.$.varname) Speichert den obersten String-Stackplatz in die lokale String-Variable varname

Eine detaillierte Beschreibung der einzelnen Systemvariablen und der vordefinierten lokalen Variablen finden Sie hier: System- und vordefinierte lokalen Variablen

Konstanten und Funktionen

Lokale Konstanten und stückweise definierte lineare Funktionen können in den Konstantendateien (constfiles, ~_constfile.txt) definiert werden. Der Aufbau jeder Konstantendatei entspricht der der Konfigurationsdatei. Es gibt lediglich drei Schlüsselwörter. Zu beachten ist: Bisher können keine Strings, sondern nur Fließkommazahlen als Konstanten definiert werden.

[const] definiert eine neue Konstante und gibt ihren Wert an:

[const]
name
wert

Beispiel:

[const]
leistung
200

[newcurve] leitet die Definition einer neuen (stückweise linearen) Funktion ein:

[newcurve]
name

[pnt] fügt der zuvor mit [newcurve] definierten Funktion ein neues x-y-Paar hinzu. Jede Funktion sollte normalerweise über mindestens zwei Paare verfügen. Die Reihenfolge der Paare muss in x-Richtung aufsteigend sein!

[pnt]
x
y

Beispiel:

[newcurve]
Drehzahl_Drehmoment_Kennlinie

[pnt]
50
0

[pnt]
300
400

[pnt]
1000
600

Konstanten werden über den Befehl (C.L.konstantenname) in den Stack geladen.

Funktionen werden über den Befehl (F.L.funktionsname) aufgerufen. Dabei wird stets der zuvor auf oberster Stackposition befindliche Wert als x-Parameter übergaben und der resultierende y-Wert der Funktion stattdessen auf dem obersten Stackplatz gespeichert. Bewegt sich der x-Wert außerhalb der Grenzen der durch die [pnt]-Einträge definierten Eckpunkte, wird stets der y-Wert des nächstliegenden Eckpunktes verwendet. Die Funktion wird also vor dem ersten und hinter dem letzten Eckpunkt ins Unendliche waagerecht verlängert.

Sound-Trigger

Soundtrigger sind nicht mit den weiter oben erwähnten Triggern zu verwechseln!

Sie stellen vielmehr eine Möglichkeit dar, direkt einen gewünschten Sound abzuspielen. Hierbei gibt es zwei Möglichkeiten:

(T.L.soundtriggername) ist ein einfacher Soundtrigger. Der so markierte Sound wird (einmalig) abgespielt.

(T.F.soundtriggername) ist ein Soundtrigger mit Dateiwechselfunktion. Hierbei wird der oberste Stackstring ausgelesen und als Dateiname verwendet, wobei der Dateipfad relativ zum Sound-Ordner des Objektes interpretiert wird.

Beispiel:

Angenommen, die Sounddatei lautet "diese.wav" und ist mit dem Trigger MeinTrigger verknüpft, dann spielt

(T.L.MeinTrigger)

die Datei "diese.wav" ab und

"andere.wav" (T.F.MeinTrigger)

die Datei "andere.wav". Alternativ zu (T.L.soundtriggername) kann beim Befehl (T.F.soundtriggername) auch ein "Nullstring" im Stack liegen (also der String: "").

System-Makros

Die bisherigen Methoden erlauben dem Script folgende Kommunikationsmöglichkeiten mit OMSI:

  • Lesen von Werten über System- und vordefinierte lokale Variablen
  • Schreiben von Werten über ebendiese
  • Empfangen von Tastatur-, Maus- oder Systemtriggern

Schließlich gibt es noch eine weitere Methode, um mit dem Hauptprogramm zu kommunizieren, die sogenannten System-Makros, gelegentlich auch unpräzise als "Callback-Funktionen" bezeichnet.

In bestimmten Situationen ist es nötig, dass das Script Werte von OMSI bekommt, die in größeren Datenbanken vorliegen und daher nicht über vordefinierte Variablen übermittelt werden können, z.B. die Inhalte der Hofdatei. Hier kommen nun die System-Makros ins Spiel.

Diese arbeiten scriptseitig sehr ähnlich wie die lokalen, selbstdefinierten Makros: Der Aufruf (M.V.macroname) ruft das System-Makro auf ("V" stand ursprünglich für "Vehicle-Macro", gilt aber genauso auch in Szenerieobjekten), das sich während der Ausführung Werte aus den beiden Stacks holt und/oder in diese schreibt. Eine Aufstellung aller System-Makros findet sich hier: System-Makros

Bedingungen und Schleifen

Die einzige Steuerung des Programmablaufs ist zur Zeit nur mit der IF-Bedingung möglich.

IF-Bedingung

'Hier muss eine Bedingung stehen, z.B.:
    (L.L.bla) 1 =
    {if}
'Dieser Abschnitt wird ausgeführt, wenn bla = 1 ist:
        2 3 +
    {else}
'Dieser Abschnitt wird andernfalls ausgeführt:
        3 4 +
    {endif}
    (S.L.blub)
{end}

Dass erst die Bedingung formuliert wird und dann erst das Schlüsselwort {if} folgt, sollte einleuchten: Zunächst muss an oberster Stelle des Stacks der Wert abgelegt werden und erst dann folgt das Schlüsselwort {if}, welches diesen Wert prüft. Hierbei bedeutet ähnlich wie bei && und || Gleichheit mit Null gleichbedeutend mit False, alle anderen Werte bedeuten True.

Wenn also im obigen Beispiel bla den Wert 1 hat, wird blub auf den Wert 5 gesetzt, andernfalls auf den Wert 7.

Verschachtelungen der IF-Bedingung dienen als Ersatz für die (nicht vorhandenen) "Else-If"-Konstruktionen:

'Hier muss eine Bedingung stehen, z.B.:
    (L.L.bla) 1 =
    {if}
'Dieser Abschnitt wird ausgeführt, wenn bla = 1 ist:
        2 3 +
    {else}
    (L.L.bla) 5 =
    {if}
'Dieser Abschnitt wird ausgeführt, wenn bla = 5 ist:
        3 4 +
    {else}
'Dieser Abschnitt wird andernfalls ausgeführt:
        13
    {endif}
    {endif}
    (S.L.blub)
{end}

Hierbei ist insbesondere auf das doppelte {endif} zu achten! Hier gilt:

  • bla = 1 ==> blub = 5
  • bla = 5 ==> blub = 7
  • sonst ==> blub = 13