cities
City data files and city picker tabs for registered cities.
api.cities. 4 members.
| Member | Returns | Summary |
|---|---|---|
api.registerCity(city) | void | Register a city in the city picker list. |
setCityDataFiles(cityCode, files) | void | Set the data file locations the city loader uses for a city code. |
getCityDataFiles(cityCode) | CityDataFiles or undefined | The data file object stored for a city code, or undefined. |
registerTab(tab) | void | Register a city picker filter tab that groups cities by code. |
getTabs() | CityTab[] | A copy of the custom tab list. |
registerCity#
api.registerCity(city: City): voidRegister a city in the city picker list.
The first call in a session, when localStorage key subway_mods_backup_done is unset, sets that key, invokes IPC channel backup-all-saves (when window.electron exists) with pre-mod-backup and logs [Modding API] Created pre-mod backup of all saves (development builds only); the Electron main process registers no handler for that channel, so no backup is written. The call overwrites city.modId with the calling mod's id. The city's identity is <modId>:<code>, so while a mod id is active (the synchronous part of a mod script or hook callback) a code shared with a built-in city or another mod's city is accepted and both coexist. A code also used by another city logs [Modding API] City code "<code>" is also used by <owner>. Both will appear; this city is identified as "<uid>".. A call with no active mod id (for example after an await) leaves modId unset; its identity is the bare code, so a code any built-in city or other mod-less city already uses is rejected, and an accepted one logs a warning that the city is identified by its bare code. An unknown difficulty is replaced with null. buildingZoomOffset is clamped to [-4, 8] (a value that is not a finite number becomes 0). The city is pushed onto the shared city list. The city picker polls that list every 500 ms after it opens, stopping once the count has been stable for 4 polls or after 15 s. Logs [Modding API] Registering custom city: <name> (<code>) (development builds only) on success. The city is removed again on reloadMods.
Parameter city (City): the city definition; code and name are required.
Errors:
- an object missing
codeorname: records a validation errorInvalid city object - must have code and namewith the mod error reporter, registers nothing. Anullorundefinedargument throws before validation and is recorded as a registration errorError registering city: <message>. - same code already registered by the same mod (with no active mod id: by any city without a mod id, built-in cities included): records a validation error
City with code "<code>" is already registered by this mod, skipping registration, registers nothing. - unknown
difficulty: records a validation error naming the accepted values, continues withdifficulty: null. buildingZoomOffsetoutside [-4, 8] or not a finite number: records a validation error naming the range and the value used, continues with the clamped value (0 when it is not a finite number).- exception: records a registration error
Error registering city: <message>. - every recorded error is also logged at warn level as
[Mod Error] <type>: <message>.
Side effects: shared city list mutated; city.modId and possibly city.difficulty and city.buildingZoomOffset rewritten on the passed object.
// at the top level of the script, so the mod id is active
api.registerCity({
name: 'Halifax',
code: 'HFX',
country: 'CA',
description: 'Harbour city with a ferry-shaped gap in its transit',
difficulty: 'medium',
initialViewState: { latitude: 44.6488, longitude: -63.5752, zoom: 12, bearing: 0 },
minZoom: 9,
buildingZoomOffset: 1,
});
// data files default to /data/HFX/; tiles need map.setTileURLOverridesetCityDataFiles#
api.cities.setCityDataFiles(cityCode: string, files: CityDataFiles): voidSet the data file locations the city loader uses for a city code.
The object is stored as given and validated against nothing. A second call for the same code replaces the whole object. demandData, roads, runwaysTaxiways, buildingsIndex, oceanDepthIndex and walkGraph are read when the city loads, buildingTags when a mod first looks up building tags; each falls back to the game's own /data/<CODE>/ file when omitted. The data loader treats a /data/ path as app-relative and serves it from the city data folder. An http:// or https:// URL on this machine (host 127.0.0.1 or localhost, any port) is fetched directly, without following redirects (gunzipped when it ends in .gz); a URL on any other host fails at load time with Data URLs must point at this machine .... Any other absolute path (leading /, drive letter or UNC) fails at load time: the loader logs and throws Absolute file paths are not supported: <path>. Put the file in the city data folder and use /data/{CODE}/<file>, or serve it from a localhost URL.. The lookup is keyed by the plain city code. Logs [Modding API] Setting data files for <code> (development builds only). In the desktop app the network request allowlist admits loopback only over plain http://, so an https:// URL on this machine passes this check and is then cancelled; serve local files from an http://127.0.0.1:<port> server.
| Param | Type | Default | Description |
|---|---|---|---|
cityCode | string | required | city code the files belong to; must be a non-empty string. |
files | CityDataFiles | required | file locations; every key optional. |
Errors:
cityCodemissing or not a string: logs[Modding API] Invalid city code, stores nothing.- exception: logs
[Modding API] Error setting city data files:.
Side effects: city data file registry entry replaced.
Changed: http(s):// URLs on this machine load for JSON files as well as binary ones, and URLs on other hosts fail (before, JSON URLs always failed and binary files accepted any host). A filesystem-absolute path fails when the file loads, with the logged error Absolute file paths are not supported: ... (PR #1090).
api.cities.setCityDataFiles('HFX', {
demandData: '/data/HFX/demand_data.json',
roads: '/data/HFX/roads.geojson',
buildingsIndex: 'http://127.0.0.1:8080/HFX/buildings_index.bin',
});
// omitted keys fall back to the game's /data/HFX/ filesgetCityDataFiles#
api.cities.getCityDataFiles(cityCode: string): CityDataFiles | undefinedThe data file object stored for a city code, or undefined.
Parameter cityCode (string): plain city code.
Returns: CityDataFiles | undefined. the object passed to setCityDataFiles (same reference); undefined when none is stored.
Errors: exception: logs [Modding API] Error getting city data files:, returns undefined.
const files = api.cities.getCityDataFiles('HFX');
console.log(files ? files.demandData : 'no overrides for HFX');registerTab#
api.cities.registerTab(tab: CityTab): voidRegister a city picker filter tab that groups cities by code.
The picker matches cityCodes against each city's plain code and shows the tab with the length of cityCodes. Tabs are cleared on reloadMods. Logs [Modding API] Registering city tab: <label> (<id>) (development builds only) on success.
Parameter tab (CityTab): tab definition; id, label and cityCodes are required.
Errors:
- missing
idorlabel: records a validation errorInvalid city tab - must have id and label, registers nothing. cityCodesnot an array of strings: records a validation errorInvalid city tab "<id>" - cityCodes must be an array of city code strings, registers nothing.- duplicate
id: records a validation errorCity tab with id "<id>" already exists, skipping registration, registers nothing. - exception: records a registration error
Error registering city tab: <message>. - every recorded error is also logged at warn level as
[Mod Error] <type>: <message>.
Side effects: custom tab list appended.
Changed: cityCodes is required; a tab without a string array is rejected with a validation error (PR #1090).
api.cities.registerTab({ id: 'atlantic-canada', label: 'Atlantic Canada', emoji: '🇨🇦', cityCodes: ['HFX', 'YYT'] });getTabs#
api.cities.getTabs(): CityTab[]A copy of the custom tab list.
Returns: CityTab[]. new array of the registered tab objects, in registration order.
const tabs = api.cities.getTabs();
if (!tabs.some((t) => t.id === 'atlantic-canada')) console.log('Atlantic tab not registered yet');City#
A city definition for registerCity. Same shape as the built-in cities.
bathymetry, bathymetryExcludeBboxes, nonEmpiricalCommutes and brokenRacData steer city data generation.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Display name; required. |
code | string | required | Short city code used for data paths, tile paths and tabs; required. |
country | Country | required | Country code. Typed as 'US', 'UK', 'CA', 'IE', 'FR', 'JP', 'NZ', 'CH' or 'AU'; registerCity does not check the value. The city picker's country filter matches the code exactly. |
secondaryCountry | Country | none | Second country; the picker's country filter matches either, and its name-sorted city list shows both flags when country is set. |
description | string | none | Description shown in the city picker. |
difficulty | CityDifficulty or null | none | Difficulty badge: 'very-easy', 'easy', 'medium', 'hard', 'very-hard'; null or omitted shows no badge; any other value is replaced with null. |
initialViewState | object | required | Starting camera: zoom in zoom levels; latitude, longitude and bearing in degrees. See City.initialViewState below. |
minZoom | number | none | Camera zoom-out floor for a city without a built-in bounds box (every mod city); zoom levels; omitted or 0 means 10. |
buildingZoomOffset | number | 0 | Shift of the zoom at which buildings and foundations appear; zoom levels; clamped to [-4, 8]; a value that is not a finite number becomes 0. |
mapImageUrl | string | none | Preview image URL or app path for the city picker; when omitted the picker uses the built-in /city-maps/<code lowercased>_dark.svg. |
devOnly | boolean | none | Hides the city from the production city picker unless the dev-maps allowlist grants access. A registered mod city is always shown. |
betaFeatureFlag | string | none | Gates the city behind a named Beta Feature flag (see next-app-2's featureFlags.ts) instead of (or on top of) devOnly. See City.betaFeatureFlag below. |
bathymetry | 'bluetopo' or 'gmrt' or null | required | Ocean depth source for data generation: omitted or 'gmrt' for GMRT, 'bluetopo' for NOAA BlueTopo with GMRT fallback, null for flat shallow water. |
bathymetryExcludeBboxes | [number, number, number, number][] | none | Boxes as [west, south, east, north]; degrees; data generation erases depth contours inside them. |
blank | boolean | none | Marks a blank sandbox city; the city loader skips its data file loads and starts with empty demand, roads and runways and no building or ocean depth index. |
nonEmpiricalCommutes | boolean | none | Data generation flag: use the gravity commute model instead of empirical origin-destination data. |
brokenRacData | boolean | none | Data generation flag: derive home-side worker counts from census population (POP20) instead of LODES RAC; US cities only. |
modId | string | none | Id of the registering mod. Set by registerCity, overwriting any value passed; absent on built-in cities. |
City.initialViewState#
| Field | Type | Default | Description |
|---|---|---|---|
zoom | number | required | Starting zoom; zoom levels. |
latitude | number | required | Starting camera center latitude; degrees. |
longitude | number | required | Starting camera center longitude; degrees. |
bearing | number | required | Starting map rotation; degrees clockwise from north. |
City.betaFeatureFlag#
Gates the city behind a named Beta Feature flag (see next-app-2's featureFlags.ts) instead of (or on top of) devOnly. While the flag is off the city is hidden from the city picker, globe, counts and search exactly like a devOnly city; a player who enables the flag sees and can play it. Used for trial cities shipped ahead of full release.
CityDataFiles#
Data file locations for a city, passed to cities.setCityDataFiles.
Each value is a /data/<CODE>/<file> path served from the city data folder or an http(s):// URL on this machine (127.0.0.1 or localhost). Any other absolute path fails at load with Absolute file paths are not supported: ....
| Field | Type | Default | Description |
|---|---|---|---|
buildingsIndex | string | '/data/<CODE>/buildings_index.bin' | Packed-binary buildings index (.bin or .bin.gz). |
buildingTags | string | '/data/<CODE>/building_tags.json' | Building OSM tags keyed by OSM id (.json or .json.gz). |
demandData | string | '/data/<CODE>/demand_data.json' | Demand data JSON (gzipped or plain). |
roads | string | '/data/<CODE>/roads.geojson' | Roads GeoJSON (gzipped or plain). |
runwaysTaxiways | string | '/data/<CODE>/runways_taxiways.geojson' | Runways and taxiways GeoJSON (gzipped or plain). |
oceanDepthIndex | string | '/data/<CODE>/ocean_depth_index.json' | Ocean depth index JSON: it sets each new blueprint track's share over water, which construction costs, achievement checks and lifetime stats read, and track validation reads it to reject track above or through the sea floor. See CityDataFiles.oceanDepthIndex below. |
walkGraph | string | '/data/<CODE>/walk_graph.bin.gz' | Packed-binary pedestrian walk graph (.bin or .bin.gz) for road-following walk paths. |
CityDataFiles.oceanDepthIndex#
Ocean depth index JSON: it sets each new blueprint track's share over water, which construction costs, achievement checks and lifetime stats read, and track validation reads it to reject track above or through the sea floor; a missing file is normal and leaves the city with no water share and no depth collision.
CityTab#
A custom filter tab for the city picker.
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Tab id; must be unique among custom tabs. |
label | string | required | Text shown on the tab chip. |
emoji | string | none | Emoji shown before the label when set. |
cityCodes | string[] | required | Plain city codes the tab shows; must be an array of strings; the chip shows its length. |