Advanced Signal Scripting

Introduction

Previous sections have looked at the basics of how to get a signal up and running. However, there are a few more features that this section will cover that is required by some signals...

Animation

Up to this point, we’ve only looked at signals that have lights which are switched on and off using the ActivateNode code function. However, some signals have moving parts that are animated using the AddTime code function. For example, the old-fashioned semaphore signals used on a number of Train Simulator’s historic steam-era routes, have one or more "arms" that tilt up and down to indicate whether the route ahead is clear or blocked.

Moving parts on a signal are usually separate "child" objects. Just as a set of lights mounted on a post is a child of that post, so the arms of a semaphore signal are separate models that are each defined in the signal’s blueprint as a child of the post they’re attached to.

In the Blueprint Editor, open a semaphore signal (for example, "RailNetwork\Signals\UK Semaphore\Wooden_Posts\B Wooden Sig_h") and scroll to the bottom of it. Click on the + next to where it says "Children" to expand its list of child objects, and then click on the + next to "S child" to view the child’s settings.

There are two important bits of information here. The child name ("Main Home Arm") must be put in front of any code functions you want to run on that particular bit of the signal. So to animate this signal’s arm, we would call "Main Home Arm:AddTime".

The other thing you need to know is the blueprint ID for the signal's arm. Like any child object, the arm has its own blueprint which defines which model and (for moving objects like this one) which animations it uses. In this case, it's called UpQuad_Arms\UqArm_home01.

If you open up that blueprint, there is an entry at the bottom saying "anim set". If you expand that, you’ll see that this signal arm has two animations. If you expand their entries, their names are displayed: "Clear01" and "Stop01". Those are the names by which this signal’s animations are referenced by in Train Simulator's scripts.

Animating an object is a bit fiddly compared to switching a light on or off. The signal cannot be told just to animate and then be left to it – the arm needs to be moved through its animation one frame at a time from the script. To do that use the Update function.

Once activated by the BeginUpdate code function, this script function is triggered every frame and has a time parameter that tells how long it is since the script last updated. So to animate a semaphore signal, call BeginUpdate from the SetState function when the signal changes state. Then in the Update function itself, check whether you are animating to clear or blocked and then AddTime to the appropriate animation. While the arm is animating, AddTime returns 0. As soon as it returns anything other than zero, the animation has finished, and you can call EndUpdate to stop the Update function running.

To run the "Clear01" animation on the signal arm described above, use the following bit of scripting in the signal’s Update script function:
if Call( "Main Home Arm:AddTime", "Clear01", time) ~= 0 then

    Call("EndUpdate")

end

If you want to stop an animation in mid-flow (if the signal changes state again before you finish animating, for example), use the Reset code function, as follows:

Call( "Main Home Arm:Reset", "Stop01" )

Of course, in reality, things are usually more complex than this. For example, UK semaphore signals sometimes have multiple arms, so it's necessary to keep a global table containing the child name, animation names and the current state of each arm. And because more than one of the arms could be animating at the same time, it's also necessary to keep track of which and how many arms are animating, so that you know when to start and stop the Update function.

If you want to see how this all works, look at the file "Common UK Semaphore Script.lua" in RailNetwork\Signals\UK Semaphore\CommonScripts.

The DefaultInitialise function initialises the tables that will contain information about each of the signal's arms, their state, and which of them (if any) are currently animating, as well as some global constants used for the various animation states the signal can be in, and to make it clear which arm each entry in the table refers to.

The SetState function changes the animation state of an arm to open, closed, opening or closing, keeps track of which arms are currently animating, and starts and stops the Update function at the appropriate time.

The DefaultUpdate function cycles through all the arms on the signal, checks which of them are animating, and updates the animation as shown above. When an arm's animation ends, DefaultUpdate calls SetState again to change its animation state to open or closed.

The script "Sem_HomeSig.lua" in RailNetwork\Signals\UK Semaphore is a simple example of a semaphore signal with just one arm. If you look at its Initialise function, you can see that it sets the child and animation names of this particular signal, filling in the blanks in the table that was set up by DefaultInitialise.

This means that these scripts can use a lot of generic functions that will work across multiple signals, with information about the specific signal the script is running on stored in global variables and tables.

Flashing Lights

Another use for the Update function is making a signal's lights flash on and off. For example, say you want a red light (let's say its node name is "Red01") to flash on and off every half second. First, declare some constants to set how long the light is switched on and off for during each cycle, and global variables to keep track of the current state of the light:

-- How long to stay off/on in each flash cycle
LIGHT_FLASH_OFF_SECS = 0.5
LIGHT_FLASH_ON_SECS  = 0.5

-- State of flashing light
gTimeSinceLastFlash  = 0
gLightFlashOn        = false
gFirstLightFlash     = true

Whenever you want the red light to start flashing, just set gFirstLightFlash to true (so you know it's just starting to flash) and call BeginUpdate to start the Update script function running each frame. In that, put the following scripting -

-- UPDATE
-- Makes lights flash
--
function Update( time )

     -- If this is the first flash, reset time since last flash and light flash on
     if gFirstLightFlash then

          -- Reset flash state
          gTimeSinceLastFlash = 0
          gFirstLightFlash    = false
          gLightFlashOn       = false

     -- Otherwise...
     else

          -- Increment the timer by however much time has passed since the last update
          gTimeSinceLastFlash = gTimeSinceLastFlash + time

          -- If it's off and has been off long enough, switch on
          if (not gLightFlashOn) and gTimeSinceLastFlash >= LIGHT_FLASH_OFF_SECS then
               Call ( "ActivateNode", "Red01", 1 )
               gLightFlashOn = true
               gTimeSinceLastFlash = 0

          -- If it's on and has been on long enough, switch off
          elseif gLightFlashOn and gTimeSinceLastFlash >= LIGHT_FLASH_ON_SECS then
               Call ( "ActivateNode", "Red01", 0 )
               gLightFlashOn = false
               gTimeSinceLastFlash = 0

          end

     end

end

When you want the light to stop flashing again, simply call EndUpdate and then switch the red light on / off based on the new state.

Train Warning Systems

All the European routes modelled in Train Simulator (excluding some historic routes, for obvious reasons) have some form of train warning system that sounds an alarm in the train cab and/or applies the emergency brakes if you drive past a signal without acknowledging it, exceed the track speed limit or pass a red signal. And all of the routes' signals also include a check for "SPADs" or Signals Passed at Danger.

The following sections describe how the system works.

Signal Passed at Danger (SPADs)

SPAD messages are very easy to generate and don't require any additional objects to be added to your track. All you need to do is add a few lines to the OnConsistPass function of your signal. Here's the relevant bit of the standard European BaseOnConsistPass function:

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

     -- Check for SPADs
     if (gSignalState == SIGNAL_BLOCKED) then

          -- If it passed a blocked signal, send a consist message
          Call( "SendConsistMessage", SPAD_MESSAGE, "" )
     end

     -- 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

All that's happening is a check if the signal is blocked when a train starts to pass link 0. If it is, a SPAD is generated by calling a code function called SendConsistMessage which (as the name suggests) sends a message (in this case the SPAD_MESSAGE) to the offending train.

Automatic Warning System (AWS)

The AWS or Automatic Warning System is used on Train Simulator's modern British routes. Approximately 180 metres before most signals there's an "AWS ramp", which is placed just like a signal except that the ramp goes in the middle of the track instead of next to it. Like a signal, it has a link, which in this case goes right above the ramp. When a train drives over the ramp it passes that link, triggering the OnConsistPass function in its script.

All that function needs to do is check the state of the next home signal up the line when a train starts passing its link. It does this by calling a code function GetNextSignalState, which looks up the line until it reaches another signal link facing in the same direction and then triggers that signal's GetSignalState script function. Then it calls SendConsistMessage to send the appropriate AWS_MESSAGE to the train.

Here's what that looks like:

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

     -- 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 before the
          -- link, the train has just started crossing it forwards
          if ( prevFrontDist > 0 and prevBackDist > 0 ) then

               -- Request state of next signal
               local nextSignalState = Call( "GetNextSignalState", "", 1, 1, 0 )

               if (nextSignalState == CLEAR) then

                    Call( "SendConsistMessage", AWS_MESSAGE, "clear" )

               elseif (nextSignalState == WARNING) or (nextSignalState == BLOCKED) then

                    Call( "SendConsistMessage", AWS_MESSAGE, "blocked" )

               end

          end

     end

end

If the signal ahead is clear, the AWS_MESSAGE is sent with the parameter "clear", and a bell rings in the train's cab to let the driver know he's clear to proceed. If the signal ahead is blocked or showing some kind of warning, the AWS_MESSAGE is sent with the parameter "blocked" and a warning sounds in the cab until the driver hits a button to acknowledge it. If the driver doesn't press the button soon enough, the train applies its brakes automatically, but that's handled by the code and it is not necessary to worry about it here.

Like any other signal, the AWS ramp also needs to have Initialise and OnSignalMessage script functions. Nothing needs to be Initialised though, so that function can be left empty, and all the OnSignalMessage function needs to do is forward on any message that it receives. Apart from that, all the script requires is for the necessary global constants (CLEAR, WARNING, BLOCKED, which are 0, 1 and 2, and AWS_MESSAGE, which is 11) to be declared. Like the shunt signal, the script is simple enough that it doesn't need to have any common files included.

Train Protection & Warning System (TPWS)

The TPWS or Train Protection & Warning System is an additional system which is used on Train Simulator's modern British routes. Like AWS, it requires a separate object to be added to the track, in this case, a "TPWS Grid" which is placed just before the home signal it protects. Like the AWS ramp, this grid has a link which should be placed directly above it (making sure that a train going in that direction will pass the grid's link before it passes the home signal's link 0), and the grid's script contains an OnConsistPass function which checks the state of the home signal just in front of it when a train drives past that link.

Unlike the AWS ramp, as well as checking whether the signal ahead is blocked the TPWS grid also checks if the train is going faster than the speed limit for this bit of track. If it is, it sends a TPWS_MESSAGE (12) to the train, which causes it to automatically apply its emergency brakes.

Here's how that works:

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

     -- 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 before the
          -- link, the train has just started crossing it forwards
          if ( prevFrontDist > 0 and prevBackDist > 0 ) then

               -- Request state of next signal
               local nextSignalState = Call( "GetNextSignalState", "", 1, 1, 0 )

               -- Find out how fast the train is going and the speed limit at this point
               local consistSpeed = Call ( "GetConsistSpeed" )
               local speedLimit = Call ( "GetTrackSpeedLimit", 0 )

               -- Send the appropriate TPWS_MESSAGE to the train
               if (nextSignalState == BLOCKED) then

                    Call( "SendConsistMessage", TPWS_MESSAGE, "blocked" )

               elseif (consistSpeed > speedLimit) then

                    Call( "SendConsistMessage", TPWS_MESSAGE, "overspeed" )

               elseif (nextSignalState == WARNING) then

                    Call( "SendConsistMessage", TPWS_MESSAGE, "warning" )

               elseif (nextSignalState == CLEAR) then

                    Call( "SendConsistMessage", TPWS_MESSAGE, "clear" )

               end

          end

     end

end

This is very similar to the AWS script, except two new code functions are called (GetConsistSpeed and GetTrackSpeedLimit) to check if the train is going too fast. GetConsistSpeed can be called from any signal's OnConsistPass function and will return the speed of the train that triggered it. GetTrackSpeedLimit can be called from any signal script function and returns the speed limit at a specific link belonging to that signal (in this case there is only one link, so the speed is checked at link 0). Both functions return the speed in metres per second, so they can be compared directly to see if the train is speeding.

Creating Your Own Warning System

Using the existing code, it's possible to create your own warning systems. For example, the German Indusi / PZB system can be replicated using a mixture of AWS and TPWS messages. This can either be done by adding separate PZB inductor magnets next to the track in the appropriate places, or by simply adding the necessary checks to the OnConsistPass function used by standard German home and repeater signals.

When you pass the 0 link of a repeater signal, if the next home signal up the line is red, the Indusi system triggers a warning just like the AWS ramp, so an AWS_MESSAGE with the "blocked" parameter should be sent to the train. When you pass the 0 link of the home signal, if it's red the train will automatically be stopped, just like the TPWS grid, so you can send a TPWS_MESSAGE with the "blocked" parameter to the train.

The Indusi system also checks to make sure that if you're passing a home signal that's set to warning you have slowed to 40km/h. Again, this can be done by sending the "overspeed" TPWS_MESSAGE if the consist's speed is more than 40km/h (which is 40 / 3.6 in metres per second).

It should be possible to simulate the behaviour of many other warning systems from around the world using a similar combination of AWS and TPWS messages.

Handling Yard Entries

Although most of a route will normally be covered by home signals, some areas (particularly in yards) are "dark". Once a train goes into a yard it probably won't encounter another home signal until it leaves the yard, and the signals outside the yard generally won't know or care about trains inside the yard.

To handle this, there are special signals which have one or more of their links flagged in their Initialise script function as being a "yard entry". These links ignore the occupancy of the track beyond them, which involves making a few simple changes to their OnConsistPass function -
  • When a train starts passing a yard entry link forwards, the link's gOccupationTable entry is not incremented, because once the train is in the yard the signal is no longer interested in it.
  • When a train finishes passing a yard entry link forwards, if that was the only train in gOccupationTable0 the signal is now clear and the NotOccupied function is run to make it turn green.
  • When a train starts passing a yard entry link backwards, that train was not known before so it's necessary to run the Occupied function to make the signal turn red now.
  • When a train finishes passing a yard entry link backwards, the link's gOccupationTable entry is not decremented.
As you don't need to worry about trains inside the yard or the state of any signals covering the other exits from the yard, you also need to edit the OnSignalMessage function to ignore any messages it receives on a yard entry link, if the message is coming from within the yard (i.e. from direction 1).

Just place the signal using that modified script on the mainline somewhere before the yard entry you want to cover, put the appropriate link a little way inside the yard, and your signal will turn green again as soon as a train finishes passing that link and disappears into the yard.

If you need one signal to cover multiple yard entries, you can flag more of the links as yard entries. Again, by making your changes as generic as possible you can re-use the same basic script functions for multiple signals with different numbers of links and yard entries. In the case of the UK Colour Light signals, for example, there are lots of yard entry signal scripts, but they only contain Initialise and (if they have any feathers) ActivateLink functions - the rest of the scripting functions for them are kept in a small set of common files that the signal-specific scripts include.

Covering Reverse Junctions

On the Bath - Templecombe route, several of the junctions are designed for trains to cross between tracks or enter a siding by driving past the junction and then reversing up it.

If the signals before the junction are only covering that one junction, that's fine. But if you want one signal to cover multiple junctions of this type, there is a problem. Normally you would place link 0 next to the signal and link 1 beyond the last junction you're covering. But on this route, trains will often drive past the signal and then reverse back up another track, without the rear of the train ever passing link 1. This means that gOccupationTable0 never gets decremented back to 0, so even after the train has reversed onto another line and the junction has switched again behind them, the signal they passed remains blocked.

The way around this is to do something which would normally be incorrect - placing multiple links for the same signal on the same line, putting one after each junction. Each link keeps track of which link (if any) it's connected to, and so wherever you drive your train and however you switch the junctions, the signal will always know whether the route in front of it is blocked.

Consider a simple case - a signal that has to cover two junctions that merge onto the track ahead of it. This signal will have three links - link 0 next to the signal as normal, link 1 just beyond the first junction, and link 2 just beyond the second junction.

First, in the Initialise function declare a new table gSpecialConnectedLink to keep track of which links are connected to each other:

-- Default to all links disconnected until you know otherwise
gSpecialConnectedLink = {}
gSpecialConnectedLink[0] = -1
gSpecialConnectedLink[1] = -1

Next, in the ReactToSignalMessage function, you need to make sure the signal reacts to messages arriving on the correct links. Most messages should be ignored unless they arrive on the last link, except the INITIALISE_SIGNAL_TO_BLOCKED and RESET_SIGNAL_STATE messages (which can be received by any link) and the JUNCTION_STATE_CHANGE message (which should trigger the OnJunctionStateChange function when it arrives on any link except the last one). You also need to make a slight change to how the OCCUPATION_DECREMENT message is handled, to make sure that it checks the occupation table entries for all of the links (0, 1 and 2) are zero before setting the signal as NotOccupied.

The OnJunctionStateChange function needs changing next so that it keeps track of which link each of the other links are connected to.

-- JUNCTION STATE
--
function OnJunctionStateChange( junction_state, parameter, direction, linkIndex )

     -- If this is the first time you've checked the junction state, check all links
     if not gInitialised then

          -- Check if link 0 is connected to link 1 and link 1 is connected to link 2
          gSpecialConnectedLink[0] = Call( "GetConnectedLink", "", 1, 0 )
          gSpecialConnectedLink[1] = Call( "GetConnectedLink", "", 1, 1 )

     -- Otherwise, if the junction change message didn't arrive on the last link...
     elseif linkIndex < 2 then

          -- check if this link is connected to the next one up the line
          gSpecialConnectedLink[linkIndex] = Call("GetConnectedLink", "", 1, linkIndex)

     end

     -- If all the links are connected, gConnectedLink = 2
     if gSpecialConnectedLink[0] == 1 and gSpecialConnectedLink[1] == 2 then

          gConnectedLink = 2

     -- Otherwise gConnectedLink = -1
     else

          gConnectedLink = -1

     end

     -- If they're connected all the way through and route is clear, signal is CLEAR
     if gConnectedLink == 2 and
          gOccupationTable[0] == 0 and
          gOccupationTable[1] == 0 and
          gOccupationTable[2] == 0 then
          NotOccupied( 0 )

     -- Otherwise set signal as BLOCKED
     else

          Occupied( 0 )

     end

end

Finally, you need to make a couple of changes to the OnConsistPass function to handle the fact that the links are now all on the same line covering converging junctions, instead of on different diverging routes.

When a train starts to pass a link other than link 0 in reverse, you should increment the occupancy of the link behind it if that link is connected to you:

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

     -- And this link is connected to the previous one...
     if (specialConnectedLink[linkIndex - 1] == linkIndex) then

          -- Increment the previous link's occupation table
          gOccupationTable[linkIndex - 1] = gOccupationTable[linkIndex - 1] + 1

     end

end

Similarly, when a train finishes passing one of those links going forwards, you should only decrement the occupancy of the link behind it if that link is connected to you:

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

     -- And this link is connected to the previous one...
     if (specialConnectedLink[linkIndex - 1] == linkIndex) then

          -- Decrement the previous link's occupation table
          gOccupationTable[linkIndex - 1] = gOccupationTable[linkIndex - 1] - 1
     end
end

Finally, when a train finishes passing link 0 in reverse, you need to check gConnectedLink is 2 and that the occupation table is empty for all links (0, 1 and 2) before telling the signal it's NotOccupied.

Now if a train drives past this signal, stops just past the first junction (link 1), switches the junction and reverses back up it onto another line, all of the occupation tables will update correctly and the signal will know it no longer has a train blocking the track ahead of it. If you're feeling really ambitious, you can even script signals that handle a mix of diverging and converging junctions in this way. An example of this is the UK Semaphore signal script "Sem_DvgeRte_hh Special 1.lua", which is used by a signal near Evercreech Junction.

Handling Signals with Lots of Aspects

The European signals created for Train Simulator are mostly fairly straightforward - they show if the line ahead is blocked or clear and (in some cases) have one or two warning aspects they can show as well.

But some signal systems (for example, North American ones) are incredibly complex and (using a combination of multiple heads, flashing lights and sometimes even moving parts) can show several different warning aspects depending on everything from the state of junctions further up the line to the type of train approaching the signal.

In these cases, using simple functions like Occupied and NotOccupied to control the state of the signal is obviously not sufficient. Instead, it sometimes makes more sense to combine all that functionality into a single function which can be called whenever something happens that might change the state of the signal (such as a signal message being received or a junction being switched), and takes all of the necessary factors into account to decide what state the signal should be in now, and which of its lights should be switched on or off.

For example, this would control a two-headed US signal as used by BNSF in California:

-- DETERMINE SIGNAL STATE
-- Figures out what lights to show on each head based on the state of the signal
--
function DetermineSignalState()

     local newState = -1
     
     -- If the junction is broken
     if gConnectedLink == -1 then

          -- Stop
          SetState( { ANIMSTATE_RED, ANIMSTATE_RED } )
          newState = SIGNAL_BLOCKED

     else
          local switchSpeed = gSwitchSpeed[gConnectedLink]

          -- Adjust speed according to train type
          if switchSpeed > SPEED_SLOW then

               -- If it is a freight train, use next speed down if diverging at switch
               if gApproaching == CONSIST_TYPE_FREIGHT then
                    switchSpeed = switchSpeed - 1
               end
          end

          -- If there's a train in this signal's block
          if (gOccupationTable[0] > 0) or
             (gConnectedLink > 0 and gOccupationTable[gConnectedLink] > 0) then

               -- Stop
               SetState( { ANIMSTATE_RED, ANIMSTATE_RED } )
               newState = SIGNAL_BLOCKED

          -- If you are going straight on at this signal...
          elseif gConnectedLink < 2 then

               -- And the route ahead is restricting
               if gRestricted[gConnectedLink] then

                    -- Restricting
                    SetState( { ANIMSTATE_LUNAR, ANIMSTATE_RED } )
                    newState = SIGNAL_RESTRICTED

               -- And the route ahead is at warning or approach restricting
               elseif gLinkState[gConnectedLink] == SIGNAL_WARNING
                   or gLinkState[gConnectedLink] == SIGNAL_APPROACH_RESTRICTED then

                    -- Approach
                    SetState( { ANIMSTATE_YELLOW, ANIMSTATE_RED } )
                    newState = SIGNAL_WARNING

               -- And the route ahead is at clear, warning2 or approach restricting2
               elseif gLinkState[gConnectedLink] == SIGNAL_CLEARED
                   or gLinkState[gConnectedLink] == SIGNAL_WARNING2
                   or gLinkState[gConnectedLink] == SIGNAL_APPROACH_RESTRICTED2 then

                    -- And you are also going straight on at the next signal
                    if gRouteState[gConnectedLink] == SIGNAL_STRAIGHT then

                         -- Clear
                         SetState( { ANIMSTATE_GREEN, ANIMSTATE_RED } )
                         newState = SIGNAL_CLEARED

                    -- And you are diverging at the next signal
                    elseif gRouteState[gConnectedLink] == SIGNAL_DIVERGING then

                         -- And the next signal's switch speed is SLOW (40mph)
                         if switchSpeed == SPEED_SLOW then

                              -- Approach Medium
                              SetState( { ANIMSTATE_FLASHING_YELLOW,ANIMSTATE_RED } )

                         -- And the next signal's switch speed is MEDIUM (50mph)
                         elseif switchSpeed == SPEED_MEDIUM then

                              -- Advance Approach
                              SetState( { ANIMSTATE_YELLOW,ANIMSTATE_GREEN } )

                         -- And the next signal's switch speed is LIMITED (60mph)
                         elseif switchSpeed == SPEED_LIMITED then

                              -- Approach Limited
                              SetState( { ANIMSTATE_YELLOW,ANIMSTATE_FLASHING_GREEN } )
                         end

                         newState = SIGNAL_CLEARED
                    end
               end

          -- If you are diverging at this signal...
          else

               -- And the route ahead is restricting
               if gRestricted[gConnectedLink] then

                    -- Restricting
                    SetState( { ANIMSTATE_RED, ANIMSTATE_LUNAR } )
                    newState = SIGNAL_RESTRICTED

               -- And the route ahead is at warning or approach restricting
               elseif gLinkState[gConnectedLink] == SIGNAL_WARNING
                   or gLinkState[gConnectedLink] == SIGNAL_APPROACH_RESTRICTED then

                    -- Diverging Approach
                    SetState( { ANIMSTATE_RED, ANIMSTATE_YELLOW } )
                    newState = SIGNAL_WARNING

               -- And the route ahead is at clear, warning2 or approach restricting2
               elseif gLinkState[gConnectedLink] == SIGNAL_CLEARED
                   or gLinkState[gConnectedLink] == SIGNAL_WARNING2
                   or gLinkState[gConnectedLink] == SIGNAL_APPROACH_RESTRICTED2 then

                    -- Diverging Clear
                    SetState( { ANIMSTATE_RED, ANIMSTATE_GREEN } )
                    newState = SIGNAL_CLEARED

               end

          end

     end

     -- If your state has changed and you have got a valid new state...
     if newState >= 0 and newState ~= gSignalState then

          -- Update your state and send back a message to let the signal behind know
          gSignalState = newState
          Call( "SendSignalMessage", newState, "", -1, 1, 0 )

     end

end

This function has to deal with a bewildering set of factors - what the state of the next signal up the line is, whether you are diverging or going straight ahead at both this signal and the next signal, the speed at which you should cross the next junction, whether you are going into an area with a restricted speed limit (such as a yard), what type of train is approaching the signal... Handling all of this through separate functions would be completely impractical. Instead, there is one function which can be called from the OnConsistPass, OnJunctionStateChange and ReactToSignalMessage functions whenever the signal's state might need to change.

Notice some functionality that's normally found in the SetState function has also been included. Instead of telling SetState what state is being switched to and letting it decide which lights to turn on and off based on that, it is all handled inside this function and tells SetState which lights need to be active on each of the signal's two heads.