Die Beispiel-Mods unten sind examples/mods/example-vehicle/ (eine Fahrzeugvariante mit Lackierung und eine Einsatzvariante) und examples/mods/example-assets/ (ein eigenes glTF-Modell, ein OGG-Sound und ein Funkspruch). Die JSON-Schemas aller Datentypen liegen in docs/modding/schemas/, beides im Quellcode des Spiels.
1. Was ein Mod ist
Ein Mod ist ein Ordner mit einer mod.json und beliebigen der folgenden Teile. Es wird nichts kompiliert; alles ist JSON-Text und gewöhnliche Dateien.
mein-mod/
mod.json Beschreibung des Mods (Pflicht)
data/*.json neue oder geänderte Definitionen: Fahrzeuge, Einsatzarten, Rollen, ...
locale/de.json Texte (Deutsch), Schlüssel -> Text
locale/en.json Texte (Englisch)
models/*.glb 3D-Modelle (glTF 2.0)
textures/*.png Bilder (Vorschaubilder, Decals)
audio/*.ogg Geräusche und Funksprüche (Ogg Vorbis)
Ein Mod kann
- Einträge hinzufügen (neue ID), zum Beispiel eine Fahrzeugvariante oder eine neue Einsatzart,
- Einträge des Spiels oder anderer Mods überschreiben (gleiche ID),
- einen Eintrag patchen (nur einzelne Eigenschaften ändern),
- Assets mitbringen: glTF-Modelle, PNG-Bilder und OGG-Sounds.
Alles, was das Spiel nutzt, sind Daten im selben Format: Die eigenen Definitionen des Spiels liegen in Game/data/content/*.json und werden genauso geladen wie die Dateien eines Mods. Sie sind gute Beispiele.
Was ist Daten? Rollen, Ausrüstung, Fahrzeuge (mit Türanordnung), die Fahrzeuge in den Hallen der Wachen, Brandmaterialien, Einsatzarten (Titel, benötigte Organisationen, Belohnung, Häufigkeit, Größenbereiche), Spielmodi und Schwierigkeitsgrade, Karten (Generator, Seed und Layout: Abschnitt 13), Lackfarben ziviler Autos, Lackierungen, zusätzliche Funksprüche und Geräusche. Code bleibt: wie ein Einsatz abläuft (eine Variante nutzt das Verhalten einer der 16 Arten), die Fähigkeiten selbst und alles an der Simulation.
2. Wohin die Mods kommen
- Der Ordner des Spielers:
user://mods/. Unter Windows ist das%APPDATA%\Godot\app_userdata\Blue Light\mods. Die Schaltfläche Ordner öffnen im Mod-Bildschirm öffnet ihn. - Zur Entwicklung: Spiel mit
--mods-dir=C:/Pfad/zum/Ordner-mit-Modsstarten. Mods dort sind für diesen Start eingeschaltet.
Jeder Mod ist ein Unterordner. Der Ordnername ist egal, die id in der mod.json zählt.
3. Schritt für Schritt: ein Fahrzeug in 10 Minuten
Wir bauen eine gelbe Werkfeuerwehr-Version des Löschfahrzeugs (das ist der Beispiel-Mod).
Schritt 1: der Ordner. Lege mein-erster-mod/ mit den Unterordnern data/ und locale/ an.
Schritt 2: mod.json.
{
"formatVersion": 1,
"id": "mein-erster-mod",
"name": "Mein erster Mod",
"version": "1.0.0",
"author": "Dein Name",
"licence": "CC0-1.0",
"description": "Ein gelbes Löschfahrzeug.",
"gameVersion": ">=0.9.0 <1.0.0"
}
id: 2 bis 48 Zeichen, Kleinbuchstaben, Ziffern, ., _, -. version: Haupt.Neben.Patch. gameVersion ist ein Versionsbereich (Abschnitt 8). Trägst du in $schema den echten Pfad von docs/modding/schemas/mod.schema.json ein, ergänzt und prüft dein Editor (VS Code, Rider, ...) die Datei.
Schritt 3: eine Lackierung (data/liveries.json). Eine Lackierung färbt Materialien eines Modells nach Namen um. Der rote Lack des Löschfahrzeugs ist das Material Paint_Fire.
{ "liveries": [ { "id": "mein_gelb", "colors": { "Paint_Fire": "#E0B000" } } ] }
Schritt 4: das Fahrzeug (data/vehicles.json). extends kopiert ein vorhandenes Fahrzeug; du schreibst nur, was anders ist.
{
"vehicles": [
{
"id": "mein-erster-mod.LF_GELB",
"extends": "FW_LF",
"name": "Gelbes Löschfahrzeug",
"livery": "mein_gelb",
"speedKmh": 90,
"cost": 3600
}
]
}
Ein Fahrzeug mit extends zeigt dasselbe 3D-Modell wie seine Vorlage, solange du kein model angibst. Es erscheint automatisch im Einsatzmenü seiner Organisation. Ein Bild für das Menü ist optional ("thumbnail": "textures/lf.png"; ohne wird das Bild der Vorlage gezeigt).
Schritt 5: die Texte (locale/de.json und locale/en.json). Jedes neue Fahrzeug braucht einen Namen unter dem Schlüssel vehicle.<id>:
{ "vehicle.mein-erster-mod.LF_GELB": "Gelbes Löschfahrzeug" }
{ "vehicle.mein-erster-mod.LF_GELB": "Yellow pumper" }
Schritt 6: testen. Ohne das Spiel zu starten:
godot --headless --path Game -- --validate-mod=C:/Pfad/zu/mein-erster-mod --lang=de
Du bekommst einen lesbaren Bericht und den Exit-Code 1 bei Fehlern (Abschnitt 9).
Schritt 7: spielen. Kopiere den Ordner in den Mod-Ordner (oder nutze --mods-dir), starte das Spiel und öffne Mods im Hauptmenü: Mod einschalten, Meldungen ansehen. Starte das Freie Spiel und öffne das Einsatzmenü der Feuerwehr: Das neue Fahrzeug steht in der Liste.
4. Schritt für Schritt: eine neue Einsatzart
Eine Einsatzart ist eine Variante einer der 16 eingebauten Arten (BuildingFire, CarFire, TrafficAccident, MedicalCall, BuildingCollapse, ShedCollapse, GasLeak, ChemicalSpill, FloodedUnderpass, Riot, Hostage, ForestFire, PersonInRiver, Jumper, BusCrash, FleeingSuspect). Sie läuft wie ihre Art ab, mit eigenem Titel, eigener Belohnung, Häufigkeit und Größenbereichen. data/incidents.json:
{
"incidents": [
{
"id": "mein-erster-mod.fahrradunfall",
"extends": "MedicalCall",
"title": "incident.mein-erster-mod.fahrradunfall",
"reward": 3500,
"weight": 5,
"scale": {
"events": { "Collapse": 0, "Fall": 100, "Breathing": 0 },
"severity": { "Fall": { "Light": 30, "Serious": 60, "Critical": 10 } },
"secondPatientChance": 0.25
}
}
]
}
weightist, wie oft die Einsatzleitung diese Art im Vergleich zu den anderen wählt (die eingebauten haben 1 bis 6; 0 = nie zufällig).scaleenthält die Größentabellen,[[Wert, Gewicht], ...]von klein nach groß. Welche Tabellen gelten, hängt von der Art ab:BuildingFire(buildings, injured, trapped, trappedNight, intensity, gasBottleChance),CarFire(cars, spreadChance),TrafficAccident(cars, injured, trapped, fuelLeakChance, fireChance),MedicalCall(events, severity, secondPatientChance). Objekte werden Eigenschaft für Eigenschaft mit der Vorlage verschmolzen; eine ausgelassene Tabelle ist die der Vorlage. Um einen Eintrag der Vorlage auszuschalten, gib ihm das Gewicht 0 (wieCollapseundBreathingoben) odernull.titleist ein Textschlüssel; trage ihn in deine Sprachdateien ein.
5. Assets
Alle Pfade sind relativ zum Mod-Ordner, mit / geschrieben, und dürfen ihn nicht verlassen.
| Art | Format | Verwendet von |
|---|---|---|
| 3D-Modell | glTF 2.0, .glb oder .gltf, 1 Einheit = 1 m, Front nach +Z, Ursprung am Boden in der Mitte |
Fahrzeug model |
| Bild | PNG | Fahrzeug thumbnail (das Bild im Einsatzmenü, 256 x 256 passt gut), Lackierung decals, Karte thumbnail |
| Ton | Ogg Vorbis .ogg |
Funksprüche (files), Geräusche |
Modelle. Benenne die Teile wie die Modelle des Spiels, dann verhalten sie sich wie diese: Materialien Paint_*, Glass*, Tyre, Light_Blue_A/Light_Blue_B (Blaulicht), Headlight, Taillight, ...; Knoten Wheel_* (drehen sich), door_fl, door_fr, door_rl, door_rr, door_slide_l, door_slide_r, door_rear_l/door_rear_r, tailgate (öffnen sich, siehe doorLayout des Fahrzeugs), HeadlightL/HeadlightR, LadderBase, LadderArm, Basket, Boom, Mast, Rotor, TailRotor, Monitor, CargoMount. Modelle ohne diese Knoten funktionieren trotzdem; die Teile bewegen sich nur nicht. Mod-Modelle haben keine LOD-Stufen, halte sie also leicht.
Lackierungen. colors ordnet einem Materialnamen (oder seinem Anfang, Paint_Fire trifft auch Paint_Fire_2) ein #RRGGBB zu. decals ersetzt den Decal-Atlas des Modells (Schriftzüge, Streifen); das PNG muss das UV-Layout des Modells verwenden, das ist also für selbst gebaute Modelle. Eine Lackierung ändert nur die Fahrzeuge, die sie nennen.
Geräusche. Ein sounds-Eintrag nimmt eine OGG in die Klangbank auf und, mit "events": ["horn"], in die Klänge, die dieses Klangereignis spielen darf (Ereignisnamen stehen in Game/data/sound_bank.json). Ein voiceLines-Eintrag fügt einen Funkspruch hinzu (deutscher und englischer Untertitel, optional eine Aufnahme je Sprache) für eine Gruppe (fire, rescue, police, technical) und eine Situation (ack, arrive, refuse, patient_stable, fire_out, person_freed).
Lizenz. Verwende nur Assets, die du selbst gemacht hast oder die frei weitergegeben werden dürfen (CC0, CC-BY, ...), und trage das in licence ein. Niemals Assets anderer Spiele verwenden.
6. Die Datentypen
Jede Datendatei ist ein JSON-Objekt, dessen Abschnitte Listen von Einträgen sind. Eine Datei darf beliebige Abschnitte enthalten; die Dateinamen sind frei. Jeder Eintrag hat eine id und die Eigenschaften seines Typs. Unbekannte Eigenschaften sind Fehler (mit einem "meintest du"-Hinweis). Schemas: docs/modding/schemas/<Typ>.schema.json.
| Abschnitt | Typ | Eigenschaften (Pflicht zuerst) |
|---|---|---|
roles |
Rolle | name, org, cost, abilities, walkSpeed; optional flags, model (die eingebaute Rolle, deren Personenmodell sie nutzt) |
equipment |
Ausrüstung | name, abilities, handsNeeded, placeable |
vehicles |
Fahrzeug | name, org, seats, allowedRoles, abilities, speedKmh, cost, size, defaultCrew, doorLayout; optional cargo, equipment, flags, waterTank, stretchers, model, thumbnail, livery |
fleets |
Flotte | org, bays: die Fahrzeuge, die in den Hallenplätzen der Wache dieser Organisation bereitstehen |
materials |
Brandmaterial | ignitionHeat, maxTemperature, burnTime, chargeTime, cooling, heatResistance, defaultHeatRadius; optional radiantFactor, explodes, explosionForce, explosionRadius |
incidents |
Einsatzart | kind, title, needs, abilities, reward, weight; optional scale |
modes |
Spielmodus | name, growth; optional timeGrowth, injuryRateScale |
maps |
Karte | name, generator (town oder districts), seed; optional description, thumbnail, order, tags, layout (Abschnitt 13) |
paints |
Lack ziviler Autos | color, metallic; die Reihenfolge bestimmt, welches Auto welche Farbe bekommt |
liveries |
Lackierung | optional colors, decals |
voiceLines |
Funkspruch | group, situation, text (de, en); optional files |
sounds |
Geräusch | file, bus; optional muffled, far, loop, events |
documents |
freies Dokument | id wie modes/economy und beliebige Eigenschaften; die Abstimmungsdateien in Game/data/modes/ (Freies Spiel: Schwierigkeiten, Wirtschaft, Wertung, Einsatzleitung) sind Dokumente, ein Mod ersetzt eines oder ändert mit "patch": true einzelne Werte |
Fähigkeiten, Organisationen und Flags werden mit Namen geschrieben ("UseHose", "Fire", "Deployable"). Die Listen stehen in common.schema.json. Ein Mod kann Fahrzeuge und Rollen aus vorhandenen Fähigkeiten erfinden; das Verhalten folgt den Fähigkeiten, ein Fahrzeug mit Ladder und LadderMonitor ist also ein Drehleiter-Fahrzeug.
Texte. Namen werden unter vehicle.<id>, role.<id>, equip.<id> gesucht; Titel von Einsatzarten, Modi und Karten nutzen den im Eintrag genannten Schlüssel. Das Einsatzmenü liest für Fahrzeuge außerdem purpose.<id>.short und purpose.<id>.full und nimmt den Kurznamen für die Schaltfläche aus der Klammer im name des Fahrzeugs ("Pumper (LF)" ergibt LF). Trage die Texte in locale/de.json und locale/en.json ein; ein fehlender ist eine Warnung, und das Spiel zeigt den Schlüssel.
7. Hinzufügen, überschreiben, patchen, extends, null
Innerhalb einer Datei zählt die Reihenfolge der Einträge, zwischen Mods die Ladereihenfolge (Abschnitt 8).
| Du schreibst | Was passiert |
|---|---|
eine neue id |
der Eintrag wird am Ende seiner Liste hinzugefügt |
eine vorhandene id (des Spiels oder eines früheren Mods) |
der Eintrag wird als Ganzes ersetzt. Das Spiel warnt davor, außer du schreibst "override": true |
"override": true |
bestätigt das Ersetzen; ein Fehler, wenn es nichts zu überschreiben gibt |
"patch": true |
nur die geschriebenen Eigenschaften ändern sich; Objekte verschmelzen Eigenschaft für Eigenschaft, Listen und Werte werden ersetzt, null entfernt eine Eigenschaft |
"extends": "andere.id" |
beginne mit einer Kopie dieses Eintrags (er muss früher definiert sein: im Spiel, in einer Abhängigkeit oder weiter oben in deiner Datei) und wende dann deine Eigenschaften an. extends mit eigener id ergibt eine Variante |
Einen Eintrag des Spiels zu ersetzen ist erlaubt (es wird als Hinweis gemeldet). Wenn du etwas änderst, das auch ein anderer Mod ändert, ist das ein Konflikt: Der später geladene Mod gewinnt, und der Mod-Bildschirm zeigt dort eine Warnung, es sei denn, dieser Mod nennt den anderen als Abhängigkeit (dann baut er absichtlich darauf auf).
8. Manifest, Versionen, Abhängigkeiten, Ladereihenfolge
Eigenschaften der mod.json: id, name, version, author, licence (Pflicht) sowie formatVersion (1), description, homepage, gameVersion, dependencies, loadAfter, incompatible.
Versionsbereiche (durch Leerzeichen getrennt, alle müssen gelten): *, >=1.2.0, >1.2, <2.0.0, <=1.9, 1.2.3 (genau), 1.x, 1.2.x, ^1.2.3 (gleiche Hauptversion, mindestens diese), ~1.2.3 (gleiche Nebenversion, mindestens diese).
"gameVersion": ">=0.9.0 <1.0.0",
"dependencies": { "basis-paket": ">=1.2.0 <2.0.0" },
"loadAfter": ["farb-anpassungen"],
"incompatible": { "alte-loeschfahrzeuge": "*" }
dependencies: Pflicht und wird vor diesem Mod geladen. Fehlt der Mod, ist er ausgeschaltet oder hat die falsche Version, ist das ein Fehler, und der Mod wird nicht geladen (mit klarer Meldung); ebenso Mods, die einen gescheiterten Mod brauchen.loadAfter: wird vor diesem geladen, wenn vorhanden, keine Pflicht.incompatible: dieser Mod wird nicht geladen, wenn der andere an ist.- Ladereihenfolge: zuerst die Abhängigkeiten; sonst die im Mod-Bildschirm eingestellte Reihenfolge (Pfeiltasten), sonst nach ID. Dieselben Mods in derselben Reihenfolge ergeben immer dasselbe Spiel.
9. Das Testwerkzeug und seine Meldungen
godot --headless --path Game -- --validate-mod=<Ordner> [--lang=de]
Es lädt den Mod mit den Mods, von denen er abhängt (neben ihm oder im Mod-Ordner gefunden), und gibt Manifestdaten, den Inhalt des Mods, einen Inhalts-Hash und alle Meldungen aus; ein Ordner voller Mods prüft jeden einzeln. Exit-Code 0: geladen (Warnungen möglich); 1: Fehler. Der Mod-Bildschirm zeigt dieselben Meldungen.
Meldungen nennen Datei, Eintrag und Eigenschaft: data/vehicles.json: vehicles[0] ("mein-erster-mod.LF_GELB").speedKmh: expected a number but found "fast". Häufige:
| Meldung (gekürzt) | Bedeutung |
|---|---|
unknown property "speed" (did you mean "speedKmh"?) |
eine falsch geschriebene Eigenschaft |
"Fier" is not one of Fire, Rescue, ... (did you mean "Fire"?) |
ein falscher Name aus einer festen Liste |
erweitert "X", das es nicht gibt |
die Vorlage ist nicht (früher) definiert |
Rolle "WIZARD" gibt es nicht |
Verweis auf eine fehlende ID |
Die Datei "models/x.glb" gibt es im Mod-Ordner nicht |
falscher Pfad oder falsche Schreibweise (Groß-/Kleinschreibung zählt, damit es auch unter Linux läuft) |
... wird auch vom Mod X geändert. Y wird später geladen, seine Version gilt. |
Konflikt (Warnung) |
Benötigt den Mod X (>=1.2.0), der nicht installiert / ausgeschaltet ist / installiert ist aber Version 1.0.0 |
Abhängigkeitsproblem |
Diese Mods hängen im Kreis voneinander ab |
zirkuläre Abhängigkeiten |
Hinweis: Die technischen Meldungen zu JSON-Pfaden und Eigenschaften (expected a number ...) bleiben englisch, weil sie JSON-Namen beschreiben; alle anderen Meldungen erscheinen in der Spielsprache.
10. Mehrspieler und Spielstände
- Beitreten: Die Mods des Hosts und des Clients müssen gleich sein: gleiche IDs, Versionen, Inhalts-Hashes (ein Hash über
mod.json,data/,locale/und die Assets) und gleiche Ladereihenfolge. Sonst lehnt der Host den Beitritt ab, und der Client sieht eine Meldung wie "Die Mods stimmen nicht mit denen des Hosts überein: der Host nutzt example-vehicle 1.0.0, den du nicht hast". Zeilenenden in Textdateien spielen keine Rolle; eine README zählt nicht mit. - Spielstände speichern die Mods und Hashes, mit denen das Spiel lief (
GameSession.Mods). Der Speicher-/Lade-Code vergleicht sie beim Laden. - Ohne Mods verhält sich das Spiel genau wie vorher; derselbe Seed ergibt dasselbe Spiel.
11. Checkliste für Mod-Autoren
$schemain jeder JSON-Datei gibt Vervollständigung und Prüfung im Editor.- Setze deine Mod-ID vor jede eigene ID (
mein-mod.ding), damit Mods nicht kollidieren. - Führe
--validate-modaus, bevor du veröffentlichst; behebe Fehler, lies Warnungen. - Teste mit einem anderen Mod, der dasselbe ändert (Konfliktwarnungen), und mit
--lang=de. - Ein Core-Test (
Core.Tests/Mod*Tests.cs) zeigt, wie man einen Mod im Code lädt:ModLoader.Discover,ModLoader.Load,ContentRegistry.Use.
12. Handbuchseiten, Hinweise und Tutorials
Das sind documents (Abschnitt 6), deren Id mit handbook/, hints/, tutorial/ oder training/ beginnt; ihre Texte sind Textschlüssel in deinen Sprachdateien. Die des Spiels sind Game/data/handbook/basic.json, Game/data/hints/basic.json und Game/data/tutorial/basic.json.
- Handbuch. Deine Fahrzeuge, Funktionen, Ausrüstung und Einsatzarten bekommen automatisch eine Seite (Bild, Daten, Verweise). Einen Text dazu gibst du unter
handbook.<Seite>.body(vehicle.<id>,role.<id>,equipment.<id>,incident.<id>; eine Einsatzvariante ohne eigenen Text zeigt den ihrer Grundart), Suchwörter unterhandbook.<Seite>.keywords. Der Text ist ein kleines Markdown:## Überschrift,- Aufzählung,1. Schritt,**fett**,Text. Eine eigene Seite: ein Dokumenthandbook/mein-modmit"pages": [ { "id": "mein-mod.wasserrettung", "chapter": "mechanics", "order": 10 } ]und den Textenhandbook.mein-mod.wasserrettung.titleund.body. - Hinweise. Ein Dokument
hints/mein-modmit"hints": [ { "id": "...", "on": "refusal", "keys": ["refuse.ein_grund"], "text": "hint.mein-mod.tipp", "page": "vehicle.mein-mod.X" } ]. Auslöser:refusal,message(Schlüssel einer Tickermeldung),new_incident,situation,situation_worst. Ein Hinweis mit vorhandener Id ersetzt den alten. - Tutorials. Ein Dokument
tutorial/<id>im Format vonGame/data/tutorial/basic.json(Schritte mit Textschlüsseln, Zielen, Hervorhebungen, Kamera undauto-Aktionen); es erscheint im Tutorial-Menü. Die Zieltypen stehen inGame/Tutorial/Logic/TutorialConditions.cs. - Trainings. Eine Szenario-Übung pro Einsatzart: ein Dokument
training/<id>wieGame/data/training/building_fire.json(training/mein-mod.bootsrettungbei einem Mod). Es erscheint im Trainingsmenü mit Bild (assets/thumbnails/training/<picture oder id>.png; ohne Bild eine schlichte Fläche mit dem Titel), Schwierigkeits-stars(1 bis 5) und einem Haken nach dem Abschluss, und die Handbuchseite des Einsatzes bekommt die Schaltfläche „Training starten“. Felder:incident(die Id einer Einsatzdefinition, auch einer Variante aus einem Mod),title,summary,stars,minutes,order,scenario(time,traffic_cars,pedestrians,injury_rate,budgetundincidentmitscalewie{ "buildings": 1, "injured": 1, "trapped": 1 },min_floors,hydrant_within),briefing(what,risks,units: Fahrzeug-Ids, mit Bild gezeigt),milestonesundtip. Ein Meilenstein ist eine Zeile der Checkliste in empfohlener Reihenfolge:text(schlicht),hint(was anzuklicken ist, mit[b]und[key:action]),goal(die Zieltypen des Tutorials plusobjective,deployed,object,detained,cannon,aboard; ein Ziel wird abgehakt, sobald es einmal galt, in beliebiger Reihenfolge),highlight(was der Hinweis umkreist:hud.*- undworld:*-Ziele wieworld:victim,world:obj:HeavyDebris,world:civ:Jumper,world:incident_building) undcamera. Ein Hinweis erscheint erst nach 20 Sekunden ohne Fortschritt (kein Befehl, nichts abgehakt, keine Einheit unterwegs) oder wenn der Spieler die Hinweistaste drückt (standardmäßig H; Einstellungen → Steuerung, „Hinweis im Training“, frei belegbar; sie teilt sich H mit „Zur Wache springen“, aber nur das Training sieht sie). Das Training endet, wennfinishgilt (Standard: der Einsatz ist gelöst) oder der Einsatz scheitert; die Ergebniskarte zeigt Zeit, Opfer, was gut lief und dentip.autoführt das erwartete Spiel als Absichten auf (dispatch:RD_NEF:2,unload:RD_NEF,care,commander, ...; die Liste steht inTutorialAutoPlayer.Intents): Der Headless-Test spielt damit jedes Training durch, ein kaputtes Training lässt den Build also scheitern. Alle Texte sind Schlüssel in deinen Sprachdateien. Ein Dokumenttraining/<id>mit einer Listecoming_laternennt Einsatzarten ohne Training; das Menü blendet sie aus. Das „Expertentraining“ (ein Schalter im Menü) würfelt die Größe des Einsatzes aus und ändert bei jedem Durchgang den Ort; dein Training braucht dafür nichts.
13. Karten
Eine Karte ist ein Eintrag im Abschnitt maps. generator: "town" ist der Stadtgenerator mit Startwert (Auenstadt); generator: "districts" baut eine handgemachte Karte aus ihrem layout (Schema map_layout.schema.json; die des Spiels: Game/data/content/maps_hafenkamp.json). Die Kartenauswahl im freien Spiel zeigt name, description (Textschlüssel), das thumbnail (ein PNG, 640 x 360 passt gut) sowie Größe und Stadtteile aus dem Layout, sortiert nach order. tags sind die Merkmale der Karte für den Einsatzkatalog (river, rail, port, industry, motorway, forest, park, ...).
Das Layout, in Metern (x nach Osten, z nach Süden, Norden ist -z, Mitte bei 0,0; Punkte [x, z], Rechtecke [minX, minZ, maxX, maxZ], Richtungen in Grad, 0 zeigt nach +z):
size(die quadratische Karte),margin(Straßen laufen mindestens 150 m über den Rand weiter).roads:id,name,type(motorway2 Spuren 100 km/h,federal_road2 Spuren 70,main_road2 Spuren 50,residential1 Spur 40,farm_track1 Spur 30,forest_track1 Spur 20),from,to(gerade, entlang x oder z; Kreuzungen entstehen, wo sich Mittellinien kreuzen), optionallanes,speedKmh,culDeSac(ein Ende vor einer Kreuzung ist eine Wendefläche). Eine Straße, die den Rand der Welt erreicht, braucht einen Eintrag inportals(road,edge); dort kommt und geht der Verkehr.water:id,kind(river,harbour,lake),area(ein Rechteck oder Punkte),edges(quayodershore, eine je Kante; bei einem Rechteck Nord, Ost, Süd, West). Boote kommen am Nordende des ersten Flusses herein; Straßen über Wasser bekommen Brücken.districts:id,name,template,area, optional Zahlen inoptions. Der Generator teilt das Land zwischen den Straßen in Blöcke und füllt die Blöcke jedes Stadtteils mit seiner Vorlage, mit einem Startwert aus Karten-Startwert und Stadtteil-Id (wer einen Stadtteil ändert, lässt die anderen, wie sie sind). Vorlagen:container_terminal(Stapel, Containerbrücken, ein Schiff, Portalhubwagen;cranes,maxTier),docks(Schuppen, festgemachte Schiffe, Poller, ein Hafenmobilkran;ships),chemical_plant(umzäunt, ein Tor, Tanklager, Prozesshallen, Kolonnen, Rohrbrücken, Fackel),logistics(Lagerhallen mit Laderampen, angedockte Lkw, ein Lkw-Hof),workers_estate(Zeilen mit 4-6 Geschossen;minFloors,maxFloors),rail_freight_yard(Abstellgleise, Kesselwagen, eine Verladebühne),motorway_junction(Schutzplanken, Lärmschutzwand, Ausfädelungen, Schilderbrücken),woodland,park. Ländlich (Lindenhöhe,maps_lindenhoehe.json):village(der Dorfplatz mit Linde, Kirche oder Kapelle, Gasthaus und Laden, wo sich die Hauptstraßen kreuzen, Höfe mit Scheunen, ein- und zweigeschossige Häuser an jeder Straße, ein Teil davon Fachwerk, Streuobstwiesen;small,timbered,farms,church),forest(Fichten-, Kiefern- und Buchenbestände, ein Grillplatz an der Wendefläche eines Waldwegs, ein Aussichtsturm;picnic,tower,spacing),fields(Feldstreifen nachseason1 Frühjahr, 2 Sommer, 3 Herbst; bis zumachinesTraktoren und Mähdrescher bei der Arbeit, Heuballen),lake(eine Badestelle mit Parkplatz, Steg, Bojenkette und Rettungsturm am Ufer nächst der Wendefläche einer Straße, Schilf; Felder ringsum),gravel_pit(Grubensohle, Brecheranlage, Förderbänder zu den Halden, Maschinen, Büro und Waage, ein Zaun mit Tor),wind_park(turbinesmindestens 110-150 m auseinander mit Kranstellflächen, Felder dazwischen). Auf dem freien Land (Wald, Felder, See, Grube, Windpark) gibt es keine Straßenlaternen, keine Hydranten und keine Gehwege.- Eine Straße kann durch einen Einschnitt laufen,
cutting:[von, bis]entlang der Straße (z bei einer Straße entlang z, x entlang x); beidseits steigen Böschungen an (fest), oben stehen Bäume. - Ein Teil eines Einschnitts kann ein Tunnel sein,
tunnel:{ "name", "from", "to", "exits": [s, ...], "exitSide": 1, "draught": -0.6, "staging": [[minX, minZ, maxX, maxZ], ...] }. Die Röhre ist die Straße zwischen zwei Wänden unter einer Decke; jeder Notausgang ist ein Fußgängerstollen durch die Böschung aufexitSide;draught(m/s, positiv Richtungto) trägt den Rauch eines Brandes im Tunnel zu einem Portal; die Bereitstellungsflächen sind befestigt und bleiben frei. Brände im Tunnel füllen ihn mit Rauch. landmarks: einprefab(ein Modell inmodels/props/port/,models/props/rural/odermodels/props/) mitat,heading,scale,variant, optionalsolid[Breite, Tiefe], durch das niemand geht oder fährt.eventSpots:tag,at,radius,heading(die Vorlagen fügen eigene hinzu). Die Tags von Hafenkamp:hazmat_leak,tank_fire,ship_fire,person_in_water,motorway_pileup,container_fall,crane_accident,warehouse_fire,tank_wagon_leak. Die von Lindenhöhe:forest_fire,barn_fire(nennt seine Scheune),tractor_accident,swimmer_in_distress(im Wasser, ein Ufer innerhalb von 25 m),lorry_crash(auf einer Spur der Bundesstraße),storm_damage(auf einer Fahrspur, ein Baum innerhalb von 20 m),person_missing(im Wald, die nächste Zufahrt bis 200 m entfernt),wind_turbine_fire(am Turm, unerreichbar: eine Absperrung innerhalb von 150 m).hq:area,mouth(auf der Mittellinie einer Straße, mindestens 40 m von der nächsten Kreuzung) undend: die eigene Zufahrt der Zentrale, mindestens 40 m bis zum Vorplatz (traffic-and-dispatch.md §1c.1).objects:suction_point(ein LF, das daneben steht, saugt Wasser aus Hafen oder See; weitere LF fördern es über Zubringerleitungen von je bis zu 200 m weiter),slipway,berth(for:fire_boat,police_boat).landscape:forest,meadow,railway,underpass,levelCrossing,bridgeSite(die Flächen, nach denen die vorhandenen Einsätze suchen).traffic:cars,pedestrians,mix(Fahrzeug-Ids mit Gewichten),speeds(km/h je Fahrzeug-Id),peaks(at"HH:MM" Spielzeit,cars,halfWidthin Spielminuten).
Die Kartenprüfung lehnt ein Layout ab, dessen Straßen nicht gerade sind, dessen Straße ohne Portal die Welt verlässt, dessen Wasserkanten nicht zum Umriss passen, dessen Vorlage unbekannt ist oder dessen Zufahrt nicht auf einer Straße beginnt (Fehler map_layout). Im Spiel speichert --map=<id> --map-tour=<Ordner> eine Übersicht und jeden Stadtteil, --map-thumbnail=<Datei> das Vorschaubild.
14. Kampagnen
Eine Kampagne ist ein document (Abschnitt 6) mit der Id campaign/<id>; die eigene des Spiels ist campaign/weiden-auen in Game/data/content/campaigns.json (gelesen von Core/Campaign/CampaignDef.cs). Jede Kampagne erscheint in der Kampagnenauswahl; Texte sind Textschlüssel in deinen Sprachdateien.
- Oberste Ebene:
title,description,region(Textschlüssel),order,startingVehicles(Fahrzeug-Ids, die ab dem ersten Einsatz alarmiert werden können),townsundmissions(Objekte mit der Id als Schlüssel, damit ein Patch hineingemischt wird),awards. - Ein Ort:
name,at([x, y] auf der Kreiskarte, 0..1, x nach rechts, y nach unten),map(eine Karten-Id; ein Ort ohne Karte kann nur geplante Einsätze haben). - Ein Einsatz:
order,kind(storyodershift),status(playable, oderplannedmitneeds: Ids, angezeigt alscampaign.need.<id>),script(duty_shiftist allgemein und braucht keinen Code; die Story-Skripte sind C#-Klassen inCore/Campaign/Missions),title,theme,briefing,town,at, optionalmapundseed,requires(alle davon vorher gewonnen),requiresAny(einer davon: hier treffen sich zwei Wege wieder),introduces(in diesem Einsatz alarmierbar),unlocks(nach dem Sieg alarmierbar),tags(für die Abzeichen),video(die Id des Einsatzfilms aus M12),start("HH:MM"),clockRate(Spielminuten je Sekunde),weather,budget,injuryRate,trafficCars,pedestrians,objectives(id,text,optional,hidden,target; Ids, die mitkeep_oderno_beginnen, gelten beim Sieg als erledigt, wenn sie nicht gescheitert sind),fail(maxDeaths,timeLimitin Sekunden,maxBuildingsLost) und bei einer Schichtshift(target,incidents: die Einsatzarten, die der Disponent starten darf,firstIncident,difficulty). - Ein Abzeichen:
id(award.<id>undaward.<id>.descriptionsind seine Texte),tagmitcountundminEfficiency(so viele Einsätze mit dem Tag mit dieser Effizienz gewonnen) oderflawless: true(jeder Story-Einsatz ohne Tote gewonnen). - Eine Dienstschicht zur Kampagne des Spiels hinzufügen:
{ "documents": [ { "id": "campaign/weiden-auen", "patch": true, "missions": { "meine_schicht": { "order": 4, "kind": "shift", "status": "playable", "script": "duty_shift", "requires": ["m02"], ... } } } ] }. Eine eigene Kampagne geht genauso mit einer neuen Id.CampaignDef.Checkfindet unbekannte Voraussetzungen, Orte, Fahrzeuge, Skripte oder Einsatzarten, einen spielbaren Einsatz ohne Karte oder Hauptaufgabe und eine Voraussetzung, die später in der Reihenfolge kommt (der Inhaltsaufbau ruft sie jedes Mal auf: ein Mod, dessen Kampagne so ein Problem hat, scheitert mit dem Fehlercampaign). - Der Fortschritt wird je Kampagne im Spielerprofil gespeichert; der Stand eines Einsatzes ist Teil der Spielstände.
The example mods used below are examples/mods/example-vehicle/ (a vehicle variant with a livery, and an incident variant) and examples/mods/example-assets/ (an own glTF model, an OGG sound and a radio line). The JSON schemas of every data type are in docs/modding/schemas/, both in the game's source code.
1. What a mod is
A mod is one folder with a mod.json and any of these parts. Nothing is compiled; everything is JSON text and ordinary files.
my-mod/
mod.json the description of the mod (required)
data/*.json new or changed definitions: vehicles, incident types, roles, ...
locale/en.json texts (English), key -> text
locale/de.json texts (German)
models/*.glb 3D models (glTF 2.0)
textures/*.png pictures (thumbnails, decals)
audio/*.ogg sounds and voice lines (Ogg Vorbis)
A mod can
- add entries (a new id), for example a vehicle variant or a new kind of incident,
- override entries of the game or of other mods (the same id),
- patch an entry (change a few properties only),
- bring assets: glTF models, PNG pictures and OGG sounds.
Everything the game uses is data in the same format: the game's own definitions are in Game/data/content/*.json and are loaded exactly like a mod's files. Read them as examples.
Which parts are data? Roles, equipment, vehicles (with door layouts), the vehicles standing in each station, fire materials, incident types (title, needs, reward, how often, size ranges), game modes and difficulties, maps (generator, seed and layout: section 13), civilian car paints, liveries, extra radio lines and extra sounds. What stays code: how an incident plays out (a variant uses the behaviour of one of the 16 kinds), the abilities themselves and everything about the simulation.
2. Where mods go
- The player's folder:
user://mods/. On Windows that is%APPDATA%\Godot\app_userdata\Blue Light\mods. The Open folder button of the Mods screen opens it. - For development: start the game with
--mods-dir=C:/path/to/folder-with-mods. Mods found there are switched on for that run.
Each mod is one sub-folder. The folder name does not matter; the id in mod.json does.
3. Step by step: a vehicle in 10 minutes
We make a yellow works-brigade version of the pumper (this is the example mod).
Step 1: the folder. Create my-first-mod/ with a data/ and a locale/ folder.
Step 2: mod.json.
{
"formatVersion": 1,
"id": "my-first-mod",
"name": "My first mod",
"version": "1.0.0",
"author": "Your name",
"licence": "CC0-1.0",
"description": "A yellow pumper.",
"gameVersion": ">=0.9.0 <1.0.0"
}
id: 2 to 48 characters, lower-case letters, digits, ., _, -. version: major.minor.patch. gameVersion is a version range (section 8). Put the real path of docs/modding/schemas/mod.schema.json in $schema and your editor (VS Code, Rider, ...) completes and checks the file.
Step 3: a livery (data/liveries.json). A livery recolours materials of a model by name. The pumper's red paint is the material Paint_Fire.
{ "liveries": [ { "id": "my_yellow", "colors": { "Paint_Fire": "#E0B000" } } ] }
Step 4: the vehicle (data/vehicles.json). extends copies an existing vehicle; you only write what differs.
{
"vehicles": [
{
"id": "my-first-mod.LF_YELLOW",
"extends": "FW_LF",
"name": "Yellow pumper",
"livery": "my_yellow",
"speedKmh": 90,
"cost": 3600
}
]
}
A vehicle made with extends draws the same 3D model as its base unless you give a model. It appears in the dispatch menu of its organisation automatically. A picture for the menu is optional ("thumbnail": "textures/lf.png"; without it the base vehicle's picture is used).
Step 5: the texts (locale/en.json and locale/de.json). Every new vehicle needs a name under the key vehicle.<id>:
{ "vehicle.my-first-mod.LF_YELLOW": "Yellow pumper" }
{ "vehicle.my-first-mod.LF_YELLOW": "Gelbes Löschfahrzeug" }
Step 6: test it. Without starting the game:
godot --headless --path Game -- --validate-mod=C:/path/to/my-first-mod
You get a readable report and the exit code 1 when there are errors (section 9). Add --lang=de for German.
Step 7: play it. Copy the folder into the mods folder (or use --mods-dir), start the game and open Mods in the main menu: switch the mod on, check the messages. Start Free Play, open the dispatch menu of the fire service: the new vehicle is in the list.
4. Step by step: a new kind of incident
An incident type is a variant of one of the 16 built-in kinds (BuildingFire, CarFire, TrafficAccident, MedicalCall, BuildingCollapse, ShedCollapse, GasLeak, ChemicalSpill, FloodedUnderpass, Riot, Hostage, ForestFire, PersonInRiver, Jumper, BusCrash, FleeingSuspect). It plays like its kind, with its own title, reward, frequency and size ranges. data/incidents.json:
{
"incidents": [
{
"id": "my-first-mod.cycling_accident",
"extends": "MedicalCall",
"title": "incident.my-first-mod.cycling_accident",
"reward": 3500,
"weight": 5,
"scale": {
"events": { "Collapse": 0, "Fall": 100, "Breathing": 0 },
"severity": { "Fall": { "Light": 30, "Serious": 60, "Critical": 10 } },
"secondPatientChance": 0.25
}
}
]
}
weightis how often the director picks it compared with the others (the built-in kinds have 1 to 6; 0 = never at random).scaleholds the size tables,[[value, weight], ...]from small to large. Which tables count depends on the kind:BuildingFire(buildings, injured, trapped, trappedNight, intensity, gasBottleChance),CarFire(cars, spreadChance),TrafficAccident(cars, injured, trapped, fuelLeakChance, fireChance),MedicalCall(events, severity, secondPatientChance). Objects merge property by property with the base, so a table you leave out is the base's. To switch an entry of the base off, give it the weight 0 (asCollapseandBreathingabove) ornull.titleis a text key; add it to your locale files.
5. Assets
All paths are relative to the mod folder, written with /, and may not leave it.
| Kind | Format | Used by |
|---|---|---|
| 3D model | glTF 2.0, .glb or .gltf, 1 unit = 1 m, front towards +Z, origin on the ground in the middle |
vehicle model |
| Picture | PNG | vehicle thumbnail (the picture in the dispatch menu, 256 x 256 works well), livery decals, map thumbnail |
| Sound | Ogg Vorbis .ogg |
voice lines (files), sounds |
Models. Name the parts like the game's models and they work like them: materials Paint_*, Glass*, Tyre, Light_Blue_A/Light_Blue_B (beacons), Headlight, Taillight, ...; nodes Wheel_* (they turn), door_fl, door_fr, door_rl, door_rr, door_slide_l, door_slide_r, door_rear_l/door_rear_r, tailgate (they open, see the vehicle's doorLayout), HeadlightL/HeadlightR, LadderBase, LadderArm, Basket, Boom, Mast, Rotor, TailRotor, Monitor, CargoMount. Models without those nodes still work; the parts just do not move. Mod models have no LOD levels, so keep them light.
Liveries. colors maps a material name (or its beginning, Paint_Fire also catches Paint_Fire_2) to #RRGGBB. decals replaces the model's decal atlas (lettering, stripes); the PNG has to use the model's own UV layout, so this is for models you made yourself. A livery changes only the vehicles that name it.
Sounds. A sound entry adds an OGG to the sound bank and, with "events": ["horn"], to the sounds that sound event may play (event names are in Game/data/sound_bank.json). A voiceLines entry adds a radio line (German and English subtitle, optional recording per language) for a group (fire, rescue, police, technical) and a situation (ack, arrive, refuse, patient_stable, fire_out, person_freed).
Licence. Only use assets you made or that are free to redistribute (CC0, CC-BY, ...) and say so in licence. Never use assets of other games.
6. The data types
Every data file is a JSON object whose sections are lists of entries. A file may contain any sections; the file names are up to you. Each entry has an id and the properties of its type. Unknown properties are errors (with a "did you mean" hint). Schemas: docs/modding/schemas/<type>.schema.json.
| Section | Type | Properties (required first) |
|---|---|---|
roles |
role | name, org, cost, abilities, walkSpeed; optional flags, model (the built-in role whose person model it uses) |
equipment |
equipment | name, abilities, handsNeeded, placeable |
vehicles |
vehicle | name, org, seats, allowedRoles, abilities, speedKmh, cost, size, defaultCrew, doorLayout; optional cargo, equipment, flags, waterTank, stretchers, model, thumbnail, livery |
fleets |
fleet | org, bays: the vehicles standing ready in the bays of that organisation's station |
materials |
fire material | ignitionHeat, maxTemperature, burnTime, chargeTime, cooling, heatResistance, defaultHeatRadius; optional radiantFactor, explodes, explosionForce, explosionRadius |
incidents |
incident type | kind, title, needs, abilities, reward, weight; optional scale |
modes |
game mode | name, growth; optional timeGrowth, injuryRateScale |
maps |
map | name, generator (town or districts), seed; optional description, thumbnail, order, tags, layout (section 13) |
paints |
civilian car paint | color, metallic; the order decides which car gets which colour |
liveries |
livery | optional colors, decals |
voiceLines |
radio line | group, situation, text (de, en); optional files |
sounds |
sound | file, bus; optional muffled, far, loop, events |
documents |
free-form document | id such as modes/economy and any properties; the tuning files of Game/data/modes/ (free play: difficulties, economy, scoring, director) are documents, a mod replaces one or changes single values with "patch": true |
Abilities, organisations and flags are written by name ("UseHose", "Fire", "Deployable"). The lists are in common.schema.json. A mod can invent vehicles and roles from existing abilities; behaviour follows the abilities, so a vehicle with Ladder and LadderMonitor is a ladder truck.
Texts. Names are looked up under vehicle.<id>, role.<id>, equip.<id>; titles of incidents, modes and maps use the key named in the entry. The dispatch menu also reads purpose.<id>.short and purpose.<id>.full for a vehicle, and takes the short name for its button from the brackets of the vehicle's name ("Pumper (LF)" gives LF). Add the texts to locale/en.json and locale/de.json; a missing one is a warning and the game shows the key.
7. Add, override, patch, extends, null
Within one file the order of entries matters; across mods the load order (section 8).
| You write | What happens |
|---|---|
a new id |
the entry is added at the end of its list |
an existing id (of the game or of an earlier mod) |
the entry is replaced as a whole. The game warns about it unless you add "override": true |
"override": true |
confirms the replacement; an error if nothing exists to override |
"patch": true |
only the properties you write change; objects merge property by property, lists and values are replaced, null removes a property |
"extends": "other.id" |
start from a copy of that entry (it must be defined earlier: in the game, in a dependency, or above in your file), then apply your properties. extends plus an own id makes a variant |
Replacing the game's own entry is allowed (it is reported as a note). Changing something another mod also changes is a conflict: the mod that loads later wins and the Mods screen shows a warning on it, unless that mod declares the other as a dependency (then it is building on it on purpose).
8. Manifest, versions, dependencies, load order
mod.json properties: id, name, version, author, licence (required) and formatVersion (1), description, homepage, gameVersion, dependencies, loadAfter, incompatible.
Version ranges (space separated, all must hold): *, >=1.2.0, >1.2, <2.0.0, <=1.9, 1.2.3 (exactly), 1.x, 1.2.x, ^1.2.3 (same major, at least this), ~1.2.3 (same minor, at least this).
"gameVersion": ">=0.9.0 <1.0.0",
"dependencies": { "base-pack": ">=1.2.0 <2.0.0" },
"loadAfter": ["colour-tweaks"],
"incompatible": { "old-pumpers": "*" }
dependencies: required and loaded before this mod. Missing, switched off, or the wrong version is an error and the mod is not loaded (with a clear message); so are mods that need a failed mod.loadAfter: loaded before this one when present, no requirement.incompatible: this mod is not loaded when the other is on.- Load order: dependencies first; otherwise the order set in the Mods screen (the arrow buttons), otherwise by id. The same mods in the same order always give the same game.
9. The test tool and reading its messages
godot --headless --path Game -- --validate-mod=<folder> [--lang=de]
It loads the mod with the mods it depends on (found next to it or in the mods folder) and prints the manifest data, what the mod adds, a content hash and every message; a folder of mods checks each of them. Exit code 0: loaded (warnings possible); 1: errors. The Mods screen shows the same messages.
Messages name the file, the entry and the property: data/vehicles.json: vehicles[0] ("my-first-mod.LF_YELLOW").speedKmh: expected a number but found "fast". Common ones:
| Message (shortened) | Meaning |
|---|---|
unknown property "speed" (did you mean "speedKmh"?) |
a misspelt property |
"Fier" is not one of Fire, Rescue, ... (did you mean "Fire"?) |
a wrong name from a fixed list |
extends "X", which does not exist |
the base is not defined (earlier) |
the role "WIZARD" does not exist |
a reference to a missing id |
The file "models/x.glb" does not exist in the mod folder |
wrong path or spelling (case counts, so it also works on Linux) |
... is also changed by the mod X. Y loads later, so its version wins. |
conflict (warning) |
Needs the mod X (>=1.2.0), which is not installed / switched off / but version 1.0.0 is installed |
dependency problem |
These mods depend on each other in a circle |
circular dependencies |
10. Multiplayer and saves
- Joining: the host's mods and the client's must be the same: same ids, versions, content hashes (a hash of
mod.json,data/,locale/and the assets) and the same load order. Otherwise the host refuses the join and the client sees a message such as "The mods do not match the host's: the host uses example-vehicle 1.0.0, which you do not have". Line ends in text files do not matter; a readme does not count. - Saves record the mods and hashes the game ran with (
GameSession.Mods). The save/load code compares them when loading. - Without mods the game behaves exactly as before; the same seed gives the same game.
11. For tool and mod authors: checklist
$schemain every JSON file gives completion and checks in editors.- Keep ids prefixed with your mod id (
my-mod.thing) so mods do not collide. - Run
--validate-modbefore you publish; fix errors, read warnings. - Test with another mod that changes the same things (conflict warnings) and with
--lang=de. - A Core test (
Core.Tests/Mod*Tests.cs) shows how to load a mod in code:ModLoader.Discover,ModLoader.Load,ContentRegistry.Use.
12. Handbook pages, context hints and tutorials
These are documents (section 6) whose ids start with handbook/, hints/, tutorial/ or training/; their texts are text keys in your locale files. The game's own are Game/data/handbook/basic.json, Game/data/hints/basic.json and Game/data/tutorial/basic.json.
- Handbook. Your vehicles, roles, equipment and incident types get a page automatically (picture, facts, links). Add a written text with the key
handbook.<page>.body(vehicle.<id>,role.<id>,equipment.<id>,incident.<id>; an incident variant without one shows its kind's text) and search words withhandbook.<page>.keywords. The body is Markdown-lite:## heading,- bullet,1. step,**bold**,text. A page of its own: a documenthandbook/my-modwith"pages": [ { "id": "my-mod.water_rescue", "chapter": "mechanics", "order": 10 } ]plushandbook.my-mod.water_rescue.titleand.body. - Hints. A document
hints/my-modwith"hints": [ { "id": "...", "on": "refusal", "keys": ["refuse.some_reason"], "text": "hint.my-mod.tip", "page": "vehicle.my-mod.X" } ]. Triggers:refusal,message(a ticker message key),new_incident,situation,situation_worst. A hint with an existing id replaces it. - Tutorials. A document
tutorial/<id>in the format ofGame/data/tutorial/basic.json(steps with text keys, goals, highlights, camera,autoactions); it appears in the Tutorial menu. The goal types are listed inGame/Tutorial/Logic/TutorialConditions.cs. - Trainings. One scenario lesson per incident type: a document
training/<id>likeGame/data/training/building_fire.json(training/my-mod.boat_rescuefor a mod's). It appears in the Training menu with its picture (assets/thumbnails/training/<picture or id>.png; a plain panel with the title without one), its difficultystars(1 to 5) and a tick once completed, and the handbook page of its incident gets a "Start training" button. Fields:incident(the id of an incident definition, a mod's variant too),title,summary,stars,minutes,target_seconds(the time a competent player needs, shown on the result card),picture_focus/picture_distance/picture_yaw/picture_after(how the menu picture is taken, see--training-thumb),order,scenario(time,traffic_cars,pedestrians,injury_rate,budget, andincidentwithscalesuch as{ "buildings": 1, "injured": 1, "trapped": 1 },min_floors,hydrant_within),briefing(what,risks,units: vehicle ids shown with their pictures),milestonesandtip. A milestone is a checklist line in the recommended order:text(plain),hint(what to click, may use[b]and[key:action]),goal(the goal types of the tutorial plusobjective,deployed,object,detained,cannon,aboard; a goal is ticked off once it held, in any order),highlight(what the hint rings:hud.*andworld:*targets such asworld:victim,world:obj:HeavyDebris,world:civ:Jumper,world:incident_building) andcamera. A hint shows only after 20 seconds without progress (no command, nothing ticked, no unit under way) or when the player presses the hint key (H by default; Settings → Controls, "Hint in a training", rebindable; it shares H with "jump to the station" but only the training sees it). The training ends whenfinishholds (default: the incident is resolved) or the incident fails; the result card shows time, casualties, what went well and thetip.autolists the expected play as intents (dispatch:RD_NEF:2,unload:RD_NEF,care,commander, ...; the list isTutorialAutoPlayer.Intents): the headless test plays every training with it, so a broken training fails the build. All texts are keys in your locale files. A documenttraining/<id>with acoming_laterlist names incident types without a training; the menu hides them. "Expert training" (a switch in the menu) rolls the incident's scale and changes the place with every run; your training needs nothing for it.
13. Maps
A map is an entry of the maps section. generator: "town" is the seeded town generator (Auenstadt); generator: "districts" builds a hand-made map from its layout (schema map_layout.schema.json; the game's own: Game/data/content/maps_hafenkamp.json). The map selection in Free Play shows name, description (text keys), the thumbnail (a PNG, 640 x 360 works well) and the size and districts from the layout, sorted by order. tags are the map's features for the event catalogue (river, rail, port, industry, motorway, forest, park, ...).
The layout, in metres (x east, z south, north is -z, the centre at 0,0; points [x, z], rectangles [minX, minZ, maxX, maxZ], headings in degrees, 0 faces +z):
size(the square map),margin(roads run on at least 150 m past the edge).roads:id,name,type(motorway2 lanes 100 km/h,federal_road2 lanes 70,main_road2 lanes 50,residential1 lane 40,farm_track1 lane 30,forest_track1 lane 20),from,to(straight, along x or z; junctions are where centre lines cross), optionallanes,speedKmh,culDeSac(an end short of a junction is a turning area). A road that reaches the edge of the world needs aportalsentry (road,edge); traffic comes and goes there.water:id,kind(river,harbour,lake),area(a rectangle or points),edges(quayorshore, one per edge; a rectangle's edges are north, east, south, west). Boats enter at the north end of the first river; roads over water get bridges.districts:id,name,template,area, optional numericoptions. The generator cuts the land between the roads into blocks and fills each district's blocks with its template, seeded from the map seed and the district id (editing one district leaves the others alone). Templates:container_terminal(stacks, gantry cranes, a ship, straddle carriers;cranes,maxTier),docks(sheds, moored ships, bollards, a harbour crane;ships),chemical_plant(fenced, a gate, tank farm, process halls, columns, pipe bridges, flare stack),logistics(warehouses with loading bays, docked lorries, a lorry yard),workers_estate(4-6 storey rows;minFloors,maxFloors),rail_freight_yard(sidings, tank wagons, a loading rack),motorway_junction(guard rails, noise wall, slip-road splays, sign gantries),woodland,park. Countryside (Lindenhöhe,maps_lindenhoehe.json):village(the square with the linden, the church or a chapel, the inn and the shop where the main roads cross, farms with barns, one- and two-storey houses along every street, a share half-timbered, orchards;small,timbered,farms,church),forest(spruce, pine and beech stands, a picnic area at a track's turning area, a lookout tower;picnic,tower,spacing),fields(strips of crops byseason1 spring, 2 summer, 3 autumn; up tomachinestractors and harvesters at work, hay bales),lake(a bathing beach with car park, jetty, buoys and lifeguard tower on the shore nearest a road's turning area, reeds; fields round it),gravel_pit(pit floor, crushing plant, conveyors to the heaps, machines, office and weighbridge, a fence with a gate),wind_park(turbinesat least 110-150 m apart with crane pads, fields between). In open country (forest, fields, lake, pit, wind park) there are no street lamps, no hydrants and no pavements.- A road may run through a
cutting:[from, to]along the road (z for a road along z, x along x); earth banks rise on both sides (solid), trees stand on top. - Part of a cutting may be a
tunnel:{ "name", "from", "to", "exits": [s, ...], "exitSide": 1, "draught": -0.6, "staging": [[minX, minZ, maxX, maxZ], ...] }. The bore is the road between two walls under a roof; each emergency exit is a walk-only gallery through the bank onexitSide;draught(m/s, positive towardsto) carries the smoke of a fire inside towards one portal; the staging pads are paved and kept free for units. Fires inside fill the tunnel with smoke. landmarks: aprefab(a model inmodels/props/port/,models/props/rural/ormodels/props/) withat,heading,scale,variant, optionalsolid[width, depth] nobody walks or drives through.eventSpots:tag,at,radius,heading(the templates add their own). Hafenkamp's tags:hazmat_leak,tank_fire,ship_fire,person_in_water,motorway_pileup,container_fall,crane_accident,warehouse_fire,tank_wagon_leak. Lindenhöhe's:forest_fire,barn_fire(names its barn),tractor_accident,swimmer_in_distress(in the water, a shore within 25 m),lorry_crash(on a federal road lane),storm_damage(on a road lane, a tree within 20 m),person_missing(in a forest, the nearest vehicle approach up to 200 m away),wind_turbine_fire(at the tower, unreachable: a cordon within 150 m).hq:area,mouth(on a road's centre line, at least 40 m from the next junction) andend: the street along the HQ (along x: a cul-de-sac, or a street through to another road whenendlies on one), with the plot north of it, at least 80 m deep and 84 m wide: the joint HQ (four doors facing the access road, its yard with an access road, four return lanes and their holding places) is laid out on it as in Auenstadt (traffic-and-dispatch.md §1c.1).objects:suction_point(an LF parked by it draws water from the harbour or lake; other LFs can relay it on through supply lines of up to 200 m each),slipway,berth(for:fire_boat,police_boat).landscape:forest,meadow,railway,underpass,levelCrossing,bridgeSite(the areas the existing incidents look for).traffic:cars,pedestrians,mix(vehicle ids with weights),speeds(km/h per vehicle id),peaks(at"HH:MM" game time,cars,halfWidthin game minutes).
The map check refuses a layout whose roads are not straight, whose road leaves the world without a portal, whose water edges don't match its outline, whose template is unknown whose HQ access road doesn't start on a road or whose HQ doesn't fit its plot (error map_layout). In the game, --map=<id> --map-tour=<dir> saves an overview and every district, --map-thumbnail=<file> the thumbnail.
14. Campaigns
A campaign is a document (section 6) with the id campaign/<id>; the game's own is campaign/weiden-auen in Game/data/content/campaigns.json (read by Core/Campaign/CampaignDef.cs). Every campaign appears on the Campaign screen; texts are text keys in your locale files.
- Top level:
title,description,region(text keys),order,startingVehicles(vehicle ids dispatchable from the first mission),townsandmissions(objects keyed by id, so a patch merges into them),awards. - A town:
name,at([x, y] on the regional map, 0..1, x right, y down),map(a map id; a town without one can only have planned missions). - A mission:
order,kind(storyorshift),status(playable, orplannedwithneeds: ids shown ascampaign.need.<id>),script(duty_shiftis generic and needs no code; the story scripts are C# classes inCore/Campaign/Missions),title,theme,briefing,town,at, optionalmapandseed,requires(all of them won first),requiresAny(one of them: a branch joins again),introduces(dispatchable in this mission),unlocks(dispatchable after winning it),tags(for the awards),video(the M12 briefing film id),start("HH:MM"),clockRate(game minutes per second),weather,budget,injuryRate,trafficCars,pedestrians,objectives(id,text,optional,hidden,target; ids starting withkeep_orno_count as done at a win unless they failed),fail(maxDeaths,timeLimitin seconds,maxBuildingsLost), and for a shiftshift(target,incidents: the incident type ids the director may start,firstIncident,difficulty). - An award:
id(award.<id>andaward.<id>.descriptionare its texts),tagwithcountandminEfficiency(that many missions with the tag won at that efficiency), orflawless: true(every story mission won without a death). - Adding a duty shift to the built-in campaign:
{ "documents": [ { "id": "campaign/weiden-auen", "patch": true, "missions": { "my_shift": { "order": 4, "kind": "shift", "status": "playable", "script": "duty_shift", "requires": ["m02"], ... } } } ] }. A campaign of your own works the same way with a new id.CampaignDef.Checkfinds unknown prerequisites, towns, vehicles, scripts or incident types, a playable mission without a map or a primary objective, and a prerequisite later in the order (the content builder runs it whenever content is built: a mod whose campaign has such a problem fails with the errorcampaign). - Progress is kept per campaign in the player profile; mission state is part of saves.