Basic Signal Scripting


The Setting Up & Using Signals demonstrates what a signal blueprint looks like and how the settings in it affect the way the signal is placed in the editor, and the Guide also shows how to position a pair of shunt exit signals to cover a junction.

Shunt exit signals tell a driver which way the junction ahead of the signal is set. If the junction is ready for a train to drive over it, the lights are on. If the junction is set against you (in which case driving over it would derail the train), the lights are off.

In Train Simulator, all of this behaviour is controlled by the script that was referenced in the signal's blueprint at \RailNetwork\signals\UK Colour Light\UK ShuntSig Exit Head.lua. This section explains how this works.
Script Functions
A signal script is made up of a series of functions that are called either by the game code or by other functions within the script. A function definition looks something like this:

-- This defines a function called MyFunction

function MyFunction()

-- Does whatever it says in here
-- And then we...


The shunt exit signal is a very simple case, as all it needs to do is keep track of the state of the junction(s) it spans, and switch its lights on or off depending on whether the junction is connected. The following sections describe these functions.


Syntax: Initialise()

Every signal must have an Initialise() function, which is called by the game code when the signal is first activated as the route loads. Here you set up any information the script needs to know about this signal and define any global constants and variables you want to use elsewhere in the script.

The shunt signal’s Initialise() function looks like this:


function Initialise()

   -- Signal Message constants
   SIGNAL_BLOCKED        = 12
   SIGNAL_CLEARED        = 13

   -- 2D Map State constants
   CLEAR   = 0
   BLOCKED = 2

   -- Initialise global variables
   gConnectedLink = -1
   gInitialised   = false

   -- Tells the game to do an update tick once the route finishes loading
   Call( "BeginUpdate" )


First, several constants are defined. These represent numbers that are used in messages sent between signal scripts and the game code. Rather than use the numbers in the scripts, it is better to give them an easy-to-remember name so that you can see at a glance what the script is doing at each stage. By convention, all of Train Simulator's constants have names that are written ENTIRELY_IN_CAPITALS.

Next two global variables are defined - gConnectedLink (which will be used to keep track of whether the junction beyond this signal is connected) and gInitialised (which starts as false and is set to true once the route finishes loading). The global variables generally have names beginning with a lower case “g” to indicate that they’re “global” (i.e. you can check and change their value from any function in the script, not just the one they’re defined in).

Note: A global variable is only global to this particular signal – each signal has its own instance of the script running on it, so it is possible to have lots of signals of the same type in a route, all running the same script, and they will all remember their own individual states.

The final step is to "Call" a function "BeginUpdate". BeginUpdate is not an LUA function defined in the signal script, but part of the main game code. Basically, Call sends a message to the game engine telling it to run a particular code function - in this case, one called BeginUpdate, which tells the game to start running the Update function.


Syntax: Update ( time )

The Update function is a script function, so when we called BeginUpdate we were telling the game code to start running this script function. The game will now keep running the Update script function every frame until we tell it to stop. The "time" parameter is the game’s way of telling the script how much time has passed since the game last did an Update. This is useful for a variety of things, such as making lights flash and controlling animations and sound effects.

This is a very simple Update function:


function Update (time)

   gInitialised = true

   -- Check state of junction
   OnJunctionStateChange( 0, "", 1, 0 )
   Call( "EndUpdate" )


Train Simulator will only start running the Update script function once the route has finished loading, by which time the state of all the junctions is known by the game. As signals can span several junctions, wait until they all know which way they’re set before calling the OnJunctionStateChange script function to check if they’re connected. Otherwise, as the route loads, the signal would keep checking whether it’s connected every time a junction between the signal’s two links initialises.

Having set gInitialised to true (so you know the route has finished loading) and checked the state of the junction, Call the code function "EndUpdate". As the name suggests, this tells the game code to stop triggering the Update script function every frame. So the game will run Update once to check whether the junction is connected, and then stop.

Obviously, any signal that calls BeginUpdate from anywhere in its script requires an Update script function for the code to run, or an error will be generated and the signal won't work properly.


Syntax: OnJunctionStateChange( junction_state, parameter, direction, linkIndex )

This function checks whether the line ahead is connected. Here’s how:

-- Called when a junction is changed. Tests if the links are connected.

function OnJunctionStateChange( junction_state, parameter, direction, linkIndex )

   -- Check to see if the track is still connected
   gConnectedLink = Call( "GetConnectedLink", "", 1, 0 )

   if ( gConnectedLink == -1) then

        Call ( "Set2DMapSignalState", BLOCKED )
        Call( "ActivateNode", "mod_trk_shunt_lightsOn", 0 )

   elseif (gConnectedLink == 1) then

        Call ( "Set2DMapSignalState", CLEAR )
        Call( "ActivateNode", "mod_trk_shunt_lightsOn", 1 )



The words in brackets after "OnJunctionStateChange" are the names of parameters that the function expects you to provide when you trigger it. These can be disregarded for the moment.

This script function is also very simple for the shunt exit signal. First, the script Calls a code function "GetConnectedLink" and assigns the value that it returns to the global variable gConnectedLink, so that the script knows which link (if any) is connected.

Again, this function requires a number of parameters – these are the values separated by commas after it says “GetConnectedLink”. The first parameter isn’t used, so leave it blank (""). The second parameter (1) is the direction in which to check the line. This will almost always be 1 so that you will be looking forward or up the line beyond the signal. The last parameter (0) is the link to start searching from.

This Call of GetConnectedLink will start at the signal’s link 0 and look up the line until it reaches a broken junction, the end of the line, or another link belonging to this signal. Then it lets you know which link (if any) is connected. As the shunt exit signal only has two links (0 and 1), the value it sends back to the script is either 1 (if all the junctions between the two links are set ready for you to drive past the signal) or -1 (if any of the junctions between the two links is set against the train, blocking its path).

Once the script knows whether the junction is connected, it sets the signal to the appropriate state by Calling two more code functions:

Set2DMapSignalState sets the colour that the signal appears on the 2D map. BLOCKED will make it appear as a red circle, CLEAR will make it turn green.

ActivateNode is used to switch a signal’s lights on and off. The first parameter is the name of the node to activate/deactivate (in this case, "mod_trk_shunt_lightsOn"), which is set in the 3D model file for the signal. The second parameter is 0 (to switch the light off) or 1 (to switch it on).


Syntax: OnSignalMessage( message, parameter, direction, linkIndex )

This is another function that every signal script must contain. Signal messages are sent up and down the track by trains, junctions and other signals to give a signal information about the state of the route around it. Whenever one of those messages hits a signal’s link, the game engine triggers the OnSignalMessage function for that signal.

OnSignalMessage’s parameters let the game code provide the script with information about the message - what kind of message it is, whether there’s any other information attached to it (parameter), which direction the message was travelling in when it hit the link (1 if it came from in front, -1 if it came from behind), and the linkIndex of the link it was received by (in the case of a shunt exit signal, that will be either 0 or 1). The arrow already placed on the links of the signals shows you which direction the link is facing in. If a message arrives at the side with the arrow on it, it’s coming from in front of the link.

There are only two messages a shunt exit signal cares about – RESET_SIGNAL_STATE and JUNCTION_STATE_CHANGE. All other messages should be forwarded on in the direction in which they were travelling, in case another signal further down the line is interested in them.

The script to handle that looks like this:

-- Called when a message is received by one of this signal’s links

function OnSignalMessage( message, parameter, direction, linkIndex )

     -- This message is to reset the signals after a scenario / route is reset
     if (message == RESET_SIGNAL_STATE) then

            -- Re-initialise the signal

     -- This message lets us know that a junction has just been switched
     elseif (message == JUNCTION_STATE_CHANGE) then 

          -- Only act on the message if it arrived at link 0
          -- and the parameter is "0" (junction has finished switching)

               if linkIndex == 0 and parameter == "0" then

                    -- Trigger the OnJunctionStateChange script function
                    OnJunctionStateChange( 0, "", 1, 0 )

                    -- Pass on the message in case more signals protect that junction
                    Call( "SendSignalMessage", message, parameter, -direction, 1, 0 )


     -- All other messages should just be forwarded


          -- Forward it on in that direction from link 0
          Call( "SendSignalMessage", message, parameter, -direction, 1, linkIndex )



This is fairly simple for a shunt signal.

If the message is RESET_SIGNAL_STATE, the game is letting you know that you need to reset the signal, as the player has just reloaded a saved position or returned to the game from the scenario editor. This can be done by Calling the Initialise script function again. Note that this message is sent directly to every signal in the route by the game engine, so there is no need to forward it on.

If the message is JUNCTION_STATE_CHANGE, check that the message arrived on link 0. As this type of message is sent by the junction itself when it switches. If it came from a junction that’s between two links it must arrive at link 0 from in front. If the message was received by link 1 instead, it must have come from another junction further up the line that is irrelevant. Ignore the message unless it came with its parameter value set to “0” (indicating that the junction has now finished switching).

When sure the JUNCTION_STATE_CHANGE message is from a junction you are interested in, trigger the OnJunctionStateChange script function. Now the parameters that are passed to that function make more sense. "Junction_state" should always be 0, as it’s confirming that the junction has finishing switching - if it was still in motion, neither track would be connected to the junction and there wouldn’t be any point in checking its state. "Parameter" is no longer used and should always be left blank. “Direction” is also obsolete and should be left as 1. Finally, "linkIndex" is the link that received the junction state change message (and therefore the link that is needed to look ahead to see if the junction that just switched is connected). For any normal signal, this will always be 0.

If the message is of any other type, the shunt signal isn’t interested in it, so it just forwards it on from the link it arrived at. This is done by Calling the SendSignalMessage code function. It takes all the same parameters as OnSignalMessage, plus an extra one. The first is the type of message, the second is the parameter containing any additional information about the message, the third is the direction the message should be sent in (note that this value is reversed – if the message arrived from in front it should be sent backwards, which is direction -1, and vice versa), and the last parameter is the linkIndex from which to send the message.

The fourth parameter (the extra 1 between direction and linkIndex) controls which signals can receive this message. This should almost always be 1, which means that only signal links which are pointing in the same direction as the one the message is being sent from will receive the message. If for some reason you wanted the message to be picked up only by signal links facing in the opposite direction, use -1 instead.


Syntax: OnConsistPass( prevFrontDist, prevBackDist, frontDist, backDist, linkIndex )

The last function that every script must have is OnConsistPass. "Consist" is just another term for a train so, as the name suggests, this script function is triggered by the game code whenever a train passes a link belonging to the signal.

The shunt signal doesn’t care about trains, only about whether or not the line ahead of it is connected. So although an OnConsistPass function
is necessary in the script, in this case, it doesn’t actually do anything:

-- Shunt signals do not care about consist passes

function OnConsistPass( prevFrontDist, prevBackDist, frontDist, backDist, linkIndex )

     -- do nothing on consist pass


The shunt exit signal’s script consists of a single .lua file with all of those function definitions in it. If you look at the actual "UK ShuntSig Exit Head.lua" script file used by the real shunt signal (it's available in RailNetwork\Signals\UK Colour Light) you’ll see that it’s slightly more complex than the script shown here. However, the script functions above are all that's needed to make a fully functional shunt signal.