Alt-F4 #18 – Der Weg zu Clusterio 2.0  18.12.2020

Geschrieben von Hornwitser, DedlySpyder, editiert von stringweasel, Nanogamer7, Conor_, Therenas, nicgarner, Firerazer,
übersetzt von EDLEXUS, Nanogamer7, lektoriert von dexteritas

Inhaltsverzeichnis

Während sich das Jahr dem Ende nähert haben wir zwei Artikel zur Modentwicklung ausgewählt. Zuerst erzählt uns Hornwitser etwas über den langen Entwicklungsprozess von Clusterio 2.0 und die damit verbundenen Schwierigkeiten. Im Anschluss berichtet DedlySpyder über die Entwicklung einer einfachen Mod und den dabei auftretenden Kompatibilitätsproblemen.

Der Weg zu Clusterio 2.0 Hornwitser

Ich möchte eine Geschichte darüber erzählen, wie es dazu kam, dass ich ein Jahr lang an Clusterio 2.0 gearbeitet habe und es immer noch ein weiter Weg bis zur Veröffentlichung ist. Falls ihr noch nicht von Clusterio gehört habt, es ist eine Open Source Software von Danielv123 (mit Hilfe von etwa 30 anderen), welche Mods dazu befähigt, serverübergreifend zu agieren. Die Mod ist wahrscheinlich am besten bekannt für das 60k Clusterio-Event in 2018, bei dem Teleportkisten verwendet wurden, um Waren zwischen rund 46 Factorioservern umherzuschicken um eine semi-vanilla Fabrik zu bauen, die 60k Wissenschaftspakete pro Minute produzierte. Diese Teleportkisten funktionierten, wie Aktive Anbieterkisten und Anforderungskisten; die eine entfernt Gegenstände aus dem Spiel und lädt sie in einen geteilten Cloudspeicher, während die andere Gegenstände aus diesem Cloudspeicher nimmt und zurück ins Spiel befördert.

Eisenerz wird auf dem linken Server abgebaut und mit Clusterio Teleportkisten zum rechten Server gesendet.

Clusterio bestand schon immer aus zwei Teilen. Der eine Teil implementiert Spielinteraktionen und läuft als Mod innerhalb des Spiels, der andere Teil stellt die serverseitige Infrastruktur bereit, welche Daten zwischen verschiedenen Spielservern hin- und herbewegt. Am Anfang war die Serverseite nur um die Teleportkisten programmiert, aber mit der Zeit verschob sich durch das Hinzufügen von Features die Idee, was Clusterio überhaupt ist, weg von Teleportkisten und hin zu einer modularen serverseitigen Plattform für serverübergreifende Spielelemente.

Im Juli 2019 fand das Gridlock-Cluster-Event statt. Anstelle von Teleportkisten zum Gegenstandtransport zwischen Servern wurden Züge verwendet, welche mit Hilfe von Teleportzughaltestellen von einer Serverkante zu einer anderen teleportieren konnten. Der dafür notwendige Code kam von Godmave und lief als Plugin in Clusterio.

Zug teleportiert sich von einer Serverkante zur Kante eines anderen.

Leider war der Code voll mit Problemen, und das war der Zeitpunkt, zu dem ich einstieg.

Bescheidene Anfänge

Ich begann im Juli 2019 damit, mich mit in die Codebasis zu hängen, um dem Gridlock-Team dabei zu helfen, die vielen Fehler zu beheben. Rechts und links stürzten Server ab, Spieler hatten Probleme und neue Bugs und Fehler kamen Stunde um Stunde dazu. Es war hektisch, aber auch mit viel Spaß verbunden. Das Event weckte auch das Interesse für den Code hinter Clusterio in mir und ich machte es mir zur Aufgabe, den Code bis zum nächsten Event zu verbessern. Doch das stellte sich als sehr viel aufwändiger heraus, als ich es mir jemals hätte vorstellen können.

Ich habe seit 16 Monaten ohne Pause an Clusterio 2.0 gearbeitet und meine Schätzung für die Fertigstellung sind immer noch die selben „einige Monate“ wie zu Beginn. Trotzdem ist meine Motivation weiterhin ungebremst, da eins der besonders motivierenden Dinge der Ausblick darauf ist, mein eigenes Clusterio-Event zu veranstalten, um meine Arbeit zu testen. Ich habe einen Teaser auf Reddit gestellt, mit dem, was ich aktuell im Kopf habe. Mein Ziel ist es, das irgendwann Anfang nächsten Jahres zu veranstalten, vielleicht im Januar, aber wir werden sehen.

Aber zurück zum Anfang. Ich installierte Clusterio auf meinem Server, um zu versuchen, die Fehler, die auf den Gridlockservern auftraten auf meinem eigenen Testcluster nachzustellen und zu beheben. Eins der ersten Dinge die mir aufgefallen sind, waren die eintausend Pakete, die als Abhängigkeiten geholt wurden und etwa 300 MB Speicherplatz wegnahmen. Dass dieses Projekt für seine Funktion so viele Pakete brauchte, wirkte lächerlich. Node.js war zu diesem Zeitpunkt noch sehr neu für mich, und obwohl es für ein Node.js-Projekt keine allzu hohe Zahl an Abhängigkeiten ist, ist es trotzdem viel. Das war ein Zeichen dafür, wie das Projekt entwickelt wurde: ein Top-Down Ansatz, der darauf hinauslief, alle Features so zu implementieren, wie es zu dem Zeitpunkt am einfachsten und schnellsten war.

Dieser Entwicklungsstil führte zur Ansammlung hoher Technischer Schulden, und ich meine wirklich hoher. Technische Schulden sind ein gebräuchlicher Ausdruck in der Softwareentwicklung, der die Annahme beschreibt, dass die Verwendung von Abkürzungen zu Beginn Zeit spart, aber am Ende zu mehr Arbeit in der Wartung und Erweiterung des Codes führt. In einer Art und Weise könnte man sagen, Clusterio war mehr eine Ansammlung zusammengehackter Codeteile als ein gut durchdachtes und strukturiertes Projekt. Ein Beispiel dafür war die Verwendung von vier verschiedenen HTTP-Clients in der selben Quelldatei. Normalerweise ist ein solcher Client genug für das gesamte Projekt, aber offensichtlich war ein Problem leichter mit einem Client zu lösen als mit einem anderen, und so sammelten sich mit der Zeit verschiedene Clients an.

Also fing ich an, den Code von Clusterio aufzuräumen und zu verbessern. Eine der ersten Sachen mit der ich begann, war die ungefähr eintausend Abhängigkeiten zu kürzen. Es stellte sich heraus, dass die meisten waren gar nicht notwendig, um Clusterio laufen zu lassen. Ungefähr die Hälfte waren Entwickungswerkzeuge, die in einer Produktionsumgebung nicht benötigt wurden, und ein weiteres Viertel war das, was ich als Schnelllösungen bezeichnen würde: große Bibliotheken, aus denen dann eine einzige Funktion benutzt wurde. Viele dieser Bibliotheken waren einfach zu entfernen, entweder durch eine lokale Implementation dieser Funktion oder durch die Verwendung einer anderen, bereits vorhandenen Bibliothek. Am Ende schaffte ich es, etwa 700 Pakete (244MB) zu entfernen, wobei man anmerken muss, dass die meisten davon Abhängigkeiten für Abhängigkeiten waren.

Das nächste Problem mit dem ich mich befasste war automatisiertes Testen. Falls automatisiertes Testen kein Begriff für dich ist, es ist im Prinzip der Gedanke, Programme zu schreiben, die prüfen, ob das Hauptprogramm funktioniert und mit neuen Änderungen nicht kaputt geht. Automatisiertes Testen ist ein Grundpfeiler um zuverlässigen Code zu schreiben, und obwohl irgendwann mal ausgiebige Tests implementiert wurden, funktionierten diese zu dem Zeitpunkt, als ich zum Projekt hinzustieß, nicht. Es ist ein weiteres Beispiel die technischen Schulden, die mitgezogen werden müssen. Die Tests zu warten und neue Tests zu implementieren, um neue Funktionalitäten zu testen, ist aufwändig, das einfach zu überspringen eine Abkürzung.

Nachdem die Tests repariert waren, bewegte sich mein Fokus hin zum beräumen des Programms selber. So Sachen wie kaputten Code zu reparieren, nicht benutzen Code zu entfernen und schlechten Code zu etwas weniger schlechten Code umzuformen. Eine dieser Veränderungen war es, den Code für die Teleportkisten aus dem Hauptcode zu entfernen und in ein separates Plugin zu bewegen. Da Clusterio hauptsächlich die Serversoftware für serverübergreifende Modinteraktionen ist, ist es verwirrend, dass diese Teleportkisten auch Clusterio heißen, wenn wir davon ausgehen, dass Clusterio in Zukunft für mehr als nur diese Kisten verwendet wird. Also entschieden wir uns dazu, das Magische-Kisten-Gegenstands-Teleportations-Konzept zu Subspace Storage umzubenennen.

Platzhaltergrafiken für Subspace Storage
Neue Platzhaltergrafiken für Gegenstand, Flüssigkeit und Strom Ein- und Ausgänge.

Diese Grafiken sind wirklich nur Platzhalter, da ich nicht wirklich ein 3D-Künstler bin, wenn es um Texturen und mechanische Modellierung geht. Ich habe mir die Zeit genommen, eine automatische Funktion mit Blender zu erstellen, die automatisch rendert, ausschneidet und die Grafiken in die Mod lädt. Typischer Programmierer: Vollständige Automatisierung.

Speicher-Patching

Eine meiner wichtigen Verbesserungen war Speicher-Patching, aber bevor ich das erkläre, möchte ich kurz auf die Probleme eingehen, die Speicher-Patching versucht zu lösen. Die Spielengine erlaubt es, das Spielverhalten durch Lua-Code in Form von Mods oder Szenarios zu ändern. Mods werden mit dem Spielstart geladen und Updates zu Mods bedürfen einem Neustart des Spiels. Szenarien sind Lua-Code in Spielspeicherstände verpackt, das Ändern eines Szenarios bedarf nur dem Laden eines anderen Speicherstandes.

Wenn Spielveränderndes Verhalten in Szenarios verpackt wird, spricht man von Soft-Modding, da man keine Mods herunterladen muss und das Spiel nicht neustarten muss, um einem Server beizutreten, der solchen Szenariocode verwendet. Während es einfach ist, eine Mod zu updaten und dann einen alten Speicherstand weiterzuspielen, ist das nicht so einfach mit Szenariocode. Es gibt im großen und ganzen drei Wege, um Szenariocode in einem Speicherstand zu updaten, welche ich jetzt grob nach ihrer Schwierigkeit geordnet erklären werde:

  • Für Szenarien, die über eine Mod verteilt werden, ist es möglich, ein Skript in der Mod zu implementieren, welches das Szenario aktualisiert wenn man den Mod aktualisiert. Während das den großen Vorteil hat, sehr einfach implementierbar zu sein, kommt es auch mit dem großen Nachteil, dass die Mod installiert sein muss.
  • Man kann den Szenariocode eines Speicherstandes ersetzen, während das Spiel nicht läuft. Das ist was ich als Speicher-Patching bezeichne und es ist relativ simpel, da Speicherstände als normale Zip-Dateien vorliegen und der Szenariocode ein einfaches Textdokument ist.
  • Man kann auch die dynamische Struktur von Lua verwenden, um neuen Code zu laden und auszuführen, während das Spiel und das Szenario laufen. Diese Option ist mit Abstand die schwierigste, aber kommt mit dem großen Vorteil, das man Reparaturen anstellen kann, während die Karte läuft. Der Nachteil ist, das es schwer ist richtig zu implementieren, was die Fehlerwahrscheinlichkeit erhöht. Zusätzlich ist die einzige Möglichkeit, Befehle zu einem laufenden Spiel zu senden über Commands, was ein Problem darstellt, wenn sie sehr lang sind.

Für den Gridlock-Cluster wurde die dritte Option über ein Hotpatch genanntes Szenario realisiert (auch bekannt als Serverseitiges Multimodszenario). Im Prinzip ist Hotpatch eine tolle Sache; es stellt die Möglichkeit dar, modähnlichen Code zu laden während das Spiel läuft und es führt den Code in einer Umgebung aus, welche die Factorio Modumgebung emuliert. Aber es gab große Probleme mit der Verwendung von Hotpatch: Es ist sehr schlecht dokumentiert, was die korrekte Verwendung erschwert; die Implementierung war nicht komplett und voll mit Bugs; das größte Problem aber war, dass der aktualisierte Szenariocode zum Spielbeitritt als lange Commands verschickt wurde. Das bedeutete, wenn Spieler einem Server während seines Start-Ups beigetreten sind, während er diese langen Commands verschickte ging alles den Bach runter. Das war eine der vielen Arten, wie die Grindlock-Server versagten.

Während viele der Probleme mit Hotpatch gefixt wurden, haben mir die Schwierigkeiten und die Komplexität der Arbeit damit eine wichtige Lektion gelehrt: Fortgeschrittene Fähigkeiten, wie Code während er läuft zu fixen oder ähnliche technische Wunder überwiegen nicht immer der Komplexität und den Problemen, die durch ihre Verwendung auftreten. Ich konnte das selbst erleben, als wir versuchten, die durch Hotpatch entstandenen Probleme zu reparieren, und keiner aus dem Team verstand, wie genau das System jetzt funktioniert, und wie man die Fehler beheben soll.

Aus diesem Grund endschied ich mich dazu, Hotpatch mit etwas einfacheren zu ersetzten: Speicher-Patching. Es ist eine weniger leistungsfähige Lösung mit mehr Beschränkungen, aber der einfachere Umgang machen das mehr als wett.

Alles Kaputtmachen

Nachdem ich Speicher-Patching implementiert habe wurde klar, das eine vollständige Überholung des gesamten Codes notwendig war. Ein besonders wunder Punkt von Clusterio war das komplette Fehlen von Fernwartung. Wenn man einen Factorioserver starten will, der Teil eines Clusters sein soll, muss man sich in den Hostcomputer einloggen und ihn manuell starten, mithilfe des Prozessmanagers seiner Wahl. Das gleiche gilt auch für alle Einstellungsänderungen für diesen Server. So einen Cluster zu managen ist sehr schwer, und das war etwas, was schnell negativ beim Clusterio 60k Event aufgefallen ist.

Für Gridlock wurde für die Fernwartung Pterodactyl verwendet, eine prinzipiell gute Idee, welche aber ein Grund für viele weitere Fehler war, aber das ist eine Geschichte für ein andermal.

Die Möglichkeit, Factorioserver in Clusterio von der Ferne zu bedienen war ein Feature, welches für lange Zeit gewünscht wurde und es kam zu vielen Implementierungsversuchen. Diese Versuche waren aber mehr eine nachträgliche Erweiterung, aber durch die grundlegende Struktur (ein einzelner Factorioserver pro Node.js-Prozess) wurde es sehr schwer, eine vernünftige Fernverwaltung einzuführen, ohne den gesamten Code zu überholen und dabei alles kaputtzumachen.

Selbstverständlich machte ich alles kaputt und implementierte Fernwartung.

Die Art und Weise wie Clusterio 2.0 funktioniert, ist, dass ein Slave-Prozess auf jedem Rechner läuft, auf dem ein Factorioserver gehostet werden soll. Diese Factorioserver werden Instanzen in Clusterio genannt, und der Slave-Prozess verbindet sich mit dem Master-Server und wartet auf Befehle zum erstellen und starten von Instanzen. Mehrere Instanzen können zur selben Zeit auf einem Slave-Prozess laufen, das bedeutet, man muss nur einen Slave für jeden Rechner aufsetzen, auf dem ein Factorioserver laufen soll, und man muss nur einen Node.js-Prozess auf jedem Rechner starten.

Eine andere Sache, die Änderung bedurfte, war der Weg, wie Clusterio zwischen Rechnern kommunizierte. In der ersten Version wurde das über einen Masterserver gelöst, auf dem ein HTTP-Server lief, der auf Anfragen antwortete. Das hatte das Problem, das der Master-Server nur auf Anfragen antworten kann, aber nicht selbständig Nachrichten zu Servern schicken kann. Um das zu umgehen ersetzte ich HTTP mit einem simplen Websocket-basiertem Protokoll, welches JSON-Dateien transportiert. Websockets erlauben es, im Gegensatz zu HTTP, dass beide Parteien zu jedem Zeitpunkt Nachrichten senden und empfangen können.

Da nun alles kaputt war, wurde dies sozusagen der Punkt, an dem die 2.0-Entwicklung wirklich begann. Ich nutzte diese Gelegenheit um viele Dinge in den folgenden Monaten neu anzugehen.

Ich hoffe ihr mochtet diesen Einblick in die Entwicklung von Clusterio 2.0. Wie ihr euch vielleicht vorstellen könnt, gibt es noch viele Dinge, die mit 2.0 in den letzten 16 Monaten passiert sind, sicherlich genug für einen zweiten Artikel zu diesem Thema. Bitte beachtet, dass 2.0 zu diesem Zeitpunkt nicht fertig ist, aber wer an der Entwicklung interessiert ist und das ganze mal testen möchte, kann sich gerne mal den Discord und das Github-Repo ansehen.

Modbarkeit: Die Geburt einer Mod DedlySpyer

In FFF-363 hat kovarex etwas gesagt, das mir im Kopf geblieben ist:

Das ist ein Beispiel eines Features, was ich einfach hinzufügen MUSSTE, weil sobald mir aufgefallen ist, dass es das Feature geben könnte, ich es fast verwenden wollte, und genervt war, dass es nicht da war.

This is an example of a feature, that I just HAD TO DO, because once I realised that the feature could be there, I was almost trying to use it and was annoyed by the fact that it wasn’t there.

— kovarex

In den letzten sechs Jahren habe ich unterschiedlich aktiv Factorio gespielt, aber seit ich mit Modding angefangen habe, liebe ich es immer mit dem Spiel zu tüfteln. Manchmal, während des Spiels, fällt mir etwas neues auf, was mich stört, aber keine Mod hat, und irgendwann komm ich zu dem Punkt, wo ich mir einfach selbst eine Mod schreibe. Normalerweise verwerfe ich dann mein momentanes Spiel, hauptsächlich weil für mich Modden die selbe Zufriedenheit wie das eigentliche Spielen bringt.

Kurz nach dem 1.0 Release ist das wieder passiert. Ich habe mit der neuesten Version von Krastorio 2 angefangen, erreichte Power Armor, und hab mich dann gewundert, warum ich meine Ausrüstung nicht drehen konnte. Sicherlich könnte ich alles ein bissl herumschieben, aber manchmal will ich einfach R drücken und alles mit minimalem Aufwand zusammenwerfen. Einmal schnell das Modportal suchen brachte mir die Rotatable Batteries Mod von GotLag; also es ist möglich, wurde aber noch nicht für alles implementiert.

Eine Mod so zu entwickeln, dass sie in jedem Szenario funktioniert, kann manchmal ziemlich viel Arbeit sein. Der sichere Weg jeden Fall abzudecken ist deine Änderungen für jeden Fall einzuprogrammieren. Das würde auf jeden Fall funktionieren, bräuchte aber ständige Beobachtung. Und wenn eine dieser Mods etwas ändert, würde meine Implementation entweder gar nicht mehr funktionieren, oder wäre inkonsistent mit der „unterstützten“ Mod. Daher bin ich seit kurzem ein großer Fan davon, meine Mods so dynamisch wie möglich zu machen, um so etwas zu vermeiden. In der Theorie sollte das auch eine Menge an Zeit sparen, was aber nicht immer funktioniert.

Diese Realität ist aber das, was ich an Factorio mag; das „Ah, aber ich muss das da machen.“ Es ist nicht lustig, wenn man einfach eine weitere Abhängigkeit hinzufügt, indem man ein paar Zeichen an eine Liste hängt. Um also eine neue Mod mit dem Ziel jede Ausrüstung ohne mein Zutun drehen zu können, musste ich davon gebrauch machen, wie Factorio Mods lädt.

Wenn du in anderen Spielen Mods hinzufügen möchtest, gibt es irgendeine Art von Modreihenfolge. Du, der Spieler, oder ein von Moddern erstelltes Programm muss dem Spiel sagen, in welcher Reihenfolge die Mods geladen werden sollen, um sicher zu gehen, dass alles gut genug ineinander greift, um nicht zu explodieren. Factorio erhält so eine Reihenfolge mit Modabhängigkeiten, aber geht gleich noch einen ganzen Schritt weiter. Factorio lädt nicht nur einfach alle Mods in der Reihenfolge einmal, es lädt sie drei mal.

Drei mal? Klingt unnötig, oder? Um genau zu sein ist es eine fantastische Idee. Das Wiki erklärt das wesentlich genauer, aber ich werde es kurz hier zusammenfassen. Jede Mod in der Reihenfolge hat eine Einstellungsphase, und dann eine Datenphase. Die Einstellungsphase ist ziemlich selbsterklärend, und die Datenphase ist für Prototypdaten wie Gegenstände, Entitäten und Rezepte. Dieser Kreislauf wiederholt sich zwei weitere Male. Mods spezifizieren in jedem Durchlauf was sie laden. Konventionen empfehlen alle Prototypen so früh wie möglich zu laden. Das erlaubt es Mods sich implizit auf andere Mods zu verlassen, ohne überhaupt zu wissen, dass sie existieren. Die Basisspiel Mod zum Beispiel macht das, um Flüssigkeiten in Fässer zu bringen.

Dadurch hat die Community diese gigantischen Überholungsmods, die Rezepte komplett überarbeiten; sie machen das einfach in einer späteren Datenphase für jedes einzelne Rezept im Spiel. Keine lange Liste an Mods, die Wartung benötigen, kein „Diese Mod muss als letztes geladen werden.“

Und so kann ich auch mit meiner Mod alle Ausrüstungen drehen. Ich kann meine Prüfungen für Ausrüstung, die eine gedrehte Version benötigt, in eine spätere Datenphase schieben, und es sollte implizit jede Ausrüstung im Spiel abdecken. Es ist nicht nötig, dass ich die Mods X, Y und Z als Abhängigkeiten benenne, oder dass der Spieler irgendetwas an seinem Ende verwaltet; es funktioniert einfach. Kein ständiges Handhaben von Namensänderungen, außer es ist ein komplexeres Problem, was ich gerne versuchen werde zu lösen.

Mit all dem im Hinterkopf, ein paar Tage und einem halbfertigen Krastorio-Spielstand im Stich gelassen, wurde Rotatable Equipment (zu Deutsch: „Drehbare Ausrüstung“) geboren.

Gedrehte Ausrüstung
Vanilla Ausrüstung und gedrehte Varianten

Beitragen

Wie immer suchen wir nach Leuten, die zu Alt-F4 beitragen wollen, sei es mit einem Artikel oder durch Hilfe bei Übersetzungen. Wenn du etwas Interessantes im Kopf hast, das du mit der Community in einer eleganten Art teilen möchtest, hier kannst du das tun. Falls du dir unsicher bist, beantworten wir gerne Fragen zu Inhalt und Struktur. Falls das nach etwas klingt, woran du interessiert bist, tritt unserem Discord bei um es nicht zu verpassen!

Da der erste Weihnachtsfeiertag auf nächsten Freitag fällt haben wir beschlossen, in der kommenden Woche keine neue Ausgabe zu produzieren, was bedeutet, dass dies die letzte Ausgabe Alt-F4 für dieses Jahr ist! Wir werden am ersten Januar zurückkommen mit einer Sonderausgabe, in der wir auf die bisherige Entwicklung des Projektes zurückschauen und uns verschiedene Perspektiven der verschiedenen Teammitglieder anhören.