Rail3D*


 
System.ArgumentOutOfRangeException: Length cannot be less than zero. Parameter name: length at System.String.Substring(Int32 startIndex, Int32 length) at R3DWeb.sParseContent(String sPageID, String sText) at cms.Page_Load(Object sender, EventArgs e)

Using Scripting As An Alternative To Diagrams


« | [[wiki/Main/Tutorials]] | »


1 Problems with diagrams

As we saw in the previous part of this tutorial ([[wiki/Tutorials/UsingTimetablesWithDiagrams]]), the Diagram feature is powerful, but also troublesome. You have to work out in advance where each train is going to be at any given moment: if you get it wrong, or if a train is delayed somewhere for some reason, it’s likely that things will happen in the wrong order and never sort themselves out again. Each train just steps through its list of pre-programmed route changes, taking no account of what else is going on. If a route change is programmed for a place, the diagram doesn’t know if the train has arrived there at the wrong time; if it’s for a time, the place could be quite wrong.

2 An alternative

[[wiki/Scripting/Scripting]] provides an alternative approach. As we saw in [[wiki/Tutorials/MoreComplicatedTimetables#toc6]], it is possible to use a document script to act on trains when they arrive at a reverse, using the OnReverse() function. In fact, as you can see from [[wiki/Scripting/Document]], you could do this when trains arrive at a stop, or indeed any marked location.

Because we can test any combination of train properties as well as the location and time, this give us all sorts of possibilities. Most of the time, we’re only likely to be matching location, time of day and route, but we could also look at the train’s length, maximum speed, type of stock, etc. (see [[wiki/Scripting/Train]]) in deciding what to do with it.

All we did in our previous script was set the destination indicator, but you could equally well change the route, set a departure time, modify the type of vehicle, add or delete vehicles, or whatever.

2.1 Dangers of scripting

With diagrams, the worst thing that can go wrong is that trains end up in the wrong place. If you make a mistake in a script, you could make irreversible changes to your layout. There are functions for ripping up track, deleting trains, and even clearing your whole layout. In addition, there is always the danger that you are the first to try this particular function in that context: there could be bugs we haven’t found yet! Read the documentation, be sure you know what you are doing, and make frequent backups.

2.2 Tips

  • Read the documentation in the [[wiki/Scripting/Scripting]] section of this Wiki.
  • If you’re not an experienced programmer, don’t panic, it’s not that hard!
  • Be careful about syntax details — if you’re not used to C++, it’s easy to get caught out by things like case-sensitivity (Route is not the same as route), missing semicolons, unmatched curly brackets, etc. If the script doesn’t work, it’s most likely that the problem is a silly syntax error.
  • While testing the script, it can be helpful to use the debug.print() function to get the script to tell you how far it’s got.

2.3 Other types of scripts

We are using a single Document [[/docs/Glossary/Script]] here to control what goes on in the layout. Be aware that there are also other places where you can put scripts: in models, attached to signals, signal panels, trains, etc. A train script could be used to control the behaviour of an individual train in a similar way to a diagram, but it has the same disadvantage of not being aware of what other trains are doing. The document script is the most powerful approach for this sort of scheduling problem.

3 Applying scripts to our layout

3.1 Managing the depot

[[/docs/Glossary/Script]]s offer powerful and flexible ways to manage depots - David Morley has described a nice example of the sort of thing you can do in [[wiki/Signals/MotivePowerDepot]]. We don’t need anything that sophisticated on our layout, but we could use scripting to make our depot a bit more “future-proof”.

As we had it set up in the previous tutorial, we had to have one track for each train in the depot, and a dedicated route name associated with that road. This allowed us to decide the order of departing trains independently of the order in which they arrived in the depot. If you look back at the table, you’ll see that train 3 is the first to arrive in the depot in the evening, but only the third to leave. With only five trains, we could cope with this arrangement, but if we had a lot more it would soon get very messy.

What we would like is an arrangement where trains just fill up depot roads as and when they arrive, and are assigned the next duties on a “first in, first out” basis. That should make it easy to adapt to any future change of timetables, and to increase the number of trains arbitrarily.

The first thing to do is to set the switches on the arrival side of the depot to “free route”, so that trains fill up the tracks randomly. See [[wiki/Signals/HiddenSidingsAndDepots]] for more on this. Instead of D1-D5, we will just use a single route, D, for all movements into the depot. When a train arrives in the depot, the script will assign it a new duty from a list. We use a persistent [[wiki/Scripting/Variables]] in the Init() block to remember where we are in this list, even if we save and close the layout halfway through the sequence:

persistent int Duty=1;

The list of duties is just a series of conditional statements (there are neater ways to program this sort of thing, but not in Rail3D…). Because we aren’t using diagrams any more, we have to give the train some memory of what route it should be assigned to when it enters service, so we will use dummy route names like “5H” for “empty stock, becoming H”. To avoid changing the route multiple times for the same train, we check that the route is “D” before we do anything. Since the Depot uses a [[/docs/Glossary/Stop]], we put this all in the OnStop() block:

OnStop()
{
	string route=Train.GetRoute();
	if((Location=="Depot")&&(route=="D"))
	{
		//allocate duties to arriving trains in order
		int NDuties=4;

		if(Duty>NDuties)
		{
			//reset if we've reached the end of the list
			Duty=1;
		}
		if(Duty==1)
		{
			Train.SetRoute("5E");
			Train.SetStartTime(5,49);	
		}
		if(Duty==2)
		{
			Train.SetRoute("5L");
			Train.SetStartTime(5,54);	
		}
		if(Duty==3)
		{
			Train.SetRoute("5H");
			Train.SetStartTime(5,59);	
		}		
		if(Duty==4)
		{
			Train.SetRoute("5L");
			Train.SetStartTime(6,14);	
		}
		Duty++;	//increment Duty counter
	}
}

We want to make sure that we start the sequence in the right place each day, so we reset at midday. This bit could go in the the OnStop() block, but to reduce the number of times it’s called, it might as well go in the OnReverse() block.

	int Hour=Document.GetHour();

	if(Hour==12)
	{
		//reset duty cycle at midday, in case it's got out of sync
		Duty=1;
	}
OnStop()
{
	string route=Train.GetRoute();
	if((Location=="Depot")&&(route=="D"))
	{
		//allocate duties to arriving trains in order
		int NDuties=4;

		if(Duty==4)
		{
			Train.SetRoute("5E");
			Train.SetStartTime(5,49);	
		}
		if(Duty==3)
		{
			Train.SetRoute("5L");
			Train.SetStartTime(5,54);	
		}
		if(Duty==2)
		{
			Train.SetRoute("5H");
			Train.SetStartTime(5,59);	
		}		
		if(Duty==1)
		{
			Train.SetRoute("5L");
			Train.SetStartTime(6,14);	
		}
		Duty++;	//increment Duty counter

		if(Duty>NDuties)
		{
			//reset if we've reached the end of the list
			Duty=1;
			Document.SetTime(5,45); //set clock to just before first departure
			//2Alert("Simulation time has been advanced to next morning");
		}
	}
}

There’s a nice bonus here, by the way: the train on route E runs far fewer miles in the day than those on route H/L: with the “last in, first out” arrangement, it turns out that this “light duty” (which happens to be the first train out in the morning and the first one home in the evening) gets assigned to a different train every day, so all sets run the same mileage.

3.2 Rethinking the timetable

Something we didn’t look at before: as it stands, trains on Route L spend 17 minutes out of every 40 standing at termini. If we were to link route H with route L (31+23 minutes), we would be able to run the 20-minute service with three trains instead of four, with only a couple of minutes slack time. Much better use of resources!

We could link the routes with a simple Route Change feature, or use a diagram to switch from H to L and vice-versa at Alfaton, but we’re going to do it by adding a couple of lines to the existing destination-setting script. At the same time, we can adjust the script to deal with the empty stock routes we defined above:

	if(Location=="Alfaton")
	{
		if((route=="L")||(route=="5H"))
		{
			Train.SetRoute("H");
			Train.QueryTimetable();
			Train.SetDestinationIndicator("Hauttel");
		}
		if((route=="E")||(route=="5E"))
		{
			Train.SetDestinationIndicator("Echo Road");
			Train.SetRoute("E");
			Train.QueryTimetable();
		}	
		if((route=="H")||(route=="5L"))
		{
			Train.SetRoute("L");
			Train.QueryTimetable();
			Train.SetDestinationIndicator("Limathorpe");
		}		
	}

Note the use of the Train.QueryTimetable() function — this makes sure that the train looks up the departure time for the new route, not the old one.

We also have to amend the timetables a bit to get rid of the long layovers, of course: the new ones look like this:

###################################

L
Alfaton [06~23]:05 [06~23]:25 [06~23]:45 00:05 
Limathorpe [06~23]:18 [06~23]:38 [06~23]:58 00:18



###################################

H
Alfaton [06~23]:10 [06~23]:30 [06~23]:50 00:10 
Hauttel [06~23]:08 [06~23]:28 [06~23]:48 00:08



###################################

E
Alfaton [06~10]:00 [06~09]:20 [06~09]:40 [16~19]:00 [16~19]:20 [15~18]:40
Echo Reverse [06~09]:09 [06~09]:29 [05~09]:49 [16~19]:09 [16~18]:29 [15~18]:49




###################################

If you follow a train with the layout set up like this, you’ll see that it does the circuit Alfaton-Limathorpe-Alfaton-Hauttel-Alfaton in just under an hour.

3.3 Helping party-goers get home

Say we want to go back to our original idea, and instead of having trains go out of service when they arrive at Alfaton between 23:00 and midnight, have them go out of service when they arrive at the outer termini after midnight. using the OnReverse()function, we just need to test if the location name is either “Hauttel” or “Limathorpe” and the hour less than 5, and then change the train’s route to D.At Echo Reverse, the same thing applies, but there we want to take the train out of service if it is after seven.

	if(Location=="Hauttel")
	{
		Train.SetDestinationIndicator("Alfaton");
	}	
	if(Location=="Echo Reverse")
	{
		Train.SetDestinationIndicator("Alfaton");
		if(Hour>18)
		{
			Train.SetRoute("D");
			Train.QueryTimetable();
			Train.SetDestinationIndicator("Do not board");			
		}
	}

	if((Location=="Hauttel")||(Location=="Limathorpe"))
	{
		Train.SetDestinationIndicator("Alfaton");
		if(Hour<5)
		{
			Train.SetRoute("D");
			Train.QueryTimetable();
			Train.SetDestinationIndicator("Do not board");			
		}
	}

There’s one small wrinkle we need here: because of the way QueryTimetable() works, we need route D to have a timetable with some departure times, otherwise the train keeps the departure time for its previous route, normally some time the next morning. It doesn’t really matter what you put in here, provided there’s at least one route D departure time from each terminus after your last train goes out of service.

D
Hauttel  [00~03]:00 [00~03]:10 [00~03]:20 [00~03]:30 [00~03]:40 [00~03]:50
Limathorpe [00~03]:00 [00~03]:10 [00~03]:20 [00~03]:30 [00~03]:40 [00~03]:50
Echo Road [19~23]:00 [19~23]:10 [19~23]:20 [19~23]:30 [19~23]:40 [19~23]:50

An alternative approach here would be to set a departure time explicitly in the script (e.g. by adding five minutes to the current sim time).

// layout route management
//Mark Hodson Aug 2008

Init()
{
	string Location;
	persistent int Duty=1;
}
OnStop()
{
	string route=Train.GetRoute();
	if((Location=="Depot")&&(route=="D"))
	{
		//allocate duties to arriving trains in order
		int NDuties=4;

		if(Duty==4)
		{
			Train.SetRoute("5E");
			Train.SetStartTime(5,49);	
		}
		if(Duty==3)
		{
			Train.SetRoute("5L");
			Train.SetStartTime(5,54);	
		}
		if(Duty==2)
		{
			Train.SetRoute("5H");
			Train.SetStartTime(5,59);	
		}		
		if(Duty==1)
		{
			Train.SetRoute("5L");
			Train.SetStartTime(6,14);	
		}
		Duty++;	//increment Duty counter

		if(Duty>NDuties)
		{
			//reset if we've reached the end of the list
			Duty=1;
			Document.SetTime(5,45); //set clock to just before first departure
			//2Alert("Simulation time has been advanced to next morning");
		}
	}
}

OnReverse()
{
	string route=Train.GetRoute();
	int Hour=Document.GetHour();

	if(Hour==12)
	{
		//reset duty cycle at midday, in case it's got out of sync
		Duty=1;
	}

	if(Location=="Alfaton")
	{
		if((route=="L")||(route=="5H"))
		{
			Train.SetRoute("H");
			Train.QueryTimetable();
			Train.SetDestinationIndicator("Hauttel");
		}
		if((route=="E")||(route=="5E"))
		{
			Train.SetDestinationIndicator("Echo Road");
			Train.SetRoute("E");
			Train.QueryTimetable();
		}	
		if((route=="H")||(route=="5L"))
		{
			Train.SetRoute("L");
			Train.QueryTimetable();
			Train.SetDestinationIndicator("Limathorpe");
		}		
	}
	if(Location=="Hauttel")
	{
		Train.SetDestinationIndicator("Alfaton");
	}	
	if(Location=="Echo Reverse")
	{
		Train.SetDestinationIndicator("Alfaton");
		if(Hour>18)
		{
			Train.SetRoute("D");
			Train.QueryTimetable();
			Train.SetDestinationIndicator("Do not board");			
		}
	}

	if((Location=="Hauttel")||(Location=="Limathorpe"))
	{
		Train.SetDestinationIndicator("Alfaton");
		if(Hour<5)
		{
			Train.SetRoute("D");
			Train.QueryTimetable();
			Train.SetDestinationIndicator("Do not board");			
		}
	}		
}

4 Download

You can find the completed layout for this part of the tutorial at: http://www.markhodson.nl/rail3d/2kdlayouts/ops_layout_05.trp


« | [[wiki/Main/Tutorials]] | »


import