Der Train Simulator und sein LUA-Framework

  • Hallo zusammen,
    unsere Entwickler hat im Nachbarforum vor einigen Tagen einen interessanten Beitrag zum Thema: " Der Train Simulator und sein LUA-Framework" veröffentlicht. Wir möchten mit euch diesen Inhalt hier ebenfalls teilen:



    Wir wissen alle, dass der Train Simulator schon einige Jahre auf dem Buckel hat. In diesen Jahren ist die Software durch mehrere Hände gegangen.


    Wer ernsthaft Inhalte erstellen möchte, freut sich einerseits über eine Menge Möglichkeiten, auf der anderen Seite wiederum kommt man mit seinen Ideen hin und wieder doch sehr schnell an die Grenzen des TS. Gerade was die mitgelieferte LUA-Umgebung angeht sind die Möglichkeiten außerhalb des Führerstands für Gameplay-Technische Aspekte ziemlich begrenzt. Was vor allen Dingen fehlt, ist eine Mechanik, die eine Kommunikation unter Entities ermöglicht.


    Auch wenn man durch geschickte Platzierung gewisser SysCall's zwischen Named-Entities kommunizieren kann, fehlt trotzdem eine Mechanik, die dies auch für Owned-Entities ermöglicht. Darum habe ich mir einmal ein paar Gedanken gemacht und die Weise, auf die Lua arbeitet komplett nachvollzogen. Gott sei Dank gibt es für die Version 5.0.2 noch Quellcode und Dokumentationen. Lua 5.0.2 wurde in Release 2 am 17. März 2004 veröffentlicht.


    Mit dem Wissen habe ich mich hingesetzt und versucht, die im TS verbaute LUA Bibliothek zu erweitern. Dabei gab es eine ganze Menge Probleme.


    1.
    Die Lua Bibliothek ist statisch in den TS gelinkt. Das bedeutet, dass der Quellcode nicht in eine Dll gepackt wurde und diese dynamisch mit dem TS gelinkt wurde, sondern dass der Quellcode direkt in den TS statisch hineingelinkt worden ist. Der Unterschied ist klar: Ich komme an Lua im TS nicht über legale Wege heran, da ich mich nicht auch auf eine vorhandene Dll beziehen kann. Eine Dll stellt Funktionen quasi durch einen Export zur Verfügung, während ein fest eingebauter Quellcode dies nicht macht. Das bedeutet, ich muss mir die Lua-Bibliothek komplett mit den gleichen Ausgangsbedingungen (Visual Studio Compiler + Linker - Version, Plattformtoolset-Version, WinSDK-Version, etc.) nachbauen.


    2.
    Nun habe ich die Bedigungen ausloten können (das war eine Arbeit, da war es nicht mit nem dumpbin-Befehl in Visual Studio getan) und kann in Lua im TS mittels "loadlib" eine DLL laden. Da meine Bibliothek nur nachgebaut ist, kann ich keine eigene Bibliothek in ein Script hinregistrieren. Ich musste jede Funktion einzeln mittels "loadlib" in den TS hineinladen.


    3.
    Als ich die Funktionen dann im TS habe aufrufen können, probierte ich die lua_State-Structs, die an jede Funktion übergeben wird, zu speichern, um Callbacks in meiner DLL registrieren zu können. Sprich, ich wollte in einem Lua-Script im TS einen EventHandler registrieren, der im Script ausgeführt wird, wenn eine bestimme Sache geschieht. Da ich doch etwas Ahnung von meinem Job habe, wusste ich zwar, dass das nicht einfach wird, habe es aber trotzdem versucht und musste wie erwartet feststellen: Wenn ich mit "loadlib" in Lua eine Dll lade (In Windows liegt dazu die WinAPI Funktion "LoadLibrary" zu Grunde) bekommt meine Dll einen eigenen Heap (Speicherbereich) anstatt sich den mit dem TS Prozess zu teilen. Wäre diese direkt im TS Prozess drin, könnte ich mich einfacher in Lua einklinken, so musste ich diesen Ansatz mit dem Callback leider verwerfen, da der TS crashed, wenn ich auf einen Zeiger in seinem Speicherbereich schreiben möchte, wenn Lua mich nicht darum bittet. Kurz gesagt: Ich darf den lua_State-Zeiger nur dann verwenden, wenn er mir aktiv in die Dll geschickt wird.


    Also ist hier dann Schluss? - Nein, denn es gab noch eine Möglichkeit, die zwar nicht so bequem, wie die erste gedachte ist, aber immerhin eine Möglichkeit.


    Wenn ich also nur dann in den lua-Stack schreiben darf, wenn man mich darum bittet, dann müssen die entsprechenden Scripts, die Nachrichten erwarten eben eine Message-Loop besitzen. Damit fällt es schonmal aus, dass man Signalen im TS generell eine Message-Loop verpasst (Diese muss ja sequentiell abgerufen werden: Performance?). Mittels Helfer-Objekten, die in einem Szenario spezifisch auf Nachrichten warten und diese dann an ein Signal oder einen Zug weiterleiten, ist dennoch einiges möglich. Und siehe da, das ganze funktioniert.


    Neben dem "Router"-Teil der Bibliothek gibt es noch andere, unfertige Teile. Diese werde ich jetzt einmal außer Acht lassen und sie ggf. später einmal vorstellen.


    Alle Funktionen sind in dem Modul "JoinTogether" hinterlegt, welches neben der Dll im "plugins"-Verzeichnis des TS liegt. In ihr befinden sich bis jetzt vier Module: Router, Core, Graphics, Network.


    Als Beispiel einmal ein ScenarioScript und ein Signal-Helfer-Script, jene die Bibliothek nutzen:




    Das Modul "Router" enthält also drei öffentliche Funktionen:


    - Result (0 / 1) = PostMessage(Sender, Target, Major, Minor, Data)
    - Result (0 / 1) = HasMessage(Target)
    - Result (0 / 1), Sender, Major, Minor, Data = GetMessage(Target)


    Im ScenarioScript ist der Router eher fehl am Platz, denn jedes Script kann in der Spielwelt ein TS-Event auslösen und den eingebauten Weg nutzen. Das würde ich sowieso, wenn immer möglich, auch empfehlen.
    Die Call's an die "SendSignalMessage"-Funktion sind nicht ausgeschrieben, da sich diese von Signal zu Signal etwas unterscheiden können. Es gibt zwar Standard-Messages durch den TS, die aber nicht zwingend überall gleich interpretiert werden müssen, jdeoch sollten.


    Schafft es der Kram zum Release?


    Das weiß ich nicht. Ich habe hier etwas aus dem Nähkästchen geplaudert und das mit den Kollegen bei der JTG noch nicht besprochen. Zum anderen funktioniert das ganze bisher nur in der 64-Bit Version des TS. Nicht, weil ich die Bibliothek nicht auch für 32 Bit programmiert habe, sondern weil ich keinen ansprechenden Weg gefunden habe, aus Lua heraus die richtige zu laden. Da "loadlib" von Lua eigentlich nur auf "LoadLibrary" in der WinAPI verweist, weiß ich, dass LoadLibrary fehl schlägt, wenn eine nicht zur Prozessarchitektur passende Dll geladen werden soll. So könnte ich zwar erst die x86-Dll laden und auf das Resultat warten und danach ggf. die x64-Dll ranziehen, aber ich finde den Weg nicht ganz so glücklich, eine Notlösung wäre das am Ende aber sicherlich, da kann nichts passieren. Ist nur ne optische Sache.