Zum Inhalt

Aufbau eines Mods

Diese Seite nimmt die Mods auseinander, die mit dem Spiel kommen: die Skript-Mods in mods/release/, die acht Kampagnenmissionen und die beiden DLC-Pakete in dlcs/. Das Basisspiel in base/ ist genauso aufgebaut. Wo das offizielle Modding-Wiki eine Datei beschreibt, folgt diese Seite dem Wiki und nennt die Stellen, an denen die ausgelieferten Dateien davon abweichen.

Ordnerstruktur

Die Seite zur Mod-Definition im Wiki zeigt, welche Dateien ein Mod haben kann:

my_mod/
├── mod.json                     technische Definition: ID, Revision, Hooks, Flags (Pflicht)
├── strings.json                 Übersetzungen der eigenen Texte des Mods (optional)
├── _metadata/
│   ├── modinfo.json             Name, Kurzbeschreibung, Beschreibung, Autoren, Tags (Pflicht)
│   ├── description.html         Beschreibung für den Mod-Browser und mod.io (optional)
│   ├── 0.png, 1.png, ...        Titelbild (0.png) und Galeriebilder, 1920 x 1080 (optional)
│   └── mod.io_fileid.txt        schreibt das Spiel nach dem ersten Hochladen
└── content/                     alles, was das Spiel lädt
    └── mod.script.tl            Skriptfunktionen für pre/run/post (optional)

Innerhalb von content/ unterscheidet das Spiel Ressourcen an der Dateiendung, nicht am Ordner (resource types). Das Wiki empfiehlt trotzdem die Ordnerstruktur des Basisspiels: einen Unterordner pro Asset und einen Ordner shared für Dateien, die mehrere Assets verwenden (guidelines).

Ein vollständiger Mod aus den Spieldateien, mods/release/urbangames_campaign_mission_01/ (Archive entpackt, Inhalt gekürzt):

urbangames_campaign_mission_01/
├── mod.json                     Mod-ID, Mod-Skript-Hooks, Flags
├── _metadata/modinfo.json       Name, Beschreibung, Autoren, Tags
├── _content.json                Liste der Inhaltsarchive und losen Dateien
├── texture_metadata_cache.lua   Größe und Mipmap-Daten pro Textur
├── tealdef/                     Teal-Typdefinitionen dieses Mods
│   ├── urbangames_campaign_mission_01_def.d.tl
│   └── mission/mission_params_01.d.tl
└── content/                     alles, was das Spiel lädt
    ├── mod.script.tl            Skriptfunktionen für pre/run/post
    ├── info.mission.lua         Missionsbeschreibung
    ├── animal.zip  assets.zip  audio.zip  cargos.zip  clima.zip
    ├── gui.zip  guide_system.zip  industries.zip  init.zip
    ├── mission.zip  terrain.zip  vehicle.zip  voiceover.zip

Nicht jeder Mod hat alle Teile:

Mod Inhalt
urbangames_no_costs, urbangames_sandbox, urbangames_tycoon mod.json, _metadata/modinfo.json, content/mod.script.tl
urbangames_vehicles_no_end_year dasselbe, mit content/mod.script.lua in Lua
urbangames_campaign mod.json, _metadata, tealdef, content/info.campaign.lua
urbangames_campaign_mission_01 … _08 vollständige Struktur wie oben
dlcs/urbangames_preorder_pack mod.json, _metadata, Inhaltsarchive animal, landmarks, vehicle
dlcs/urbangames_deluxe_upgrade_pack dasselbe, dazu tealdef/fun_elements/balloon.d.tl und die Archive characters, fun_elements, music

Die DLC-Pakete sind wie Mods gebaut. Sie haben keine Mod-Skript-Hooks und unterscheiden sich nur in den Flags ("autoActivate": true, "cosmetic": true, "severityRemove": "Warning").

mod.json

mod.json beschreibt den Mod technisch. Diese Schlüssel kommen in den ausgelieferten Dateien vor:

Schlüssel Werte in den ausgelieferten Mods Bedeutung
modId "urbangames_no_costs_1", "urbangames_campaign_mission_01", "urbangames_deluxe_upgrade_pack" Kennung, nur a-z, 0-9 und _. Skriptverweise stellen sie als Präfix voran: urbangames_no_costs_1::/mod.script@preRunFn. Im Basisspiel ist modId in base/mod.json leer.
revision 1 Version des Mods, steht auf der Detailseite des Mods. Erhöhe sie bei jedem veröffentlichten Update.
preRunScript, runScript, postRunScript { "fileName": "<modId>::/mod.script@<fn>" } oder "" Skriptfunktionen, die das Spiel beim Laden aufruft, siehe Mod-Skript-Hooks
dependencies immer null Mods, die dieser Mod braucht, siehe Abhängigkeiten
incompatibilities immer null Mods, mit denen dieser Mod nicht laufen kann
params null, außer in base/mod.json Parameter, die der Spieler für ein Spiel einstellt, siehe Mod-Parameter
options immer null Weder in den Definitionen noch im Wiki beschrieben
severityAdd, severityRemove "None", "Warning" Warnstufe, wenn der Mod einem Spielstand hinzugefügt oder daraus entfernt wird: None, Warning oder Critical. Ohne Angabe gelten None (Hinzufügen) und Warning (Entfernen).
cosmetic true bei Kampagne, Missionen und DLCs Erfolge bleiben erreichbar, solange alle aktiven Mods kosmetisch sind. Lass den Wert auf false, wenn der Mod die Simulation verändert.
visible false bei den Kampagnenmissionen false blendet den Mod in der Mod-Liste aus. Standard ist true. Ein ausgeblendeter Mod kann sich nicht automatisch aktivieren.
autoActivate true bei den DLCs „If the mod is automatically activate by the game“
url "" in base/mod.json „URL to the mod page“

Die drei Warnstufen beschreibt das Wiki so (mod.json): None, wenn Hinzufügen oder Entfernen nichts schadet, etwa bei einer rein optischen Änderung. Warning, wenn der Spielstand etwas verliert, zum Beispiel Fahrzeugmodelle, aber stabil bleibt. Critical, wenn er kaputtgehen kann, etwa weil Gütertypen fehlen. Die technischen Anforderungen verlangen ein korrekt gesetztes severityRemove.

Laut Wiki hat die modId anders als in den früheren Transport-Fever-Spielen kein Suffix für die Hauptversion wie _3. Die ausgelieferten Skript-Mods enden trotzdem auf _1 (urbangames_no_costs_1, urbangames_sandbox_1, urbangames_tycoon_1, urbangames_vehicles_no_end_year_1), ihre Ordner, die Kampagne und die DLCs dagegen haben kein Suffix. Für ein Update, das Spielstände unbrauchbar macht, empfiehlt das Wiki eine neue modId und einen eigenen Mod statt einer höheren revision.

Das Spiel findet einen Mod über seine modId, egal wo er installiert ist. Ist dieselbe ID zweimal installiert, gewinnt der Staging-Bereich vor einer manuellen Installation und diese vor einem Abo über mod.io.

Die zitierten Beschreibungen stammen aus Mod.ModDesc in api/tealdef/api/type/mod.d.tl. Diesen Typ verwendet das Spiel für eine geladene Mod-Beschreibung. In ModDesc sind severityAdd und severityRemove Ganzzahlen („0: None, 1: Warning, 2:Critical“), in den JSON-Dateien stehen die Namen. Die installierten Mods liest das Spiel über ModRep (app.getUserProfile():getModRep() in base/content/gui/gui/menu/mod_util.tl), dessen getModDesc diesen Typ zurückgibt.

Abhängigkeiten

Kein ausgelieferter Mod gibt eine Abhängigkeit an. Das Wiki (dependencies and incompatibilities) zeigt die JSON-Form. Ein Repaint, der den Mod mit dem Originalfahrzeug braucht, würde Folgendes angeben:

{
    "dependencies": [
        {
            "mod": { "modId": "jdoe_base_vehicles", "revisionMin": 2, "revisionMax": -1 },
            "modInfo": { "displayName": "Base Vehicles", "url": "https://example.com/base-vehicles" },
            "loadBefore": true,
            "optional": false
        }
    ],
    "incompatibilities": [
        { "mod": { "modId": "jdoe_hard_mode" } }
    ]
}
Feld Bedeutung Standard
mod.modId modId aus der mod.json des anderen Mods Pflicht
mod.revisionMin niedrigste akzeptierte Revision keine (-1 oder weggelassen)
mod.revisionMax höchste akzeptierte Revision; selten nötig, weil Updates kompatibel bleiben sollten keine (-1 oder weggelassen)
modInfo.displayName Name im Fenster „Mod-Abhängigkeiten nicht erfüllt“ (Mod Dependencies Not Satisfied), wenn die Abhängigkeit nicht installiert ist
modInfo.url optionaler Download-Link, zum Beispiel auf einer fremden Seite
loadBefore die Abhängigkeit vor diesem Mod laden true
optional empfohlen, nicht erforderlich false

Eine Inkompatibilität besteht nur aus dem Teil mod. Beides zeigt das Spiel an, während der Spieler Mods aktiviert. Es aktiviert Abhängigkeiten zuerst, bietet fehlende zum Herunterladen an und warnt, bevor ein Spiel mit fehlender Abhängigkeit oder mit einem aktiven inkompatiblen Mod startet.

Die Typen passen dazu: Mod.ModDependency hat mod, optional, loadBefore und modInfo. Mod.ModRef enthält modId, revisionMin und revisionMax, Mod.ModRefInfo enthält displayName und url. Eine Inkompatibilität (Mod.ModIncompatibility) ist nur ein ModRef. Die Mod-Auswahl in base/content/gui/gui/menu/mod_util.tl prüft diese Felder: Sie meldet Abhängigkeiten als fehlend mit dem Grund "NotFound" oder "IncorrectVersion", berücksichtigt optional und loadBefore und listet aktive inkompatible Mods auf.

_metadata/modinfo.json

Was der Spieler über den Mod zu sehen bekommt, steht in einer eigenen Datei. Aus mods/release/urbangames_campaign_mission_01/_metadata/modinfo.json:

{
    "authors": [
        {
            "name": "Urban Games",
            "role": "CREATOR"
        }
    ],
    "description": "...",
    "name": "Campaign Mission 01",
    "tags": [
        "Campaign"
    ],
    "url": ""
}

Die Skript-Mods und DLCs tragen als Werte Übersetzungsschlüssel wie "MOD_NO_COSTS_NAME" ein. Deren Übersetzungen kommen mit den Texten des Spiels (siehe Übersetzte Texte). Ein eigener Mod bringt seine Übersetzungen in derselben Datei mit, wie der Abschnitt zu modinfo.json im Wiki zeigt:

{
    "name": "Soft Brakes",
    "summary": "Trains brake more gently.",
    "description": "Lowers the global train brake deceleration.",
    "localization": {
        "de": {
            "name": "Sanfte Bremsen",
            "summary": "Züge bremsen sanfter.",
            "description": "Senkt die globale Bremsverzögerung von Zügen."
        }
    },
    "authors": [ { "name": "jdoe", "role": "CREATOR" } ],
    "tags": [ "Script Mod" ],
    "dependencies": [ "5499706" ],
    "url": "https://example.com/soft-brakes"
}
Schlüssel Regeln
name Anzeigename, höchstens 32 Zeichen, ohne Zeilenumbruch. Die Richtlinien verlangen englische Titelschreibung (Title Case).
summary Tooltip auf der Kachel des Mods im „Mod-Hub“ (Mod Hub), höchstens 100 Zeichen, ohne Zeilenumbruch
description Langer Text mit Zeilenumbrüchen. Wird ignoriert, wenn _metadata/description.html existiert. Muss auf Englisch sein.
localization name, summary und description pro Sprachcode; die Werte auf oberster Ebene gelten für alle anderen Sprachen
authors Liste aus name und role; die ausgelieferten Dateien verwenden nur "CREATOR"
tags offiziell unterstützte Tags, im „Mod-Hub“ als Filter genutzt ("Script Mod", "Campaign", "Town Building", "Depot" kommen in den Dateien und im Wiki vor)
dependencies mod.io-IDs (keine modIds) der Mods, die dieser Mod braucht, damit mod.io sie verknüpft. Ein hier entfernter Eintrag bleibt auf mod.io bestehen; dort musst du ihn auf der Website löschen.
url Link zu weiteren Informationen. Das Spiel zeigt ihn nicht an, und Konsolen können ihn nicht öffnen. Alles, was ein Spieler wissen muss, gehört deshalb in die Beschreibung.

Beim Hochladen bekommt mod.io den englischen Namen und eine Beschreibung, in der alle Sprachen hintereinanderstehen, weil mod.io noch keine Texte pro Sprache kennt. Das Wiki erwartet eine Änderung bis zum 2. Quartal 2027. Jedes Hochladen überschreibt Name und Beschreibung auf mod.io mit den Werten aus modinfo.json.

Für den Mod-Hub gibt es einen eigenen Record, Modhub.ModInfo, mit title, summary, description, tags, logoImage, Zählern für Downloads und Bewertungen und Angaben zur Veröffentlichung. Das Wiki ordnet die Felder nicht einzeln zu. Es sagt nur, dass der englische Name als Titel hochgeladen wird und dass die Dateien _metadata/<Nummer>.png das Titelbild (0.png) und die Galeriebilder sind.

Mod-Skript-Hooks

Die Mod-Skript-Hooks sind die drei Schlüssel preRunScript, runScript und postRunScript in mod.json. Jeder nennt genau eine Funktion. Basisspiel und offizielle Mods setzen sie für unterschiedliche Aufgaben ein:

Hook Wann er läuft (Wiki) Signatur in den ausgelieferten Skripten Was die ausgelieferten Mods dort tun
preRunScript bevor die Ressourcen von Spiel und Mods geladen werden preRunFn(captureParams, configDict, allModParams, baseConfig) Die Basiskonfiguration (BaseConfig) ändern. urbangames_no_costs setzt baseConfig.noCosts = true; das Basisspiel füllt hier fast die ganze Konfiguration.
runScript wenn der Mod geladen wird; registrierte Modifier und Filter wirken beim Laden der Ressourcen, nachdem alle runFn gelaufen sind runFn(captureParams, configDict, allModParams) (base/content/base/base/base_mod.lua; urbangames_vehicles_no_end_year nennt das zweite Argument settings und lässt das dritte weg, Mission 01 nimmt nur captureParams) Ressourcen-Modifier mit addModifier und Dateifilter mit addFileFilter registrieren.
postRunScript einmal, nachdem alle Ressourcen geladen sind postRunFn(captureParams, configDict, allModParams) Ressourcen über api.res bearbeiten, hinzufügen oder ausblenden.

Jeder Hook läuft zuerst für das Basisspiel und dann für die Mods in der Reihenfolge ihrer Aktivierung (script functions). Abhängigkeiten mit loadBefore werden vor den Mods aktiviert, die sie brauchen. Das Wiki rät, Filter und Änderungen möglichst in postRunFn zu erledigen und Modifier in runFn nur für das zu verwenden, was dort nicht geht.

Die Argumente:

Argument Inhalt
captureParams die Tabelle params des Skriptverweises (resource types); die Hook-Verweise in mod.json haben keine
configDict das gewählte Klima, die Wirtschaft und die Namensliste
allModParams Parameterwerte aller Mods, nach Mod-ID geordnet; allModParams[getCurrentModId()] liefert die des eigenen Mods (getCurrentModId)
baseConfig die Basiskonfiguration, nur bei preRunFn

Das Wiki führt die Hook-Parameter ohne captureParams auf. Jede ausgelieferte Funktion in mod.script nimmt es aber als ersten Parameter, und das entspricht der Regel des Wikis für Skriptverweise. Schreib deine Funktionen also mit diesem Parameter. Das Basisspiel liest seine eigenen Parameter aus allModParams[""] (base/content/base/base/base_mod.lua).

Ein Teal-Modul gibt eine Tabelle mit Funktionen zurück (urbangames_no_costs, zu sehen unter Erste Schritte). Eine Lua-Datei definiert data() und gibt die Tabelle daraus zurück (mods/release/urbangames_vehicles_no_end_year/content/mod.script.lua). Wie runFn Ressourcen-Modifier und Dateifilter registriert, samt der Liste der Kategorien, steht unter Ressourcen-Modifier und Dateifilter. Code zur Ladezeit zeigt, wofür die einzelnen Phasen da sind.

Eine postRunFn arbeitet mit den Repositorys in api.res. Aus mods/release/urbangames_campaign_mission_01/content/mod.script.tl:

mod.postRunFn = function(captureParams)
    local baseConfig = api.res.getBaseConfig()
    baseConfig.advancedOptions.subventionMode = 0

    local model = api.res.modelRep.get(api.res.modelRep.find("::/vehicle/truck/horse_cart_medium/horse_cart_medium.mdl"))
    local tvMetadata = model.metadata["transportVehicle"] as ModelMetadata.TransportVehicle
    tvMetadata.loadSpeed = tvMetadata.loadSpeed * 10

    -- ...
    local allBriges = api.res.bridgeTypeRep.getAll()
    for id, name in pairs(allBriges) do
        if name ~= "::/infrastructure/bridge/trestle.bridge" then
            api.res.bridgeTypeRep.setVisible(id, false)
        end
    end
end

ResTypeRep ist das Interface aller Repositorys: find(resName) gibt eine ID zurück (-1, wenn es die Ressource nicht gibt), get(id) die Ressource und getAll() eine Zuordnung von IDs zu Namen. setVisible blendet eine Ressource für den Spieler aus, add registriert eine neue. Die postRunFn des Basisspiels legt mit api.res.moduleRep.add für jeden Gleistyp ein Bahnhofsmodul an.

Zum Bearbeiten nehmen die Beispiele im Wiki die Tabellenform: getAsTable(id) gibt eine Lua-Tabelle zurück, die du beliebig ändern kannst, setAsTable(id, data) schreibt sie zurück, und addAsTable(name, data) fügt sie als neue Ressource hinzu. modelRep.forEachModelWithMetadata durchläuft alle Modelle mit einem bestimmten Metadaten-Schlüssel. Ein Kapazitätsfaktor aus einem Mod-Parameter:

mod.postRunFn = function(captureParams, configDict : {{string, string}}, allModParams : {string : {string : integer}})
    local factor = allModParams[getCurrentModId()].capacityFactor
    api.res.modelRep.forEachModelWithMetadata("transportVehicle", function(name : string)
        local id = api.res.modelRep.find(name)
        local model = api.res.modelRep.getAsTable(id)
        local tv = model.metadata["transportVehicle"] as ModelMetadata.TransportVehicle
        for _, compartment in ipairs(tv.compartments) do
            for _, loadConfig in ipairs(compartment.loadConfigs) do
                if loadConfig.cargoEntry and loadConfig.cargoEntry.capacity then
                    loadConfig.cargoEntry.capacity = loadConfig.cargoEntry.capacity * factor
                end
            end
        end
        api.res.modelRep.setAsTable(id, model)
    end)
end

Das folgt dem Beispiel zur Ressourcenänderung im Wiki. Neue 3D-Daten können Skripte nicht anlegen: Ein so hinzugefügtes Modell kann über seinen modelPath nur auf vorhandene .mdl-Dateien zeigen.

Mod-Parameter

base/mod.json legt die Einstellungen für ein neues Spiel als params fest. Ein Eintrag:

{
    "defaultIndex": 1,
    "key": "advancedOptions.reforestation",
    "name": "Reforestation",
    "tooltip": "Enable or disable reforestation.\n\nIf reforestation is on, bald spaces after removing constructions will regrow their initial trees.",
    "uiType": "Button",
    "values": [
        "Off",
        "On"
    ],
    "yearFrom": 0,
    "yearTo": 0
}

Die Felder entsprechen ScriptParam und dem Abschnitt zu Mod-Parametern im Wiki:

Feld Bedeutung
key Name, unter dem Skripte den Wert lesen
name Beschriftung in der Oberfläche; ein Übersetzungsschlüssel, siehe Übersetzungen
tooltip optionaler Text beim Darüberfahren mit der Maus; ein Übersetzungsschlüssel, siehe Übersetzungen
uiType Button, Slider, ComboBox, IconButton oder CheckBox
values Beschriftungen, bei IconButton Bildpfade; Beschriftungen sind Übersetzungsschlüssel, siehe Übersetzungen
numbers optionale Werte, die die Skripte pro Option erhalten, einer pro Eintrag in values (script_param_util.tl verwirft die Liste, wenn die Anzahl nicht passt); Standard 1, 2, 3 usw. ScriptParam typisiert die Liste als {number}, die Hook-Signaturen typisieren die empfangenen Werte als integer, und kein ausgelieferter Parameter nutzt Kommazahlen. Prüf einen gebrochenen Wert also im Spiel.
defaultIndex voreingestellte Option, ab 0 gezählt (anders als Lua-Indizes); Standard ist die erste Option

"defaultIndex": 1 im Eintrag zur Wiederaufforstung wählt also "On". Ohne numbers bekommt ein Skript 1 für die erste Option, 2 für die zweite und so weiter. Der Spieler stellt die Werte in der Mod-Auswahl ein, wenn er ein neues Spiel startet oder einen Spielstand lädt. Konstruktionen haben eine ähnliche Parameterliste in Lua, dort beginnt defaultIndex bei 1 (siehe Parameter). Manche Einträge haben tags wie "desktop", "console" oder "experimentalMapFeatures", und derselbe Schlüssel kommt dann mehrmals mit unterschiedlichen Wertelisten je Tag vor. Die Skriptfunktionen bekommen die gewählten Werte in allModParams, einer Tabelle mit der Mod-ID als Schlüssel; das Basisspiel liest seine eigenen aus allModParams[""]. Zur Laufzeit lesen Game-Scripts sie mit api.engine.config.getModParams(), zum Beispiel api.engine.config.getModParams()[""]["isMapEditor"] in base/content/game_mechanics/game_mechanics/towns/towns.script.tl. Kein ausgeliefertes Game-Script liest die Parameter seines eigenen Mods. getOwningModId(), in der Game-Script-Datei selbst aufgerufen, müsste den Schlüssel für getModParams() liefern, aber keine ausgelieferte Datei bestätigt das.

Wie Inhalte verpackt sind

Zuordnung der Zip-Archive

Das Spiel liefert content/ als Satz von Zip-Archiven und losen Dateien aus. _content.json listet beides auf. Aus mods/release/urbangames_campaign_mission_01/_content.json (gekürzt):

{
    "archives": [
        { "fileName": "animal.zip", "entries": [ { "fileName": "animal/alligator/alligator.mdl", ... } ] },
        { "fileName": "mission.zip", "entries": [ { "fileName": "mission/mission_story.tl", ... } ] }
    ],
    "files": [ "info.mission.lua", "mod.script.tl", "mission01conlua.txt" ]
}

Die Namen der Einträge gelten relativ zu dem Ordner, in dem das Archiv liegt. mission.zip liegt direkt in content/ und enthält mission/mission_story.tl; das Spiel spricht die Datei als urbangames_campaign_mission_01::/mission/mission_story.tl an. Archivnamen können auch Ordner enthalten: Im Basisspiel hat assets/buildings/ind.zip den Eintrag ind/mat/assets_ind_1_body.mtl, der Pfad der Datei ist also /assets/buildings/ind/mat/assets_ind_1_body.mtl. Das Basisspiel hat 1.092 Archive, darunter assets/stations.zip und buildings/a/c1/1x1_01.zip, und lose Dateien wie industries/industry_workers.gs.lua. Im entpackten Verzeichnisbaum, aus dem diese Seiten zitieren, wurde jedes Archiv zu einem Ordner mit dem Namen der Zip-Datei. Daher kommen doppelte Pfade wie content/mission/mission/mission_story.tl.

Ein selbst geschriebener Mod braucht keine Archive: Im Wiki liegen die Dateien eines Mods lose in content/. Ob das Spiel die Archive und _content.json erzeugt, wenn es einen Mod aufbereitet („cooking“), steht weiter unten.

Dateiverweise im Inhalt

Die Regeln stehen im Wiki im Abschnitt zu Dateinamen und Verweisen. Ein Pfad mit / am Anfang ist absolut und beginnt im content/-Ordner des aktuellen Mods (bei Dateien des Basisspiels im Basisspiel). Ein Pfad ohne / am Anfang gilt relativ zur verweisenden Datei. Ein Präfix wählt einen anderen Mod:

Form Zeigt auf Beispiel aus den Spieldateien
/path absolut, im aktuellen Mod ug_require "/mission/mission_params_01.tl" in Mission 01; ug_require "/scripts/vec3.tl" im Basisspiel
path relativ zur aktuellen Datei, im aktuellen Mod "small_stops.script@updateFn", resolve("small_new.mdl")
::/path absolut, im Basisspiel "::/gui/construction/construction_desc_hud_icons.script@configureStationHudIconsFn"
::path relativ zur aktuellen Datei, aber in den Ordnern des Basisspiels "::buildings/ind/mat/assets_ind_1_body.mtl" in /assets/instrument_box.mdl von Mission 04, also die Basisdatei /assets/buildings/ind/mat/assets_ind_1_body.mtl
<modId>::/path absolut, im Mod <modId> "urbangames_campaign_mission_01::/mission/hud_replacement.script"
<modId>::path relativ zur aktuellen Datei, im Mod <modId> "ug_wiki_example_mod::mod.script@preRunFn" aus dem Wiki, in mod.json

Dateien des Basisspiels laden andere Basisdateien oft mit /..., weil / für sie das Basisspiel bedeutet. Wenn du eine solche Zeile in einen Mod übernimmst, schreib ::/...: Aus require "/scripts/mat4.tl" in einer Basisdatei wird in deinem Mod require "::/scripts/mat4.tl".

../ ist nicht erlaubt, eine Datei kann also nicht auf ihren übergeordneten Ordner verweisen. resolve(relativePath): „Resolves a file paths relative to the current file“.

Die ausgelieferten mod.json-Dateien schreiben die Hook-Verweise mit Schrägstrich (urbangames_no_costs_1::/mod.script@preRunFn), die Beispiele im Wiki ohne. Da mod.json im Wurzelordner des Mods liegt und mod.script direkt in content/, zeigen beide Formen auf dieselbe Datei.

Datei- und Ordnernamen dürfen nur die Kleinbuchstaben a-z, die Ziffern 0-9, _ und - enthalten, keine Leerzeichen, und sollten kurz bleiben. Textdateien (.lua, .tl, .json, .mdl, .msh usw.) sind in UTF-8 ohne Byte Order Mark gespeichert.

Skriptverweise zeigen auf eine Funktion: file.script@function. Die Endung .lua oder .tl fällt weg, und der Teil nach @ kann ein Pfad mit Punkten in die zurückgegebene Tabelle sein ("modules.script@bulk.updateFn" in base/content/warehouses/warehouses/wh_bulk.module.lua). ScriptRef beschreibt dasselbe Prinzip für eine ältere Schreibweise der Form models/model/animal/animal.flock.crane.

Code wird mit ug_require geladen: „Used instead of the regular lua require to include game related script“. Die Basisskripte casten das Ergebnis auf seinen Typ: local mathutil = ug_require "::/scripts/mathutil.lua" as MathUtil.

Die Lua-Dateien des Basisspiels laden Module mit einfachem require und denselben Pfaden, zum Beispiel require "/scripts/soundsetutil.lua" oder require "::/scripts/construction/constructionutil.lua". Beide Wege funktionieren, denn base/content/base/base/init.lua ersetzt require, sodass ein Pfad mit der Endung .lua oder .tl wie jeder andere Dateiverweis aufgelöst wird, und ug_require ruft nur dieses require auf. In Teal-Dateien nimmst du ug_require ... as Type: Nur ug_require ist in main.d.tl deklariert, und der Cast mit as ist Teal-Syntax, die in einer .lua-Datei fehlschlägt. Für Pfade ohne diese Endungen behält init.lua das ursprüngliche require.

Die Ordner in base/content

Die obersten Ordner von base/content und die Dateitypen, die dort üblich sind. Die Tabelle zur Ordnerstruktur im Wiki beschreibt dieselben Ordner, die Tabelle der Dateiendungen listet jeden Ressourcentyp nach Endung auf.

Ordner Typische Dateien
animal, vehicle, characters .mdl-Modelle mit .mtl-Materialien, Animationen (.ani)
assets, buildings Modelle für Assets und Stadtgebäude; Gebäude haben zusätzlich .con.lua
stations, depots, industries, landmarks, warehouses Konstruktionen (.con.lua), Module (.module.lua), Metakonstruktionen (.metacon.tl) und ihre .script.lua/.script.tl
infrastructure Straßen (.street.lua, .street_template.lua), Gleise, Brücken, Tunnel, Signale
game_mechanics Game-Scripts (.gs.lua) für Städte, Unternehmen, Finanzen, Benachrichtigungen usw. (siehe Spielmechaniken)
gui die Benutzeroberfläche in Teal, Stylesheets (.css.lua), GUI-Ressourcen (.gres.lua)
mission Missions-Framework, Aufgaben, Spielführer
scripts gemeinsame Bibliotheken (vec3, mat4, util, Hilfsfunktionen für Konstruktionen, react.d.tl)
rendering Materialtypen (.mat.lua), Shader-Programme (.prog.lua), Techniken, Eigenschaften
climates, terrain, economy, cargos, names, music, locale Klimas (.clima.lua), Geländematerialien, Wirtschaften (.eco.lua), Gütertypen, Namenslisten, Playlists, Sprachen

Binärdateien (Meshes als .msh, Texturen als .dds und .tga, Sounds) stehen in _content.json, gehören aber nicht zu den entpackten Quellen, aus denen diese Anleitungen entstanden sind.

Generische Ressourcen

Konfiguration, die weder Modell noch Konstruktion ist, steckt oft in einer .res.lua-Datei mit type und einer Tabelle data. Aus base/content/game_mechanics/game_mechanics/game_time/game_time_config.res.lua:

function data()
    return {
        type = "game_script_config",
        data = {
            timeOfDayMode = "Dynamic"
        }
    }
end

Skripte lesen sie über api.res.genericRep, entweder nach Name (api.res.genericRep.find("::/game_mechanics/game_time/game_time_config.res")) oder nach Typ (api.res.genericRep.getAllOfType("notification")). Die 295 .res.lua-Dateien in den Spieldateien verwenden diese Typen (Pfade unterhalb von base/content/, sofern kein Mod genannt ist):

Typ Dateien Beispiel Was er hinzufügt Siehe
menu_filter_category 75 gui/gui/construction/menu/filter_categories/gravel.res.lua einen Filter im Baumenü Das Baumenü
menu_category 49 gui/gui/construction/menu/menu_categories/category_road_buildings.res.lua einen Reiter im Baumenü oder bei den Modulen Das Baumenü
react-plugin ::<ExtensionPoint> 44 gui/gui/entity_window/warehouse/warehouse_eow_stocks.res.lua ein GUI-Plugin an einem Erweiterungspunkt GUI
notification 33 game_mechanics/game_mechanics/notifications/types/stuck_vehicle.res.lua; Mission 05 und das Deluxe-DLC bringen eigene mit einen Benachrichtigungstyp Benachrichtigungen
construction_tool 22 gui/gui/construction/tools/asset_eraser_tool.res.lua ein Werkzeug im Baumenü Werkzeuge im Baumenü
company_exploration 12 game_mechanics/game_mechanics/company/explorations/exploration_coal.res.lua eine Prospektion als Bonus Unternehmensboni
stamp 11 gui/gui/construction/shared/tex/stamps/desert_hill1.res.lua eine Heightmap (.png plus Icon) für das Werkzeug „Gelände zerklüften“ im Baumenü, das nur der Karten-Editor und der Sandkastenmodus anbieten Geländestempel
missiontask_boot 8 mission/mission/mission.res.lua in jeder Kampagnenmission startet eine Mission Wie eine Mission startet
guide_system_boot 6 mission/mission/guide_system/guide_system.res.lua; Missionen 01, 02, 03 und 07 startet einen Spielführer Wie eine Mission startet
game_script_config 6 game_mechanics/game_mechanics/game_time/game_time_config.res.lua Einstellungen, die ein Game-Script des Basisspiels nach Name liest Die Game-Scripts des Basisspiels
subvention 5 game_mechanics/game_mechanics/subventions/deliver_cargo/deliver_cargo.res.lua; Mission 06 fügt deliver_visitors hinzu einen Subventionstyp Subventionen
rename_scheme_component 5 gui/gui/line_vehicle_mgmt/auto_rename/scheme_components/scheme_component_town.res.lua einen Bestandteil eines automatisch erzeugten Namens, mit seiner renameFn keine
react-replacement-config 4 mission/mission/hud_replacement.res.lua in Mission 01 ersetzt ein Recipe der Basis-GUI Ein Basis-Recipe ersetzen
firstStopToSend_scheme 4 gui/gui/line_vehicle_mgmt/first_stop_to_send/scheme_firststop.res.lua eine Regel, welche Haltestelle ein neues Linienfahrzeug zuerst anfährt keine
rename_scheme 3 gui/gui/line_vehicle_mgmt/auto_rename/schemes/scheme_firststop_laststop.res.lua ein Schema für automatische Namen (formatString = "{key1} - {key2}") keine
drag_and_drop 3 gui/gui/line_vehicle_mgmt/dnd_train_composition.res.lua eine leere Markierung für etwas, das die GUI ziehen kann, wird nach Name gelesen keine
company_permit_key 3 game_mechanics/game_mechanics/company/permitKeys/hq.res.lua einen Genehmigungsschlüssel als Bonus Unternehmensboni
tutorial_boot 1 mission/mission/tutorial/mission.res.lua startet das Tutorial Wie eine Mission startet
game_config 1 infrastructure/traffic_light/traffic_light/traffic_light_config.res.lua trafficLightYearFrom, wird nach Name gelesen keine

Nur notification, subvention, missiontask_boot, guide_system_boot und react-replacement-config kommen in einem ausgelieferten Mod oder DLC vor. Die übrigen Typen sammelt das Basisspiel mit getAllOfType ein (menu_category, menu_filter_category, react-plugin, construction_tool, company_exploration, company_permit_key, stamp, rename_scheme, rename_scheme_component, firstStopToSend_scheme, tutorial_boot). Eine Datei desselben Typs aus einem Mod müsste deshalb neben den Basisdateien auftauchen, aber kein ausgelieferter Mod bestätigt das. Ressourcen vom Typ game_script_config, game_config und drag_and_drop liest das Spiel über feste Namen, eine neue Datei dieser Typen wird also nicht berücksichtigt.

Platzhalter für fehlende Dateien

base/content/placeholders/placeholders/ enthält Ersatz für fehlende Inhalte: generic.con.lua (Name MISSING_CONSTRUCTION, dargestellt als Ausrufezeichen-Markierung /assets/markers/marker_exclamation.mdl), missing_signal.con.lua, missing_street_terminal.con.lua, missing_gtex.gtex.lua und je ein missing_*.mdl pro Modellart (missing_rail_vehicle.mdl, missing_road_vehicle.mdl, missing_animal.mdl, missing_person.mdl usw.). Verweist ein Spielstand oder eine Konstruktion auf eine Ressource, die es nicht mehr gibt, etwa weil ein Mod entfernt wurde, müsste das Spiel an ihrer Stelle einen dieser Platzhalter zeigen. Darauf deuten nur die Dateinamen und MISSING_CONSTRUCTION hin; im Spiel ist es nicht bestätigt. Die Basisskripte verwenden die Platzhalter auch direkt: base/content/scripts/scripts/construction/constructionutil.lua greift auf "::/placeholders/missing_generic.mdl" zurück, wenn eine Modellgruppe leer ist, und 48 Dateien nutzen /placeholders/generic.tga als Icon.

Übersetzte Texte

Texte in Skripten stehen in _(). Ein Mod übersetzt seine Schlüssel in strings.json im Wurzelordner des Mods, mit einer Tabelle pro Sprachcode. Die Funktionen für Übersetzungsschlüssel, das Format von strings.json, die Sprachcodes und die .lang.lua-Dateien erklärt Übersetzungen. Name, Kurzbeschreibung und Beschreibung des Mods werden dagegen in modinfo.json übersetzt (siehe oben).

Vom Spiel erzeugte Dateien

_content.json und texture_metadata_cache.lua (Texturgrößen und Anzahl der Mipmaps pro .dds) liegen in jedem ausgelieferten Mod. Die Modhub-Typen erwähnen das Aufbereiten („cooking“): Modhub.ModValidatorResult hat cookedPcPath und cookedConsolePath, und zu ModPublishHelper.publish heißt es „Validates and cooks the mod once more first and uploads the cooked folders“. Weder die Dateien noch das Wiki sagen, ob beim Aufbereiten diese beiden Dateien entstehen oder ob ein von Hand gebauter Mod sie braucht. Die Mod-Struktur im Wiki führt sie nicht auf. Ein Mod ohne sie ist also der dokumentierte Ausgangspunkt.

Die einzige Datei, die das Wiki als erzeugt bezeichnet, ist _metadata/mod.io_fileid.txt. Sie entsteht nach dem ersten Hochladen auf mod.io, enthält die mod.io-ID und verknüpft spätere Uploads mit demselben Eintrag (siehe Veröffentlichen).

Technische Anforderungen

Jeder Upload durchläuft eine automatische Prüfung für PC und Konsolen, und nur ein Mod, der sie besteht, wird veröffentlicht. Die technical requirements im Wiki:

Bereich Anforderung
Integrität alle Ressourcen sind syntaktisch korrekt und laden ohne Fehler und Warnungen
Sicherheit keine Batch-Dateien, Shell-Skripte oder andere ausführbare Dateien
Kodierung Textdateien in UTF-8 ohne BOM
Größe höchstens 500 MB entpackt pro Mod
Namen Dateien und Ordner kleingeschrieben, ohne Leer- und Sonderzeichen
Ballast keine Quelldateien (.psd, .fbx) und keine temporären Dateien
Meshes Mesh-Blobs der niedrigsten LOD-Stufe höchstens 256 KB
Texturen DDS mit Mipmaps, Größen als Zweierpotenz (512, 1024, 2048 usw.)
Skripte nur die offizielle Skript-API
Audio .wav mit 16-Bit-Samples bei 48.000 Hz
Spielstände severityRemove korrekt gesetzt
Nur Konsolen keine eigenen Shader, keine Eingriffe ins Kern-Gameplay, die die Leistung verschlechtern

Die Richtlinien nennen außerdem Zielwerte für Inhalte, zum Beispiel 20.000 bis 50.000 Dreiecke für ein typisches Fahrzeug (die größten haben bis zu 100.000), eine Texeldichte von 128 bis 170 Pixeln pro Meter und 1 bis 3 LOD-Stufen. Diese Werte gehören auf die Seite Modelle und Assets.

Offizielles Wiki: Mod definition, Mod parameters and scripts, Resource types and structure, Syntax, Resource modifiers and file filters, Localizations, Guidelines and requirements, Best practices, Publish a mod.