Properties
Railroader uses a properties system for persistence and multiplayer state synchronization. This system is designed for low-frequency updates. Train and player avatar position updates go through lower-level mechanisms that aren’t designed to be modded.
Properties are stored on an object with a globally unique identifier — all Cars have them, and Ops industries and other game state use property objects too. Properties for an object are essentially a dictionary of string keys to values, similar to a JSON object.
For a mod, this is how you attach your own data to a game object without needing a save file format of your own — a flag on a car, a counter on an industry, a scenario flag on the world itself. Prefix your keys with something identifying your mod ("mymod.tag") so they can’t collide with the game’s or another mod’s.
Reading and writing from a script
Section titled “Reading and writing from a script”A script reaches a property object through world.getPropertyObject() or, for a car, its properties. Values are read and written with index syntax, and a key that has never been set reads as null:
var props = world.getPropertyObject("some-industry-id")props["mymod.visits"] = (props["mymod.visits"] ?? 0) + 1An unset key reads as null, so supply a default with ?? before using a value in arithmetic or a </> comparison — those throw on null. A car can also start out with keys already set, by listing them as defaultProperties on its definition, which avoids the question entirely.
See Properties in the Relay API reference for the full surface, including nesting a group of related keys under a single entry with scoped().
Multiplayer and permissions
Section titled “Multiplayer and permissions”Writing to a property causes that change to be propagated to the host, who then sends it to other clients. Some objects and properties can only be written by players with a certain permission level (President, Officer, Trainmaster, Dispatcher, Crew, Passenger).
Because every client runs the same scripts, a write made in response to a game event happens once per player unless you guard it. Wrap writes to shared state in a world.isHost check so only the host performs them — the resulting change reaches everyone anyway. See Multiplayer for details and examples.
Observing changes
Section titled “Observing changes”Specific properties on an object can be observed. A common pattern is to observe a property and use the callback to respond to changes. We strongly recommend not polling for changes.
var observer = null
func onModelLoad(car) { // The third argument is called now with the current value, and again on every change. observer = car.properties.observe("mymod.hatchOpen", true, (value) => { car.components["Hatch"].enabled = value ?? false })}
func onModelUnload(car) { observer?.dispose() observer = null}observe() returns a disposable handle. Hold onto it and call dispose() when whatever you set it up for goes away — a script component’s model unloading, a UI window closing — or the callback keeps firing for an object you’re no longer using.
© 2026 Giraffe Lab LLC. All rights reserved.