Skip to main content

The entire game content is mod data. The base game itself ships as the base_game mod in res://mods/base_game/. Anything it defines, a mod can redefine; disable it entirely and a total-overhaul mod can replace the whole content set.

  • Official mods live in the game's own mods/ folder (res://mods).
  • Community mods live in user://mods. On Windows that is %APPDATA%\WhistleStop\mods\.

The Mods page in the main menu shows both, with enable and disable checkboxes.

Package structure

<mods root>/<mod_id>/
  mod.json                 manifest (nothing else goes in here):
                           {"name": "...", "version": "1.0",
                            "author": "...", "depends": ["base_game"]}
  rules/*.json             economy knobs (flat key -> value)
  goods/*.json             tiers + fixed prices
  resources/*.json         map geography + spawn rules
  facilities/*.json        the production chains
  locomotives/*.json       engine kinds
  towns/*.json             the town growth ladder
  names/*.json             name pools: towns (prefixes/roots/suffixes)
                           and applicants (first_male/first_female/
                           surnames/company_patterns for Form 27-B)
  substitutes/*.json       demand substitutions
  achievements/*.json      goals (the in-game kind)
  avatars/*.json           skin tones + banker dialogue
  start/*.json             the campaign opening map (see below)
  sounds/*.json            explicit links: event key -> sound asset key
  <category>/assets/models/*.glb   models (flat key namespace)
  <category>/assets/icons/*        indexed for future use
  <category>/assets/sounds/*       sound files (ogg/wav/mp3, flat key
                           namespace; played where a sounds/*.json
                           entry links an event to their key)

Every category folder is optional. Files are free-form: any *.json, each holding one or more entries keyed by name. One file per entry (how base_game ships facilities) or everything in one file, your call. Files read in sorted order within a mod.

Dependencies and load order

depends lists mod ids that must load first. Load order is: dependencies before dependents; ties broken official-first, then alphabetically. Later mods win: the same key (facility name, good, achievement id, model key and so on) replaces the earlier entry whole.

A mod whose dependency is missing or disabled is auto-disabled, and the Mods page shows why. Dependency cycles disable every mod in the cycle. Disabling base_game is allowed, but the game refuses to START unless the enabled set still defines a town ladder, at least one facility and a locomotive. Overhaul mods must bring their own.

Categories

rules

Flat {key: value}; keys are the lowercase names of the game's economy constants. The full list is in mods/base_game/rules/economy.json: wagon costs, loans, interest, town and facility economy timing, price bounds, immigration. Unknown keys warn and are ignored. Engine geometry (track gauge, hex math) is not moddable.

goods

{"wood": {"tier": 1}, "woodchips": {"tier": 1, "price": 3}}

tier feeds town-demand sorting; price pins a base price (waste goods). Everything without a price derives from the recipe that produces it, so a new chain prices itself.

"food": true puts a good in the balloon-provisions catalog. "spoils" (default true for food) decides whether it holds the sea-odds discount: smoked fish keeps, fish does not. Feed crops like corn are NOT food, and the aeronaut declines them.

Stock piles

goods/assets/models/stock_<good>.glb is the good waiting at a station, replacing the colored bar. Exactly three nodes named pile_small / pile_medium / pile_large (the game shows ONE, by stored fraction, in thirds), pivot ground-center, y=0 ground contact, no ground pad, radius no more than 0.30, height no more than 0.35, no animation. Goods without a stock model keep the colored bar.

Goods icons

goods/assets/icons/<good>.svg; spaces in good names become underscores, so smoked_fish.svg. A 64x64 canvas, artwork inside x/y 4..58, a #141210 keyline at stroke 3.4 with round joins, one flat fill plus ONE darker shade. No gradients, and no detail below roughly 4px, because icons render at 24 to 48px.

The UI composes them with wagon_flatbed.svg: the load is drawn under the wagon frame at translate(10.9, 0.6) scale(0.66), and the load's OPACITY is the fill level, 0.28 empty to 1.0 full. Never bake combined icons. Goods without an icon fall back to a tinted crate on the flatbed.

resources

{"gum trees": {"spawn": "inland", "weight": 1}}

spawn is one of:

  • "inland" - cluster lottery; weight is FRACTIONAL (1.0 common, 0.2 rare, 0 never by chance)
  • "coastal_land" - chance per super-hex
  • "coastal_water" - chance, plus "pair": true for two adjacent tiles

base_game entries carry an explicit order that keeps map generation reproducible; mod resources join after them. "prospect_only": true keeps a kind out of natural campaign generation entirely: surveyors place it on demand when a town's appetite calls for it (base_game uses this for clay). Sandbox ignores the flag.

"biome" decides which band the kind spawns in: "coast" (default), "plains", or a list such as ["coast", "plains"] (base_game: forest). Bands are concentric super-rings. Surveys ignore the lock; chance spawns honor it.

facilities

Same shape as base_game's files: tier, inputs, outputs, interval, optional byproducts and boost_input, plus these extras:

Key Meaning
extractor_of the resource it must sit beside
grows_resource the facility grows its own resource
extensive occupies surrounding land
field_model model used for its fields
station_tag the name a serving station suggests
biome allow-list of bands the facility may FOUND in, checked at founding only and never retroactively (base_game: farm, sheep farm and cattle ranch are plains; corn farm is coast)
player_built never founds or spawns on its own; the player places it (base_game: balloon shed)
essential the FIRST founded instance of the kind is too essential to fail: the bank lends it money instead of closing it, and the debt blocks growth until repaid (base_game: the eight starter-chain kinds)

locomotives

{"express": {"cost": 120, "speed": 1.6, "cap": 2, "drag": 0.1,
             "upkeep": 20, "steam": true}}

towns

{"levels": [...]} REPLACES the whole ladder; see mods/base_game/towns/levels.json for the shape. {"patch": [{"level": 4, "kickoff": {...}, "upkeep": {...}}]} amends single levels instead.

names

{"towns": [...], "prefixes": [...], "suffixes": [...]} replaces the respective pool when present.

achievements

{"my_goal": {"order": 10, "title": "...", "desc": "...",
             "condition": {...}, "reward": "none"}}

desc is the citation shown in the Awards panel, sealed until earned.

Condition DSL: town_level {level}, trains_running {count}, deliveries {count}, money {amount}, good_delivered {good, count}, towns {count}, stations {count}, signals {count}, dual_pieces {count}, track_pieces {count}, line_stops {count}, handcars_running {count}, single_train_profit {amount}, months {count}, crashes {count}, renames {count}, loan_maxed {}, cash_no_loan {amount}, bankrupt {}, all_others {} (every other goal done, for completionist capstones).

"hidden": true conceals the title AND citation until earned. reward: "ring" goals form the sequential campaign chain, by order; mods should normally leave reward out.

Platform (Steam) achievements only report on vanilla runs: exactly base_game enabled, nothing else.

avatars

{"skin_tones": ["#..."],
 "banker_male": [["...level 0 lines..."], ["...level 1..."]],
 "banker_female": [...]}

The banker draws one line from the pool matching the profile's bankruptcy count.

start

The campaign opening. There are two ways to define it, and a mod may ship both:

  1. A hand-authored map (start/map.json, a "map" key): explicit coordinate to tile pairs. This is what base_game ships, so the vanilla opening is an authored map, made in the in-game editor (Mods -> Modding tools -> Map editor).
  2. Procedural dials (start/start.json): knobs for a generated opening, used only when NO enabled mod ships an active "map".

Whenever a "map" is active it replaces the procedural opening entirely and the dials are ignored. Precedence follows the normal merge rules: later mods overwrite the whole "map" key. The merge can overwrite keys but never REMOVE them, so a mod that wants the procedural generator back ships "map": false. That deactivates any earlier mod's map and hands control to the dials.

Hand-authored maps (start/map.json)

Coordinate to tile pairs. Beyond the authored area the infinite procedural world continues; the ocean derives its level from your boundary water unless you override it.

{
  "map": {
    "0,0":  {"t": "land", "camp": true},
    "-3,0": {"t": "land", "town": 2},
    "2,-2": {"t": "land", "res": "forest"},
    "1,-2": {"t": "land", "fac": "woodcutters hut", "founding": true},
    "1,0":  {"t": "water", "harbor": true},
    "3,0":  {"t": "water", "res": "coast fish"}
  },
  "coast_phase": 0.0,
  "ocean_cut": 4.2
}

coast_phase (shore wiggle beyond the map) and ocean_cut are optional; ocean_cut is derived from boundary water when absent.

Keys are "x,y" axial coords. "t" is "land" (default) or "water". One overlay per tile:

  • res - a resource kind; water kinds go on water tiles
  • town - a level index into the town ladder. Note that a high-level town owes its full upkeep immediately, so give it supply.
  • fac - a facility name. This marks the PREFERRED site, not a day-one building: the facility founds there when demand for it arises, and if the hex is occupied by then (tracks, a station, anything) the normal founding rules take over. One exception: one facility opens at generation, the FOUNDING one, marked with founding: true. The intro reveal plays over it ("A woodcutters hut opened by the forest"). Without the flag, the fac tile nearest the camp is the founding one, ties broken by file order. Flag it and stop worrying.
  • harbor: true - water only; links to an adjacent town
  • camp: true - land, exactly one: where the settlers land

Unknown kinds are skipped, so a partial overhaul degrades gracefully. Tiles you do not author inside the start area follow the same shoreline rule as the world outside.

The recommended workflow is the Map editor. It opens with the merged map of your enabled mods loaded, Save As scaffolds a complete mod under user://mods, and the Validate button runs the full checklist: a single camp, harbors wet and linked, extractors beside their resources, and every resource chain able to found its facility under the spacing rules.

Procedural dials (start/start.json)

The generated opening, active only without a map:

{
  "seed": 424242,
  "ocean_fraction": 0.3333,
  "estuary_length": 3,
  "estuary_funnel": true,
  "clusters": [
    {"kind": "forest", "min": 4, "max": 6, "count": 3, "phase": "founding"},
    {"kind": "clay",   "min": 5, "max": 8, "count": 2, "phase": "trades"}
  ],
  "coast_fish":  {"kind": "coast fish", "count": 3, "min": 4, "near": 5, "far": 8},
  "salt_flats":  {"kind": "salt flat",  "count": 1, "min": 4, "max": 8},
  "facility": "woodcutters hut",
  "harbor": true
}

seed is the fixed start: the same seed gives the same layout. ocean_fraction is the share of the start mega-hex under water, estuary_length the channel tiles carved inland (0 is an open beach), and facility the one founded beside the camp, whose demand is goal 1.

min, max, near and far are hex distances from the camp. Cluster phase fixes placement order around the coastal rolls ("founding" before, "trades" after); reordering changes rng consumption and therefore the whole layout. Unknown resource kinds and facilities are skipped.

Placement self-verifies against the spacing rules (facility_town_gap for processors, extractor_town_gap for resource-pinned extractors, both in rules): clusters and salt flats re-roll until every starter chain can legally found its facility, so tuning the gaps never silently strands a chain.

Models

<category>/assets/models/*.glb, looked up by filename in one flat namespace. Mods can add new models and re-skin base ones; the same key wins by load order. The full designer contract (scale, orientation, animation clips, state nodes, wagon_<good>.glb and bank_NN_* disguise nodes) ships with the game in the base_game mod's own README.

Official mods' models go through Godot's import pipeline; user-mod models load at runtime via glTF. Missing models fall back to procedural placeholders, so data-only mods work while you iterate.

Robustness rules

  • Saves store names, never indices. Loading a save whose mods are missing turns modded facilities into RUINS with the kind preserved (re-enable the mod to restore), and unknown goods become worthless. A news line reports it.
  • Co-op hard-blocks on mismatch: host and client need the identical enabled-mod content, hash-checked on join.

Developing a mod

The Mods page has "Refresh mods" (rescan now) and "Refresh mods on every new game and load". With the second one on you never restart the game to see changes, and re-exported models reload too.

Worked example

user://mods/rubber_industry/:

mod.json                     {"name": "Rubber Industry", "version": "1.0",
                              "depends": ["base_game"]}
resources/rubber_trees.json  {"rubber trees": {"spawn": "inland", "weight": 1}}
facilities/plantation.json   {"rubber plantation": {"tier": 1, "inputs": {},
                              "outputs": {"rubber": 1}, "interval": 9.0,
                              "extractor_of": "rubber trees",
                              "grows_resource": true, "station_tag": "Plantation"}}
facilities/bootmaker.json    {"bootmaker": {"tier": 3,
                              "inputs": {"rubber": 2, "fabric": 1},
                              "outputs": {"boots": 2}, "interval": 8.0}}
goods/goods.json             {"rubber": {"tier": 2}, "boots": {"tier": 3}}
towns/patch.json             {"patch": [{"level": 4, "kickoff": {"boots": 6},
                              "upkeep": {"boots": 1}}]}
achievements/boots.json      {"boot_town": {"title": "Deliver 50 boots",
                              "condition": {"type": "good_delivered",
                              "good": "boots", "count": 50}}}
facilities/assets/models/rubber_plantation.glb   (optional)
goods/assets/models/wagon_boots.glb              (optional)

Start a campaign: rubber trees spawn, plantations get founded beside them, level-4 towns demand boots, and the achievement fires at 50, without a single line of GDScript.


Questions, or something in here that turns out to be wrong? The Discord is where modding gets discussed.