NIMBY Rails devblog 2024-11

Major track editor internals rewrite and predictive editing

Implementing predictive editing for the track editor turned out to be a massive undertaking. Most of November was spent in this task. And it started by realizing that the track editor internals, which have code dating back to 5 years, were in no state to accomplish the goal of predictive editing. So the first task was to basically throw away around 1/3 of the track editor code. Some of it ended up as part of the new commands implemented in October, but the majority was useless.

After gutting so much code it the editor I seized the opportunity to restructure the internals into clear separate modal interfaces for each tool. Previously the tools were implemented as a set of functions over some shared state, and a couple “controller” functions knew what to call based on the current mode. This was okay 5 years ago when there was less than 10 modes in the editor and it had 1/10 of the code. In 1.15 all of these functions and shared state have been replaced with classes with proper private state, and a generic event-based interface API for them. But the real change is on how they interact with the database, in particular when creating new tracks.

The reason is that the track creation modes and tools assumed that track creation is an incremental process on the database. For example, if you are creating continuous track by extending an existing dead end node, a new track node was created immediately, and then move commands were issued based on your mouse positions for the new track node. The player was directly editing a live database object at 60FPS. This is very bad for multiplayer, but also for single player, from both a performance and correctness point of view. Objects being in a state of half-edited should be an implementation detail of the editor, not a cross-layer responsibility of the game logic. These editor intermediate states should be private to the editor. Only after the user confirms the edit in some way (double click, final click, releasing the mouse button, etc.) should the real objects in the database be created or edited (via a command, which is then relayed over the network in the case of multiplayer).

The commands themselves, which have existed since the game was made command-based in 1.11, were also gutted out and replaced in many cases, and for all cases involving track creation. These commands are now always a single step: they create object(s) in their final positions and configurations. In 1.15 there is no editing operation in the entire game which first “creates something” and is then followed by one or more “moves something”.

But how to preserve the UX of the editor? How to make all the familiar tools behave like in the past, with the live editing feedback expected by the players? This is solved with predictive editing. In 1.15 the track editor has its own private game database. It is a fully featured database, like the main one, capable of running any editor command of any complexity. But it is empty. When the user starts an edit, like moving some track selection, the contents of the selection are copied into the predictor database, and then the commands to move the selection are run in the editor database, not the main game database. It is only when the user releases the mouse button that a final command is run in the main database (or sent to the server in MP). To make the illusion work, the game map disables the drawing of objects being edited, and a private map rendered by the track editor displays the objects stored in its database. I made this editor map display its objects with some transparency, to give a hint to the player their changes are not final until the editing action is finalized.

It was a huge amount of work to implement this and get it to a reasonable bug free state, but the results are worth it. It enables the new command-based multiplayer system, and unexpectedly, it also provides a noticeable performance improvement. The reason is that a very empty database is a very fast database. If you also delay some verifications until the final command it is even faster. I initially wanted to keep the old systems around for the sake of single player, but the new system feels so much smoother in comparison, that I decided to implement it also for single player. I also made it even faster by implementing two features: delayed track-map validation, and delayed station validation. The second one is mandatory since only the stations involved in the editing are copied in the temporary editor database, but the first one is an extra. I understand some players will dislike it, so it is possible to re-enable it in Performance game options.

Full undo rewrite

As mentioned in the October post, the internal APIs undo relied upon do not exist anymore in 1.15. So it was time to rewrite undo from (almost) scratch. In the new simplified game database used in 1.15 most of the concepts used by undo ceased to exist, but one was kept around, changesets. It’s still possible to ask the database to keep a log of everything that happens to it when a command is run. From this log it is then possible to generate another log, reversing all the operations (creating a track deletes it, moving a track somewhere moves it back to the original position, etc). This code is similar to the old systems.

The new aspect is how these reversed changesets are applied to the database when an undo operation is requested by the player. In earlier versions there existed a veritable “alternate universe” of APIs and code to apply undo changesets (actually it was just the system used in multiplayer, repurposed for undo), separate from the normal code that runs user edits. This makes sense: these changesets are nothing like what is produced by the user tools. In 1.15 these APIs have been deleted and replaced with a much simpler system. In particular, the validation of changesets, which decide if a undo can be applied, is now an automated system, rather than a loooong set of manual rules which sometimes worked, and sometimes did not (if the game only supported single player, undo validation wouldn’t be needed). Rather than declaring undo fixed or solved, I will only say I expect this new undo to work better and have less cases of invalidation.

Station editor

The new major station editor was finalized to a level adequate for its first version in 1.15. It is a bit sparse for now, it will grow new features over time. Most of the station editing features have been moved from the track editor to this new editor. It is still possible to (re)name and delete stations in the track editor for quicker editing, but all other parameters are now controlled from the station editor.

Walk link buildings do not exist in 1.15, and have been replaced with a new widget in the station editor:

Stations under the auto walk link limit are always allowed for walk transfers, and cannot be disabled. All other surrounding stations up to the 2.3km limit can be enabled manually, replacing the old function of the walk link buildings.

Other performance improvements

While profiling the new systems in large saves to make sure they did not introduce performance regressions I discovered two major problems in how the UI deals with large numbers of objects, which eclipsed any possible performance issue from the new code.

First, the simulation copies the train state of every train for every frame. This is okay with up to a few thousand trains, but with more than that it starts to become a major cost in the frame time. In 1.15 this copy is now staggered over multiple frames. This means trains are synchronized with the UI at 1/16th of the frame rate, unless they are on the currently viewed rectangle of the map, then they are updated at the full 1/1 rate. This cuts down the copy time by a massive 10+ factor for most of the common views and zoom levels. Zooming out to see most of the map nullifies this optimization, but in this case the frame time is dominated by other factors anyway.

Second, the listings displayed in editors and drop downs are now optimized for many thousands of objects. In 1.15 these listings have an internal cache system, so all the processing required to turn the data into graphics commands only happens when the data is new or edited, not on every frame. This cache in turn also allows to precisely draw only the visible portion of the scroll, for another massive win. These are not obvious or trivial optimizations, they require very tight control over data being changed and how it is rendered, to make sure it does not break when the player edits something and expects the listing to update immediately.

These optimizations are not noticeable unless your save is pushing multiple thousands of stations and trains, and maybe combined with a weaker CPU.

Multiplayer is disabled until later in the beta

There’s been many changes under the hood in this version. This means the first builds of the beta are likely to be buggy. In addition to these bugs, multiplayer will also add its own share, due to the fact it’s been completely redesigned, as explained in the October post. So in order to make sure I am chasing the right kinds of bugs, I first want the game to become reasonably bug free in single player, before I tackle multi player fixes. For this reason multiplayer is disabled in the first beta build(s).