cities

City data files and city picker tabs for registered cities.

api.cities. 4 members.

MemberReturnsSummary
api.registerCity(city)voidRegister a city in the city picker list.
setCityDataFiles(cityCode, files)voidSet the data file locations the city loader uses for a city code.
getCityDataFiles(cityCode)CityDataFiles or undefinedThe data file object stored for a city code, or undefined.
registerTab(tab)voidRegister a city picker filter tab that groups cities by code.
getTabs()CityTab[]A copy of the custom tab list.

registerCity#

api.registerCity(city: City): void

Register 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 code or name: records a validation error Invalid city object - must have code and name with the mod error reporter, registers nothing. A null or undefined argument throws before validation and is recorded as a registration error Error 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 with difficulty: null.
  • buildingZoomOffset outside [-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.

js
// 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.setTileURLOverride

setCityDataFiles#

api.cities.setCityDataFiles(cityCode: string, files: CityDataFiles): void

Set 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.

ParamTypeDefaultDescription
cityCodestringrequiredcity code the files belong to; must be a non-empty string.
filesCityDataFilesrequiredfile locations; every key optional.

Errors:

  • cityCode missing 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).

js
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/ files

getCityDataFiles#

api.cities.getCityDataFiles(cityCode: string): CityDataFiles | undefined

The 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.

js
const files = api.cities.getCityDataFiles('HFX');
console.log(files ? files.demandData : 'no overrides for HFX');

registerTab#

api.cities.registerTab(tab: CityTab): void

Register 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 id or label: records a validation error Invalid city tab - must have id and label, registers nothing.
  • cityCodes not an array of strings: records a validation error Invalid city tab "<id>" - cityCodes must be an array of city code strings, registers nothing.
  • duplicate id: records a validation error City 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).

js
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.

js
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.

FieldTypeDefaultDescription
namestringrequiredDisplay name; required.
codestringrequiredShort city code used for data paths, tile paths and tabs; required.
countryCountryrequiredCountry 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.
secondaryCountryCountrynoneSecond country; the picker's country filter matches either, and its name-sorted city list shows both flags when country is set.
descriptionstringnoneDescription shown in the city picker.
difficultyCityDifficulty or nullnoneDifficulty badge: 'very-easy', 'easy', 'medium', 'hard', 'very-hard'; null or omitted shows no badge; any other value is replaced with null.
initialViewStateobjectrequiredStarting camera: zoom in zoom levels; latitude, longitude and bearing in degrees. See City.initialViewState below.
minZoomnumbernoneCamera zoom-out floor for a city without a built-in bounds box (every mod city); zoom levels; omitted or 0 means 10.
buildingZoomOffsetnumber0Shift 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.
mapImageUrlstringnonePreview image URL or app path for the city picker; when omitted the picker uses the built-in /city-maps/<code lowercased>_dark.svg.
devOnlybooleannoneHides the city from the production city picker unless the dev-maps allowlist grants access. A registered mod city is always shown.
betaFeatureFlagstringnoneGates 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 nullrequiredOcean 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][]noneBoxes as [west, south, east, north]; degrees; data generation erases depth contours inside them.
blankbooleannoneMarks 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.
nonEmpiricalCommutesbooleannoneData generation flag: use the gravity commute model instead of empirical origin-destination data.
brokenRacDatabooleannoneData generation flag: derive home-side worker counts from census population (POP20) instead of LODES RAC; US cities only.
modIdstringnoneId of the registering mod. Set by registerCity, overwriting any value passed; absent on built-in cities.

City.initialViewState#

FieldTypeDefaultDescription
zoomnumberrequiredStarting zoom; zoom levels.
latitudenumberrequiredStarting camera center latitude; degrees.
longitudenumberrequiredStarting camera center longitude; degrees.
bearingnumberrequiredStarting 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: ....

FieldTypeDefaultDescription
buildingsIndexstring'/data/<CODE>/buildings_index.bin'Packed-binary buildings index (.bin or .bin.gz).
buildingTagsstring'/data/<CODE>/building_tags.json'Building OSM tags keyed by OSM id (.json or .json.gz).
demandDatastring'/data/<CODE>/demand_data.json'Demand data JSON (gzipped or plain).
roadsstring'/data/<CODE>/roads.geojson'Roads GeoJSON (gzipped or plain).
runwaysTaxiwaysstring'/data/<CODE>/runways_taxiways.geojson'Runways and taxiways GeoJSON (gzipped or plain).
oceanDepthIndexstring'/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.
walkGraphstring'/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.

FieldTypeDefaultDescription
idstringrequiredTab id; must be unique among custom tabs.
labelstringrequiredText shown on the tab chip.
emojistringnoneEmoji shown before the label when set.
cityCodesstring[]requiredPlain city codes the tab shows; must be an array of strings; the chip shows its length.