Modular Constructions
Modular constructions are built like every other construction with parameters but can be edited afterwards by adding and removing some parts like pieces of a puzzle. These parts are called modules. The locations where they can be placed are indicated by slots.
To provide different start configurations, it is possible to define construction templates.
Method Cycle
Whenever a construction shall be previewed or actually built, a meta function is executed that orchestrates the execution of update functions for the whole construction as well as the individual modules.
It first calls the updateFn function of the construction and stores its result. If the parameters contain modules - both via template calls or during construction reconfiguration - a second part is executed. For each module, sorted in ascending order of the slotIds, the following steps are executed:
- Search for the
slotIdin the list of existing slots in the current result set from the construction and previous modules. - If the
slotIdis not in the list and invalid slots are not accepted (result.callInvalidModulesunset orfalse) skip the current module. - Fetch the slot location and tag name.
- Prepare the
addModelfunction. - Call the
updateFnfunction of the module and provide theaddModelfunction.
Finally if the construction added a terminateConstructionHook, that will be executed.
Construction UpdateFn
The updateFn function of the construction is used to assemble the core elements of the construction add put them in the result data struct. This may cover basic models, ground faces and terrain alignments as well as additional functions that can be called from modules later:
... updateFn = function(captureParams, params) ... return result end ...
Parameters
Beside the usual parameters of a construction updateFn, it receives a list of all modules that should be built for the construction in modules:
modules = { [10001000] = { metadata = {}, name = "airfield_main_building.module", updateScript = { fileName = "", params = {} }, variant = 0 }, ... },
The index is the slotID where the module should be placed. The properties are:
metadatais a struct containing all the metadata that was set in themetadatastruct of the module.nameis the reference to the module relative to the construction.updateScriptis the reference to the module updateFn function.variantis an integer that is increased and decreased by pressing M and N while placing a module. It can be used to provide rotated variants of models in a module or cycle through different assets. The value can be positive and negative, default is 0.
Return Data Struct
The result data struct is based on the one from normal constructions. On top there are additional structs specifically for modular constructions:
result.slotConfigfor slot type restrictions.result.slotsfor a list of slots currently available in the construction.
It is possible that construction extend the result by custom structs, e.g. helper functions that can then be used by modules at a later point.
The result.cost is just the cost of the main construction. Modules may add their cost later.
In the result.slotConfig list, there can be restrictions for some slot types. The list uses the slot type as a key and contains two properties for each entry:
maxModulesis the maximum amount of modules with this slot type that can be built in the whole construction. IfmaxModulesis set to-2, it depends on the value ofmessageif the module button is enabled or disabled. No messages results in the button being enabled, a set message disables the button.
messageis a message that is displayed on modules of this type in the menu, when they can't be build due to the slot configuration restrictions.skipCollisionCheckis an optional boolean value that is used to disable the ui collision checks. With skipped checks, the slot markers are not painted red when they are obstructed by something else, e.g. free tracks. The potential collider collisions of the module models still apply.
The result.slots struct is a list of all currently existing slots in the construction. Each entry in the list has the following properties:
idis the unique slot id that is used as reference for the placed modules. It is a non-negative number.typeis the key that is used to restrict the compatible modules on this position.transfis the position relative to the construction origin as a transformation matrix.spacingis a four value vector that is used to set the size of the slot around thetransfposition. It is{-x, x, -y, y }.shapeis a value that is used to select the style of the slot marker symbol. It is either0for a square,1for a triangle,2for a transverse rectangle or3for a longitudinal rectangle.heightis the height of the slot counted from the position oftransf. Together with thespacingthis is used for selection and bulldozing purposes.autofillis a boolean value used for industry fields/satellites. If set to true, these slots are considered to be filled on initial construction of the industry.alignToTerrainis a boolean value used for constructions that should adapt to the terrain. If set to true, the slot is considered to be aligned to the terrain height at its position.replaceableis a boolean value used to mark slots where modules may be overridden by modules of the sametype.
With result.callInvalidModules set to true it is possible to allow modules in slots that were not predefined, e.g. because they have an ID that is lower than the slot that defines them.
Construction Upgrade Script
The upgradeScript function for modular buildings is only slightly different than the one for non-modular constructions. The slotId parameter contains the slotId where the cursor pointed on when applying the tool.
Modules
A module is a construct of one or more models that can be placed in certain positions in modular constructions. They are defined in .module.lua files. The basic structure of a module is:
function data() return { availability = { ... }, menuCategory = { ... }, description = { ... }, buildMode = "SINGLE", autoRemovable = false, type = "shed", metadata = { price = 12000, maintenanceCost = 2000, } getModelsScript = { fileName = "roundhouse_modules.script@shed.getModelsFn", params = { ... }, }, updateScript = { fileName = "roundhouse_modules.script@shed.updateFn", params = { ... }, }, } end
Beside the usual availability, description, menuCategory and buildMode known from constructions, each modules has additional properties:
autoRemovableis an optional property to flag this module for automatic removal when something else is built over it. This is used for fields of industries. If unset, it is considered asfalsetypeis a key that is used to restrict modules to a certain slot type
To provide additional static metadata, the modules can provide more information in a metadata struct. It might be used by the construction and other modules to identify this module properly. It also may contain info about the build and maintenance cost of the module.
Before a module is actually placed, the getModelsScript is used to provide the models for a floating module preview attached to the cursor. The updateScript references the update function that is executed on update of the construction. It may receive additional parameters as captureParams. See below for more details.
Module GetModelsFn
The getModelsFn is a function without any parameters other than captureParams. It returns a list of models that should be used for the floating preview that follows the mouse cursor while not pointing on a slot. The function looks like:
... getModelsFn = function() local result = { { id = "hangar_preview.mdl", transf = transf.identity(), } } return result end, ...
Module UpdateFn
The updateFn function of modules is called after the construction updateFn to add the models of the module to the construction as well as do all the stuff that is needed for the module, e.g. adding terminals. It receives several parameters:
- the
captureParamsprovided by the script reference in the.module.lua - the
resultof theupdateFnfrom the construction or the module that was called before containing all previously added models, data etc. - the
transformationof the slot relative to the construction origin as a transformation matrix. This should be applied to all models of the module if they are located relative to the slot. Be aware that thetransformationis nil if the construction makes use of thecallInvalidModulesproperty. - a
tagthat should be added to the models in the result to assign them to the current module at that slot. This is used to identify models that are removed when a module is demolished. - the
slotIdthat is a unique numeric identifier of this slot. - the
addModuleFnfunction is a function that adds models to the construction and ensures that the correcttagis set. It is provided in theconstruction.script.tlinconstructWithModules. - the
constrParamsstruct containing all params that the construction got for itsupdateFnfunction too.
Note that not all modules need every parameter. It is possible to omit parameters from the end of the function header. A function that uses only some of the parameters could look like:
... updateFn = function(captureParams, result, transform, tag) result.subconstructions[1].models[#result.subconstructions[1].models + 1] = { id = "hangar.mdl", transf = transform, tag = tag } local faces = { {-25.0, -25.0, 0.0, 1.0}, {25.0, -25.0, 0.0, 1.0}, ... } modulesutil.TransformFaces(transform, faces) result.subconstructions[1].groundFaces[#result.subconstructions[1].groundFaces + 1] = { face = faces, modes = { { type = "FILL", key = "airfield_hangar.gtex.lua", texCoords = { {.0, .0}, {1.0, .0}, {1.0, 1.0}, {.0, 1.0} } }, } } -- add further stuff end, ...
The updateFn should return the result that was received where the content of the module was inserted.
Make sure to apply the transformation matrix received as transform parameter to all models, ground faces, terrain alignments, … of the module before adding them to the result if you intend to place them relative to the slot position.
Rail Station Module
This example provides a custom roof module for the vanilla train station that allows to span large hall roofs over multiple tracks. It is available for download.
Explanation
The platform_passenger_roof_hall.module.lua defines an additional module for the vanilla rail station:
... local modelConfig = { stands = resolve("roof_hall_stands.mdl"), } ... updateScript = { fileName = "station_hall_modules.script@platformPassengerHallRoof.updateFn", params = { modelConfig = modelConfig, }, }, getModelsScript = { fileName = "station_hall_modules.script@platformPassengerHallRoof.getModelsFn", params = { modelConfig = modelConfig, }, }, ...
At the very top, there is a local struct named modelConfig which contains some data about models to be used, here the pillar models for the station roof. This data is provided to the module script functions as captureParams.
Other relevant properties are the type and metadata:
... type = "passenger_platform_roof", metadata = { platform_roof = true, platform_roof_curved = true, platform_roof_hall_style = "test", maintenanceCost = 0, }, ...
The type is the same as the one of the vanilla roofs, so this roof module can be built wherever the other roofs could be placed. The metadata is available to be used as context info by other modules. This possibility is used to identify a second placed module with the same metadata and calculate the roof span between them. This happens in the updateFnPlatformPassengerHallRoof function in the station_hall_modules.script.lua:
local updateFnPlatformPassengerHallRoof = function(captureParams, result, transform, tag, slotId, addModel, constrParams, params) local modelConfig = captureParams.modelConfig result = result.cargoStation local addModelFn = function(a, b, c) addModel(a, b, c, result.models) end addModelFn(modelConfig.stands, transf.rotZTransl(math.rad(90), vec3.new(0, 0, -2))) local coords = result.GetCoord(slotId) local i = coords[1] local j = coords[2] for i2 = i+1, i+9 do local roofAti2 = result.GetRoofAt(i2, j) if roofAti2 and roofAti2.metadata.platform_roof_hall_style == result.GetRoofAt(i, j).metadata.platform_roof_hall_style then local roofModel = hallRoofStyle2Size2ModelsConfig[roofAti2.metadata.platform_roof_hall_style][(i2-i)*5] addModelFn(roofModel, transf.rotZTransl(math.rad(-90), vec3.new(0, 0, -2))) break end end end
First, the pillar model is added to the result by calling addModelFn(modelConfig.stands, transf.rotZTransl(math.rad(90), vec3.new(0, 0, -2))). This addModelFn is a function that is defined directly above. It basically just forwards the call to the addModel function provided as parameter in the updateFn.
Next, the coordinates in the vanilla station grid are calculated. The relevant info is coded into the slotId and the vanilla station offers a helper function to retrieve the coordinates, called GetCoords(slotId).
Based on these coordinates, the neighbors to one side are checked for the presence of another roof module. This is done by retrieving info with the GetRoofAt(x,y) function in a loop that steps further away step by step. If a roof module is present at a position, roofAti2 is not nil. Then the metadata of the current module and the found one are compared to check if platform_roof_hall_style is equal. In that case, the correct model for the distance is selected by using the look up table hallRoofStyle2Size2ModelsConfig and it is added to the result as well. Afterwards the loop is stopped with break.
Standalone Modular Construction
This example provides a simple independent modular construction. It showcases how to set up slots and how modules may react depending on neighboring slots. It is available for download.
Explanation
The example vaguely resembles a turntable with adjacent roundhouse. Every locomotive stand can be set as seperate module, either with a shed or without. If a shed has no other shed module as neighbor, a wall should fill the side.
The mod contains several files relevant for the construction:
.mdlfiles and the required materials and meshes for turntable, empty stand, shed stand and a left and right sidewall for the shed.- the static
roundhouse.con.luafor the definition of the construction and itsroundhouse.script.luafor the scripts. - two module files
shed.module.luaandempty.module.luafor the two module types and a sharedroundhouse_modules.script.luafile for the scripts of both modules.
The roundhouse.con.lua is rather simple and follows the standards described in the construction basics. It could be extended by providing parameters or even different templates for the player to choose.
The roundhouse.script.lua adds the turntable model to the result:
... mainSubconstruction.models[#mainSubconstruction.models + 1] = { id = "turntable.mdl", transf = transf.identity() } ...
Additionally it sets up the slots that can be filled by the player by placing modules:
... result.slots = { } for i = 1, 30 do result.slots[i] = { id = i, type = "shed", transf = transf.mul(transf.rotZ(math.rad(-12 + i * 12)), transf.transl(vec3.new(30, 0, 0))), spacing = {4, 4, 1, 1}, shape = 1, height = 1, replaceable = true, } end ...
The full circle is divided in 30 12-degree segments and with a for loop, a slot is added for each segment. The slots are 30 meters away from the turntable center and rotated to align to the rays from the center. Their type is set to shed, so any module of the same type can be placed there.
Both module definition files are rather simple. The big difference is that the shed.module.lua has an additional entry type = "shed" in the metadata. This is relevant for the neighbor logic in the script. As required, both modules reference both a getModelsScript and an updateScript.
The roundhouse_modules.script.lua returns a hierarchical table with the functions:
function data() return { shed = { updateFn = updateFnShed, getModelsFn = getModelsFnShed, }, empty = { updateFn = updateFnEmpty, getModelsFn = getModelsFnEmpty, }, } end
When editing the construction, the player sees what is returned by the getModelsFn floating at the cursor. To ensure the shed looks proper, the function returns not just one but three models. All these models are shifted by -30 meters to compensate the slot offset that was previously defined in the main construction:
... local getModelsFnShed = function(captureParams) return { { id = "shed_wall_left.mdl", transf = transf.transl(vec3.new(-30,0,0)) }, { id = "shed_segment.mdl", transf = transf.transl(vec3.new(-30,0,0)) }, { id = "shed_wall_right.mdl", transf = transf.transl(vec3.new(-30,0,0)) } } end ...
The update function of the empty module is rather simple, it just adds a single .mdl. The updateFnShed starts doing the same, but has some additional logic:
... local leftIndex = (slotId % maxSegments) + 1 local rightIndex = ((slotId - 2) % maxSegments) + 1 local moduleLeft = constrParams.modules[leftIndex] local moduleRight = constrParams.modules[rightIndex] ...
Based on the slotId, the left and right neighbor ids are calculated, using the modulo operation %. To safely wrap around from 1 to 30, some shifting is done. Then for both potential neighbors, data is retrieved with constrParams.modules[index]. If these slots are not occupied or the modules in the slots do not have the metadata type = "shed", the side wall models are added as well:
... if not (moduleLeft ~= nil and moduleLeft.metadata ~= nil and moduleLeft.metadata.type == "shed") then addModel("shed_wall_left.mdl", transf.transl(vec3.new(-30, 0, 0)), tag, result.subconstructions[1].models) end if not (moduleRight ~= nil and moduleRight.metadata ~= nil and moduleRight.metadata.type == "shed") then addModel("shed_wall_right.mdl", transf.transl(vec3.new(-30, 0, 0)), tag, result.subconstructions[1].models) end ...