Skip to content
You're reading the docs for Railroader 0.6 (the MoW Update), currently available on the Experimental branch.

Main.rl

Main.rl is an optional Relay script that runs after the world loads. It allows mods to add runtime behavior: custom UI, console commands, and more.

Place it at Scripts/Main.rl in your asset pack.

Called when the world loads, on every peer that has the pack loaded (see Multiplayer below). Use this to register UI extensions, commands, and other runtime behavior.

from railroader import ui
func onWorldLoad(ctx) {
ui.extend("carInspector.equipment.customize")
.id("my-customization")
.matching({"identifier": "rr/my-car"})
.priority(100)
.with(buildUI)
}
func buildUI(builder, car) {
builder.section("My Section", (b) => {
b.rebuildOnPropertyChange(car.properties, "customize.option")
b.fieldToggle("Enable Feature",
() => car.properties["customize.option"] ?? false,
(value) => { car.properties["customize.option"] = value }
)
})
}

Called when the world unloads (returning to menu), on every peer that ran onWorldLoad. Use this to clean up any resources.

func onWorldUnload(ctx) {
// cleanup
}

Main.rl handlers run on every peer that has the pack loaded — the host and all connected clients for an unscoped pack, or just whichever peers happen to have it installed for an optional-scope pack. UI extensions and property observers run on all peers that have them and synchronize through the property system automatically.

If your mod needs to perform logic that must happen exactly once across the whole session (spawning trains, initializing shared state, managing operations), use world.isHost — the engine does not gate onWorldLoad/onWorldUnload for you, even for optional-scope packs that only some peers have installed:

from railroader import world, ui
func onWorldLoad(ctx) {
// Runs on all peers that have this pack — UI setup
ui.extend("carInspector.equipment.customize")
.id("my-customization")
.matching({"identifier": "rr/my-car"})
.with(buildUI)
// Runs only on host — game state initialization
if world.isHost {
var loc = world.getMarkerLocation("yard-lead")
world.placeTrain(loc, ["rr/gp38-2"])
}
}

When writing to shared properties (via car.properties or a property object from world.getPropertyObject()), ensure only one peer writes to a given key. If both host and clients write to the same property, the values will “fight” — last write wins based on network timing, causing subtle bugs.

Properties driven by UI interactions (toggles, dropdowns) are safe — only the player who interacted writes the value, and it syncs to all peers.

Properties driven by game events (coupling, time, car state) should be guarded with world.isHost so only the host writes:

from railroader import world
func onWorldLoad(ctx) {
var cars = world.getCars("locomotive")
for car in cars {
car.observe(CarEvent.coupledDidChange, () => {
if world.isHost {
car.properties["my-mod.consist-length"] = countConsist(car)
}
})
}
}

For the full API, see the UI Module and Car reference pages.