The concept of constructions, introduced with Transport Fever, is very powerful. It allows to configure and create a lot of different building types and assets in just one format. This includes stations, depots, industries, town buildings and different kinds of assets. They are stored in .con
files in the folder res/construction/
.
The format mainly consists of meta information and an update function. The construction is configured and created with the function. It can return different configurations of the construction depending on the input parameters.
function data() return { type = "INDUSTRY", -- other meta information params = { }, -- parameters changeable by the player -- update function, which adjusts the construction according the parameters updateFn = function(params) local result = { } result.models = { { id = "industry/chemical_plant/building_small.mdl", transf = transf.transl(vec3.new(20, -10 , 0)) } } -- terrain alignment, streets, tracks, stock lists, etc. return result end } end
There are several general properties available for use with constructions:
... type = "ASSET_DEFAULT", description = { name = _("Bench"), description = _("A bench.") }, availability = { yearFrom = 1925, yearTo = 1925 }, ...
The type
property is an identifier for the type of construction. See the construction types documentation for further details on their specific specialities and individual properties.
The description
block contains a desc
property for the description displayed in the menu. The name is set in the name
property. Both can be translated in a strings.lua file.
The availability is set by yearFrom
for the year from when the construction should be available. Unset or values below 1851 mean from start. The end limit is set by yearTo
. It is the year until when the construction should be available. Unset or value 0 means unlimited availability, values below 1850 result in a never available construction.
Parameters can be used to let the user set some preferences and influence the outcome of the construction script. They are defined in the params
block outside of the updateFn
function:
... params = { { key = "postbox_type", name = _("Postbox"), uiType = "BUTTON", values = { _("Germany"), _("Switzerland"), _("Austria"), }, tooltip = _("Choose the type of postbox you'd like to place."), defaultIndex = 0, }, ... }, ...
Each parameter block has several parameters:
Property | Value | Default | Description |
---|---|---|---|
key | String | mandatory | Unique identifier for the parameter |
name | String | “” (empty) | Display name for the parameter menu |
values | {String,String,…} | mandatory | Display values for the parameter menu, see below |
defaultIndex | Integer | 0 | Pre-selected option on first call |
uiType | “BUTTON” or “SLIDER” or “COMBOBOX” or “ICON_BUTTON” or “CHECKBOX” | “BUTTON” | Type of parameter, see below |
yearFrom | Integer | 0 | Parameter is displayed from |
yearTo | Integer | infinite | Parameter is displayed until |
tooltip | String | “” (empty) | Tooltip that is displayed above the parameter name when hovering |
It is recommended to choose key names that are totally unique, not only on mod level. Otherwise it could happen, that a preselected parameter value from another third-party construction is used once your construction is loaded. This could result in unintended behavior, if you do not check for the parameter range.
In the updateFn
function of the construction, the selected options can be called up with params.<key>
. Numbers starting from 0 are displayed. The first entry of a dropdown, for example, returns a 0. For a checkbox, 0 corresponds to the non-activated state, 1 to the activated state.
The individual characteristics of the different types are shown below.
Simple text buttons are already known from Transport Fever. They are lined up in the menu as a centered list. The labels of the buttons are passed as a string list in the values
parameter, they can also be translated in strings.lua.
{ key = "postbox_type", name = _("Postbox"), uiType = "BUTTON", values = { _("Germany"), _("Switzerland"), _("Austria"), }, tooltip = _("Choose the type of postbox you'd like to place."), },
Especially for many linear values, such as numerical series or size increments, sliders can be used as easy-to-use controllers. The name of the currently selected position is displayed to the right of the slider. The display names of the buttons are passed as a string list in the values
parameter, these can also be translated in the strings.lua.
{ key = "parcelstation_length", name = _("Parcel Station Length"), uiType = "SLIDER", values = { _("5m"), _("8m"), _("10m"), _("12m"), _("15m"), _("20m"), }, tooltip = _("Choose the length of the parcel station."), },
If a large number of text options should be available, a simple list of buttons is not sufficient. Then a combo box, also known as a drop-down menu, can be implemented to offer a compact number of options. The entries of the list are passed as a string list in the values
parameter, they can also be translated in strings.lua.
{ key = "post_assets", name = _("Post Assets"), uiType = "COMBOBOX", values = { _("Few Parcels"), _("Many Parcels"), _("Oversized Parcels"), _("Letterboxes"), _("Empty"), }, tooltip = _("Choose the decoration that should lay around."), },
Unlike the text buttons, the icon buttons display small images in TGA format. Unlike the text buttons, the list is currently left-aligned. If the list of buttons becomes wider than the parameter menu, it will wrap to another line. It is therefore advisable that the images for the buttons all have the same height. Also, the border of the buttons should be transparent so that you can see which option is currently selected. The file paths to the images are passed as a string list in the values
parameter, the paths are relative to res/textures/
. The tooltip is used for hovering over the parameter name. It is currently not possible to add tooltips for the individual icons.
{ key = "post_horns", name = _("Post Horns"), uiType = "ICON_BUTTON", values = { "ui/parameters/post_de.tga", "ui/parameters/post_ch.tga", "ui/parameters/post_at.tga", }, tooltip = _("Choose the post horn that should be displayed."), },
If only one value is assigned with yes or no, a checkbox is also suitable as parameter type. Only the name is displayed, but the values
parameter must still be defined with two values, but it's values are never shown.
{ key = "post_box_open", name = _("post box is open"), uiType = "CHECKBOX", values = {"0", "1"}, tooltip = _("Choose if the post box is shown in opened state."), },
In this function, the construction is configured and specified. You can use the content of the table params
to configure the construction. It also contains the selection of the user parameters you have specified in the params
list above. See the list of construction types to find out, which additional properties are available in the params
table depending on the construction type.
The updateFn
returns a data struct with several mandatory and many optional properties. They are described below. Please note, that the return data struct is referred as result
.
It is possible and very common to add models to a construction. Almost every model can be added by adding it to the list of models
in the result
data struct.
result.models = { { id = "industry/chemical_plant/building_small.mdl", transf = transf.transl(vec3.new(20, -10 , 0)) } } -- alternative with dynamic index depending on the already existing list result.models = { } ... result.models[#result.models + 1] = { id = "industry/chemical_plant/building_small.mdl", transf = transf.transl(vec3.new(20, -10 , 0)), tag = "office", }
Each entry contains some properties:
id
is the path to the .mdl
file relative to res/models/model/
.transf
is the transformation matrix relative to the point where the player clicked. It may either be provided as a 16 value list or by use of one of the functions from transf.lua
that can be found in res/scripts/
.tag
is an optional property to label the model with a certain string. It can be used as reference in code elsewhere.There is no limitation in the number of models a construction can contain and the order they were added does not matter.
There are two additional properties params.paramX
and params.paramY
that are controlled by keyboard keys. Per default the keys are O/P and Ü/+ or [/] (depending on keyboard layout). They each have steps of 2π/32 so that 32 steps can be mapped to a full rotation around an axis.
To support rotation around all three axis, encapsulate the transformation matrix of the model with the rotateTransf
function
transf = constructionutil.rotateTransf(params, { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 })
and make sure to import the constructionutil
with local constructionutil = require “constructionutil”
at the begin of the file (before function data()
).
Edge lists are used to add streets and tracks to a construction. The following code adds a street segment and a track segment to the construction:
result.edgeLists = { -- specify a street segment { type = "STREET", params = { type = "standard/country_new_small.lua", tramTrackType = "YES" }, edgeType = "BRIDGE", -- optional edgeTypeName = "cement.lua", -- optional edges = { -- one entry refers to a position and a tangent { { .0, -79.0, .0 }, { .0, 15.0, .0 } }, -- node 0 (snap node) { { .0, -64.0, .0 }, { .0, 15.0, .0 } } -- node 1 }, snapNodes = { 0 } , -- node 0 is allowed to snap to other edges of the same type freeNodes = {}, tag2Nodes = {}, }, -- specify a track segment { type = "TRACK", params = { type = "standard.lua", catenary = true }, edgeType = "BRIDGE", -- optional edgeTypeName = "cement.lua", -- optional edges = { { { -19.1, .0, .0 }, { -20.0, .0, .0 } }, -- node 0 { { -39.1, .0, .0 }, { -20.0, .0, .0 } }, -- node 1 (snap node) }, snapNodes = { 1 }, -- node 1 is allowed to snap to other edges of the same type freeNodes = {}, } }
Each block in the edgeList
has several properties:
type
selects if the edges should be “STREET”
or “TRACK”
edges.params
is a block with several properties depending on the type selected above.edgeType
decides if the segment is built as a “BRIDGE”
, “TUNNEL”
or normal (unset).edgeTypeName
selects the type of bridge or tunnel to be used. It is relative to res/config/bridge/
or res/config/tunnel/
.edges
is a list. Every two entries form an edge. The first entry is the start node of the edge, the second entry is the end node of the edge. Every node has two vectors with three values:snapNodes
is a list of node indizes from the edges
list. These nodes can be used to snap streets or tracks while manually building later.freeNodes
is a list of nodes, that are not part of the construction. Edges with two free nodes can be deleted or modified independently.
The parameters for streets in the params
block are:
type
is a reference to a tracktype relative to res/config/track/
.hasBus
is a boolean value. If set to false
, no bus lane will be built. If set to true
, a bus lane will be built. tramTrackType
is a string value. If set to “NO”
, no tramtracks will be built. If set to “YES”
, tram tracks will be built. If set to “ELECTRIC”
, tram tracks with catenary will be built.
The parameters for tracks in the params
block are:
type
is a reference to a tracktype relative to res/config/track/
.catenary
is a boolean value. If set to true
, the track will be electrified.Some game objects can be attached to edges, for example signals and waypoints. This is not only possible by the player, but also possible in constructions for edges contained by the constructions, as shown in the following code sample:
-- we need some edges to attach edge objects to them taxiway = { } -- edge 0 taxiway[#taxiway + 1] = { { 180.0, 20.0, .0 }, { .0, -20.0, .0 } } taxiway[#taxiway + 1] = { { 180.0, 0.0, .0 }, { .0, -20.0, .0 } } -- edge 1 taxiway[#taxiway + 1] = { { 180.0, 0.0, .0 }, { .0, -47.1, .0 } } taxiway[#taxiway + 1] = { { 150.0, -30.0, .0 }, { -47.1, 0.0, .0 } } -- add taxi way to edge lists... -- attach a signal to one of the above defined edges result.edgeObjects = { { edge = 1, -- attach object to edge 1 param = .5, -- param along the edge left = false, model = "station/airport/asset/signal_runway_old.mdl" -- see res/models/model/ } }
The edgeObjects
list contains all object attachments with properties:
edge
is the number of the edge. Edge 0 is between node 0 and 1, edge 1 is between node 2 and 3, …param
is the offset along the edge from the start node in meter.left
is set to false, if the object should be placed on the right side of the edge. true
leads to objects on the left side.model
is a reference to a .mdl
file relative to res/models/model
.Please be aware that it is not possible to add more than one object per edge by script.
The adjustment of terrain height to the construction is done with the terrain alignment. If the required alignment can't be done due to other nearby constructions or infrastructure, the construction can't be built.
result.terrainAlignmentLists = { { type = "EQUAL", -- accepted values: "EQUAL", "LESS" and "GREATER" faces = { { { -10, -10, 0 }, { 10, -10, 0 }, { 10, 10, 0 } } }, -- a list of polygons slopeLow = 0.3, slopeHigh = 0.6, triangles = { { -10, -10, 0 }, { 10, -10, 0 }, { 10, 10, 0 }, } optional = false, }, ... }
The terrainAlignmentLists
list contains 0 or more blocks with several properties:
type
is the policy that should be used for terrain alignment. With “EQUAL”
the terrain is aligned exactly to the specified faces, with “LESS”
only higher areas are taken down, with “GREATER”
areas below the faces will be filled up.faces
is a list of polygons. Each polygon consists of at least three points with three coordinates relative to the construction origin each.slopeLow
is the gradient for the slope on the outside of the construction that is needed to make the transition to the modified terrain when the surrounding terrain is lower or higher than the construction area.slopeHigh
is the steeper gradient for the slope if the height difference is too big and the slope would get too wide or when another object constraints the sloping.triangles
is a list of points. The number of points needs to be a multiple of 3. Every three points are considered as the corners of triangles. This property is an alternative to the faces
property.optional
should be set to true if the alignment should not throw collision errors when competing against other terrain alignments in the same construction.Ground faces are used to paint the terrain below the construction to blend in with the construction.
result.groundFaces = { { face = { { -10, -10, 0 }, { 10, -10, 0 }, { 10, 10, 0 } }, modes = { { type = "FILL", key = "industry_floor.lua" } }, loop = true, alignmentOffsetMode = "OBJECT", alignmentDirMode = "OBJECT", alignmentOffset = { -2.0, -1.0 }, }, ... }
The groundFaces
list contains 0 or more blocks with the following attributes:
face
is the list of coordinates that span the surface which should be painted. Each point has a value relative to the construction origin for x,y and z axis, but the z axis is ignored. At least three points are required.modes
is a list of paint modes that should be applied to the face:type
is the actual painting mode. “FILL”
fills the whole face, “STROKE”
paints a line with soft sides along the face edge, “STROKE_INNER”
paints a line with a soft side to the inside of the face and “STROKE_OUTER”
paints a line with a soft side to the outside of the face.key
is the reference to the ground texture that should be used relative to res/config/ground_texture/
.loop
is a boolean value used to describe if the face is closed. If set to false
, the edge between last and first coordinate will not get a stroke paint.alignmentOffsetMode
specifies whether the offset is applied relative to the construction orientation (“OBJECT”
) or the game world orientation (“WORLD”
).alignmentOffset
contains the offset values in x and y direction.alignmentDirMode
specifies if the uv direction setting is applied relative to the construction orientation (“OBJECT”
) or the game world orientation (“WORLD”
). alignmentDir
contains the direction rotation values in x and y axis. They are computed as tan2(y,x)
to calculate the actual rotation.Colliders are used to define the areas where no other object can be built. This is useful e.g. to reserve space for industry expansions with later level upgrades.
result.colliders = { params = { halfExtents = { 1.5, 1.5, 1.5, }, }, transf = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, }, type = "BOX", }, { colliderutil.createBox({ 0, 0, 0 }, { 1.5, 1.5, 1.5 }) }
The colliders
list contains 0 or more blocks with the following properties:
params
is a list of parameters. These depend on the type
used.“BOX”
the required parameter is provided in halfExtents
. That is a 3 value vector with the half box lengths for all three axis.“CYLINDER”
it is provided in halfExtents
too. The 3 values represent the half sizes for all three axis.“POINT_CLOUD”
, a list of points is provided in the points
parameter. Each point has three values for the position on all three axis relative to the construction origin. The transf
parameter is ignored. transf
is transformation matrix that point on the center of the collider. It is relative to the construction origin. type
is the type of collider. It can be either “BOX”
, “CYLINDER”
or “POINT_CLOUD”
for constructions.
The colliderutil.lua
in res/scripts/
provides some useful functions to prepare the collider definition based on some parameters.
There are three properties that can be used to set the costs of the construction:
cost
is paid when the construction is built.bulldozeCost
is paid when the construction is demolished.maintenanceCost
is paid monthly to maintain the infrastructure.
For models with “CUSTOM”
typed labels, the content can be provided here:
result.labelText = { [12] = { "Label", "SomeText"} }
The labelText
list contains a mapping of keys to value lists. The key number equals the id of the model in result.models
that should be provided with some custom label texts. The list contains the strings. The first string is for the first label of the model, the second for the second label of this model and so on.
To improve performance, for simple constructions that do not contain ground faces, terrain alignments, edges or depot/station/industry/town building definitions, there is no construction placed in the game but just the seperate mdls. These can not be configured afterwards or be deleted all at once. Furthermore these types of assets do not support the labelText
custom texts from constructions.