Zum Inhalt

Erste Schritte

Auf dieser Seite richtest du einen Editor für Teal und Lua ein, erfährst, wo Mods auf der Festplatte liegen, und baust einen kleinen Mod, der funktioniert. Danach geht es ums Testen und Veröffentlichen. Die Beispiele stammen aus den Mods, die mit dem Spiel ausgeliefert werden, und aus dem offiziellen Modding-Wiki.

Die Einführung im Wiki nennt die Grundregel: Ändere keine Dateien im Installationsordner. Jede Änderung gehört in einen Mod, also in einen Ordner mit einer mod.json, einer _metadata/modinfo.json und einem content/-Ordner.

Werkzeuge

Für .lua-, .tl- und .json-Dateien reicht jeder Texteditor. Die Wiki-Seite zu externen Werkzeugen empfiehlt Visual Studio Code mit zwei Erweiterungen:

Erweiterung Zweck
Lua (sumneko) Lua-Syntax, Vervollständigung und Diagnose
Teal (Patrick Desaulniers, pdesaulniers.vscode-teal) Teal-Syntax und Typprüfung gegen die Definitionsdateien des Spiels

Die Teal-Erweiterung braucht den Teal-Compiler (tl) aus dem offiziellen Teal-Repository. Trage nach der Installation den Pfad zur ausführbaren Datei tl in den Einstellungen der Erweiterung ein.

Teal ist freiwillig. Du kannst Mods in reinem Lua schreiben, und die offiziellen Mods verwenden beides: mods/release/urbangames_vehicles_no_end_year/content/mod.script.lua ist Lua, mods/release/urbangames_no_costs/content/mod.script.tl dagegen Teal. Die Syntax-Seite im Wiki ist eine kurze Einführung in Lua und Teal. Dort steht, dass das Spiel keine globalen Variablen unterstützt. Deklariere deshalb jede Variable als local. Die einzige globale Definition in Lua-Ressourcendateien ist function data(), wie in der mod.script.lua oben.

Der Entwicklungsarbeitsbereich

Laut Wiki legt der Model Editor beim ersten Start einen fertigen Arbeitsbereich an: tlconfig.lua, workspace_def.tl und .vscode/extensions.json, und zwar im Staging-Bereich (siehe Wo Mods liegen und Model Editor). In der Dateiliste des Wikis heißt die Datei workspace_def.lua, in den späteren Schritten workspace_def.tl. Gemeint ist die globale Teal-Definitionsdatei, die global_env_def in tlconfig.lua nennt, daher heißt sie auf dieser Seite workspace_def.tl. Klappt das nicht, sollst du laut Wiki die Dateien aus <tf3_install_folder>/dev-workspace kopieren und die Pfade in tlconfig.lua anpassen.

In den Spieldateien, aus denen diese Dokumentation entsteht, hat die Vorlage einen anderen Namen. Der Ordner heißt vscode-template/, und die globale Definitionsdatei darin ist all_def.tl, nicht workspace_def.tl:

vscode-template/
├── .vscode/extensions.json
├── tlconfig.lua
└── all_def.tl

Nimm das, was deine Installation mitbringt. Das Prinzip ist in beiden Fällen dasselbe. .vscode/extensions.json empfiehlt die Teal-Erweiterung:

{
    "recommendations": [
        "pdesaulniers.vscode-teal"
    ]
}

tlconfig.lua sagt dem Teal-Compiler, wo die Definitionen liegen:

return {
    include_dir = {
        -- ensure to set proper paths for the definitions for the base game
        "<tf3_install_folder>api/tealdef",
        "<tf3_install_folder>base/tealdef",

        -- list your mods here, if you provide your own d.tl definitions
        -- "example_mod_1",
    },
    global_env_def = "all_def"
}

Ersetze <tf3_install_folder> durch den Pfad deiner Spielinstallation. Die beiden Ordner enthalten:

Ordner Inhalt Referenz
api/tealdef Die Engine-API: api.engine, api.cmd, api.gui, api.res, api.type, api.util, api.modhub, app und die globalen Funktionen Engine-API
base/tealdef Definitionen der Skriptbibliotheken des Basisspiels Skripte des Basisspiels

global_env_def nennt die Datei, die Teal als globale Umgebung für jede geprüfte Datei lädt. Die all_def.tl der Vorlage ist kurz:

require "api_def"
require "content_def"

-- list your mods here, if you provide your own d.tl definitions
-- require "example_mod_1"

api_def ist api/tealdef/api_def.d.tl und bindet alle Definitionen der Engine-API ein (api.type, api.res, api.engine, api.cmd, app, api.gui, main usw.). content_def ist base/tealdef/content_def.d.tl und bindet die Definitionen des Basisspiels ein (Farben, Soundeffekte, GUI-Ressourcen usw.). Danach kennt die Typprüfung globale Namen wie api, app, ug_require, _ und debugPrint. Sie sind unter main und api dokumentiert.

Zum Testen der Einrichtung schlägt das Wiki eine Datei example.tl in deinem Mod vor:

local function myFirstTealFunction()
    log.warning("Hello World!")
end

Strg+Klick oder F12 auf warning sollte zur Definition springen (log.warning).

Definitionen für deinen eigenen Mod

Eigene Typdefinitionen schreibst du in *.d.tl-Dateien in deinem Mod. Nimm den Ordner des Mods in include_dir in tlconfig.lua auf und ergänze in der globalen Definitionsdatei (all_def.tl oder workspace_def.tl) ein require für seine Haupt-Definitionsdatei. Im Beispiel des Wikis wird "example_mod_1" in beiden Dateien einkommentiert und example_mod_1/example_mod_1.d.tl angelegt:

global record MyFirstRecord
    myFirstField : string
end

Danach lässt sich MyFirstRecord in example.tl als Typ verwenden. Weil der Arbeitsbereich im Staging-Bereich liegt, meint der Eintrag "example_mod_1" den Mod-Ordner neben tlconfig.lua.

Die Kampagnen-Mods legen ihre Definitionen in einem eigenen tealdef/-Ordner neben content/ ab:

mods/release/urbangames_campaign_mission_01/tealdef/
├── urbangames_campaign_mission_01_def.d.tl
└── mission/mission_params_01.d.tl

urbangames_campaign_mission_01_def.d.tl besteht aus einer einzigen Zeile, require "mission.mission_params_01". Dieser tealdef/-Ordner ist also eine Include-Wurzel, genau wie base/tealdef. Bei diesem Aufbau lautet der Eintrag in include_dir "my_mod/tealdef", und all_def.tl bekommt require "my_mod_def".

Wo Mods liegen

Das Spiel kennt mehrere Quellen für Mods. Der „Mod-Hub“ (Mod Hub, base/content/gui/gui/menu/mod_manager_react_util.tl) gruppiert Mods nach Mod.GameModDesc.modSource. Die möglichen Werte stehen im auskommentierten Enum ModSource in api/tealdef/api/type/mod.d.tl:

Quell-ID Anzeige im Spiel Ordner
StagingArea „Meine Mods“ („My Mods“) staging_area im Benutzerdatenordner
UserMods „Benutzer-Mod“ („User Mod“) von Hand installierte Mods; das Wiki nennt den Pfad nicht
BuiltInMods „Built-in Mods“ („Built-In“) die offiziellen Mods, ausgeliefert in mods/release/ der Installation
DLC „DLC“ ausgeliefert in dlcs/ der Installation
ein Mod-Hub-Backend Name und Symbol des Backends, zum Beispiel mod.io abonnierte Mods, die das Spiel verwaltet

Im Staging-Bereich entwickelst du einen Mod, und von dort veröffentlichst du ihn auch. Die Veröffentlichungsseite nennt den üblichen Ort für die Steam-Version unter Windows:

C:\Program Files (x86)\Steam\userdata\<your Steam ID>\3493540\local\staging_area\

Den Benutzerdatenordner deines Systems öffnest du im Hauptmenü unter Einstellungen > Erweitert > Userdata-Verzeichnis öffnen (Settings > Advanced > Open User Data Folder). Der Knopf ruft app.openUserDataFolder() auf. In der Staging-Ansicht hat jeder Mod einen Knopf, der seinen Ordner über ModPublishHelper.openFolder öffnet.

Auch der Model Editor braucht diesen Ordner. Beim ersten Start trägst du ihn als userDataPath in seine Konfigurationsdatei ein (wo die Dateien liegen, steht unter Model Editor).

Das Spiel erkennt einen Mod an seiner modId. Eine Kopie im Staging-Bereich hat Vorrang vor einer veröffentlichten Kopie mit derselben ID. Beim Testen ersetzt deine Arbeitskopie also die veröffentlichte Version (Installationsreihenfolge).

Ein minimaler Mod

Die „Demo Config Change“ aus dem Wiki und die ausgelieferten Mods urbangames_no_costs und urbangames_sandbox sind die kleinste Art von Mod: ein Skript, das die Basiskonfiguration ändert. Dafür braucht es drei Dateien:

my_mod/
├── mod.json
├── _metadata/modinfo.json
└── content/mod.script.tl

(Die ausgelieferten Ordner enthalten außerdem _content.json, eine Liste der Inhaltsdateien und Archive. Mehr zu dieser Datei steht unter Aufbau eines Mods.)

Für Ordner und ID gelten ein paar Regeln von der Wiki-Seite zur Mod-Definition:

  • Der Ordnername hat keine Funktion. Die Seite zur Mod-Definition erlaubt A-Z, a-z, 0-9, _ und Leerzeichen. Die Upload-Anforderungen verlangen aber für alle Dateien und Ordner Kleinbuchstaben ohne Leerzeichen. Nimm deshalb die modId als Ordnernamen.
  • Die modId darf nur a-z, 0-9 und _ enthalten. Empfohlen wird die Form <author>_<modname>.
  • Anders als in den früheren Transport-Fever-Spielen hat die modId kein Suffix für die Hauptversion wie _3. Versionsänderungen gehören in revision.

Die offiziellen Skript-Mods tragen trotzdem ein Suffix _1 (urbangames_no_costs_1). Lass es in deinen eigenen IDs weg (die Fälle stehen unter mod.json).

mod.json legt die ID fest und verweist auf die Skriptfunktionen, die das Spiel beim Laden aufruft. Aus mods/release/urbangames_no_costs/mod.json:

{
    "dependencies": null,
    "incompatibilities": null,
    "modId": "urbangames_no_costs_1",
    "options": null,
    "params": null,
    "postRunScript": {
        "fileName": ""
    },
    "preRunScript": {
        "fileName": "urbangames_no_costs_1::/mod.script@preRunFn"
    },
    "revision": 1,
    "runScript": {
        "fileName": ""
    },
    "severityAdd": "None",
    "severityRemove": "None"
}

Die Schlüssel mit null oder "" kannst du weglassen. Die minimale mod.json im Wiki enthält nur modId, revision, severityAdd, severityRemove, visible und cosmetic. Alle Schlüssel sind unter Aufbau eines Mods beschrieben.

_metadata/modinfo.json enthält die Texte, die der Spieler im Mod-Browser sieht. Die ausgelieferten Mods tragen hier Übersetzungsschlüssel ein. In deinem eigenen Mod schreibst du den englischen Text direkt hinein oder ergänzt einen localization-Block (siehe modinfo.json):

{
    "authors": [
        {
            "name": "Urban Games",
            "role": "CREATOR"
        }
    ],
    "description": "MOD_NO_COSTS_DESCRIPTION",
    "name": "MOD_NO_COSTS_NAME",
    "summary": "MOD_NO_COSTS_SUMMARY",
    "tags": [
        "Script Mod"
    ],
    "url": ""
}

content/mod.script.tl ist das Skript, auf das "urbangames_no_costs_1::/mod.script@preRunFn" verweist. Der Verweis nennt den Mod (urbangames_no_costs_1::), die Datei in dessen content/-Ordner ohne die Endung .tl (/mod.script) und die Funktion in der Tabelle, die die Datei zurückgibt (@preRunFn):

local mod = {}

mod.preRunFn = function(captureParams, configDict : {{string, string}}, allModParams : {string : {string : integer}}, baseConfig : BaseConfig)
    baseConfig.noCosts = true
end

return mod

Für einen eigenen Mod kopierst du die drei Dateien, wählst eine neue modId, verwendest dieselbe ID in den Skriptverweisen und änderst, was preRunFn tut. BaseConfig listet jedes Feld der Basiskonfiguration auf. Die Wiki-Seite zur Spielkonfiguration nennt Standardwerte und Bedeutungen, und unter Basiskonfiguration steht, wo das Basisspiel sie setzt und liest.

Die folgende Skizze macht dasselbe wie die Demo im Wiki und hat zusätzlich einen Mod-Parameter: Der Spieler wählt, wie stark Züge bremsen, und das Skript schreibt die Wahl in trainBrakeDeceleration (Standardwert laut Wiki 2.5, kleinere Werte bremsen langsamer). jdoe_soft_brakes ist eine erfundene ID:

{
    "modId": "jdoe_soft_brakes",
    "revision": 1,
    "severityAdd": "None",
    "severityRemove": "None",
    "cosmetic": false,
    "preRunScript": { "fileName": "jdoe_soft_brakes::/mod.script@preRunFn" },
    "params": [
        {
            "key": "brake_factor",
            "name": "brake_intensity_name",
            "tooltip": "brake_intensity_tooltip",
            "uiType": "Slider",
            "values": [ "50%", "75%", "100%" ],
            "numbers": [ 1.25, 1.875, 2.5 ],
            "defaultIndex": 2
        }
    ]
}
{
    "en": {
        "brake_intensity_name": "Brake Intensity",
        "brake_intensity_tooltip": "Adjust the global brake intensity."
    },
    "de": {
        "brake_intensity_name": "Bremsintensität",
        "brake_intensity_tooltip": "Passen Sie die globale Bremsintensität an."
    }
}
{
    "name": "Soft Brakes",
    "summary": "Trains brake more gently.",
    "description": "Lowers the global train brake deceleration.",
    "authors": [ { "name": "jdoe", "role": "CREATOR" } ],
    "tags": [ "Script Mod" ],
    "url": ""
}
local mod = {}

mod.preRunFn = function(captureParams, configDict : {{string, string}}, allModParams : {string : {string : integer}}, baseConfig : BaseConfig)
    baseConfig.trainBrakeDeceleration = allModParams[getCurrentModId()].brake_factor
end

return mod

params legt einen Schieberegler mit drei Optionen an. defaultIndex zählt in mod.json ab 0, 2 wählt also „100%“ vor, den Standardwert aus dem Wiki. Das Skript findet den numbers-Eintrag der gewählten Option unter dem key des Parameters in allModParams[getCurrentModId()]. name und tooltip sind Übersetzungsschlüssel, deren Texte in strings.json im Hauptordner des Mods stehen (Übersetzungen). Kein ausgelieferter Mod hat eine strings.json, daher folgt diese Datei dem Beispiel aus dem Wiki. Kein ausgelieferter Parameter hat Zahlen mit Nachkommastellen in numbers. Prüfe also im Spiel, ob das Skript wirklich 1.875 erhält und keinen gerundeten Wert. Alle Felder sind unter Mod-Parameter erklärt.

Jedes Zug-, Straßenbahn- und Waggonmodell des Basisspiels setzt außerdem in seinem landVehicle-Block ein eigenes brakeDeceleration = 2.5. Wie dieser Wert pro Modell und das globale trainBrakeDeceleration zusammenwirken, verraten die Spieldateien nicht. Zeigt der Schieberegler im Test keine Wirkung, ändere stattdessen die Modelle in einer postRunFn (siehe Mod-Skript-Hooks).

cosmetic steht auf false, weil das Bremsen die Simulation verändert. Das Wiki bittet darum, cosmetic: true nur bei Mods ohne Einfluss auf das Spielgeschehen zu setzen, denn damit bleiben Erfolge verfügbar.

Die drei Mod-Skript-Hooks (preRunScript, runScript, postRunScript), ihre Argumente und die Reihenfolge, in der das Spiel sie aufruft, sind unter Mod-Skript-Hooks beschrieben. Wofür du die einzelnen Phasen nutzt, steht unter Scripting.

Einen Mod testen

Lege den Mod-Ordner in den Staging-Bereich. Dann erscheint er im Mod-Hub unter „Meine Mods“ und in der Mod-Liste, wenn du ein neues Spiel startest.

  • Mods werden pro Spiel aktiviert. app.startGame2 erhält die Liste der Mods, die Spieleinstellungen (configDict) und die Mod-Parameter (modParams) (App). Aufgerufen wird es von der Seite „Neues Spiel“ (New Game) in base/content/gui/gui/menu/new_game_or_map_settings_page.tl. Laut Wiki kann der Spieler Mods und Mod-Parameter auch beim Laden eines Spielstands wählen.
  • debugPrint(...) gibt Text in Konsole und Logdatei aus, auch Tabellen (laut Definition „Prints text to the console/log file“, siehe main). Die API-Seite im Wiki empfiehlt die Konsole im Spiel, um Befehle auszuprobieren. Die Basisskripte nutzen außerdem die globale Tabelle log mit log.verbose, log.message, log.warning und log.error (log).
  • Lass die Validierung oft laufen, nicht erst vor dem Veröffentlichen (Best Practices). Die Definition von ModPublishHelper.validate sagt: „Validates a staging mod, this is an async operation ... Does not cook, only publish does.“ Die Prüfung läuft also asynchron und bereitet den Mod nicht auf (das passiert erst beim Veröffentlichen). Das Ergebnis ist ein Modhub.ModValidatorResult. Jede Meldung darin nennt die Datei, den Schweregrad und ob das Problem nur Konsolen betrifft. Mit dem Knopf „Export“ im Mod-Hub speicherst du diesen Bericht in eine Datei. Die einzelnen Prüfungen stehen unter Aufbau eines Mods.
  • Mit severityAdd und severityRemove in mod.json warnst du Spieler, bevor sie deinen Mod zu einem Spielstand hinzufügen oder daraus entfernen. Die Werte sind None, Warning und Critical, siehe Aufbau eines Mods.

Hinweis

Das Wiki führt „Ingame Tools“ als „coming soon“. Weder das Wiki noch die Spieldateien beschreiben ein Hot-Reload oder wie man die Konsole öffnet. Der „Console State“ (Konsolen-Zustand) wird in api/tealdef/api/cmd.d.tl nur erwähnt.

Auf mod.io veröffentlichen

Der offizielle Mod-Browser läuft über mod.io und erreicht Windows, macOS, Linux, PS5 und Xbox Series. Die Veröffentlichungsseite beschreibt den Ablauf:

  1. Lege den Mod in den Staging-Bereich und starte das Spiel. Im Mod-Hub im Hauptmenü gibt es dann den Reiter „Meine Mods“.
  2. Wähle den Mod aus. Dort legst du Name, Tags, Titelbild und Galeriebilder, Sichtbarkeit und eine Nachricht für das Änderungsprotokoll fest.
  3. Klicke auf „Hochladen“. Das Spiel validiert den Mod zuerst. Erfüllt der Mod auch die strengeren Regeln für Konsolen, bereitet das Spiel eine Version mit angepassten Texturen vor, und diese Version wird automatisch für PS5 und Xbox freigegeben.
  4. Nach dem Hochladen öffnet sich der Mod auf mod.io. Profil, Medien, Dateiverlauf, Abhängigkeiten, Vorschauzugang für Betatester, Teammitglieder, Meldungen und Statistiken bearbeitest du dort.

Nach dem ersten Hochladen schreibt das Spiel _metadata/mod.io_fileid.txt. Die Datei enthält die mod.io-ID und sagt dem Spiel, welchen Eintrag es aktualisieren soll. Geht sie verloren, legst du sie mit der ID aus dem Infokasten auf der mod.io-Seite des Mods neu an. Erkennt das Spiel das Ziel der Aktualisierung, steht in der Veröffentlichungsansicht neben dem Knopf „Aktualisieren“ der Satz „Die bestehende Mod auf mod.io wird aktualisiert.“ (englisch „The existing mod on mod.io will be updated.“).

Für ein Update erhöhst du revision in mod.json und schreibst eine Nachricht für das Änderungsprotokoll. Macht ein Update bestehende Spielstände unbrauchbar, empfiehlt das Wiki stattdessen eine neue modId und einen eigenen Mod. Name und Beschreibung aus modinfo.json überschreiben alles, was du auf mod.io daran geändert hast. Bewahre solche Änderungen also auch lokal auf.

Beim Veröffentlichen gelten diese Regeln: Du lädst nur Inhalte hoch, an denen du die Rechte hast, und nichts Anstößiges. Die Bedingungen von mod.io gelten, und Mods müssen kostenlos sein. Spenden, Auftragsarbeiten und Unterstützerprogramme wie Patreon sind erlaubt, ein früherer Zugang für Unterstützer aber nur für höchstens 31 Tage. Mods zu verkaufen oder dauerhaft hinter eine Bezahlschranke zu stellen, ist nicht erlaubt. Dateien aus Transport Fever, Transport Fever 2 und Transport Fever 3 darfst du in Mods für Transport Fever 3 verwenden, wenn du die Quelle nennst.

Andere Seiten, etwa die Filebase auf transportfever.net, nehmen Uploads von Hand und nach eigenen Bedingungen an. Trag die Download-URL in url in der modinfo.json ein, damit Spieler den Mod finden, wenn er ihnen fehlt.

Die Richtlinien im Wiki regeln auch die Präsentation: ein Name in englischer Titelschreibung mit höchstens 32 Zeichen, ein Titelbild mit 1920 × 1080 Pixeln, höchstens 8 MB groß und ohne Text, eine englische Beschreibung und Tags.

Offizielles Wiki: Modding manual, Introduction, Mod definition, Mod parameters and scripts, Syntax, External tools, Model Editor, Publish a mod, Guidelines and requirements, Best practices.