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.
Entry Points
Section titled “Entry Points”onWorldLoad
Section titled “onWorldLoad”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 } ) })}onWorldUnload
Section titled “onWorldUnload”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}Multiplayer
Section titled “Multiplayer”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"]) }}Writing Shared Properties
Section titled “Writing Shared Properties”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.
© 2026 Giraffe Lab LLC. All rights reserved.