Skip to content

Mod structure

This page takes apart the mods that ship with the game: the script mods in mods/release/, the eight campaign missions, and the two DLC packages in dlcs/. The base game in base/ has the same layout. Where the official modding wiki documents a file, this page follows it and says where the shipped files differ.

Folder layout

The wiki's mod definition page shows the files a mod can have:

my_mod/
├── mod.json                     technical definition: id, revision, hooks, flags (required)
├── strings.json                 translations of the mod's own strings (optional)
├── _metadata/
│   ├── modinfo.json             name, summary, description, authors, tags (required)
│   ├── description.html         description for the mod browser and mod.io (optional)
│   ├── 0.png, 1.png, ...        cover (0.png) and gallery images, 1920 x 1080 (optional)
│   └── mod.io_fileid.txt        written by the game after the first upload
└── content/                     everything the game loads
    └── mod.script.tl            pre/run/post script functions (optional)

Inside content/, the game tells resources apart by their file ending, not by their folder (resource types). The wiki recommends the base game's folder layout anyway, with one subfolder per asset and a shared folder for files several assets use (guidelines).

A complete mod from the game files, mods/release/urbangames_campaign_mission_01/ (archives unpacked, content shortened):

urbangames_campaign_mission_01/
├── mod.json                     mod id, script hooks, flags
├── _metadata/modinfo.json       name, description, authors, tags
├── _content.json                list of content archives and loose files
├── texture_metadata_cache.lua   size and mipmap data per texture
├── tealdef/                     Teal type definitions for this mod
│   ├── urbangames_campaign_mission_01_def.d.tl
│   └── mission/mission_params_01.d.tl
└── content/                     everything the game loads
    ├── mod.script.tl            pre/run/post script functions
    ├── info.mission.lua         mission description
    ├── animal.zip  assets.zip  audio.zip  cargos.zip  clima.zip
    ├── gui.zip  guide_system.zip  industries.zip  init.zip
    ├── mission.zip  terrain.zip  vehicle.zip  voiceover.zip

Not every mod has every part:

Mod Contents
urbangames_no_costs, urbangames_sandbox, urbangames_tycoon mod.json, _metadata/modinfo.json, content/mod.script.tl
urbangames_vehicles_no_end_year the same, with content/mod.script.lua in Lua
urbangames_campaign mod.json, _metadata, tealdef, content/info.campaign.lua
urbangames_campaign_mission_01 … _08 full layout as above
dlcs/urbangames_preorder_pack mod.json, _metadata, content archives animal, landmarks, vehicle
dlcs/urbangames_deluxe_upgrade_pack the same plus tealdef/fun_elements/balloon.d.tl and archives characters, fun_elements, music

The DLC packages are built like mods. They have no script hooks and differ only in flags ("autoActivate": true, "cosmetic": true, "severityRemove": "Warning" for the deluxe pack).

mod.json

mod.json is the technical description of the mod. The keys found in the shipped files:

Key Values in the shipped mods Meaning
modId "urbangames_no_costs_1", "urbangames_campaign_mission_01", "urbangames_deluxe_upgrade_pack" Identifier, only a-z, 0-9 and _. Script references use it as prefix: urbangames_no_costs_1::/mod.script@preRunFn. The base game's base/mod.json has an empty modId.
revision 1 Version of the mod, shown on the mod's detail page. Raise it with every published update.
preRunScript, runScript, postRunScript { "fileName": "<modId>::/mod.script@<fn>" } or "" Script functions called while the game loads, see below
dependencies always null Mods this mod needs, see dependencies
incompatibilities always null Mods this mod cannot run with
params null, except in base/mod.json Settings shown to the player, see mod parameters
options always null Documented neither in the definitions nor in the wiki
severityAdd, severityRemove "None", "Warning" Warning level when the mod is added to or removed from a savegame: None, Warning or Critical. Unset, they act as None (add) and Warning (remove).
cosmetic true for the campaign, missions and DLCs Achievements stay available if all active mods are cosmetic. Leave it false if the mod affects the simulation.
visible false for the campaign missions false hides the mod from the mod list. Default true. A hidden mod cannot auto-activate.
autoActivate true for the DLCs "If the mod is automatically activate by the game"
url "" in base/mod.json "URL to the mod page"

The wiki describes the three severity values like this (mod.json): None when adding or removing does no harm, for example a cosmetic change; Warning when the savegame loses something, such as vehicle models, but stays stable; Critical when it can break, for example because cargo types go missing. The technical requirements make a correct severityRemove mandatory.

The wiki says that, unlike in the earlier Transport Fever games, the modId has no major-version suffix such as _3. The shipped script mods still end in _1 (urbangames_no_costs_1, urbangames_sandbox_1, urbangames_tycoon_1, urbangames_vehicles_no_end_year_1), while their folders, the campaign and the DLCs have no suffix. For an update that breaks savegames, the wiki recommends a new modId and a separate mod, not a higher revision.

The game finds a mod by its modId, wherever it is installed. With the same id installed twice, the staging area wins over a manual installation, and that wins over a mod.io subscription.

The quoted descriptions come from Mod.ModDesc in api/tealdef/api/type/mod.d.tl, which is the type the game uses for a loaded mod description. ModDesc has severityAdd/severityRemove as integers ("0: None, 1: Warning, 2:Critical") while the JSON files use the names. The game reads installed mods through ModRep (app.getUserProfile():getModRep() in base/content/gui/gui/menu/mod_util.tl), whose getModDesc returns this type.

Dependencies

No shipped mod declares a dependency. The wiki (dependencies and incompatibilities) shows the JSON form. A repaint that needs the mod with the original vehicle would declare:

{
    "dependencies": [
        {
            "mod": { "modId": "jdoe_base_vehicles", "revisionMin": 2, "revisionMax": -1 },
            "modInfo": { "displayName": "Base Vehicles", "url": "https://example.com/base-vehicles" },
            "loadBefore": true,
            "optional": false
        }
    ],
    "incompatibilities": [
        { "mod": { "modId": "urbangames_sandbox" } }
    ]
}
Field Meaning Default
mod.modId modId from the other mod's mod.json required
mod.revisionMin lowest accepted revision none (-1 or left out)
mod.revisionMax highest accepted revision; rarely needed, since updates should stay compatible none (-1 or left out)
modInfo.displayName name shown in the mod browser when the dependency is not installed
modInfo.url optional download link, for example on a third-party site
loadBefore load the dependency before this mod true
optional recommended, not required false

An incompatibility has only the mod part. The game shows both while the player activates mods. It activates dependencies first, offers to download missing ones, and warns before a game starts with a missing dependency or an active incompatible mod.

The types match: Mod.ModDependency has mod, optional, loadBefore and modInfo; Mod.ModRef holds modId, revisionMin and revisionMax; Mod.ModRefInfo holds displayName and url; an incompatibility (Mod.ModIncompatibility) is only a ModRef. The mod selector in base/content/gui/gui/menu/mod_util.tl checks these fields: it reports dependencies as missing with reason "NotFound" or "IncorrectVersion", honours optional and loadBefore, and lists active incompatible mods.

_metadata/modinfo.json

The player-facing information sits in a separate file. From mods/release/urbangames_campaign_mission_01/_metadata/modinfo.json:

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

The script mods and DLCs use translation keys such as "MOD_NO_COSTS_NAME" as values. Their translations come with the game's own strings (see translated strings). A mod brings its translations along in the same file, as the wiki's modinfo.json section shows:

{
    "name": "Soft Brakes",
    "summary": "Trains brake more gently.",
    "description": "Lowers the global train brake deceleration.",
    "localization": {
        "de": {
            "name": "Sanfte Bremsen",
            "summary": "Züge bremsen sanfter.",
            "description": "Senkt die globale Bremsverzögerung von Zügen."
        }
    },
    "authors": [ { "name": "jdoe", "role": "CREATOR" } ],
    "tags": [ "Script Mod" ],
    "dependencies": [ "5499706" ],
    "url": "https://example.com/soft-brakes"
}
Key Rules
name Display name, at most 32 characters, no line breaks. The guidelines ask for title case.
summary Tooltip in the mod browser, at most 100 characters, no line breaks
description Long text with line breaks. Ignored if _metadata/description.html exists. Must be in English.
localization name, summary and description per language code; the top-level values apply to all other languages
authors list of name and role; the shipped files only use "CREATOR"
tags officially supported tags, used as filters in the browser ("Script Mod", "Campaign", "Town Building", "Depot" appear in the files and the wiki)
dependencies mod.io ids (not modIds) of the mods this one needs, so mod.io links them. Removing an entry here does not remove it on mod.io; do that on the website.
url link for more information. The game does not show it, and consoles can't open it, so the description must contain everything a player needs.

On upload, mod.io gets the English name and a description with all languages joined, because mod.io has no per-language texts yet; the wiki expects that to change by Q2/2027. The upload overwrites name and description on mod.io with the values from modinfo.json.

The mod hub side has its own record, Modhub.ModInfo, with title, summary, description, tags, logoImage, download and rating counters and release information. The wiki doesn't map the fields one by one. It says the English name is uploaded as the title, and that the _metadata/<number>.png files are the cover (0.png) and gallery images.

Mod script entry points

preRunScript, runScript and postRunScript each name one function. The base game and the official mods use them for different jobs:

Hook When it runs (wiki) Signature in the shipped scripts What the shipped mods do there
preRunScript before game and mod resources are loaded preRunFn(captureParams, configDict, allModParams, baseConfig) Change the BaseConfig. urbangames_no_costs sets baseConfig.noCosts = true; the base game fills almost the whole configuration here.
runScript when the mod is loaded; registered modifiers and filters apply while resources load, after all runFn have run runFn(captureParams, settings) (urbangames_vehicles_no_end_year), runFn(captureParams) (mission 01) Register load-time modifiers with addModifier.
postRunScript once, after all resources are loaded postRunFn(captureParams, configDict, allModParams) Edit, add or hide resources through api.res.

Each hook runs for the base game first and then for the mods in activation order (script functions). Dependencies with loadBefore are activated before the mods that need them. The wiki recommends doing filtering and changes in postRunFn where possible and keeping runFn modifiers for what can't be done there.

The arguments are:

Argument Content
captureParams the params table of the script reference (resource types); the hook references in mod.json have none
configDict the selected climate, economy and name list
allModParams parameter values of all mods, keyed by mod id; allModParams[getCurrentModId()] gives this mod's own (getCurrentModId)
baseConfig the base configuration, preRunFn only

The wiki lists the hook parameters without captureParams. Every shipped mod.script function takes it as the first parameter, which matches the wiki's own rule for script references, so write your functions with it. The base game reads its own parameters from allModParams[""] (base/content/base/base/base_mod.lua).

The two styles of script file both work as a target. A Teal module returns a table of functions (mods/release/urbangames_no_costs/content/mod.script.tl, shown in Getting started). A Lua file defines data() and returns the table from it, as in mods/release/urbangames_vehicles_no_end_year/content/mod.script.lua:

function data()
return {
    runFn = function (captureParams, settings)
        addModifier("loadModel", function (fileName, data)
                if data.metadata.transportVehicle and data.metadata.availability then
                    data.metadata.availability.yearTo = 0
                end

                return data
            end)
    end
}
end

addModifier(key, fn) registers a function that receives each loaded file's name and data and returns the (changed) data. The wiki covers modifiers and filters on its resource modifiers page. The keys available are listed in base/content/base/base/mod.lua: loadModel, loadModule, loadMultipleUnit, loadStreet, loadStreetTemplate, loadTrack, loadBridge, loadTunnel, loadConstruction, loadMetaConstruction, loadSoundSet, loadScript, loadGameScript, loadClimate, loadEconomy, loadCargoType, loadCampaign, loadMission, loadLanguage and about thirty more. The same file defines addFileFilter(category, fn), a filter that drops a file when fn(fileName, data) returns false, with categories such as "model/vehicle", construction and gameScript.

A postRunFn works on the repositories in api.res. From mods/release/urbangames_campaign_mission_01/content/mod.script.tl:

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

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

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

ResTypeRep is the interface of every repository: find(resName) returns an id (-1 if missing), get(id) the resource, getAll() a map of ids to names, setVisible hides a resource from the player, and add registers a new one. The base game's postRunFn uses api.res.moduleRep.add to create one station module per track type.

For edits, the wiki's examples use the table form: getAsTable(id) returns a Lua table you can change freely, setAsTable(id, data) writes it back, and addAsTable(name, data) adds it as a new resource. modelRep.forEachModelWithMetadata loops over models with a given metadata key. A capacity multiplier from a mod parameter:

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

This follows the wiki's resource modification example. Scripts can't add new 3D data: a new model added this way can only point at existing .mdl files through its modelPath.

Mod parameters

base/mod.json declares the new-game settings as params. One entry:

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

The fields match ScriptParam and the wiki's mod params section:

Field Meaning
key name under which scripts read the value
name label in the UI
tooltip optional mouse-over text
uiType Button, Slider, ComboBox, IconButton or CheckBox
values labels, or image paths for IconButton
numbers optional values the scripts receive per option; default 1, 2, 3, ...
defaultIndex default option, counted from 0 (unlike Lua indices); default the first option

So "defaultIndex": 1 in the reforestation entry above selects "On". Without numbers, a script receives 1 for the first option, 2 for the second and so on. The player sets the values in the mod selection when starting a new game or loading a savegame. The wiki uses the same parameter types for constructions. Some entries carry tags such as "desktop", "console" or "experimentalMapFeatures", and the same key appears several times with different value lists per tag. The script functions receive the chosen values in allModParams, a table keyed by mod id; the base game reads its own from allModParams[""]. At runtime, game scripts read them with api.engine.config.getModParams(), for example api.engine.config.getModParams()[""]["isMapEditor"] in base/content/game_mechanics/game_mechanics/towns/towns.script.tl.

How content is packaged

How the zips map to folders

The game ships content/ as a set of zip archives plus loose files. _content.json lists both. From mods/release/urbangames_campaign_mission_01/_content.json (shortened):

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

Entry names are relative to the folder the archive sits in. mission.zip is at the top of content/ and contains mission/mission_story.tl, which the game addresses as urbangames_campaign_mission_01::/mission/mission_story.tl. Archive names can contain folders too: the base game's assets/buildings/ind.zip has the entry ind/mat/assets_ind_1_body.mtl, so the file's path is /assets/buildings/ind/mat/assets_ind_1_body.mtl. The base game has 1,092 archives, among them assets/stations.zip and buildings/a/c1/1x1_01.zip, and loose files such as industries/industry_workers.gs.lua. In the unpacked tree these docs quote from, each archive became a folder named after the zip, which gives doubled paths such as content/mission/mission/mission_story.tl.

A mod you write yourself doesn't need archives: the wiki's mod layout has loose files in content/. Whether the game builds the archives and _content.json when it cooks a mod is covered below.

File references inside content

The wiki's file names and references section gives the rules. A path starting with / is absolute from the content folder of the current mod (for base game files, the base game). A path without a leading / is relative to the referencing file. A prefix picks another mod:

Form Resolves to Example from the game files
/path absolute, in the current mod ug_require "/mission/mission_params_01.tl" in mission 01; ug_require "/scripts/vec3.tl" in the base game
path relative to the current file, in the current mod "small_stops.script@updateFn", resolve("small_new.mdl")
::/path absolute, in the base game "::/gui/construction/construction_desc_hud_icons.script@configureStationHudIconsFn"
::path relative to the current file, but in the base game's folders "::buildings/ind/mat/assets_ind_1_body.mtl" in mission 04's /assets/instrument_box.mdl, which is the base file /assets/buildings/ind/mat/assets_ind_1_body.mtl
<modId>::/path absolute, in mod <modId> "urbangames_campaign_mission_01::/mission/hud_replacement.script"
<modId>::path relative to the current file, in mod <modId> the wiki's "ug_wiki_example_mod::mod.script@preRunFn" in mod.json

../ is not allowed, so a file can't refer to its parent folder. resolve(relativePath) "Resolves a file paths relative to the current file".

The shipped mod.json files write the hook references with a slash (urbangames_no_costs_1::/mod.script@preRunFn), the wiki's examples without one. Since mod.json sits at the mod root and mod.script directly in content/, both forms point at the same file.

File and folder names may only use lowercase a-z, 0-9, _ and -, with no spaces, and should stay short. Text files (.lua, .tl, .json, .mdl, .msh, ...) are UTF-8 without a byte order mark.

Script references point at a function: file.script@function. The .lua or .tl extension is left out, and the part after @ can be a dotted path into the returned table ("modules.script@bulk.updateFn" in base/content/warehouses/warehouses/wh_bulk.module.lua). ScriptRef describes the same idea for an older models/model/animal/animal.flock.crane notation.

Code is loaded with ug_require, "Used instead of the regular lua require to include game related script". The base scripts cast the result to its type: local mathutil = ug_require "::/scripts/mathutil.lua" as MathUtil.

What the base content folders hold

The top-level folders of base/content and typical file types in them. The wiki's directory structure table describes the same folders, and its file endings table lists every resource type by extension.

Folder Typical files
animal, vehicle, characters .mdl models with .mtl materials, animations (.ani)
assets, buildings asset and town building models; buildings also have .con.lua
stations, depots, industries, landmarks, warehouses .con.lua constructions, .module.lua modules, .metacon.tl metaconstructions, their .script.lua/.script.tl
infrastructure streets (.street.lua, .street_template.lua), tracks, bridges, tunnels, signals
game_mechanics game scripts (.gs.lua) for towns, company, finance, notifications, ...
gui the user interface in Teal, stylesheets (.css.lua), GUI resources (.gres.lua)
mission mission framework, tasks, guide system
scripts shared libraries (vec3, mat4, util, construction utilities, react.d.tl)
rendering material types (.mat.lua), shader programs (.prog.lua), techniques, properties
climates, terrain, economy, cargos, names, music, locale climates (.clima.lua), terrain materials, economy (.eco.lua), cargo types, name lists, playlists, languages

Binary files (.msh meshes, .dds and .tga textures, sounds) are listed in _content.json but are not part of the unpacked sources these guides were written from.

Generic resources

Configuration that is not a model or construction is often a .res.lua file with a type and a data table. From base/content/game_mechanics/game_mechanics/game_time/game_time_config.res.lua:

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

Scripts read them through api.res.genericRep, either by name (api.res.genericRep.find("::/game_mechanics/game_time/game_time_config.res")) or by type (api.res.genericRep.getAllOfType("notification")). Types used this way in the base game include notification, subvention, missiontask_boot, guide_system_boot, tutorial_boot, menu_category, construction_tool, react-replacement-config and react-plugin ::<ExtensionPoint>. The last two are how mods extend the GUI (see GUI); missiontask_boot is how a mission starts (see Missions).

Translated strings

Text in scripts is wrapped in _("KEY") and its variants pGetText(context, id), nGetText and npGetText (main). Context strings use the separator <|ctx|>, as in "map-size<|ctx|>Small" in base/mod.json.

The base/content/locale/locale/*.lang.lua files describe a language (name, locale code, font maps) but contain no translations. The wiki's localizations page says where they are: compiled gettext files in strings/<locale>/LC_MESSAGES/base.mo. Keys such as MISSION_01_NAME or MOD_NO_COSTS_NAME are translated there. Those files are not part of the sources these docs use.

A mod puts its translations into strings.json at the mod root, one table per language code:

{
    "en": {
        "brake_intensity_name": "Brake Intensity",
        "brake_intensity_tooltip": "Adjust the global brake intensity."
    },
    "de": {
        "brake_intensity_name": "Bremsintensität",
        "brake_intensity_tooltip": "Passen Sie die globale Bremsintensität an."
    }
}

A key missing for the player's language falls back to English, and a key missing in English is shown as the key itself (strings.json). Language codes are the locale values of the .lang.lua files, such as de, de_CH or de_AT. The mod's name, summary and description are translated in modinfo.json instead (see above). The wiki asks for English as the main language; other languages are optional.

Files generated by the game

_content.json and texture_metadata_cache.lua (texture sizes and mipmap counts per .dds) are present in every shipped mod. The modhub types mention cooking: Modhub.ModValidatorResult has cookedPcPath and cookedConsolePath, and ModPublishHelper.publish "Validates and cooks the mod once more first and uploads the cooked folders". Neither the files nor the wiki say whether cooking produces these two files or whether a hand-made mod needs them. The wiki's mod layout doesn't list them, so a mod without them is the documented starting point.

The one file the wiki names as generated is _metadata/mod.io_fileid.txt, written after the first upload to mod.io. It holds the mod.io id and links later uploads to the same entry (see publishing).

Technical requirements

Every upload goes through an automatic validation for PC and consoles, and a mod must pass it to be released. The wiki's technical requirements:

Area Requirement
Integrity all resources are syntactically correct and load without errors or warnings
Security no batch files, shell scripts or other executables
Encoding text files in UTF-8 without BOM
Size at most 500 MB unpacked per mod
Names lowercase files and folders, no spaces or special characters
Ballast no source files (.psd, .fbx) or temporary files
Meshes lowest LOD mesh blobs at most 256 KB
Textures DDS with mipmaps, power-of-two sizes (512, 1024, 2048, ...)
Scripts only the official scripting API
Audio .wav with 16-bit samples at 48,000 Hz
Savegames severityRemove set correctly
Consoles only no custom shaders, no changes to core gameplay that hurt performance

The guidelines also give content targets, for example 20,000 to 50,000 triangles for a typical vehicle, a texel density of 128 to 170 pixels per metre and 1 to 3 LODs. Those belong to the models guide.

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