NIMBY Rails devblog 2022-12

Discarded station tape tool, replaced with platform promotion

At the end of last month blog post I shared a new tape tool coming in the (then unreleased) 1.6 beta, the station tool. At the time it was still half implemented. It was a challenging tool, since the idea was to be able to place a station on any existing set of tracks and have it reproduce all the features of the stand alone station tool. Some of these features (signals and auto scissor switches) make this task very complex. But the feedback I got from the post was more on the lines “cool but I would rather use the stand alone tool most of the time”, so I decided to simplify the tool to the bare basics, and turn it into the platform promotion tool.

This new tool detects pairs of tracks under the mouse cursor, and if these pairs of tracks would validate as a station platform, allows to turn them into a platform. Combined with the split track tool it allows to place any desired platform over existing tracks. I also added a “platform demontion” option in the track properties panel, for the inverse operation on existing platforms.

Track network rebuilding determinism

As a side effect investigating game sim indeterminism, I discovered the track network itself was a source of indeterminism. Basically if was possible for two clients with the exact same save file to dependant recreate tracks (parallel children and branch tips) in very slightly different places (with differences in the order of 1/1000 of a mm, but enough for the maths to not be identical). This could cause timing differences on the trains, to the point of sometimes invalidating runs.

The root of the issue is how tracks are processed from their data to the “traced” rendering of the track. Tracks are stored only by their control point and their relationships with parents, if any. Then they are converted into actual geometry as required by game logic or visual rendering. For independent tracks this process if robust, but once tracks start depending on a parent, it is vital to keep a deterministic order of track rebuilding for this to always produce the same results. It turns out game loading was an optimized for loading speed system, and normal game run time was using a different procedure, more oriented to correctness. These procedures did not produce identical results (again, just in the order of 1e-6, but enough to matter).

The fix was to make the loading processing system used in game loading to be as correct as the normal procedure, and stop using the normal procedure. This make loading slightly slower but removed a persistent source of indeterminism. This eliminated one of the sources of mysterious stale trains on loading a game in 1.6, for example.

Pathfind probes for trains

It is now possible to visualize the current (projected) path of a running train:

There’s options for both tracing to the current train destination and for tracing to any point in the track network by just clicking on a track.

Bulk train editing

The train listing now allows multiple selection with shift-click, and when the selection is two or more trains, a new bulk options panel is displayed, allowing to change the order mode, issue interventions or sell the selected trains:

These are still limited options for this interface, but now there’s at least a place to keep adding more bulk train tools, just like the track editor bulk tools have been growing for the past couple releases.

Station rebuild speedups

In another series of improvements to basic game systems which came to be thanks to multiplayer fixes, station rebuilding was optimized for much improved performance. Station rebuilding is the process of recognizing when platform footprints collide and thus require a reevaulation of station composition.

The first optimization was to introduce an “environment hash” for each station. This hash encodes the positions of all platforms in the bounding box of the station platforms, even of platforms not belonging to the station. Then on station rebuild, a snapshot is taken of that bounding box, and a new hash is computed. If it matches, there is no need to rebuild the station.

The second one was to replace the existing procedure used for summing the station population coverage. It was based on first calculating the geometry of the are of influence, triangulating it, and then rasterizing the triangles over the population and buildup maps. Calculating the geometry and its triangulation is quite fast, but the rasterization was very slow. It could have been optimized, but if I was going to write an optimized triangle rasterized, why not just write a custom, very efficient rasterizer for the exact problem at hand, which is just based on circle centers and distances? So I wrote what is essentially a tweaked Voronoi field rasterizer with SSE2. It ended up being 25x faster than the old triangle rasterizer so it was a big win.

The third optimization was again looking at what the game does on loading, and during gameplay. It turns out there was another optimized path for loading which I never enabled for gameplay because it depended on a certain part of the game data (the track spatial index) to always be 100% correct at the moment station rebuild. Due to how the old track rebuilding worked this was only true on game load, but now thanks to the game always using the bulk track rebuilder, it was always true. I switched to this optimized path for station rebuild and the speedup became very noticeable when editing large stations.

Track editor keybind hold mode

I often find myself quickly toggling tools for one or two actions, then going back to selection mode. An optional mode now turns the track editor keybinds into “hold” keybinds to make this more natural:

This means that for using a track editor tool, you must keep its keybind pressed at all times. When releasing the key the track editor goes back selection mode.

Private simulation mode for multiplayer

With the split and tape tools more or less finished, and the issues found with track validation and station validation under control, it was time for the “public” part of version development, with the beta deployed. Multiplayer usability was the focus and I as explained back in November, I failed in my original idea of “fixing” multiplayer by having a perfectly deterministic simulation. But that did not mean it was the only way of achieving the goal of drastically reducing the required bandwidth. Since back in the v1.1 days I had an idea in mind I wanted to try. What if the multiplayer session just didn’t care about the simulation?

To try this idea I introduced a new multiplayer mode in the 1.6 beta: private simulation. In this mode each client (including the hoster) has a private view of train motion, pax and the clock. Multiplayer coordination is only done for edited player objects, like tracks or lines. The existence of a train itself and its topmost level order (just which order mode it is running) is also part of this sharing, but never the location or current run of the train.

It proved successful, and it allows to host must larger games than with shared simulation, at the gameplay cost of not experiencing the same simulation as your friends, and having the unlimited money mode force enabled. It’s still a bit too high in the baseline bandwidth cost, with about 20KB/s per client, and I will look into reducing this further in the future.

Direct connection mode for multiplayer

But even with this new mode, the background problem persisted. The multiplayer is essentially limited by the networking layer underneath: the Steam Relay Network. This CDN, provided for free by Steam, is optimized for very low latency and ease of use (since it relays connections, no open ports or NAT punching is required), with the tradeoff of being bandwidth limited, at least in my experience, to just 128KB/s per client. Can’t look at a free horse in the teeth I guess. This low bandwidth limit is crippling for how the simulation state is synchronized in the game, which is much, much larger than your average multiplayer game. If I could remove the cap, shared simulation games would only be limited by how much upload bandwidth the hoster has.

Fortunately the very same API provided by Steam for its Relay network also allows to create normal UDP server sockets, so I implemented a new multiplayer option to host games in this way. It then becomes the responsibility of the hoster to properly forward and firewall the listening UDP port. When I tested this mode shared simulation stopped having a bandwidth ceiling and it became CPU bound, as it should be. It even helped with private sim, since the initial game synchronization still needs to send multiple MB to the client.

1.6 beta ends soon, 1.7 has been planned

I will end the 1.6 beta series soon. After the huge, long effort that was 1.5 I preferred to have a smaller release cycle for 1.6, and it will end soon, with only bugfix builds until it is made the default.

I’ve been also planning and researching what to do for 1.7, and I’ve decided on the two headline features:

  • Save sharing: a system for sharing saves with other players will be implemented. I want to make this a bit more interesting that just selecting some file in some listing, but I don’t want to disclose too much about it, since it is a quite complex idea and it might change or be discarded.

  • Freeform train compositions: players will be able to compose trains from train units in the train editor (a train unit is every indivisible part of a train, like a locomotive or coach car). A listing of all available units will be provided and a new editing system will be implemented to mix and match them following some set of rules. I will make sure to give modders control of how their creations are used by this feature, including disabling it completely (which will be the default).