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

Data.rl

Data.rl is an optional Relay script that runs during game startup and during map loading. It allows mods to modify, extend, or replace definitions and to mutate map data JSON before it is loaded.

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

Data.rl exposes two optional handlers:

  • modifyDefinitions(context) — runs once during game startup, before the map loads. Modifies object definitions.
  • modifyMap(map) — runs each time map data JSON is loaded, after parsing but before deserialization. Mutates the map JSON.

Data.rl doesn’t run in the in-game editor, so the editor shows and saves each pack’s own definitions and map data.

Define a modifyDefinitions function that receives a context object:

func modifyDefinitions(context) {
context.modify("rr/boxcar-40ft", (obj) => {
obj["definition"]["basePrice"] = 8000
})
}

The context provides methods for working with definitions. Each method receives definitions as dictionaries matching the Definitions.json structure, with identifier, metadata, and definition keys.

Modify an existing definition in place. The handler receives an independent copy of the definition dictionary.

context.modify("rr/boxcar-40ft", (obj) => {
obj["definition"]["basePrice"] = 8000
obj.merge({
"definition": {
"components": {
"my-script": {
"kind": "Script",
"scriptAsset": "mydomain/my-script"
}
}
}
})
})

The handler’s return value is ignored — only changes made to obj are kept. Use the mutating forms (obj[...] = ..., obj.merge(patch)) inside the closure; the non-mutating obj.merging(patch) builds a new dictionary and silently discards it here.

Apply an RFC 7396 merge patch directly to a definition without a closure. This is the preferred way to add or update fields when you do not need to read existing values first.

context.merge("rr/boxcar-40ft", {
"definition": {
"basePrice": 8000,
"components": {
"my-script": {
"kind": "Script",
"scriptAsset": "mydomain/my-script"
}
}
}
})

The patch rules are the same as map.merge: objects merge recursively, null deletes a key, arrays replace wholesale, and everything else overwrites.

Create a new definition by copying an existing one and applying changes.

context.modifyAs("rr/boxcar-40ft", "myhandle/weathered-boxcar", (obj) => {
obj["metadata"]["name"] = "Weathered 40ft Boxcar"
obj["definition"]["basePrice"] = 6000
})

The new identifier is used exactly as given. Prefix it with your pack’s domain so it doesn’t collide with other packs.

Completely replace a definition with a new dictionary.

context.replace("rr/boxcar-40ft", {
"identifier": "rr/boxcar-40ft",
"metadata": { "name": "Replaced Boxcar" },
"definition": { "kind": "Car", ... }
})

Remove a definition from the game.

context.remove("rr/unwanted-car")

Define a modifyMap function that receives a map context:

func modifyMap(map) {
map.modify((data) => {
// data mirrors the on-disk MapData.json structure: data["graph"], data["ops"], etc.
})
}

The handler runs once per map load, after the JSON has been parsed but before it is turned into runtime objects. Mutations from earlier packs (in dependency order) are visible to later packs. If two packs modify the same field, the pack loaded later wins.

map.merge is the preferred way to patch map data for most mods. Pass a Relay dictionary — typically loaded with json.load() — and the game merges it into the existing map data:

import json
func modifyMap(map) {
var patch = json.load("ExtraTrack.json")
map.merge(patch)
}

The patch argument must be a dictionary; passing any other type throws. The merge rules are:

  • Objects are merged recursively into the existing object.
  • null deletes the corresponding key (a no-op if the key is absent).
  • Arrays replace the existing value entirely — elements are not merged individually.
  • Everything else (strings, numbers, bools) replaces the existing value.

Example: updating an industry component field

To increase the log unload rate at Whittier Saw Mill, supply only the field you want to change. All other components in the industry — and all other industries — are untouched:

{
"ops": {
"areas": {
"whittier": {
"industries": {
"whittier-sawmill": {
"components": {
"logs": {
"carUnloadRate": 108000000
}
}
}
}
}
}
}
}

Example: adding a new node and segment

To insert new track, add entries that do not yet exist. The game creates them on load:

{
"graph": {
"nodes": {
"myhandle/spur-end": {
"position": [1234.5, 102.3, 567.8],
"rotation": [0, 0.707, 0, 0.707]
}
},
"segments": {
"myhandle/spur-01": {
"a": "myhandle/spur-end",
"b": "rr/existing-node"
}
}
}
}

Or delete segments:

{
"graph": {
"segments": {
"myhandle/spur-01": null,
"myhandle/spur-02": null
}
}
}

map.merge() and map.modify() can be combined freely within the same handler. Both see mutations from earlier packs (in dependency order) already applied.

This follows RFC 7396.

Pass a handler that receives the parsed map JSON as a dictionary and mutates it in place. Changes are written back to the underlying JSON before deserialization.

map.modify((data) => {
data["graph"]["nodes"]["myhandle/extra-node"] = {
"position": [0, 0, 0],
"rotation": [0, 0, 0, 1]
}
data["graph"]["segments"]["myhandle/extra-seg"] = {
"a": "myhandle/node-a",
"b": "myhandle/node-b",
"groupId": ""
}
})

Nodes, segments, and groups are dictionaries keyed by ID.

As with context.modify, the handler’s return value is ignored — mutate data rather than returning a new dictionary.

If the handler throws, the error is reported to the in-game message log and the remaining packs continue. Any changes the handler made before throwing are discarded.

The json module provides sandboxed JSON file loading in pack-scoped scripts. It’s available anywhere a script runs with knowledge of its owning asset pack: Data.rl, Main.rl, and any Script component whose scriptAsset points at a file inside a pack. It is not available to a Script component’s scriptInline source, since an inline script has no file of its own to scope paths against.

import json

Load a JSON file and return its contents as a Relay dictionary.

var data = json.load("MyData.json")

The path is relative to the asset pack root. Absolute paths and directory traversal (e.g. ../) are rejected. Throws if the file does not exist.