Custom Cities
Add new cities with custom maps and data
The cities API lets you add custom cities with their own maps, demand data, and buildings.
Register a City#
Use registerCity() to add a new city to the game.
window.SubwayBuilderAPI.registerCity({
name: 'Montreal',
code: 'MTL',
description: 'Build metros beneath the Underground City',
population: 4_300_000,
initialViewState: {
zoom: 13.5,
latitude: 45.5017,
longitude: -73.5673,
bearing: 0
},
minZoom: 10,
// Optional: Custom thumbnail for city select screen
mapImageUrl: 'http://127.0.0.1:8080/MTL/thumbnail.svg'
});City Properties#
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
code | string | Yes | Unique code (uppercase, e.g., 'MTL') |
description | string | No | City description |
population | number | No | City population |
initialViewState | object | Yes | Starting map position |
initialViewState.zoom | number | Yes | Initial zoom level |
initialViewState.latitude | number | Yes | Initial latitude |
initialViewState.longitude | number | Yes | Initial longitude |
initialViewState.bearing | number | No | Initial rotation (degrees) |
minZoom | number | No | Minimum zoom level |
mapImageUrl | string | No | Custom thumbnail URL |
Modded Cities Tab#
When you register custom cities, they appear in a dedicated "Modded" tab in the city selector. Modded cities are displayed with:
- Purple-tinted styling to distinguish from built-in cities
- A puzzle icon badge
- "MOD" label if no population data is available
- A count badge on the tab showing how many modded cities are available
Custom City Tabs#
Group multiple cities under a custom tab (e.g., for a "Canada" or "Europe" pack):
// First, register your cities
window.SubwayBuilderAPI.registerCity({
name: 'Montreal',
code: 'MTL'
// ... city config
});
window.SubwayBuilderAPI.registerCity({
name: 'Toronto',
code: 'YYZ'
// ... city config
});
// Then register a tab to group them
window.SubwayBuilderAPI.cities.registerTab({
id: 'canada',
label: 'Canada',
emoji: 'π¨π¦',
cityCodes: ['MTL', 'YYZ']
});Tab properties:
| Property | Required | Description |
|---|---|---|
id | Yes | Unique identifier for the tab |
label | Yes | Display name shown in the tab button |
emoji | No | Emoji shown next to the label (e.g., country flag) |
cityCodes | Yes | Array of city codes that belong to this tab |
Custom tabs appear between the built-in country tabs (US, UK) and the "Modded" catch-all tab.
City Data Files#
Cities need data files to function properly. Set them using setCityDataFiles():
window.SubwayBuilderAPI.cities.setCityDataFiles('MTL', {
buildingsIndex: '/data/MTL/buildings_index.json.gz',
demandData: '/data/MTL/demand_data.json.gz',
roads: '/data/MTL/roads.geojson.gz',
runwaysTaxiways: '/data/MTL/runways_taxiways.geojson.gz',
oceanDepthIndex: '/data/MTL/ocean_depth_index.json.gz' // Optional
});Required data files:
| File | Description |
|---|---|
demandData | Population demand points and commuter groups |
buildingsIndex | Building footprints and foundation depths |
roads | Road network for collision detection |
runwaysTaxiways | Airport areas (optional) |
oceanDepthIndex | Ocean depth data (optional) |
Data File Schemas#
The modding API exposes Zod schemas for validating your data files:
const schemas = window.SubwayBuilderAPI.schemas;
// Validate demand data
const demandData = { points: [...], pops: [...] };
const result = schemas.DemandDataSchema.safeParse(demandData);
if (result.success) {
console.log('Demand data is valid!');
} else {
console.error('Validation errors:', result.error.errors);
}Demand Data Format#
{
points: [
{
id: "dp_001",
location: [-97.1463, 49.8718], // [longitude, latitude]
jobs: 500,
residents: 1200,
popIds: ["pop_001", "pop_002"]
}
],
pops: [
{
id: "pop_001",
size: 100, // Number of commuters
residenceId: "dp_001", // Where they live
jobId: "dp_002", // Where they work
drivingSeconds: 1800, // Driving time in seconds
drivingDistance: 15000, // Distance in meters
drivingPath: [ // Optional: route geometry
[-97.1463, 49.8718],
[-97.1320, 49.8850]
]
}
]
}Available schemas:
DemandDataSchema- Complete demand fileDemandPointSchema- Single demand pointPopSchema- Single commuter groupBuildingIndexSchema- Building indexRoadsGeojsonSchema- Roads fileRunwaysTaxiwaysGeojsonSchema- Airports file
Custom Tiles#
For custom map tiles from localhost:
window.SubwayBuilderAPI.map.setTileURLOverride({
cityCode: 'MTL',
tilesUrl: 'http://127.0.0.1:8080/MTL/{z}/{x}/{y}.mvt',
foundationTilesUrl: 'http://127.0.0.1:8080/MTL/{z}/{x}/{y}.mvt',
maxZoom: 15
});Layer Visibility#
Disable map layers by default for cities that don't have certain data:
window.SubwayBuilderAPI.map.setDefaultLayerVisibility('MTL', {
buildingFoundations: false,
oceanFoundations: false,
trackElevations: false
});Complete Example#
Here's a complete example adding Winnipeg:
// 1. Register city
window.SubwayBuilderAPI.registerCity({
name: 'Winnipeg',
code: 'YWG',
description: 'Build a transit system for the Prairie metropolis',
population: 850000,
initialViewState: {
zoom: 13.5,
latitude: 49.871881,
longitude: -97.146345,
bearing: 0
}
});
// 2. Point to localhost tiles
window.SubwayBuilderAPI.map.setTileURLOverride({
cityCode: 'YWG',
tilesUrl: 'http://127.0.0.1:8080/YWG/{z}/{x}/{y}.mvt',
foundationTilesUrl: 'http://127.0.0.1:8080/YWG/{z}/{x}/{y}.mvt',
maxZoom: 15
});
// 3. Set data files
window.SubwayBuilderAPI.cities.setCityDataFiles('YWG', {
buildingsIndex: '/data/YWG/buildings_index.json.gz',
demandData: '/data/YWG/demand_data.json.gz',
roads: '/data/YWG/roads.geojson.gz',
runwaysTaxiways: '/data/YWG/runways_taxiways.geojson.gz'
});
console.log('Winnipeg mod loaded!');For more details on generating city data, see the Custom Cities Guide.