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:
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.