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, mod 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 mod script hooks and differ only in flags ("autoActivate": true, "cosmetic": true, "severityRemove": "Warning").
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 mod script hooks |
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 |
Parameters the player sets for a game, 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": "jdoe_hard_mode" } }
]
}
| 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 Dependencies Not Satisfied" window 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 on the mod's tile in the Mod Hub, 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 Mod Hub ("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 hooks¶
The mod script hooks are the three mod.json keys preRunScript, runScript and postRunScript. Each names 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 base config (BaseConfig). urbangames_no_costs sets baseConfig.noCosts = true; the base game fills almost all of it here. |
runScript |
when the mod is loaded; registered modifiers and filters apply while resources load, after all runFn have run |
runFn(captureParams, configDict, allModParams) (base/content/base/base/base_mod.lua; urbangames_vehicles_no_end_year names the second argument settings and leaves out the third, mission 01 takes only captureParams) |
Register resource modifiers with addModifier and file filters with addFileFilter. |
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 config, 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).
A Teal module returns a table of functions (urbangames_no_costs, shown in Getting started); a Lua file defines data() and returns the table from it (mods/release/urbangames_vehicles_no_end_year/content/mod.script.lua). How runFn registers resource modifiers and file filters, with the list of categories, is in Resource modifiers and File filters. Load-time code shows what each phase is used for.
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 parameters section:
| Field | Meaning |
|---|---|
key |
name under which scripts read the value |
name |
label in the UI; a translation key, see Translations |
tooltip |
optional mouse-over text; a translation key, see Translations |
uiType |
Button, Slider, ComboBox, IconButton or CheckBox |
values |
labels, or image paths for IconButton; labels are translation keys, see Translations |
numbers |
optional values the scripts receive per option, one per entry in values (script_param_util.tl drops the list when the counts differ); default 1, 2, 3, .... ScriptParam types the list as {number}, the hook signatures type the received values as integer, and no shipped parameter uses a fraction, so check a fractional value in game. |
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. Constructions use a similar parameter list in Lua, where defaultIndex is 1-based (see Parameters). 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. No shipped game script reads its own mod's parameters. getOwningModId(), called in the game script file itself, should give the key for getModParams(), but no shipped file confirms it.
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 |
Base game files often load other base files with /..., because for them / means the base game. When you copy such a line into a mod, write ::/...: require "/scripts/mat4.tl" in a base file becomes require "::/scripts/mat4.tl" in yours.
../ 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.
The base game's Lua files load modules with plain require and the same paths, for example require "/scripts/soundsetutil.lua" or require "::/scripts/construction/constructionutil.lua". Both functions work, because base/content/base/base/init.lua replaces require so that a path ending in .lua or .tl is resolved like any other file reference, and ug_require only calls that require. In Teal files, use ug_require ... as Type: only ug_require is declared in main.d.tl, and the as cast is Teal syntax that fails in a .lua file. init.lua keeps the original require for paths without those extensions.
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, ... (see Game mechanics) |
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, economies (.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")). The 295 .res.lua files in the game files use these types (paths below base/content/ unless a mod is named):
| Type | Files | Example | What it adds | See |
|---|---|---|---|---|
menu_filter_category |
75 | gui/gui/construction/menu/filter_categories/gravel.res.lua |
a filter in the build menu | The build menu |
menu_category |
49 | gui/gui/construction/menu/menu_categories/category_road_buildings.res.lua |
a build menu tab or module tab | The build menu |
react-plugin ::<ExtensionPoint> |
44 | gui/gui/entity_window/warehouse/warehouse_eow_stocks.res.lua |
a GUI plugin at an extension point | GUI |
notification |
33 | game_mechanics/game_mechanics/notifications/types/stuck_vehicle.res.lua; mission 05 and the deluxe DLC add their own |
a notification type | Notifications |
construction_tool |
22 | gui/gui/construction/tools/asset_eraser_tool.res.lua |
a tool in the build menu | Tools in the build menu |
company_exploration |
12 | game_mechanics/game_mechanics/company/explorations/exploration_coal.res.lua |
an exploration perk | Company perks |
stamp |
11 | gui/gui/construction/shared/tex/stamps/desert_hill1.res.lua |
a heightmap (.png plus icon) for the "Roughen Terrain" tool in the build menu, which only the Map Editor and sandbox mode offer |
Terrain stamps |
missiontask_boot |
8 | mission/mission/mission.res.lua in each campaign mission |
starts a mission | How a mission starts |
guide_system_boot |
6 | mission/mission/guide_system/guide_system.res.lua; missions 01, 02, 03 and 07 |
starts a guide system | How a mission starts |
game_script_config |
6 | game_mechanics/game_mechanics/game_time/game_time_config.res.lua |
settings that a base game script reads by name | The base game scripts |
subvention |
5 | game_mechanics/game_mechanics/subventions/deliver_cargo/deliver_cargo.res.lua; mission 06 adds deliver_visitors |
a subsidy type | Subsidies |
rename_scheme_component |
5 | gui/gui/line_vehicle_mgmt/auto_rename/scheme_components/scheme_component_town.res.lua |
one part of an automatic name, with its renameFn |
none |
react-replacement-config |
4 | mission/mission/hud_replacement.res.lua in mission 01 |
replaces a base GUI recipe | Replacing a base recipe |
firstStopToSend_scheme |
4 | gui/gui/line_vehicle_mgmt/first_stop_to_send/scheme_firststop.res.lua |
a rule for the stop a new line vehicle goes to first | none |
rename_scheme |
3 | gui/gui/line_vehicle_mgmt/auto_rename/schemes/scheme_firststop_laststop.res.lua |
an automatic naming scheme (formatString = "{key1} - {key2}") |
none |
drag_and_drop |
3 | gui/gui/line_vehicle_mgmt/dnd_train_composition.res.lua |
an empty marker for something the GUI can drag, read by name | none |
company_permit_key |
3 | game_mechanics/game_mechanics/company/permitKeys/hq.res.lua |
a permit key perk | Company perks |
tutorial_boot |
1 | mission/mission/tutorial/mission.res.lua |
starts the tutorial | How a mission starts |
game_config |
1 | infrastructure/traffic_light/traffic_light/traffic_light_config.res.lua |
trafficLightYearFrom, read by name |
none |
Only notification, subvention, missiontask_boot, guide_system_boot and react-replacement-config appear in a shipped mod or DLC. For the other types that the base game collects with getAllOfType (menu_category, menu_filter_category, react-plugin, construction_tool, company_exploration, company_permit_key, stamp, rename_scheme, rename_scheme_component, firstStopToSend_scheme, tutorial_boot), a mod's file of the same type should show up next to the base ones, but no shipped mod confirms it. game_script_config, game_config and drag_and_drop resources are read by their fixed names, so a new file of these types is not picked up.
Placeholders for missing files¶
base/content/placeholders/placeholders/ holds stand-ins for missing content: generic.con.lua (name MISSING_CONSTRUCTION, drawn as the exclamation mark marker /assets/markers/marker_exclamation.mdl), missing_signal.con.lua, missing_street_terminal.con.lua, missing_gtex.gtex.lua and one missing_*.mdl per model kind (missing_rail_vehicle.mdl, missing_road_vehicle.mdl, missing_animal.mdl, missing_person.mdl, ...). When a savegame or construction refers to a resource that no longer exists, for example after a mod was removed, the game should show one of these stand-ins in its place. Only the file names and MISSING_CONSTRUCTION point to this; it is not confirmed in game. The base scripts also use them directly: base/content/scripts/scripts/construction/constructionutil.lua falls back to "::/placeholders/missing_generic.mdl" when a model group is empty, and 48 files use /placeholders/generic.tga as their icon.
Translated strings¶
Text in scripts is wrapped in _(). A mod translates its keys in strings.json at the mod root, one table per language code. Key functions, the strings.json format, the language codes and the .lang.lua files are explained in Translations. The mod's name, summary and description are translated in modinfo.json instead (see above).
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 (the largest have up to 100,000), 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.