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

Styles

Railroader’s style system enables modders to customize visual aspects of rolling stock: colors, text, decal images, visibility, and so forth, without having to modify the car definitions themselves or create duplicate definitions. Styles are used to drive the appearances of practically all cars in the game. The system also enables the player to individually set custom colors, text, and more using the Customize window.

As a mod author you can enable your equipment mods to participate in the built-in styles (such as for freight and passenger equipment) by adopting the same component style classes the base game’s cars use — chiefly a body renderer group and the standard decal contents.

Styles are defined in mods and are automatically applied to cars arriving at interchange based on filter parameters (reporting mark and car type), or manually applied to your own cars via the Customize window or the /style console command.

A style is composed of a series of entries, each with a:

  • Selector: which components are being styled (for example, all number board decals)
  • Property: which aspect of those components is being controlled (for example, the color)
  • Value: the value being applied to the property (for example, white)

Selectors have a specific syntax. They must contain at least one of the following:

  • Component type (e.g., Decal)
  • Component id — the unique identifier for this component
  • Component style class — a component can be part of one or more classes; by specifying a class we select all components in that class

The selector is a string; the parts above are combined with a specific syntax, usually in one of these forms:

  • ComponentType#componentId
  • ComponentType.componentStyleClass
  • #componentId
  • .componentStyleClass

To follow the example above, we could set the color of the left lettering decal to white with the following:

  • Decal#left-lettering (the selector, assuming the id of the decal is left-lettering)
  • color (the property)
  • ffffff (hex color white)

This works, but we probably want the lettering on both sides to have the same appearance. If we give all the lettering decals the same style class in the car definition — a property on the Decal component — we can target both at once:

  • Decal.lettering
  • color
  • ffffff

We’ve split those parts out to show them individually, but an actual style is defined with nested dictionaries:

"styles": {
"Decal.lettering": {
"color": "ffffff",
"font-size": 10
},
"Renderer.grabIrons": {
"color": "000000"
}
}

If you’ve done any web development this will probably look familiar to you: it’s inspired by Cascading Style Sheets (CSS).

When the components that use styles are built at runtime (such as when a car’s model is loaded), they ask the system for the values of specific style properties. For example, a Decal might ask what color its text should be. The answer is resolved by matching that component’s type, id, and class against three sources of styles, read in this order:

  1. The car definition’s own styles map — defaults baked into the model itself.
  2. The style assigned to the car, if any. The Customize window describes this as “Paint Scheme”.
  3. Inline overrides on that individual car, from the Customize window or /style set.

All three cascade together as one sheet: specificity decides, and the order above only breaks a tie between rules aimed equally narrowly. So a style’s broad Renderer rule does not override a car definition’s Renderer.smokebox, while a style’s Renderer.body does beat a definition’s Renderer.body.

Each source contributes only the properties it actually names, so a player recoloring one decal keeps the rest of the style underneath. If nothing specifies a property, a default value is used.

This has an added benefit: cars that agree on component classes can share a style. That’s how the base game colors foreign road cars spawning at interchange, and how one set of passenger car styles covers many cars without configuring each one individually.

A car only needs the classes that make sense for it — give a component the class that describes what it is, and leave out the rest. A style naming a class the car doesn’t have has nothing to apply there, so a boxcar with no roof renderer group is styled perfectly well by a style that mentions one.

To define a new style, add a CarStyle object to your asset pack’s Definitions.json:

{
"identifier": "myhandle/style-sou-green",
"metadata": { "name": "Southern Railway Green" },
"definition": {
"kind": "CarStyle",
"name": "Southern Railway Green",
"reportingMark": "SOU,SR",
"carTypeFilter": "PB*",
"styles": {
"Renderer.body": { "color": "1B3D2F" },
"Renderer.trim": { "color": "D4AF37" },
"Decal": { "color": "D4AF37" },
"Decal.railroadName": { "text": "SOUTHERN" }
}
}
}
FieldRequiredDescription
kindYesMust be "CarStyle"
nameYesDisplay name shown in the Customize window
reportingMarkNoComma-separated reporting marks for auto-selection, or "*" for wildcard
carTypeFilterNoCar type filter restricting which cars can use this style. When absent, available to all car types
carIdentifiersNoRestricts this style to specific car definition identifiers (e.g. ["myhandle/gp7"]), bypassing carTypeFilter entirely. Use this when a style depends on car-specific geometry (like a herald control texture sized for one model) rather than being paintable onto any car of a given type
carSeriesNoRoad number series this style keeps, keyed by reporting mark — see Car Spawn Identity
stylesYesSelector-to-properties map — see Selectors and Properties

Style selectors follow a CSS-like pattern with three dimensions: element type, style class, and an id.

  • Renderer — Colorizable material groups (body paint, trim, etc.). These correspond to RendererGroup components.
  • Decal — Text and image decals (lettering, road numbers, data stencils, heralds). These correspond to Decal components.

The full set of forms, and what each matches:

SelectorExampleMatches
Element only"Renderer"All Renderer components
Class only".body"Any component (Renderer or Decal) with class body
Element + class"Renderer.body"Only Renderer components with class body
Several classes".body.roof"Components carrying both classes
Id only"#left-number-board"The single Decal component whose definition key is left-number-board
Element + id"Decal#left-number-board"Same as above, scoped to Decal (ids are only meaningful for decals)
Class + id".roadNumber#left-number-board"That component, and only if it carries class roadNumber
Everything"Decal.roadNumber#left-number-board"All three parts must match

Writing several classes together is an and: ".cab.trim" matches only a component that carries cab and trim, not either one. Their order does not matter — ".trim.cab" is the same selector. This is how a modifier class earns its keep: a component classed "cab, trim" is painted by a generic ".cab" rule, and a style that wants the trim a different color targets ".cab.trim" without needing a class name of its own.

A component only participates in style matching if it has the right shape:

  • RendererGroup components match by their styleClass field (e.g. "body", "trim") — there’s no id-based selector for renderer groups, since a car typically has only a few, each a distinct paintable zone.

  • Decal components match by both a style class and an id:

    • The class comes from the decal’s styleClass field if set. Otherwise it defaults to a class derived from the decal’s content value: the five built-in values map to roadNumber, reportingMark, railroadName, data, and image, and any custom value is used as the class as-is.
    • The id is the decal’s own key in the definition’s components map — e.g. a component keyed "left-number-board" also matches #left-number-board and Decal#left-number-board.

    Selectors match case-insensitively, in every part — element, class, and id alike. Decal.RoadNumber, decal.roadnumber, and Decal.roadNumber are the same selector. Property names inside a rule (color, text, uvRect) are still matched exactly.

Prefer an explicit styleClass over the id form as your styling point. Where decals need styling independently — cab and side lettering, say — give each one the class for the role it plays plus a modifier for where it sits, and target the pair as Decal.lettering.cab, rather than reaching for #left-cab-lettering:

Decal IdStyle Class
left-cab-letteringlettering, roadNumber, cab
right-cab-letteringlettering, roadNumber, cab
left-side-letteringlettering, roadNumber, side
right-side-letteringlettering, roadNumber, side

A class names the role a part plays, so it reads for itself, and any number of components filling that role can share it. It also doesn’t depend on whatever key the definition happens to use, so it keeps working if a decal is ever renamed or reorganized.

Listing several classes, as the rows above do, makes the component a member of each. It answers to a rule naming any one of them, so .lettering paints all four decals while Decal.lettering.cab reaches only the two on the cab — no cabLettering class needed to tell them apart. Where two rules match at the same specificity, the one naming the class listed later on the component wins.

Order the list from the broadest name to the narrowest: the region first, then the finer class, then where it sits. See Standard Classes for the names to use.

Sometimes you want a specific result on a specific model: a herald whose control texture is cut for that car’s geometry, or a set of offsets that only make sense against one body. Give the car its own purpose-named style classes for those parts, and pin the style to the model with carIdentifiers so it’s never offered for a car it can’t paint properly. Reach for an id selector only for the one-off tweak that doesn’t justify a class of its own.

Pinning only controls which cars your style can be applied to. It doesn’t stop a general-purpose style matching the same reporting mark from being offered alongside it. To make yours the only ones offered, set requiresDedicatedStyles on the car definition.

Style classes and component ids can only include specific characters, since a selector has to be able to say both. See Names Within a Definition.

When multiple selectors match, properties merge in specificity order (lowest to highest):

  1. Element only ("Renderer")
  2. Class only (".body")
  3. Element + class ("Renderer.body")
  4. Two classes (".body.roof"), then three, and so on
  5. Id only ("#left-number-board")
  6. Element + id ("Decal#left-number-board")
  7. Class + id (".roadNumber#left-number-board"), and again more classes beat fewer
  8. Element + class + id ("Decal.roadNumber#left-number-board")

The rule behind the list: an id outranks any number of classes, which outrank an element type, and between two rules with the same count the one naming more classes wins. When two rules are equally specific, the one matching the class listed later on the component wins.

Higher-specificity selectors override lower ones per-property. Set defaults on an element selector, then override for individual style classes:

"styles": {
"Renderer": { "color": "2D5A27" },
"Renderer.trim": { "color": "D4AF37" },
"Decal": { "color": "FFFFFF" },
"Decal.railroadName": { "text": "SOUTHERN", "color": "D4AF37" },
"Decal.numberBoard.front": { "color": "metallic(#D4AF37, 0.8)" }
}

All renderers default to green, but trim gets gold. All decals default to white, but the railroad name gets gold text, and the front number board gets a shiny gold-metal finish.

Id selectors sit above class in specificity, for the rare case where two decals share a class but one needs a one-off tweak without splitting off a whole new class:

"Decal.numberBoard": { "color": "D4AF37" },
"Decal#left-number-board": { "offset": "0.02 0" }
PropertyApplies toDescription
colorRenderer, DecalZone/decal color — a hex string, or a paint-material function (see below)
offsetRenderer, DecalPositional nudge — "x y [z]" in meters
offset-spaceRenderer, DecalCoordinate frame for offset: "local" (default, the component’s own surface-aligned frame) or "parent" (the car’s frame)
enabledAny componentWhether the component is present on the car (boolean) — false removes it, true brings it back. If no rule sets enabled for a component, the style leaves that component as it is rather than enabling it. A style and a Relay script should not both drive the same component’s enabled!

Decals have more properties, for text and images. See Decal.

Those are the properties the game resolves itself. A style rule is an open key/value map, so it can carry any property name, and a Relay script can read whatever it likes out of one with car.resolveStyle(). Paint Maps work this way: their color1–color3 properties are defined and applied by the shared paint-map scripts rather than by the game, and those scripts read image as the decal’s control map rather than as a plain texture.

Color and the paint-material DSL: a bare hex string ("1B3D2F", with or without #) is a flat, matte color. Decals additionally understand two function forms for a metallic finish:

  • "metallic(#hex, m)" — metallic amount m (0–1) with a shiny default smoothness
  • "metallic(#hex, m, s)" — metallic amount m plus an explicit smoothness s (0–1)
  • "smooth(#hex, s)" — non-metallic, with explicit smoothness s (shorthand for metallic(#hex, 0, s))

The metallic amount m is a finish: 0 is flat enamel, 0.5 is metallic flake paint, and 1 is bare polished metal — brass, chrome, a bell — with no sparkle at all.

A decal left with no color at all gets a sensible fallback: if a named style is applied, its lettering contrasts against the Renderer.body color (light text on a dark body, or vice versa); otherwise it uses a neutral default.

The Renderer element’s built-in automatic colorizer reads color in all three forms — see Colorable Components. A bare hex sets the base color and leaves the material’s authored finish alone; metallic()/smooth() additionally override the finish, which takes effect on materials using one of the car paint shaders. Decals support the full DSL natively, no scripting required.

A car’s Customize window shows options for customizing its appearance. A style can be chosen there as well, and specific properties can be further customized by the player.

By default, styles are not exposed to the player for customization. Instead, properties must be opted in by means of a customize-<property> key whose value is the label to show:

"styles": {
"Renderer.jacketing": { "color": "1B2F17", "customize-color": "Jacketing" },
"Decal.lettering": { "color": "metallic(#CBA45A, 1.0)", "customize-color": "Lettering" },
"Decal.railroadName": { "customize-text": "Railroad Name" },
"Renderer.smokebox": { "color": "2B2B2B" }
}

In this example the jacketing, lettering and railroad name are editable and the smokebox is not.

Note that the property is only shown as editable when the car actually has that part. A shared style naming Renderer.body gives a boxcar its Body control and leaves it off a steam locomotive, whose renderer groups are classed jacketing, cab and the like. Likewise customize-text is shown only on decals with content types Text and RailroadName.

These keys work in a CarStyleDefinition and in a car definition’s own styles.

Customization can also be enabled from Relay, with ui.registerStyleProperty() — which additionally offers a slider, and can name a property no rule sets.

Railroader ships a standard vocabulary of classes that the base game’s cars and styles use. Using it is optional, but we think it keeps packs and styles readable against each other, and it lets the same style work across multiple cars. A freight car classed body and lettering takes the built-in generic styles without needing one of its own, and a style you write against these names applies to anyone else’s car that follows the same convention.

Of course, sufficiently detailed models will have classes of their own.

Renderer ClassUsed for
bodyPrimary carbody: shell, hood, tender tank. Every car with a painted body has one
roofA roof the model paints separately. Stands alone, never body, roof — whether a roof matches the body is the style’s decision
truckTruck sideframes, on Truck definitions only. A car whose trucks follow its frame targets Renderer.truck beside its frame rule

Every decal already answers the class matching its content — roadNumber, reportingMark, railroadName, data, or image. Put lettering in front of that class to make the car’s painted words and numbers reachable as one region:

{ "kind": "Decal", "content": "RoadNumber", "styleClass": "lettering, roadNumber" }

Decal.lettering then letters the whole car in one rule, while Decal.roadNumber still picks the number out of it.

lettering means paint. A decal that renders a physical object — a lit number board, a cast brass plate, a herald — stays outside it, so a style’s lettering color can’t reach it. The test is what the decal sits on, not what the component is named.

The classes below are locomotive and tender vocabulary. No freight or passenger car in the base game declares any of them.

body on the carbody, roof where the model has one, and lettering on every stencil. Freight lettering is nearly all Data content — capacity, light weight, dimensional stencils — which is still lettering:

{ "kind": "RendererGroup", "styleClass": "body" },
{ "kind": "Decal", "content": "Data", "styleClass": "lettering, data" }

A generic interchange style then paints the car with two rules:

"Renderer.body": { "color": "6A1B0B" },
"Decal.lettering": { "color": "FFFFFF" }

Cabooses (NE) style as freight.

The same two classes. Every passenger decal is a road number or a railroad name, and styles letter both in one color.

ClassUsed for
cabCab exterior. Cab trim is cab, trim; a cab roof carries roof alone
underframeFrame, sills, underbody equipment
pilotPilot, plow, coupler shroud. Sub-parts ride on it: pilot, beam, pilot, bumper, pilot, belt
handrailsHandrails and railings
grabIronsGrab irons. Its own region, not a kind of handrails — a model declaring both colors them apart
pipingExposed piping and conduit
interiorCab interior
numberBoardNumber board housing. The lit numerals on it are a decal classed bare numberBoard, with no content class, so a lettering color leaves them alone

Where a style colors parts apart by position, a placement token follows the region: grabIrons, nose, grabIrons, rear, lettering, roadNumber, cab. front, side, left, and right are reserved for the same purpose but unused today.

Steam locomotives have no body — a generic style paints them with a bare Renderer rule and overrides.

ClassUsed for
jacketingBoiler jacketing
smokeboxSmokebox and front
domesSteam and sand domes. A number on one is lettering, roadNumber, dome
cylindersCylinder block. Caps are cylinders, caps
driversDrivers and wheel centers
tiresWheel tires
appliancesPumps, generators, boiler fittings
runningBoardRunning boards and walkways
headlightHeadlight casing
bellMountBell and bell mount
axleEndsAxle ends and hub caps
spotPlateThe painted inset behind the smokebox number plate. Its numerals are a decal classed roadNumber, spotPlate — cast brass, not lettering, so the car colors them itself

Tenders are Car definitions: body for the tank sides, body, deck for the deck, body, coalBoard for the coal boards, plus handrails, runningBoard, and truck as on the locomotive.

A second class must name a kind of the first. cab, trim and body, deck hold; body, roof doesn’t, because a roof is a separate assembly rather than a kind of body.

For classes of your own — exhausts, sideSkirts, fuelTank — use lowerCamelCase, plural only for a set of like parts (domes, drivers), and don’t reuse a standard name for something else. A group classed body must be the carbody, or generic styles will paint the wrong thing.

A Car definition can also carry its own built-in styles map — the same selector-to-properties shape as a CarStyleDefinition, but baked directly into the car rather than selectable as a separate style:

{
"kind": "Car",
"carType": "XM",
"styles": {
"Decal.dataStencil": { "font-size": 8 }
}
}

This is the lowest layer of the style cascade. It’s useful for defaults that should hold regardless of style, like a decal color for one car model.

Prop definitions take styles too. A prop declares RendererGroup components and a base styles map with the same selectors as a car (Renderer.siding). A DetailModel on the prop is recolored with it.

A placement in map data can override these with its own styles map, which sits on top of the definition’s styles in the cascade. Props have no style definitions or player overrides.

A customize-color key in the definition’s styles adds a row to the map editor’s Colors section. Styles apply when the prop loads, and live while you edit in the map editor. They can’t be changed at runtime or from scripts.

The Message Log reports placement selectors that match nothing on the prop and entries that aren’t a property map, under Prop Styles. It also reports a RendererGroup whose material isn’t in the model’s MaterialMap.

/style inspects and tweaks a car’s live style overrides from the in-game console — useful for dialling in a look before baking it into a style definition.

/style get <car> [<selector> <property>]
/style set <car> <selector> <property> <value>
/style export <car> <filename>
/style export-full <car> <filename>
/style import <car> <filename>
  • get with no selector/property dumps the car’s whole style KV value; with both, it shows the resolved value plus the raw inline override (if any).
  • set writes an inline override — the same kind of change the Customize window makes. value of null clears it.
  • export writes a .rrstyle variant: a reference to the car’s base style plus its inline overrides. export-full writes a snapshot: the fully resolved selector→property map with no base reference, flattening everything into one self-contained style. Both save to Documents/Railroader/Styles/<filename>.rrstyle.
  • import applies a .rrstyle file to a car, dropping any selectors the target car doesn’t actually have (e.g. importing a style authored for a different decal layout).
/style set MYRR-1234 Renderer.body color "metallic(#1B3D2F, 0.6)"
/style set MYRR-1234 Decal.numberBoard.front offset "0.1 -0.05"
/style get MYRR-1234 Renderer.body color
/style export MYRR-1234 my-green-style
/style import MYRR-5678 my-green-style

A car that arrives without a style is given one automatically. This covers cars whose reporting mark is already settled before their paint is — one the player buys, a catalog photo, a car loaded from a save written before the style system. Foreign traffic spawning at interchange does not come through here: its mark and style are chosen together, described under Car Spawn Identity.

The pick is made from the styles the car is eligible for, preferring those whose reportingMark lists the mark it already carries, and is stable for a given car. A car no style suits keeps its definition’s own appearance rather than borrowing one written for another road. Setting requiresDedicatedStyles on the car narrows the pick to the styles that name it.

reportingMark only steers this automatic pick — it never stops a player choosing the style by hand in the Customize window.

Players can change styles on owned cars using the Customize window via the Car inspector:

  • Paint Scheme dropdown to select a style
  • Per-property color and text overrides, one per control a style rule or an asset pack asked for
  • Copy to Coupled applies the style to all coupled cars owned by the player

Every control is declared. A style rule asks for one with a customize-<property> key; an asset pack can also register one from Relay — with a slider, or for a property no rule sets — via ui.registerStyleProperty().

Each car’s active style is stored in its properties under the style key. It has two sub-keys:

  • id — The identifier of the base style definition (e.g. "myhandle/style-sou-green")
  • styles — Optional inline overrides from player customization or /style set, using the same selector-to-properties format as the definition

These inline styles are the top layer of the cascade, so a player override (like changing a lettering color) rides on top of the style rather than being folded into it. If the style can no longer be resolved — its asset pack was removed or disabled — the id and the inline overrides both stop applying; the car falls back to its definition’s base styles.

{
"id": "myhandle/style-sou-green",
"styles": {
"Decal.railroadName": { "text": "MY RAILROAD" }
}
}

You can read or write the style via the car’s properties using the "style" key in Relay scripts — see Car → Styling for getComponentsForStyle()/resolveStyle() — as well as using the /style and /set console commands.