NIMBY Rails devblog 2022-08

Timetable support for train AI

August started with the decision to base timetables on the “train run” system as explained last month. Trains will have a sequence of “runs”, which is a simple combination of start time plus a line to run. These runs are tightly packed and must cover 100% of the week seconds. How the player interacts with runs was the big headache, so I decided to stop thinking about it for awhile and finally update train AI to 1.5 timetables.

The straightforward design of the run system not only helps pax and the line network logic to quickly build a timetabled map, it also helps trains run timetables easily. Thanks to train AI “roll on rails” code being 100% decoupled from “what to do now” code, it took just two method changes to switch to timetables. In particular the “what to do now” code now looks up what is the run the train should be running at the moment it is asked to, unless the train is not done with its current run. So on spawn (including interventions) trains will be spawned on their current stop according to the time of the week, and assigned that run. Then, only when finishing their current run, they will consider running a new run (usually the next one, but if it’s late enough it can skip one or more).

Line stops are still edited just like in 1.4, with leg speed control and a min. stop value. This forms a relative timing for the line, which is then translated to a real time when a train runs a line, based on the run start time. Trains about the fixed arrival time for a line stop, and if the line settings allow them to, they will run extra fast in case they are late. Stops min. stop time is no longer the minimal amount for the train stop. It is now used to calculate the departure time for the train stop, and the train will consider departing the station once that time arrives. The system of waiting extra time for loading pax still exists, so it can depart late if the reserved min. stop time is not long enough. But if it arrives late and loads fast enough it will depart on time. Combining this with the existing max speed setting for lines, allows trains to catch up with their timetable, so it might be a good idea to reserve more than 10s of min. stop time in 1.5.

Out of two months of development for 1.5, train AI implementation took a total of two days.

The previous video shows a busy station with many trains coming and going, and most of them making their timetable in time (with a buggy visualization, ignore the wrong platforms). This was the state of the AI in early august, it is now much improved.

Final timetable design: multiple lines, runs, orders and automation

Up until this point the entire design of timetables had a big limitation: trains are forever fixed to run a single line. I was planning to improve lines with all sort of extra options to make them more flexible, but the more I thought about it the worse it looked. It would be better to keep lines relatively simple and instead work on the new thing, which was trains runs.

So I decided to make it possible again for trains to be assigned to any number of lines, by making runs explicitly store which line they are going to run. This also restores the old vision of having trains being independent of lines, which is of course the more realistic. The main problem with this feature is the “glue pathing” required to go from one line to the next one. Initially I was going to force lines match on their final and first stops to make them compatible, but realized this made every 1.4 line invalid and every line design tip up until this point invalid. So in another design decision, it was made possible to mix and match any possible line with any other possible line, even if the “glue path” to transition from one line to the next is multiple hours. This “glue path” is properly accounted for by the train run logic and its leg run time is used to adjust the time of the first stop of the line, so the timetable accounts for it. That being said it is not a fully traced path like a line leg path. It does a simple thing (estimate the time with a 90% max train speed, then run the train at 100% on the tracks), and it also respects line tags, but its timing will probably be a bit off compared to real line legs.

I had now system for timetables which I liked and was flexible. Trains were running it, pax were planning on top of it. Only the final boss remained: timetable editor UI.

My plan up to this point had always been to make players edit train runs. But imagine having to edit this:

And this is just for one train. Even the most dedicated players would find this too much. So from the very start it was clear this design required the help of tools for editing the timetables. Runs are easy to understand but one train can have hundreds of runs per week, it is just too much to ask for the players to edit them by hand.

I kept thinking ever more elaborate tools and ideas to make it easier, but always kept arriving to the same conclusions: generally speaking it is not needed to carefully consider every single run start time by itself. The game should allow to input some start times, and the rest should be automatically filled in, including adding as many runs as needed. A tool was required to do that.

But such a tool kind of sucks by itself. It means editing runs essentially “destroys” the input into these auto generated runs. If you need to edit them again, good luck remembering what were the original values. No, the game should remember these player inputs. Even keep them around in case the line changes! Mmmmh this is starting to sound a lot like… train orders.

So train orders were resurrected. They are now used to autogenerate train runs for a given line. Train AI never interacts with train orders in 1.5. The train AI only knows about runs. Players never edit train runs, they only edit train orders. A new piece of game logic, the run builder, compiles and expands the player orders into train runs, and the train runs the runs. The run builder applies some obvious rules, like ignoring orders which don’t fit even one run of its line, or adding padding stop times if the final run of an order does not match exactly with the starting time of the next run.

I will now present an example of this entire design from the POV of the train orders editor. The example is for a single train with manual orders set to run a regular commuter line, and to overnight in a storage yard.

This is the most basic piece of timetable information for a train: a train run. You can see how a train is selected to run a specific line, with specific arrival and stop times for each stop (actually fixed departure times, but for simplicity in this is panel they are expressed as wait times). None of this is player editable, but take note of the curiously exact arrival time for the first stop.

The selected train run is now shown in the context of all the runs for the selected train. This “run map” is what the train (tries) to follow with its scheduling AI, the “what to do now” mentioned earlier. Again nothing in this screenshot is editable, but things appear to be lining up nicely anyway. This is because the runs are expanded from manual, editable player orders:

Here you can see how the new order system works in 1.5. Each (manual orders) train has a listing of orders, which are always sorted by a fixed start time, and select a line to run. Nothing more, nothing else. The game automatically keeps the order listing sorted and handles things like the sunday-monday transitions automatically. There is copy-paste support (also between trains), and very simple but very useful bulk editing tool, which can shift order start times by a given amount. Using that tool I just created two manual orders for monday, set their absolute times, and then copy pasted it six times, just clicking once the time shift tool to add 24 hours to the pasted orders.

The run builder took care of everything else. Remember the dead set 07:00:00 from the first screenshot? The run builder considered the time required for the train to run from storage to the first regular line stop, and padded the start time of just that particular run to make sure the first arrival of the first run of the player order exactly matched the player time. The run builder is re-run every time you change an order, and it is also run if you change the lines referenced by the orders.

To run another train with similar orders (hopefully not the exact same!), you can just select all orders and paste them into another manual orders train, then use the bulk edit tool to shift times as desired to achieve intervals as you see fit. But doing this manually can get old fast, so I introduced a second train orders mode, auto copy manual orders:

Trains set to this mode automatically copy the selected train orders (only if said train is in manual mode), and apply a time shift to them before running the run builder. They do this automatically if their reference train ever changes their orders, including the implicit modifications caused by changing the lines (the Apply button is here only to avoid half entered shift times to apply to the running game). The one piece of manual input required for this work is the shift time. An auto interval value here does not really work since there is no concept of a set of trains forever running the same line. If it had to pick one, which one of the lines in the orders? At what moment in the week, since the trains running a line at any given time can change? I could not find a proper automated way for this shifting, so it is manual input. I think it is quite a minimal thing to ask players interested in manual orders to do, but if I find a reasonable automation for this it will be added later (as an option, since I think the manual shift is important to keep).

If you have been shaking your head for the past five minutes and are already yearning for the simplified timing system of 1.4, fear not, the third train orders mode is for you:

In two clicks (actually one since auto run lines is the default) you can have your trains running an interval line like in 1.4, with zero more editing required. The game will auto adapt the interval when you buy or sell trains for the line too. The difference is that this also generates a fixed timetable, so the interval is static in time, expressed as the individual arrival and departure times for each train in the timetable. The one extra manual step you might require from time to time is to issue a whole line intervention if trains get whacked too much out of their timetable when adding trains to the line or editing the line itself, but this is also a single click in 1.5.

All 1.4 imported trains are set to auto run the first line they had in their 1.4 orders.

Secondary platforms for stops

An often requested feature is now in 1.5: line stops can pick more than one platform. 1.5 made this possible since now line timing is fixed, and a design decision around this is easier. There is a main platform, which players pick as they have done since 1.1, and 1.5 then lets you pick zero or more secondary platforms for said stop. Line timing ignores secondary platforms, and it always assumes trains pick the main platform. If you add secondary platforms to a stop it is a very good idea to also add extra min. stop time to pad the small differences in routing to the different platforms.

But picking a different platform means changing the train destination, and as fans of depots prior to 1.5 know, that is a huge headache in this game. So the game also makes the player pick a path signal to function as the one, single point the train is allowed to change its picked platform. This signal should have a direct path into every platform, both main and secondary, for this feature to work. There is no logic to enforce this, so make sure you follow this guideline!

Tired: depots. Wired: depot lines and storage yards

Speaking of depots, they have been removed in 1.5, for now. The concept of black holing trains during down times is not very realistic but it makes for an easier game, and I agree with that, but for now I want to see how a different idea plays out: depot lines and storage yards.

A depot line is a line set to “empty trains”, and it will usually only have a single stop with a long min. stop time, at least as long that the run builder won’t be capable of fitting two “runs” of this line in a single night (for example), but short enough that it allows it to pad the wait to match exact launch times in the morning. Pax also won’t be capable of planning trips which involve “empty train” lines. Depot lines are not actually “a thing” for the game logic. It is a design pattern for schedule designers (players). But to recognize this utility the game changes the line icon to the depot icon when lines are set to “empty trains”.

A “storage yard” is just a stop on a station with zero radius (not required but good idea) which is only serviced by depot lines. Pax will completely ignore this station for all purposes. Adding many secondary platforms to this stop allows it to fit many trains on the same depot line, all waiting for their departure in the morning, as in orders example. Again this is just a design pattern I’m proposing. The game knows nothing about storage yards! It’s just an emergent property of combining game features.

Multiplayer dynamic speed

Multiplayer is, and has been for some versions, in a bad state. Or at least it could be in a much better state. 1.5 will improve multiplayer just by virtue of its extra performance, and by adding a new feature: automatically slowing down sim speed when one or more clients are having trouble to catch up, then speeding back up when possible. This often happens when a client joins the server, or after some large edits (and line edits are going to be large edits for most purposes).

That being said the original ideas of the MP architecture in the game assumed players building a lot less than they do, essentially because 1.0 and 1.1 were around 100x slower than the current game build, so it was impossible to scale to thousands of trains and lines, no PC could run that in 1.1. Since it was impossible, the architecture assumed smaller saves and was designed around it. But performance improved immensely in the past year and a half. Many patches and changes have been applied to the MP code to catch up, but the underlying assumptions make it impossible to fix it once for all in an easy way.

The multiplayer architecture needs a redesign and a rewrite, and I already have an idea on how to proceed, but it is a very large task which might require a full 1.x release by itself.