Blue Light
Mods selbst bauen

Modding

Eigene Fahrzeuge, Einsatzarten, Modelle, Sounds, Karten und Kampagnen. Ein Mod ist ein Ordner mit JSON-Dateien und gewöhnlichen Dateien; nichts wird kompiliert. Die Anleitung kommt direkt aus dem Spiel.

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-Mods starten. 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
      }
    }
  ]
}
  • weight ist, wie oft die Einsatzleitung diese Art im Vergleich zu den anderen wählt (die eingebauten haben 1 bis 6; 0 = nie zufällig).
  • scale enthä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 (wie Collapse und Breathing oben) oder null.
  • title ist 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

  • $schema in 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-mod aus, 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 unter handbook.<Seite>.keywords. Der Text ist ein kleines Markdown: ## Überschrift, - Aufzählung, 1. Schritt, **fett**, Text. Eine eigene Seite: ein Dokument handbook/mein-mod mit "pages": [ { "id": "mein-mod.wasserrettung", "chapter": "mechanics", "order": 10 } ] und den Texten handbook.mein-mod.wasserrettung.title und .body.
  • Hinweise. Ein Dokument hints/mein-mod mit "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 von Game/data/tutorial/basic.json (Schritte mit Textschlüsseln, Zielen, Hervorhebungen, Kamera und auto-Aktionen); es erscheint im Tutorial-Menü. Die Zieltypen stehen in Game/Tutorial/Logic/TutorialConditions.cs.
  • Trainings. Eine Szenario-Übung pro Einsatzart: ein Dokument training/<id> wie Game/data/training/building_fire.json (training/mein-mod.bootsrettung bei 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, budget und incident mit scale wie { "buildings": 1, "injured": 1, "trapped": 1 }, min_floors, hydrant_within), briefing (what, risks, units: Fahrzeug-Ids, mit Bild gezeigt), milestones und tip. 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 plus objective, deployed, object, detained, cannon, aboard; ein Ziel wird abgehakt, sobald es einmal galt, in beliebiger Reihenfolge), highlight (was der Hinweis umkreist: hud.*- und world:*-Ziele wie world:victim, world:obj:HeavyDebris, world:civ:Jumper, world:incident_building) und camera. 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, wenn finish gilt (Standard: der Einsatz ist gelöst) oder der Einsatz scheitert; die Ergebniskarte zeigt Zeit, Opfer, was gut lief und den tip. auto führt das erwartete Spiel als Absichten auf (dispatch:RD_NEF:2, unload:RD_NEF, care, commander, ...; die Liste steht in TutorialAutoPlayer.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 Dokument training/<id> mit einer Liste coming_later nennt 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 (motorway 2 Spuren 100 km/h, federal_road 2 Spuren 70, main_road 2 Spuren 50, residential 1 Spur 40, farm_track 1 Spur 30, forest_track 1 Spur 20), from, to (gerade, entlang x oder z; Kreuzungen entstehen, wo sich Mittellinien kreuzen), optional lanes, speedKmh, culDeSac (ein Ende vor einer Kreuzung ist eine Wendefläche). Eine Straße, die den Rand der Welt erreicht, braucht einen Eintrag in portals (road, edge); dort kommt und geht der Verkehr.
  • water: id, kind (river, harbour, lake), area (ein Rechteck oder Punkte), edges (quay oder shore, 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 in options. 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 nach season 1 Frühjahr, 2 Sommer, 3 Herbst; bis zu machines Traktoren 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 (turbines mindestens 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 auf exitSide; draught (m/s, positiv Richtung to) 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: ein prefab (ein Modell in models/props/port/, models/props/rural/ oder models/props/) mit at, heading, scale, variant, optional solid [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) und end: 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, halfWidth in 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), towns und missions (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 (story oder shift), status (playable, oder planned mit needs: Ids, angezeigt als campaign.need.<id>), script (duty_shift ist allgemein und braucht keinen Code; die Story-Skripte sind C#-Klassen in Core/Campaign/Missions), title, theme, briefing, town, at, optional map und seed, 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 mit keep_ oder no_ beginnen, gelten beim Sieg als erledigt, wenn sie nicht gescheitert sind), fail (maxDeaths, timeLimit in Sekunden, maxBuildingsLost) und bei einer Schicht shift (target, incidents: die Einsatzarten, die der Disponent starten darf, firstIncident, difficulty).
  • Ein Abzeichen: id (award.<id> und award.<id>.description sind seine Texte), tag mit count und minEfficiency (so viele Einsätze mit dem Tag mit dieser Effizienz gewonnen) oder flawless: 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.Check findet 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 Fehler campaign).
  • 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
      }
    }
  ]
}
  • weight is how often the director picks it compared with the others (the built-in kinds have 1 to 6; 0 = never at random).
  • scale holds 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 (as Collapse and Breathing above) or null.
  • title is 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

  • $schema in 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-mod before 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 with handbook.<page>.keywords. The body is Markdown-lite: ## heading, - bullet, 1. step, **bold**, text. A page of its own: a document handbook/my-mod with "pages": [ { "id": "my-mod.water_rescue", "chapter": "mechanics", "order": 10 } ] plus handbook.my-mod.water_rescue.title and .body.
  • Hints. A document hints/my-mod with "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 of Game/data/tutorial/basic.json (steps with text keys, goals, highlights, camera, auto actions); it appears in the Tutorial menu. The goal types are listed in Game/Tutorial/Logic/TutorialConditions.cs.
  • Trainings. One scenario lesson per incident type: a document training/<id> like Game/data/training/building_fire.json (training/my-mod.boat_rescue for 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 difficulty stars (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, and incident with scale such as { "buildings": 1, "injured": 1, "trapped": 1 }, min_floors, hydrant_within), briefing (what, risks, units: vehicle ids shown with their pictures), milestones and tip. 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 plus objective, deployed, object, detained, cannon, aboard; a goal is ticked off once it held, in any order), highlight (what the hint rings: hud.* and world:* targets such as world:victim, world:obj:HeavyDebris, world:civ:Jumper, world:incident_building) and camera. 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 when finish holds (default: the incident is resolved) or the incident fails; the result card shows time, casualties, what went well and the tip. auto lists the expected play as intents (dispatch:RD_NEF:2, unload:RD_NEF, care, commander, ...; the list is TutorialAutoPlayer.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 document training/<id> with a coming_later list 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 (motorway 2 lanes 100 km/h, federal_road 2 lanes 70, main_road 2 lanes 50, residential 1 lane 40, farm_track 1 lane 30, forest_track 1 lane 20), from, to (straight, along x or z; junctions are where centre lines cross), optional lanes, speedKmh, culDeSac (an end short of a junction is a turning area). A road that reaches the edge of the world needs a portals entry (road, edge); traffic comes and goes there.
  • water: id, kind (river, harbour, lake), area (a rectangle or points), edges (quay or shore, 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 numeric options. 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 by season 1 spring, 2 summer, 3 autumn; up to machines tractors 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 (turbines at 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 on exitSide; draught (m/s, positive towards to) 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: a prefab (a model in models/props/port/, models/props/rural/ or models/props/) with at, heading, scale, variant, optional solid [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) and end: the street along the HQ (along x: a cul-de-sac, or a street through to another road when end lies 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, halfWidth in 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), towns and missions (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 (story or shift), status (playable, or planned with needs: ids shown as campaign.need.<id>), script (duty_shift is generic and needs no code; the story scripts are C# classes in Core/Campaign/Missions), title, theme, briefing, town, at, optional map and seed, 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 with keep_ or no_ count as done at a win unless they failed), fail (maxDeaths, timeLimit in seconds, maxBuildingsLost), and for a shift shift (target, incidents: the incident type ids the director may start, firstIncident, difficulty).
  • An award: id (award.<id> and award.<id>.description are its texts), tag with count and minEfficiency (that many missions with the tag won at that efficiency), or flawless: 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.Check finds 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 error campaign).
  • Progress is kept per campaign in the player profile; mission state is part of saves.