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;weightis FRACTIONAL (1.0 common, 0.2 rare, 0 never by chance)"coastal_land"-chanceper super-hex"coastal_water"-chance, plus"pair": truefor 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:
- 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). - 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 tilestown- 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 withfounding: 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 towncamp: 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.