User Tools

Site Tools

Urban Games

modding:missions

Missions

Transport Fever 2 ships with three campaign chapters. For modders, it is possible to provide additional campaigns or single missions. These are then available over CAMPAIGN in the main menu.

Folder Structure

The campaign related files are located in the res/campaign/. Each campaign has its own subfolder there.

Campaign Folder

Inside these folders are subfolders for each mission that are named by their order starting with 01/. Beside the mission folders, there is an info.lua file for the campaing properties:

function data()
return {
  name = _("Chapter IV"),
  icon = "ui/campaign/campaign_era_d.tga",
  map = "ui/campaign/worldmap.tga",
  description = _("MISSION_ERA4_INFO"),
  order = 4,
  voiceOver = "MISSION_ERA4_INFO.wav",
  disclaimer = _("CAMPAIGN_DISCLAIMER"),
}
end

The properties are:

  • name for the title of the campaign that is displayed in the campaign selection menu.
  • icon for the image button that is used to select the campaign. The double sized <name>@2x.tga image should have a resolution of 420 × 240 pixels.
  • map for the background image that is used for the mission map in the mission selection dialog. The double sized <name>@2x.tga image should have a resolution of 1600 × 800 pixels. It can be a grayscale image without transparency channel too, then black is masked to transparency.
  • description for the descriptive text in the campaign selection menu.
  • order for the sort order in the campaign selection menu. The vanilla campaigns have places 1, 2 and 3.
  • voiceOver for the reference to a soundfile that is played once the campaign is selected. The reference is relative to res/audio/effects/voice_over/<language>/campaign/<campaignfoldername>/.
  • disclaimer for the text that should be displayed once the player clicks on the EDITOR'S NOTE button.

Mission Folder

Inside each mission folder, there are two files. The actual savegame .sav file as well as an info.lua that provides the neccessary metadata:

function data()
  return {
    name = _("MISSION_SILVERCITY_NAME"),
    description = _("MISSION_SILVERCITY_CAMPAIGNSCREEN"),
    image = "m01_loading_screen.tga",
    savegame = "era_a/01/savegame.sav",
    medals = {
      {
        id = "MEDAL_PONDEROSA",
        name = _("MISSION_SILVERCITY_MEDAL_PONDEROSA_CAMPAIGNSCREENNAME"),
        iconLocked = "ui/campaign/bonus_tasks/locked_mission_medal.tga",
        iconVisible = "ui/campaign/bonus_tasks/visible_mission_silvercity_medal_ponderosa.tga",
        iconCompleted = "ui/campaign/bonus_tasks/completed_mission_silvercity_medal_ponderosa.tga",
      },
      ...
    },
    voiceOver = "MISSION_SILVERCITY_CAMPAIGNSCREEN.wav",
    mapPosition = { 234 / 2, 249 / 2 },
    startEndYears = { 1865, 1875 }
  }
end

The properties are:

  • name for the name that should be displayed in the mission selection menu and the loading screen.
  • description for the descriptive text that should be displayed in the mission selection menu.
  • image for the mission preview image that should be displayed in the mission selection menu. It is relative to res/textures/ui/campaign and the image size should be 1920 × 1080 pixels as it is used for the loading screen too.
  • savegame for the reference to the actual savegame file. it is relative to res/campaign/.
  • medals for a list of up to 3 medals that can be earned in this mission. Each medal block has several properties:
    • id for a unique identifier that is used in scripts and progress storage.
    • name for the name that should be displayed in the medal section of the mission selection menu.
    • iconLocked for an image reference that points to a file relative to res/textures/ that should be shown if the medal is still locked. The double sized <name>@2x.tga image should have a resolution of 120 × 120 pixels.
    • iconVisible for the reference that is used once the medal goal is known to the player.
    • iconCompleted for the reference that is used once the medal is achieved by the player.
  • voiceOver for the reference to a soundfile that is played once the mission is selected. The reference is relative to res/audio/effects/voice_over/<language>/campaign/<campaignfoldername>/.
  • mapPosition for the x and y coordinates where the mission flag should be positioned on the map. The coordinates are based on the non double size image with a resolution of 800 × 400 pixels.
  • startEndYears for the start and end year that should be displayed in the mission selection menu.

You can either create a new game or use an existing savegame. Important is that the campaign mod is active, when creating/loading the game so that the game scripts for the mission are loaded. Then you can save the game and copy the savegame to your mission folder.

Mission Scripting

Basically a mission is the combination of an orchestrating game script and a savegame that is played on.

Script Utils

The utility scripts that are related to the campaigns and missions are located in the res/scripts/mission/ folder:

  • arrivaltracker.lua provides various functions that can be used to track the arrival of passengers and cargo at their destination.
  • calendar.lua provides various functions that can be used to calculate durations depending on day length.
  • colors.lua provides a list of basic color definitions.
  • constructionupgrader.lua provides a function to upgrade a construction by entity id.
  • defaultguides.lua provides a function to reset the guide system.
  • modifier.lua provides a function to execute a batch of modifiers by providing a property tree.
  • nameutil.lua provides a function to set the name of entities by id.
  • outline.lua provides functions to return simple polygons matching the outlines of the vanilla depots and stations
  • params_mt.lua provides a metadata table function to distinguish between different key properties.
  • proposalutil.lua provides functions to check if a build or bulldozing operation is allowed in a certain area.
  • taskutil.lua provides the function for managing the tasks. See below for a detailed explanation.
  • vehiclestore.lua provides functions to restrict the types and numbers of vehicles in the vehicle manager.
  • zone.lua provides a function to generate a circular zone around a given location.

The res/scripts/TaskManager.lua and res/scripts/missionutil.lua are legacy scripts from Transport Fever 1 and should not be used for new projects.

Mission Tasks

The goals of a mission usually are realized as tasks. To manage the tasks, the taskutil.lua can be used. It allows adding new tasks, updates task progress, saves and loads tasks and manages the use of callbacks.

Task Lifetime

To add a new task, call the taskutil:new(…) function:

taskutil:new("mytaskname", {
  onStart = function(self) ... end,
  onUpdate = function(self) ... end,
  onFinish = function(self) ... end,
  onGuiStart = function(self) ... end,
  onGuiUpdate = function(self) ... end,
  onGuiFinish = function(self) ... end,
  getInfo = function(self) return { .... } end,
  handlers = { ... },
  guiHandlers = { ... },
})

It registers the task with the name “mytaskname” and assigns several callbacks. Providing onStart, onUpdate, onFinish, onGuiStart, onGuiUpdate or onGuiFinish is not required.

Note the argument self that is provided to the callback functions which passes in the current task. For example, if a task should be set to finished by onUpdate, simply use the shorthand

self:finish()

which, in this case, is equivalent to

taskutil.tasks["mytaskname"]:finish()
Starting Tasks

The above function onStart can not be directly called by the modder. To set up the task and call the onStart callback, the following functions can be used:

taskutil.tasks["mytaskname"]:start()

After it is called once, it will be set to nil to ensure that the task can't be started twice by calling the above function again. Instead an error will be thrown to help the modder debug this constellation.

An alternative function is:

taskutil:start("mytaskname")

This function just calls the other one if it is not set to nil already.

Updating Tasks

Once the task is started, the provided onUpdate callback function will be called in every step.

Finishing Tasks

To finish a task, use

taskutil.tasks["mytaskname"]:finish()

It works analoguously to the start function and will be set to nil after the onFinish callback function is called. There is an alias for this one too that works analoguously to the alias above:

taskutil:finish("mytaskname")

Once a task is finished, the onUpdate callback function will no longer be called.

GUI Callbacks

onGuiStart is called once the ui thread loads the task and onGuiUpdate and onGuiFinish work in the same way. These are not often used in the default campaign, but an example is a task that checks the camera:

local camera = game.gui.getCamera()

Since game.gui is not available in onStart, onUpdate or onFinish, this can only be done in the corresponding gui callback functions.

Get Info

The getInfo callback is used to provide a table with the content for the task window.

getInfo = function(self)
  return {
    name = _("MISSION_VICECOUNTY_TASK_RUM_BOAT_NAME"),
    paragraphs = {
      { text = _("MISSION_VICECOUNTY_TASK_RUM_BOAT_TEXT") % params },
      { type = "TASK", text = _("MISSION_VICECOUNTY_TASK_RUM_BOAT_TASK") % params },
      { type = "HINT", text = _("MISSION_VICECOUNTY_TASK_RUM_BOAT_HINT") % params },
    },
    subTasks = {
      { name = _("MISSION_VICECOUNTY_TASK_RUM_BOAT_SUB1") % params },
      { name = _("MISSION_VICECOUNTY_TASK_RUM_BOAT_SUB2") % params },
      { name = _("MISSION_VICECOUNTY_TASK_RUM_BOAT_SUB3") % params },
    },
    options = { { "Debug: Skip", "finish" } },
    parentId = "1",
    camera = params.jump_miami,
    voiceOver = "MISSION_VICECOUNTY_TASK_RUM_BOAT_TEXT.wav",
  }
end,

There are various properties in the table:

  • name is the headline of the task. It is also shown in the right column with the tasklist.
  • paragraphs is a list of text blocks that are displayed as description of the task. Each block contains:
    • text for the actual text that should be displayed. It may contain variables like ${cigars_amount} and links for camera jumps like [link=jump_cigarfactory]cigars[/link] that use the params_mt.lua function. The needed reference has to point on an entity. See the params.lua in a vanilla mission mod for a list of examples.
    • type defines the type of text paragraph. Currently there are three variants:
      • unset results in the text being displayed normally.
      • TASK will display the text in a blue box.
      • HINT will display an information icon in front of the text.
  • subTasks is a list of intermediate goals that are displayed once triggered by the mission scripts.
  • options is a list of button definitions. Each definition has two values. The first one is the text that should be displayed on the button, the second is the name of the guiHandlers callback that should be called.
  • parentId is optional and refers to the identifier of parent task. This is used to setup a task as a subtask of another task.
  • camera is an entity id on which the camera should point once the task is started.
  • voiceOver is a reference to the sound file that should be played once the task is started. It is relative to res/audio/effects/voice_over/<language>/campaign/<campaignfolder>/

Task Handlers

Using the function taskutil:new, a table containing handlers as well as guiHandlers can be provided. If the corresponding event is triggered, the taskutil then looks if there is a callback handler function registered and calls it if it exists.

GUI Handler Example

A simple example for a gui handler is the side quest with the Ponderosa Ranch in mission 1. Its task is scripted in mods/urbangames_campaign_mission_01_1/res/scripts/medal1ponderosa.lua:

taskutil:new("m1", {
  getInfo = function(self)
    return {
      ....,
      options = {
        { _("Accept"), "accept" },
        { _("Decline"), "decline" },
      },
    }
  end,
  guiHandlers = {
    accept = function(self) ... end,
    decline = function(self) ... end,
  },
})

The options table in the getInfo function returns a list of button definitions like

	{ _("Accept"), "accept" },

where the first value is the translated string on the button and the second one is the name of the handler function to be called. Once the player clicks on the Accept button, the taskutil catches this event and forwards it to the task, by looking in the guiHandlers table of the task and selecting an entry with the event name key of the second value in the button definition.

Handler Example

In mission 2, there is an example for a non gui handler. You can find the script for this side quest in mods/urbangames_campaign_mission_02_1/res/scripts/medal3monuments.lua:

taskutil:new("m3c", {
  onStart = function(self)
    taskutil:setMarker("temple1", { entity = params.temple1, type = "question" }, self.name, "temple1")
  end,
  handlers = {
    temple1 = function(self)
      ...
    end,
  },
})

The function taskutil:setMarker(…) sets up a question mark in the world that the user can click on. The parameters are:

  1. an identifier string for the marker
  2. some parameters for the marker
    • entity is a reference for the location of the marker.
    • type sets the model type of marker that should be used. It may either be “question” or “exclamation”.
  3. name of a task that is looked for when the player clicks on the marker
  4. name of the handler function that will be called in this task when the player

To remove the marker, just call the function again but only with the first parameter.

taskutil:setMarker("temple1")

Then the marker will be removed.

SendScriptFn Example

A third example involves both gui and non gui parts. You can find it in mods/urbangames_campaign_mission_01_1/res/scripts/part1.lua

taskutil:new("1a", {
  onGuiUpdate = function(self)
    local camera = game.gui.getCamera()
    taskutil:sendScriptFn(self.name, "cameraChanged", { camera })
  end,
  handlers = {
    cameraChanged = function(self, param)
      print("hello")
    end,
  },
})

Since engine and ui threads are separated, there needs to be another way of communication. See the game script documentation and below for more information on this topic.

The function call taskutil:sendScriptFn(self.name, “cameraChanged”, { camera }) causes the taskutil to look in the handlers table of task self.name (in this example “1a”) and picks the function “cameraChanged” from it. The third argument { camera } expands into the second argument ('param') of the handler function.

The function cameraChanged will not be called immediately, but it will be called asynchronously later by the engine thread.

More formally:

taskutil:sendScriptFn("mytaskname", "myhandlername", { myarg1, myarg2, myarg3 })

will call a handler registered in the form

handlers = {
  myhandlername = function(self, myarg1, myarg2, myarg3) end,
},

by using the following steps:

  1. taskutil looks for a task named “mytaskname”
  2. taskutil gets the handlers table that was provided when calling taskutil:new(“mytaskname”, { … })
  3. taskutil finds function named “myhandlername” in said table.
  4. taskutil calls the function by expanding the argument table.

Engine and GUI Communication

As the mission scripts are game scripts, it is neccessary to deal with the seperated engine and gui thread. The taskutil provides convenient abstractions for easier coding, for example the seperate onStart and onGuiStart functions that the user can provide per task.

For the same reason there are two seperated tables 'handlers' and 'guiHandlers' that can be provided when creating a new task:

taskutil:new("mytaskname", {
  ...
  handlers = {
    foo = function(self) end,
    ...
  },
  guiHandlers = {
    bar = function(self) end,
    ...
  },
})

Functions in handlers (here foo) operate on the engine, so just like in onStart, api.gui is not available. Similarly, functions in guiHandlers (here bar) operate on the ui thread, so api.gui is available, but engine functions are not.

The taskutil has an internal guiHandleEvent callback that is triggered whenever e.g.

  • a user clicks on an option in the task description window (see example above)
  • a user clicks on a marker (see example above)
  • the music track or voiceover ends

In these cases, the taskutil will look for the guiHandler table of a task. On the other hand, for events that arrive in handleEvent, it will look for the handler table.

The option example above uses the guiHandlers for the task to catch the option click event. This makes sense, since option clicking is a gui event.

The marker example actually has the click event as a trigger, thus it is started in the gui thread. Even though the handling function is in the handler list. This works as the taskutil first looks for a handler in guiHandlers and if it doesn't find any uses the sendScriptFn function to cause a second lookup, this time in the handler list. This second lookup is then done by the engine thread at a later point.

This is done merely by convenience, because otherwise if the handler would like to use functions of the engine, but the event is caused by a gui event, then the user would have to manually call taskutil:sendScriptFn inside guiHandlers to then get a (non-gui) handler called later in which the function would be available again.

Tasks can only be started, finished and their progress modified from the engine thread. Therefore, all task-modifying functions in taskutil.lua assert that they are not called from the gui thread. Otherwise, an error will occure and the user is notified immediately about using a function from the gui thread.

However, there is an exception for the start and finish functions of a task. They make use of taskutil:sendScriptFn(self.name,<key>) to refer to the engine thread when called from the gui thread. This allows calling task:start() and task:finish() also from the gui thread as an exception.

Takeaway

  1. Functions in handlers operate on engine thread (api.gui unavailable, engine functions available)
  2. Functions in guiHandlers operate on ui thread (api.gui available, engine functions unavailable)
  3. The taskutil will call functions inside the handlers table of a task for events caught by the script's handleEvent function
  4. The taskutil will call functions inside the guiHandlers table of a task for events caught by the script's guiHandleEvent function
  5. If a handler 'f' can't be found in the guiHandlers table, taskutil will automatically fire an event that causes 'f' to be called within the handlers table

Parameter Files for Missions

The params.lua file in the scripts folder of every mission has three main usecases:

  1. It acts as a central place to balance the mission (adjust all difficulty variables).
  2. Modifying the vehicles using a tree visitor modifier.
  3. Dispatching events that are encoded into the campaign texts.

Tree Visitor Modifier

To individually adjust a batch of models, it is possible to use the tree visitor modifier by using the call modifier.treevisitor(params.restree) inside the mod.lua of the mission mod. This allows to comfortably modify the vehicles but is nothing more than a short hand of the regular way of doing it via addModifier.

A simple example is:

restree = {
  models = {
    model = {
      vehicle = {
        waggon = {
          usa = {
	    ["gondola_1850.mdl"] = modifier.util.cargotypes({ "SILVER_ORE" }),
            ["pullman_1850.mdl"] = 1,
            default = modifier.util.disable,
          },
    ...

This causes the gondola to have a modified cargo configuration, the pullman wagon to be unchanged and all other mdl files in this folder to be unavailable. Note that if the default line was not there, then the line for the pullman wagon to stay unchanged would have no effect and could be omitted. In that case all usa waggons would be as usual except gondola with the modified configuration.

Events from Texts

As the instructions in tasks are an essential part of the mission, it is possible to integrate them in mission control by dispatching events that are encoded into the campaign texts. An example text containing a link would be:

Deliver logs from the [link=jump_forestCarson]ranch[/link]...

If such a link on is pressed, taskutil receives this event here:

guiHandleEvent = function (id, name, param)
  ...
  elseif id == "mainView" and name == "link.click" then
    local task = tasks[taskutil.settings.mainTask]

And forwards the event to the event handlers of the task named according to the taskutil.settings.mainTask variable (which is default to “1”, but could be changed by the user). This is why the task “1” of every mission usually contains the line:

guiHandlers = mainguihandlers,

and above a definition of mainguihandlers which may look like this:

local mainguihandlers = {
  voiceOverEnded = function(self) taskutil:finish(self.name) end,
}
 
setmetatable(mainguihandlers, { __index = function(self, key)
  local n = #"jump_"
    if key:sub(1, n) == "jump_" then
      game.gui.setAutoCamera(params[key])
    end
  end
})

This basically means that the event voiceOverEnded is handled immediately by finishing the task, but if another handler is requested (such as a jump via link to an entity), the metatable is looked up. The metatable checks for the prefix “jump_” and if it finds it, it passes the handler on to the params table. params.lua looks up the name of the entity (here “forestCarson”) in its own table (the table inside params.lua) to find the and returns its coordinates. And finally, the camera position is modified in the metatable of mainguihandlers via game.gui.setAutoCamera(params[key]).

Similarly, the prefix “name_” in texts such as

Connection between ${name_virginiaCity} and ${name_carsonCity}

are forwarded to params_mt.lua to return the name of the entity:

  if key:sub(1, m) == "name_" then
    return game.interface.getName(self[key:sub(m + 1)])
  end

This ensures that the names of entities are not written into every string in the .po files they are used in. This makes it easier to keep the names consistent. It works because of the trailing % params in the getInfo function when adding new tasks:

{ text = _("MISSION_SILVERCITY_TASK_MINE_TEXT") % params },

It looks up variables such as name_virginiaCity in the params table and since it can't find it, it looks into the metatable.

Userstate

Tasks can not influence each other directly other than starting and finishing them. One exception are also the events, since an arbitrary task can be set as a receiver, e.g. when setting up a marker where the click event calls a handler function of another task.

However, the tasks can share data (that is also stored in savegames) the userstate table taskutil.userstate For example with (mods/urbangames_campaign_mission_01_1/res/scripts/part2.lua)

taskutil.userstate.busStationVirginiaForest = param.result[1]

Proposal Checks

If desired, proposal checks can be activated via taskutil:enableProposalCheck(). The taskutil will then pass on the proposal events it receives to its tasks starting from here (in guiHandleEvent):

if state.proposalCheckActive then
  local messages = handleProposal(id, name, param)
  if messages ~= nil then return messages end
end

These can be deactivated at any time via taskutil:disableProposalCheck()

Two modes are supported:

  • taskutil:setProposalCheckWhitelist() that generally blocks all incoming proposals, except the ones returning true
  • taskutil:setProposalCheckBlacklist() that generally allows all incoming proposals, except the ones returning false

Once activated, a task can register itself to the proposals via taskutil:setProposal(“p2a”, self.name, “checkProposal”) which will cause the taskutil to call the tasks checkProposal guiHandler. It is stored via the identifier “p2a” in this case. This means a task can add as many checks as it wants, as long as it uses different identifiers. It can then turn itself off again, by omitting the last two parameters (just like the markers in the example above) taskutil:setProposal(“p2a”).

This is often used to disallow certain actions (by returning an error message in the handler) or to progress a task once a certain proposal is built. In the vanilla campaign this is mainly used in mission 1 and 2 where the player is restricted the most.

The relevant code can be found in taskutil.lua (search for the substring proposal case insensitive) and in res/scripts/mission/proposalutil.lua

Potential Problems

There are some potential problems that may cause crashes while playing campaign missions.

1. The debug modus might cause problems once activated, as player can remove entities the campaign relies on. However, notice that it is possible to activate debug mode, tamper with the savegame and not producing the crash (because it may occur at a later stage of the campaign mission), then reloading the savegame with debug mode off. This could cause such a nil value crash without the debug mode actually being active at that point.

Similarly, sandbox mode / scenario editor can mess the campaign savegames.

2. A frequent pitfall is the assert(game.gui == nil) or assert(game.gui ~= nil) firing up while coding in the campaign. In this case, check above on when to use which functions from the engine thread or the ui thread.


modding/missions.txt · Last modified: 2020/06/20 11:14 by yoshi