The existing user interface of Transport Fever 2 can be extended as well as additional interface components can be added via modding.
The visual style of user interface elements is controlled by style sheets in res/config/style_sheet/
. These files are combined to an internal stylesheet by the game. To tweak some values or add new style definitions, add a custom stylesheet in your mod with a unique name.
require "tableutil" local ssu = require "stylesheetutil" function data() local result = { } local a = ssu.makeAdder(result) -- helper function a( "ConstructionMenuTab::ButtonsLayout", -- Selector { margin = { 0, 0, 30 + 35, 0 }, -- style ... } ) return result end
To generate the correct stylesheet information, usually a helper function in the stylesheetutil.lua
is used. It receives two parameters: A selector and the actual styling.
The selectors are similar to CSS selectors but have some smaller differences. The whole selector is a string and several comma seperated alternatives can be used with the same styleset.
Basically there are three ways to select an element:
Name
selects by the element name.!a-class-name
selects by a class name.#anIDOfAnElement
selects by the id.
To select by a hierarchy, you can seperate the levels with a blank character: Parent !child-class
. To specifically address individual referenced elements, :: can be used: WarningsButton::Icon
.
There are several pseudoclasses that can be used with selectors:
:enabled
is used for elements that can be used.:disabled
is used for elements that are locked.:hover
is used on mouseover.:active
is used when in active state, e.g. selected image buttons.To find out the name or class of an ui element, you can use the ingame tools to pick elements. Then the name of the element that the cursor points on is visible in the console.
There are different kinds of styles that can be set for an element.
To define dimensions of an ui element, there are several properties:
size
is the default size as a two integer vector. The first value is the horizontal size 1. The second value is the vertical size 2. minSize
is the minium size that a component may get. This is the lower limit for resizable windows. It is a two integer vector too.maxSize
is the same for the upper limit of resizable windows.From the frame that is defined by the size properties, there are offsets to the inside and to the outside:
padding
is the inner blank space that is hold free of content 3. It is a vector with 4 integers for the top, right, bottom and left padding.margin
is the outer blank space that is hold free of other elements 4. It is a vector analogue to padding
.
The gravity
is a vector with 2 float values that is used for the positioning of the element. It is a vector of two float numbers, one for the horizontal and one for the vertical positioning. Both can be set to 0.0
for left/top position, 0.5
for center positioning and 1.0
for right/bottom positioning. If set to -1
, the element will grow in that dimension to fill up space.
For layout components, there are additional properties that control the spacing between the contained elements.
innerSpacing
is a vector with 2 integer values. These control the additional padding between the elements that are contained for horizontal and vertical direction.outerSpacing
is another vector with 2 integer values. These control the additional padding between the elements and the outer layout for horizontal and vertical direction.
The scaling
property contains a single float value that is used as a scaling factor on the whole element. Further scaling is applied through the global scale factor in the user settings.
Elements with texts have several other properties:
color
is the color of the text. It is a vector of 4 float numbers between 0 and 1. The values are for red, green, blue and alpha.fontFamily
is a reference to a .ttf
font file relative to the res/fonts/
. Currently, the Lato
family is used as default for all texts except the console where Noto
is used.fontSize
is the height of text in pixels that is used for the text. textTransform
is an operation that is applied to the text input that should be displayed. It may be:“NONE”
for the text as it is (default).“CAPITALIZE”
to ensure that the leading letter of each word is a capital letter.“UPPERCASE”
converts all characters to uppercase.“LOWERCASE”
converts all characters to lowercase.textAlignment
is a vector of two float numbers, one for the horizontal and one for the vertical alignment of the text content. Both can be set to 0.0
for left/top alignment, 0.5
for center alignment and 1.0
for right/bottom alignment.
The background of ui elements has several layers:
In the back, just above the shadow is a color only layer that is controlled by the backgroundColor
2.
Above the color backgrond, is the layer that is controlled by backgroundImage1
and backgroundColor1
3. backgroundImage1
has three properties:
fileName
is a reference to an image relative to res/textures/
. It may either be a colored image or it is a greyscale image. In the later case, black is masked as transparent while the rest is colored with the color defined in backgroundColor1
.horizontal
is a vector with 4 integer values. They correspond to:vertical
is another vector with 4 integer values. They correspond to:
If horizontal
or vertical
are set, the referenced image repeats the area between the second and third value until the image is large enough to fill the element. Otherwise it is used as a static image.
There is another background layer 4 that is controlled by backgroundImage2
and backgroundColor2
like the other background image layer.
For the border 5, there is a set of three properties available:
borderImage
is a block with properties analogue to backgroundImage1
above.borderColor
is is a vector of 4 float numbers between 0 and 1. The values are for red, green, blue and alpha.borderWidth
is a vector with 4 integer values. They correspond to:
The properties for shadows 1 are defined identical to borders. Their names are shadowNinePatch
, shadowColor
and shadowWidth
.
To blur the content behind the window, it is possible to set a blurRadius
to some positive float value.
transitionProperty
is a list of property names that should have a smooth transition to new values, e.g. due to a new pseudo class getting active. If unset and transitionDuration
is set, all properties are used.transitionDuration
is a vector of float value that define the length of the transitions. If only one value is set, it is applied to all transitions.transitionTimingFunction
is used to define how the value gradually changes:LINEAR
has a linear playback.EASE
has an S like speed curve.EASEIN
starts slow and gets faster.EASEOUT
starts fast and then slows down to the end.
Animations are keyframed changes of color properties. They require their own animation keyframe definition in result.animations
:
result.animations.attention = { [.0] = { color = { 1.0, 1.0, 1.0, 1.0 }, backgroundColor1 = { 1.0, .0, .0, .0 }, backgroundColor2 = { 1.0, 1.0, 1.0, 1.0 }, }, [.5] = { color = { 1.0, .0, .0, 1.0 }, backgroundColor1 = { 1.0, .0, .0, .5 }, backgroundColor2 = { 1.0, .0, .0, 1.0 }, }, [1.0] = { color = { 1.0, 1.0, 1.0, 1.0 }, backgroundColor1 = { 1.0, .0, .0, .25 }, backgroundColor2 = { 1.0, .5, .5, 1.0 }, } }
This animation then is referenced by its key in the animationName
property. The total duration of one animation playback is specified as a float value in animationDuration
. It is possible to limit the number of loops with the animationIterationCount
and the animation can use a non linear playback with animationTimingFunction
. Possible values are:
LINEAR
has a linear playback.EASE
has an S like speed curve.EASEIN
starts slow and gets faster.EASEOUT
starts fast and then slows down to the end.
Some ui elements may play a sound when they are activated or the mouse is hovered over them. The references to the sounds are defined in the stylesheets too. There are two properties for sounds, soundEffect1
and soundEffect2
. Which is used when depends on the ui element. To reference a sound, the get(reference)
function from soundeffectsutil.lua
is used. The reference is relative to res/audio/effects/
. See the sound effect manual for further information regarding sound files.
Here are some examples that show simple use cases of stylesheet modding.
This example hides the text that usually is shown while playing a keyframe sequence from the camera tool and the cockpit camera. It adds a stylesheet that overrides the styling of various gui components and setting their background and text color to full transparency.
This example is available for download.
There are several ways of modifying or extending the user interface. Those ways all use the game scripts as a base.
The functions used to manipulate the user interface are available as part of the api.gui
module. It provides functions to retrieve existing ui elements like api.gui.getById(<id>)
as well as constructor functions to create new elements: api.gui.TextInputField.new(<id>)
. See the API reference for a more in detail description of all functions.
Beside the load
callback, there are three different ui related callbacks that are called for different purposes:
Once the user interface is loaded, the guiInit
callback is called once. It is a parameterless function:
function data() return { guiInit = function() -- create and initialize ui elements end, ... } end
The game regularily updates its interface. For this purpose, there is a guiUpdate
callback for game scripts. It is a parameterless function too:
function data() return { guiUpdate = function() -- update elements if neccessary end, ... } end
Beside the interval timed guiUpdate
callback, there are some events which can be fetched with the guiHandleEvent
callback. These are caused e.g. by button clicks. The callback is a function with three parameters:
function data() return { guiHandleEvent = function(id, name, param) -- do something end, ... } end
The properties are:
id
is the identifier of the ui element that caused the event.name
is the identifier of the event type.params
are parameters that depend on the context of the event.By default, the following gui events may occur (only on elements with an id or tag) :
Causing Element | Event Name | Parameters |
---|---|---|
“cutscene” | “play” | - |
“cutscene” | “end” | - |
“cutscene” | “deactivate” | - |
“voiceover” | “start” | - |
“voiceover” | “end” | - |
“musicPlayer” | “trackEnded” | - |
“voiceover” | “end” | - |
CameraController, e.g. “mainView” | “camera.keyScroll” | true movement begins false movement ends |
CameraController, e.g. “mainView” | “camera.mouseRotateTilt” | true movement begins false movement ends |
CameraController, e.g. “mainView” | “camera.mousePan” | true movement begins false movement ends |
CameraController, e.g. “mainView” | “camera.mouseWheel” | float value >= 0 for scroll position |
CameraController, e.g. “mainView” | “hover” | entity id or -1 for no entity |
“constructionBuilder” | “builder.apply” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. result contains a list of new built and/or modified construction entities. |
“constructionBuilder” | “builder.proposalCreate” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. |
“constructionBuilder” | “builder.rotate” | - |
“streetTerminalBuilder” | “builder.apply” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. result contains a list of new built and/or modified construction entities. |
“streetTerminalBuilder” | “builder.proposalCreate” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. |
“streetBuilder” | “builder.apply” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. result contains a list of new built and/or modified construction entities. |
“streetBuilder” | “builder.proposalCreate” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. |
“streetBuilder” | “builder.level” | float value for slope level |
“streetBuilder” | “builder.slope” | float value for slope gradient |
“trackBuilder” | “builder.apply” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. result contains a list of new built and/or modified construction entities. |
“trackBuilder” | “builder.proposalCreate” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. |
“trackBuilder” | “builder.level” | float value for slope level |
“trackBuilder” | “builder.slope” | float value for slope gradient |
“streetTrackModifier” | “builder.apply” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. result contains a list of new built and/or modified construction entities. |
“streetTrackModifier” | “builder.proposalCreate” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. |
“bulldozer” | “builder.apply” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. result contains a list of new built and/or modified construction entities. |
“bulldozer” | “builder.proposalCreate” | proposal contains the requested changes in abstract form. data contains the concrete changes or error data. |
“vehicleManager” | “accept” | entity id of existing vehicle if update else -1 vehicleConfig list of model references |
By default, the following gui component events may occur on elements with id or tag:
Causing Element | event name | parameters |
---|---|---|
any Component | “destroy” | - |
any Component | “visibilityChange” | true when becoming visible false when becoming invisible |
any Button Component | “button.click” | - |
any List Component | “list.select” | integer value for the selected list index |
any TabWidget Component | “tabWidget.currentChanged” | index integer value for the selected tab id id of the selected tab element if existing |
any ToggleButton Component | “toggleButton.toggle” | true if activated false if deactivated |
any Window Component | “window.close” | - |
In addition to the global callbacks described above, some ui components have their own event callbacks, e.g. buttons have an onClick
callback. Below is a table showing which callback type is available for which component.
Callback | AbstractSlider | Button | Chart | CheckBox | ColorChooser | ColorChooserButton | ColorPicker | ComboBox | Component | DoubleSpinBox | GameUI | ImageView | ProgressBar | RendererComponent | ScrollArea | Slider | Slider2 | SpinBox | TabWidget | Table | TextInputField | TextView | Window |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
onChange | ✔ | ✔ | |||||||||||||||||||||
onClick | ✔ | ||||||||||||||||||||||
onClose | ✔ | ||||||||||||||||||||||
onColorChanged | ✔ | ||||||||||||||||||||||
onCurrentChanged | ✔ | ||||||||||||||||||||||
onDestroy | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
onIndexChanged | ✔ | ||||||||||||||||||||||
onMove | ✔ | ||||||||||||||||||||||
onSelect | ✔ | ✔ | |||||||||||||||||||||
onStep | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
onToggle | ✔ | ||||||||||||||||||||||
onValueChanged | ✔ | ✔ | ✔ | ||||||||||||||||||||
onVisibilityChange | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
All these ui components inherit some properties from the general Component
that supports three general callbacks:
onDestroy
is called when the component is deleted. The function does not receive parameters.onStep
is the update callback that is called in every step. It receives two parameters:onVisibilityChange
is called whenever the visibility changes, e.g. if the parent tab in the tab widget is selected. It receives a boolean parameter that is true
when the element changed to visible state and false
if it became invisible. A more in detail explanation of the other callbacks can be found in the API reference.
Here are some examples that show simple use cases of ui modding.
This example adds a game script that shows a real time clock in the bottom game info bar. It is a simple use case of the guiUpdate
callback and the api.gui
interface.
local clockstate = nil function data() return { guiUpdate = function() ... end } end
The local variable clockstate
will hold a reference to the ui element that should be regularily updated. Then it is not neccessary to fetch the element reference on every update.
The callback contains several things. At first it is checked if the ui element was created before:
... if not clockstate then -- element for the divider local line = api.gui.comp.Component.new("VerticalLine") -- element for the icon local icon = api.gui.comp.ImageView.new("ui/clock_small.tga") -- element for the time clockstate = api.gui.comp.TextView.new("gameInfo.time.label") -- add elements to ui local gameInfoLayout = api.gui.util.getById("gameInfo"):getLayout() gameInfoLayout:addItem(line) gameInfoLayout:addItem(icon) gameInfoLayout:addItem(clockstate) end ...
The three elements are created:
Component
with the name “VerticalLine”
. Elements with this name get styled by the default.lua
stylesheet to be only one pixel wide and have a gray background. This results in the small vertical dividing lines.ImageView
with a reference to the image file that should be used relative to res/textures/
. The image file is a grayscale image with no transparency, thus black is masked as transparent. If there is a file with <filename>@2x.tga
in the same folder, this gets used and scaled down to the half size. This allows for higher resolution in image files to support larger interface scalings.TextView
that receives an initial text as parameter. This is later overriden by the callback.Then the existing layout in the bottom section is fetched. The id of the containing element can be gathered by using the debug tool window that provides console output when hovering over ui elements.
After that, the three elements are added to the layout. gameInfoLayout:addItem(line)
is a shortened version of gameInfoLayout.addItem(gameInfoLayout, line)
.
The previous code section could be moved to a guiInit
callback function too.
... local time = os.time() if clockstate and time then clockstate:setText(os.date('%H:%M', time)) end ...
This section updates the actual text. At first it retrieves the current system time in milliseconds with os.time()
. Then the text of clockstate
is set to a formated version of the time displaying hours and minutes. Like above, a shortened version is used with :
to omit the self referencing argument.
This example is available for download.
This example adds a game script that shows an FPS counter in the bottom game info bar. It is a simple use case of the onStep
callback from the TextView
component and the api.gui
interface.
local stopwatch = nil local function updateText(totaltime, steptime) stopwatch:setText(math.floor(1000000/steptime + 0.5) .." FPS") end function data() return { guiInit = function() ... end } end
The local variable stopwatch
will hold a reference to the ui element that should be regularily updated. Then it is not neccessary to fetch the element reference on every update.
The function updateText is the function that is used for the onStep
callback. It drops the first parameter and calculates the current frames per second (FPS) based on the length of the last step.
The callback guiInit
contains the setup of the needed ui elements:
guiInit = function() -- element for the divider local line = api.gui.comp.Component.new("VerticalLine") -- element for the icon local icon = api.gui.comp.ImageView.new("ui/fps.tga") -- element for the time stopwatch = api.gui.comp.TextView.new("") stopwatch:onStep(updateText) -- add elements to ui local gameInfoLayout = api.gui.util.getById("gameInfo"):getLayout() gameInfoLayout:addItem(line) gameInfoLayout:addItem(icon) gameInfoLayout:addItem(stopwatch) end, ... end
The three elements are created:
Component
with the name “VerticalLine”
like in the example above.ImageView
like in the example above.TextView
that is saved to the local variable stopwatch
to reference it later. Its onStep
callback is set to the function updateText
that was defined above.Finally the existing layout in the bottom section is fetched and the three elements are added to the layout.
This example is available for download.