Signal Scripting Essentials

Introduction

The shunt exit signal as described in the Basic Signal Scripting section is the simplest signal script used in Train Simulator. This section introduces some additional scripting that's required to get other types of signals working.

Track Occupancy

All a shunt signal cares about is which way the junction they span is set. This is fine when you’re just shunting wagons around a yard at low speeds, but when you’re driving up a mainline you also need to know whether there’s another train ahead of you. This is handled in Train Simulator by "occupation tables".

Each "home" signal has its own occupation table, which is the script’s way of remembering how many trains are in each part of the track covered by that signal (known as a "signal block"). This data is stored by the signal scripts in a table called "gOccupationTable". Each section of the track covered by this signal has its own entry in gOccupationTable.


For example, in the situation shown above, the number of trains on the track marked in green between link 0 and the forward links (1, 2 and 3) is stored in gOccupationTable0; gOccupationTable1 stores the number of trains on the track marked in red beyond link 1; gOccupationTable2 covers the yellow track beyond link 2; and gOccupationTable3 covers the blue track beyond link 3.

Normally, on a real railway, there would be another home signal further up each of those three lines. In that case, the occupation table entry for each of this signal's links would cover the track between that link and the link 0 of the next signal up the line. Once a train passes the next signal up the line, it's no longer our responsibility.

As trains drive past links belonging to a signal, the signal's occupation table entries go up and down. For example, when a train starts driving forwards past a signal's link 0, its gOccupationTable0 is set to 1. If the train is going straight ahead, gOccupationTable1 is set to 1 when the train starts to pass link 1. When the train finishes passing link 1 there is no longer anybody in the first bit of track, so gOccupationTable0 is set back to 0. Finally, when the train finishes passing link 0 of the next signal up the line beyond link 1, that signal sends back a message to say it’s gone, and gOccupationTable1 is set to 0 again.

The reality is a little more complex, as shown in the script examples below, but this is basically how home signals know whether or not they have a train blocking the line ahead of them.

OnConsistPass

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

Every signal script must have an OnConsistPass function, which is triggered by the game code when a train passes one of the signal’s links. In the case of the shunt signal, this function didn’t actually do anything. But for a home signal, it’s our main way of keeping track of how many trains are in our block.

The parameters that the code sends to a script when it triggers the OnConsistPass function tell us exactly what’s going on. prevFrontDist and prevBackDist tell us how far the front and back of the train were from the signal link last time it was checked. frontDist and backDist are the current positions. In all cases, a positive distance means it’s behind the link and a negative distance means it’s beyond the link. Finally, linkIndex is the number of the link that’s being crossed.

From this information, we can tell whether the train has just started or finished crossing the link, and which direction it’s travelling in. This can be broken down as follows:

If the train just started crossing a link...
    If it’s going forwards...
        If it’s passing link 0, link 0 is now blocked and the signal needs to turn red
        If it’s passing any other link, that link is now blocked but the signal is already red
    If it’s going backwards...
        If it’s passing link 0, send a message to let the signal behind us know it’s coming
        If it’s passing the connected link, it’s now blocking link 0

    If the train just finished crossing a link...
        If it’s going forwards...
            If it’s passing link 0, send a message to let the signal behind us know it’s cleared
            If it’s passing the connected link, link 0 was blocked before but is now clear
        If it’s going backwards...
            If it’s passing link 0, link 0 is now cleared and the signal may need to turn green
            If it’s passing any other link, that link is now clear

Here’s what this looks like in script, with comments to explain each step:

-- ON CONSIST PASS
-- Called when a train passes one of the signal’s links

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

     -- Declare local variables used to remember what the train’s doing
     local crossingStart = 0
     local crossingEnd   = 0

     -- If the front of the train is before the link and the back is beyond it,
     -- or vice versa, the train is in the process of crossing the link
     if ( frontDist > 0 and backDist < 0 ) or ( frontDist < 0 and backDist > 0 ) then

            -- If the front and back of the train were previously both beyond or
            -- both before the link, the train has just started crossing the link

            if ( prevFrontDist < 0 and prevBackDist < 0 ) or ( prevFrontDist > 0 and prevBackDist > 0 ) then

                 -- Set crossingStart to 1 so you know you’ve just started crossing
                 crossingStart = 1
            end

     -- Otherwise the front and back of the train are both on the same side of
     -- the link now, in which case it’s already passed the link

     else

          -- If the train previously had its front and back on opposite sides of
          -- the link, it’s just finished crossing the link

          if ( prevFrontDist < 0 and prevBackDist > 0 ) or ( prevFrontDist > 0 and prevBackDist < 0 ) then

               -- Set crossingEnd to 1 so you know you’ve just finished crossing
               crossingEnd = 1

          end

     end

     -- If the train just started crossing the link...
     if (crossingStart == 1) then

          -- If the train was behind the link before, it’s crossing it forwards
          if (prevFrontDist > 0 and prevBackDist > 0) then

               -- If the train just started crossing link 0 forwards...
               if (linkIndex == 0) then

                    -- Let the signal know its block is now occupied
                    Occupied( )

                    -- And increment the number of trains blocking link 0
                    gOccupationTable[0] = gOccupationTable[0] + 1

               -- If the train just started crossing another link forwards...
               elseif (linkIndex > 0) then

                    -- Increment the number of trains on the track beyond that link
                    gOccupationTable[linkIndex] = gOccupationTable[linkIndex] + 1

               end

         -- If the train was beyond the link before, it’s crossing it backwards
         elseif (prevFrontDist < 0 and prevBackDist < 0) then

              -- If the train just started crossing link 0 backwards...
              if (linkIndex == 0) then

                   -- Send a message back down the line to let the signal behind know
                   -- that a train has just entered its block
                   Call( "SendSignalMessage", OCCUPATION_INCREMENT, "", -1, 1, 0 )

              -- If the train just started crossing another link backwards...
              elseif (linkIndex > 0) then

                    -- And that link is connected to link 0...
                    if (gConnectedLink == linkIndex) then

                         -- Increment the number of trains blocking link 0
                         gOccupationTable[0] = gOccupationTable[0] + 1

                    -- If the link the train is crossing isn’t connected to link 0...
                    else

                        -- The train isn’t going to pass link 0, so do nothing

                    end

              end

        end

     -- If the train just finished crossing the link...
     elseif (crossingEnd == 1) then

          -- If the train is before the link now, it just crossed it backwards
          if (frontDist > 0 and backDist > 0) then

               -- If the train just finished crossing link 0 backwards...
               if (linkIndex == 0) then

                    -- Decrement the number of trains blocking link 0
                    gOccupationTable[0] = gOccupationTable[0] - 1

                    -- The signal’s block might not be occupied now
                    NotOccupied( 0 )

               -- If the train just finished crossing another link backwards...
               elseif (linkIndex > 0) then

                    -- Decrement the number of trains on the track beyond that link
                    gOccupationTable[linkIndex] = gOccupationTable[linkIndex] - 1
               end

          -- If the train is beyond the link now, it just crossed it forwards
          elseif (frontDist < 0 and backDist < 0) then

               -- If the train just finished crossing link 0 forwards...
               if (linkIndex == 0) then

                    -- Send a message back down the line to let the signal behind know
                    -- that a train has just left its block
                    Call( "SendSignalMessage", OCCUPATION_DECREMENT, "", -1, 1, 0 )

               -- If the train just finished crossing another link forwards...
               elseif (linkIndex > 0) then

                    -- If this link is connected to link 0...
                    if (gConnectedLink == linkIndex) then

                         -- Decrement the number of trains blocking link 0
                         gOccupationTable[0] = gOccupationTable[0] – 1

                    -- If this link isn’t connected to link 0...
                    else

                       -- The train didn’t pass link 0 to get here, so do nothing

                    end

               end

          end

     end

end

In a real signal script it’s slightly more complex. For example, Train Simulator often doesn’t put links on tracks that are only used by trains going in the opposite direction to the one the signal is facing in. If a train comes from that bit of track, the signal will be red anyway because none of its links are connected to link 0, but the signal won’t know that a train is approaching it until it passes the signal’s link 0. So the real signal scripts include a check to make sure that gOccupationTable0 is greater than 0 before trying to decrement its value. Also, most signals can also show "warning" aspects which indicate that although their block is clear, the next signal up the line isn’t.

Occupation Increment/Decrement Messages

OCCUPATION_INCREMENT and OCCUPATION_DECREMENT are signal messages sent back by a signal when a train passes its link 0, to let the signal behind it know that the train has just entered/left its signal block. The message will always arrive on the link that the train it’s warning about is approaching/leaving.

As with the junction state change message, this message triggers the OnSignalMessage script function when the message reaches a signal’s link.

Here’s the extra scripting that needs adding to that function to handle the new messages:

-- This message lets us know that a train has just passed the next signal up the
-- line and is no longer in our signal block
elseif (message == OCCUPATION_DECREMENT) then

     -- Decrement the occupation table for the link the message arrived on
     gOccupationTable[linkIndex] = gOccupationTable[linkIndex] - 1


      -- The signal’s block might not be occupied now
      NotOccupied( linkIndex )

-- This message lets you know that a train has just passed the next signal up the
-- line backwards and is now entering your signal block
elseif (message == OCCUPATION_INCREMENT) then

     -- Increment the occupation table for the link the message arrived on
     gOccupationTable[linkIndex] = gOccupationTable[linkIndex] + 1

     -- If this is the connected link, the signal’s block is now occupied
     if (gConnectedLink == linkIndex) then

          Occupied()

     end

Signal State

Once you know if there’s a train blocking the track ahead and which way any junction(s) between you and the next signal up the line are set, you can work out the state of a simple two aspect (stop or go) home signal, as shown in the following Signal State sections.

Occupied/NotOccupied

The two functions called Occupied and NotOccupied are Called from OnConsistPass and OnSignalMessage whenever a train enters or leaves a signal’s block. Here’s what those functions look like for a simple signal:

-- NOTOCCUPIED
-- Tells the signal to display a clear track ahead

function NotOccupied( linkIndex )

     -- If link 0 has been unoccupied, find the connected link
     if (linkIndex == 0 and gConnectedLink > 0) then

          linkIndex = gConnectedLink

     end

     -- If link 0 or the connected link has been unoccupied...
     if (gConnectedLink == linkIndex) then

          -- And the track ahead is clear...
          if (gOccupationTable[0] == 0) and (gOccupationTable[linkIndex] == 0) then

               -- And wasn’t already set as clear...
               if gSignalState ~= SIGNAL_CLEARED then

                    -- Set the signal state to clear
                    SetState( SIGNAL_CLEARED )

               end

          end

     end

end

-- OCCUPIED
-- Tells the signal to display a blocked track ahead

function Occupied()

     -- If not already blocked...
     if gSignalState ~= SIGNAL_BLOCKED then

          -- Set the signal state to blocked
          SetState( SIGNAL_CLEARED )

     end

end

These functions are just a way of keeping script tidy, and making it easy to maintain and update. Instead of repeating the scripting that checks for a signal being blocked or cleared several times in different functions, it is all put in a pair of simple sub-functions which can be Called from anywhere else in the script.

The global variable called gSignalState keeps track of the current state of the signal. In other examples, this is either clear or blocked. This means no time is wasted trying to change a signal to a state it’s already in.

SetState

SetState is another function which keeps commonly used functionality wrapped up in a simple subfunction which can be Called from anywhere in the script. Here’s what that function looks like for a simple 2 aspect signal head -

-- SET STATE
-- Sets the current state of the signal

function SetState( newState )

      -- If the signal is cleared now...
      if (newState == SIGNAL_CLEARED) then

          -- Set this signal to green on the 2D map
          Call ("Set2DMapSignalState", CLEAR)

          -- Turn on the green light and turn off the red light
          Call ( "ActivateNode", "mod_hd2_green", 1 )
          Call ( "ActivateNode", "mod_hd2_red", 0 )

      -- If the signal is blocked now...
      elseif (newState == SIGNAL_BLOCKED) then

          -- Set this signal to red on the 2D map
          Call ("Set2DMapSignalState", BLOCKED)

          -- Turn on the red light and turn off the green light
          Call ( "ActivateNode", "mod_hd2_green", 0 )
          Call ( "ActivateNode", "mod_hd2_red", 1 )

      end

      -- Remember the signal state
      gSignalState = newState

end

This should be familiar from the shunt signal script. In that case a light was being switched on or off directly from the OnJunctionStateChange function, based on whether or not the track ahead was connected. Here, there are two lights (red and green) which are toggled on and off based on whether the track ahead is clear or blocked. It’s the same basic functionality but split off into its own sub-function for convenience.

After switching the lights and 2D map state of the signal, set gSignalState to its new value. That line could have been put in the individual functions that Call SetState, but as it is important to remember the state of the signal, it makes more sense to include that line here.

Note: Different types of signal have different names for their light "nodes". These are set in the 3D model itself, so if you are unsure about what a signal’s nodes are called you should ask the artist (if it’s a new custom-made 3D model) or check an existing script used by a signal that has the same signal head as the one you’re using to find out what name to use.

Another important thing to bear in mind is that not every signal is a free floating head. Some signal blueprints have their head ready mounted on a post. In this case, the signal head is a "child" of the post it’s mounted on, and the ActivateNode function will need to know the name of the "child" object you’re trying to activate a light on.

To find out what the child’s name is, open the signal blueprint in the Blueprint Editor and scroll to the bottom of the information displayed there. You should see an entry marked "Children". Click on the + next to it to expand the view of that section, and then click on the + next to the child entry stored inside it to see the child’s information.

At the top of the child’s settings is an entry called "child name". This is the name that you need to refer to in your script when you switch one of this head’s lights on or off. For example, the post-mounted version of the 2 aspect signal referenced in Basic Signal Scripting has its signal head as a child called "2 Aspect Signal Head". To change lights on the signal, use the following lines :

-- Turn on the red light and turn off the green light
Call ( "2 Aspect Signal Head:ActivateNode", "mod_hd2_green", 0 )
Call ( "2 Aspect Signal Head:ActivateNode", "mod_hd2_red", 1 )

All that's been done is to add the child’s name and a colon before ActivateNode. This tells the game code to run the ActivateNode function on the part of the signal called "2 Aspect Signal Head".

GetSignalState

Syntax: GetSignalState()

Most home signals require the GetSignalState signal function. All of the home signals on the Oxford - Paddington and Newcastle - York routes have this function, and if there is a warning system such as AWS or TPWS on the route, the signals will probably need this function.

All it does is return the current state of the signal when it's triggered by the code. Since the signal's state as a global variable gSignalState is kept track of anyway, all you need to do is turn that information into a form that the code will understand. Here's how:

-- GET SIGNAL STATE
-- Gets the current state of the signal - blocked, warning or clear.

function GetSignalState()

     -- Initialise signal state to -1 (to signify an error)
     local signalState = -1

     -- Set signalState based on the current state of the signal
     if (gSignalState == SIGNAL_CLEARED) then

          signalState = CLEAR

     elseif (gSignalState == SIGNAL_WARNING) or  (gSignalState == SIGNAL_WARNING2) then

          signalState = WARNING

     elseif (gSignalState == SIGNAL_BLOCKED) then

          signalState = BLOCKED

     end

     return signalState

end

This sends the code the same constants (CLEAR, WARNING and BLOCKED) that the Set2DMapSignalState function uses. SIGNAL_WARNING and SIGNAL_WARNING2 are covered in the Route State section.

Route State

It is possible to determine which of a signal’s links is connected to its link 0 (using OnJunctionStateChange) and whether there are any trains blocking the track ahead (using the occupancy table generated by OnConsistPass and the OCCUPATION_INCREMENT and DECREMENT signal messages). Based on this, the lights on a signal can be switched to red (stop) or green (go).

This is fine for a simple two aspect signal, but most signals in Train Simulator can show one or more "warning" aspects as well, indicating the state of the next signal up the line. This requires a global variable to remember the state of the signal(s) ahead, and a set of signal messages to pass on that information. Functions already in place will also need to be adjusted to take account of this new data.

gLinkState

Store this information in a table called gLinkState. Each signal's links will have an entry in the table storing the state of the next signal up the line in front of it. That way, if a junction covered by the signal switches, you will always know the state of the next signal beyond the link that's now connected, and can show the appropriate aspect.

Like any variable, gLinkState needs to be initialised. In this case create one entry for each link and default all their values to SIGNAL_CLEARED.

-- Find out how many links this signal has
gLinkCount = Call( "GetLinkCount" )

-- Create an empty table called gLinkState
gLinkState = {}

-- Cycle through all this signal’s links
for link = 0, gLinkCount - 1 do
     -- Set the default state for that link to CLEARED
     gLinkState[link] = SIGNAL_CLEARED
end

Notice that another new code function has been called here – GetLinkCount. As the name suggests, this counts how many links the signal has. As the signal’s links are numbered starting at 0, the number of the signal’s last link is one less than the link count – e.g. a signal with 3 links has links 0, 1 and 2. This information can be useful elsewhere, so gLinkCount remembers that number as a global variable.

Once you know how many links the signal has, create an empty table called gLinkState and cycle through all the signals links, creating an entry for each of them in that table and setting its default value to SIGNAL_CLEARED.

Signal State Messages

As each signal is controlled by its own independent script, it is necessary to send messages between them to keep track of the state of any other signals ahead. Whenever a signal’s state changes from blocked to clear or vice versa, it sends back a message to let any signals behind it know its new state.

Here’s the extra scripting needed to add to the OnSignalMessage function to handle these messages:

-- If the signal ahead is showing clear or warning, you are clear
elseif ( message == SIGNAL_CLEARED or message == SIGNAL_WARNING ) then
     NotOccupied( linkIndex )

-- If the signal ahead is showing blocked, you should be at warning
elseif ( message == SIGNAL_BLOCKED ) then
     Warning( linkIndex )

Here a script function called Warning is being used to set the signal to its warning state when the next signal up the line is blocked:

-- WARNING
-- Tells the signal to display that there is a warning ahead

function Warning( linkIndex )

     -- If the message arrived on the connected link...
     if (gConnectedLink == linkIndex) then

          -- And there aren’t any trains between you and the next signal...
          if (gOccupationTable[0] == 0) and (gOccupationTable[linkIndex] == 0) then

               -- If the signal wasn’t already set to WARNING...
               if gSignalState ~= SIGNAL_WARNING then

                    -- Set the signal state to WARNING
                    SetState( SIGNAL_WARNING )

                    -- Send back a message to let signals behind know you are at warning
                    Call( "SendSignalMessage", SIGNAL_WARNING, "", -1, 1, 0 )

               end

          end

     end

     -- Remember that this link is at WARNING
     gLinkState[linkIndex] = SIGNAL_WARNING
end

This is just like the NotOccupied function in the Track Occupancy section, except that it uses SIGNAL_WARNING instead of SIGNAL_CLEARED, and there are some additional bits of functionality.

Now a message is being sent back to let the signal behind know what state you are in if your state has just changed. The same thing should be done in the Occupied and NotOccupied functions, but using SIGNAL_BLOCKED/SIGNAL_CLEARED as appropriate.

Also, at the end of the function is a line to store the state of the line ahead of us on this link. In this case it remembers that if this link is connected you should be at warning, because the next signal is blocked. The NotOccupied function should behave the same way, setting gLinkStatelinkIndex to SIGNAL_CLEARED.

However, the Occupied function should NOT set the link state to SIGNAL_BLOCKED – the Occupied function is only called if the track directly ahead of the signal is blocked by a train or a broken junction, and gLinkState is only interested in what the next signal up the line is showing, not what this signal is showing.

OnJunctionStateChange

The whole point of remembering the state of the next signal in front of each of your links is so that when a junction between you and that next signal switches, you know what should be showing. This requires some changes to the OnJunctionStateChange function. Also, you need to take account of the fact that the signal might have more (or less) than two links, in which case you need to know not only whether or not the line ahead is connected, but which of the possible lines the signal's link 0 is connected to.

Here’s an example of a generic junction state change function that handles this:

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

function OnJunctionStateChange(junction_state, parameter, direction, linkIndex)

     -- Only need to check which link is connected if there is more than one link!
     if gLinkCount > 1

          -- Find the link that is now connected to the signal
          local newConnectedLink = Call( "GetConnectedLink", "" , 1, 0 )

               -- Only continue if the link connected to has changed
               if newConnectedLink ~= gConnectedLink then

                    -- Remember which link you are now connected to
                    gConnectedLink = newConnectedLink

                    -- If you are connected to a valid link...
                    if gConnectedLink > 0 then

                         -- If there aren’t any trains on the connected path
                         if (gOccupationTable[0] == 0) and
                      (gOccupationTable[gConnectedLink] == 0) then

                              -- If the connected link is clear...
                              if gLinkState[gConnectedLink] == SIGNAL_CLEARED then

                                   -- Set the signal as NotOccupied
                                   NotOccupied(gConnectedLink)

                              -- If the connected link is at warning...
                              elseif gLinkState[gConnectedLink] ==SIGNAL_WARNING then

                                   -- Set the signal to Warning
                                   Warning(gConnectedLink)
                              end

                         -- If a train is blocking the connected path
                         else

                              -- Set the signal as Occupied
                              Occupied()

                    -- If the track ahead is blocked by a junction set against you...
                    elseif gConnectedLink == -1 then

                         -- Set the signal as Occupied
                         Occupied()
                    end
               end
          end
     end
end

This is fairly simple. It just checks which link is currently connected, and then (assuming a valid link is connected and there isn’t a train blocking the path to the next signal) checks the state of that link before deciding how to set the signal.

Signal Message Directionality

At this point, it's worth mentioning the directionality of signal messages. Unless otherwise specified, messages are only picked up by signals whose links are facing in the same direction as the link that the message was sent from. But it's also important to take account of the direction we send the message in from that link.

Most messages are sent backwards (direction -1) to let the signal behind us know what state we're in. When you send a message backwards, it should normally be sent from link 0. This ensures that it doesn't get intercepted by any other links belonging to the signal that sent it, or other signals covering the same junction(s).

Messages that are sent forwards (direction 1) are occasionally used to tell the next signal up the line that there's a train approaching it. When you send a message forwards, it should normally be sent from the connected link. Again, this ensures that it doesn't get intercepted by any other links belonging to the same signal.

Sometimes it makes sense to break these rules, but normally they should be observed.

PASS_ Messages

The OnSignalMessage script functions in the sections above work fine for a simple signal setup, but in the real world things are rarely simple. For example, consider a crossover between two parallel tracks.
Both tracks have signals on them just before the crossover, and both of those signals have a link just beyond the converging junction at the top right of the diagram. But signal links consume the messages they receive, so if a message comes down the track from beyond that junction, it will only be received by the first signal link it reaches (the blue one, belonging to the signal on the lower track). For the other signal to receive any messages from further up that line, the blue link will need to forward them on to the red link.

The way to do this is using PASS_ messages. For every signal message in the game, there is an equivalent PASS_ message. For example, PASS_SIGNAL_BLOCKED is the PASS_ version of SIGNAL_BLOCKED. When a signal receives SIGNAL_BLOCKED on any link other than link 0, it forwards on a PASS_SIGNAL_BLOCKED in the same direction from that link.

If PASS_SIGNAL_BLOCKED is received by another non-zero link (in this example, the red link just beyond the junction), that link must belong to another signal covering the same junction, and so it will react to the message and then forward it on too.

This continues until the PASS_ message reaches a link 0. When that happens it must have gone past the junction, and can now be safely ignored and consumed by that link.

Note: this is one situation where the normal message directionality rules mentioned in Signal Message Directionality don't apply - in this case, it is necessary for the message to be intercepted by all of the other signal links covering this junction, rather than skipping over the junction as normal, so the messages are sent on from the link they arrived at.

The scripting that handles passing on messages looks like this:

-- ON SIGNAL MESSAGE
-- Called when a message is received by one of this signal’s links
function OnSignalMessage( message, parameter, direction, linkIndex )

     -- Check for signal receiving a message it might need to forward on
     if (linkIndex > 0) then

          -- If you have received a PASS_ message
          if message > PASS_OFFSET then

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

          -- Don't pass on a reset signal or junction state change message!
          elseif message ~= RESET_SIGNAL_STATE and message ~= JUNCTION_STATE_CHANGE then

               -- Forward the message as a PASS_ message by adding PASS_OFFSET to it
               message = message + PASS_OFFSET
               Call( "SendSignalMessage", message, parameter, -direction, 1, linkIndex )
          end
     end

     -- always check for a valid link index
     if (linkIndex >= 0) then

          -- If the message is a PASS_ message...
          if message > PASS_OFFSET then

               -- Only pay attention to it if you are not the base link of the signal
               if linkIndex > 0 then

                    -- Convert it back into a normal message and process it
                    message = message – PASS_OFFSET
                    ReactToSignalMessage( message, parameter, direction, linkIndex )
               end

          -- Otherwise, it's a normal signal so just process it as normal
          else

               ReactToSignalMessage( message, parameter, direction, linkIndex )
          end
     end
end

Here, there is a constant called PASS_OFFSET. Every PASS_ message is defined as the original signal message constant plus PASS_OFFSET. That means it is possible to turn any signal message into a PASS_ message by adding PASS_OFFSET, and then convert it back into a normal signal message by subtracting PASS_OFFSET. Any message with a value greater than PASS_OFFSET must be a PASS_ message.

To keep things simple, this function has been removed from all the scripting that reacts to the various signal messages, and a subfunction called ReactToSignalMessage handles this. This is then called from OnSignalMessage when necessary.

As well as keeping the script tidy, this also means that the OnSignalMessage function is now generic enough that it can be used by pretty much any signal without needing any modification – all the scripting which handles the specific messages that various signals need to react to is held within the ReactToSignalMessage subfunction, and all that the OnSignalMessage function does is decide when to forward on a message and when to react to it.

Including Common Script Functions

Some of the LUA script functions that control a signal can be written in such a way that they’re generic enough to work on a wide range of different signals. Rather than just copying and pasting these functions into every script that needs them, you can put them all into separate .lua files (which we refer to as Common Script Files) and "include" that file in your individual signal scripts.

For example, the script for a UK 3 aspect signal post has the following lines at the top of it:

--include=CommonScripts\Common UK Colour Light Script.lua
--include=..\CommonScripts\Common Signal Script.lua

This just means that when the Blueprint Editor comes to build that script ready for use in the game, it includes the entire contents of those two Common .lua files at the bottom of the signal’s own individual script. This is incredibly useful, because if you have several variants of the same basic signal, you can create individual scripts for each of them that only have a handful of functions in them, and then put any functions that are the same for all of those signals into a Common file and include that in each of the individual scripts.

Then, if you need to make a change to one of the core functions that's in the Common file, rather than having to edit every signal-specific script you can just edit that one Common script and then re-export all the signal-specific scripts from the Blueprint Editor to update them ready for use in the game.

It also means that if you’re making a new signal which is similar to one that already exists in the game, you can probably use many of the generic functions already scripted by including the appropriate Common files into your own script.

Most of Train Simulator's signal scripts work on three or four levels:

At the top is a signal-specific script which is tailored to a particular signal or range of signals (for example, standard UK 3 aspect signal posts with one or more links). This contains all of the core functions that every signal script requires, plus a few subfunctions that include information that’s specific to that type of signal, such as ReactToSignalMessage and a function to switch the appropriate lights on and off depending on the state of the signal.

However, many of the functions in this script will look something like this:

-- ON CONSIST PASS
function OnConsistPass (prevFrontDist, prevBackDist, frontDist, backDist, linkIndex)

     -- Use the Default function for this
     DefaultOnConsistPass (prevFrontDist, prevBackDist, frontDist, backDist, linkIndex)
end

As this particular signal doesn’t need to do anything unique when a train passes, it can use a generic function called DefaultOnConsistPass to handle this event. These Default functions can be used by a range of similar signals – in this case, UK Colour Lights – and are kept in a separate script file called "Common UK Colour Light Script.lua", which is kept in a subfolder of the "UK Colour Lights" signal folder called "CommonScripts".

Some of the functions in the script call a further level of Base functions. For example, DefaultOnConsistPass just calls BaseOnConsistPass. These Base functions are generic enough that they can often be used by signals from different routes and countries, and are kept in a script called "Common Signal Script.lua" in a "CommonScripts" folder in RailNetwork\Signals.

In some cases, there is an extra layer between the signal-specific and Default functions. For example, modern British signals often have route indicators known as "feathers" that light up to show which route is connected. There are literally dozens of variations of these signals in the game, with different numbers of links, feathers and lights. Each one has its own unique script, but if you open up one of those scripts all you’ll see is something like this:

--------------------------------------------------------------------------------------
-- UK 4 Aspect Signal Post
-- KUJU / Rail Simulator
--------------------------------------------------------------------------------------
--include=CommonScripts\Common UK 4 Aspect Feather Script.lua
--include=CommonScripts\Common UK Colour Light Script.lua
--include=..\CommonScripts\Common Signal Script.lua
--------------------------------------------------------------------------------------
-- INITIALISE
--
function Initialise ()

     -- for POST signals, the lights are manipulated via a child node,
     -- and therefore it is necessary to specify the child’s name.
     gLightNodeName = "4 Aspect Signal Head:"

     DefaultInitialise()
end

--------------------------------------------------------------------------------------
-- ACTIVATE LINK
--
function ActivateLink( connectedLink )

     if (connectedLink == 1) then
          Call( "Route Indicator:ActivateNode", "feather_1", 0 )
          Call( "Route Indicator:ActivateNode", "feather_2", 0 )
     elseif (connectedLink == 2) then
          Call( "Route Indicator:ActivateNode", "feather_1", 1 )
          Call( "Route Indicator:ActivateNode", "feather_2", 0 )
     elseif (connectedLink == 3) then
          Call( "Route Indicator:ActivateNode", "feather_1", 0 )
          Call( "Route Indicator:ActivateNode", "feather_2", 1 )
     end
end

The script only contains an Initialise function and a subfunction called ActivateLink which lights up the appropriate "feather" depending on which link is connected. The rest of the functions required by this signal are kept in the "Common UK 4 Aspect Feather Script.lua" script, which (as the name suggests) is included in all of the scripts for UK 4 Aspect signals which have one or more feathers. If each of these functions had been put into the individual signal scripts, every time you wanted to make a change you would have had to edit dozens of scripts instead of just three (Common UK 2, 3 and 4 Aspect Feather scripts).

You may have noticed that the Initialise function calls a DefaultInitialise function after it has set some information that’s specific to this signal. This is another way in which common script functions are used. Initialise sets data specific to this particular signal type (how many arms a semaphore signal has), DefaultInitialise sets data specific to this group of signals (initialising the state of each of those arms), and BaseInitialise sets data that’s common to all signal types (initialising the occupation table for the signal).

In the Common Signal Script, there are definitions of all the global constants and global variables referred to in these script functions – things like signal messages, 2D map states, PASS_OFFSET and gConnectedLink. Again, defining these basic constants in a single file that’s shared by all scripts means that if (for some reason) the value of one of those constants had to be changed, you would only need to edit one file and then re-export all of the signal scripts to update them.

SetLights

Syntax: SetLights( newState )

In many cases, functions can be made generic enough to be moved to common script files by splitting any signal-specific functionality off into a subfunction. For example, the SetState function was created to switch a signal’s lights on and off based on its new state.

As it stands, that function can only be used for UK 2 aspect signals. If you tried to run it on a UK 3 aspect signal, for example, none of the lights would ever change (3 aspect signals have different names for their light nodes), and the signal wouldn’t know what to do when it was told to switch to "warning" (because 2 aspect signals don’t have a warning state).

But SetState can easily be turned into a generic function by making a new sub-function called SetLights to switch the lights on and off. For example, here’s the SetLights function for a UK 3 Aspect Signal Post:

-- SET LIGHTS
-- Called by SetState in Common UK Colour Light script to switch the appropriate
-- lights for this signal type on/off according to its new state
function SetLights ( newState )

     if (newState == SIGNAL_CLEARED) then
          Call ( "3 Aspect Signal Head:ActivateNode", "mod_hd3_green", 1 )
          Call ( "3 Aspect Signal Head:ActivateNode", "mod_hd3_orange", 0 )
          Call ( "3 Aspect Signal Head:ActivateNode", "mod_hd3_red", 0 )
     elseif (newState == SIGNAL_BLOCKED) then
          Call ( "3 Aspect Signal Head:ActivateNode", "mod_hd3_green", 0 )
          Call ( "3 Aspect Signal Head:ActivateNode", "mod_hd3_orange", 0 )
          Call ( "3 Aspect Signal Head:ActivateNode", "mod_hd3_red", 1 )
     elseif (newState == SIGNAL_WARNING) then
          Call ( "3 Aspect Signal Head:ActivateNode", "mod_hd3_green", 0 )
          Call ( "3 Aspect Signal Head:ActivateNode", "mod_hd3_orange", 1 )
          Call ( "3 Aspect Signal Head:ActivateNode", "mod_hd3_red", 0 )
     end
end

Our generic SetState would then look like this:

-- SET STATE
-- Sets the current state of the signal
--
function SetState( newState )

     -- If the signal is cleared now...
     if (newState == SIGNAL_CLEARED) then

          -- Set this signal to green
          Call ("Set2DMapSignalState", CLEAR)
          SetLights(SIGNAL_CLEARED)

     -- If the signal is blocked now...
     elseif (newState == SIGNAL_BLOCKED) then

          -- Set this signal to red
          Call ("Set2DMapSignalState", BLOCKED)
          SetLights(SIGNAL_BLOCKED)

     -- If the signal is at warning now...
     elseif (newState == SIGNAL_WARNING) then

          -- Set this signal to yellow
          Call ("Set2DMapSignalState", WARNING)
          SetLights(SIGNAL_WARNING)

     -- If the signal is at warning2 now...
     elseif (newState == SIGNAL_WARNING2) then

          -- Set this signal to double yellow
          Call ("Set2DMapSignalState", WARNING)
          SetLights(SIGNAL_WARNING2)

     end

     -- Remember the signal state
     gSignalState = newState

end

There are no references to specific light nodes in this function any more, and it can also handle the SIGNAL_WARNING and SIGNAL_WARNING2 states now, which means that it can be used by 2, 3 and 4 aspect UK signals. As a result, it’s kept in the Common UK Colour Light Script file and gets called by the Occupied, NotOccupied, Warning and Warning2 functions of all modern UK signals (apart from shunt signals, which are so simple they don’t really need any common functionality).

The same thing can be done for most functions, and doing this will help keep your scripts as concise, tidy and easy to edit as possible.