utils
Shared libraries and helpers: game constants, the map, city data loading, React, Lucide icons, UI components, Recharts, i18n, and the binary city-data encoders.
api.utils. 14 members.
| Member | Returns | Summary |
|---|---|---|
getCities() | City[] | The city list. |
getConstants() | typeof RULES | The game rules object. |
getColors() | typeof GAME_COLORS | The GAME_COLORS map rendering constants (colors, widths and zoom thresholds). |
getPathfindingRules() | typeof PATHFINDING_RULES | The pathfinding rules. |
getMap() | any or null | The MapLibre map instance. |
loadCityData(path) | Promise<T> | Load a JSON city-data file and parse it. |
getCityCode() | string or null | The loaded city's code from the store. |
icons | typeof LucideIcons | The Lucide icon component set, keyed by icon name. |
components | object | The game's UI components for mod interfaces. |
React | typeof React | The React module the game runs on, for createElement and hooks in mod components. |
charts | typeof Recharts | The Recharts module, every export. |
i18n | object | Mod translations and the game's current language: create, getCurrentLanguage, getSupportedLanguages and translate. |
buildings | object | Encoder, validator and loader for the packed binary buildings index (buildings_index.bin, magic SBBI). |
walkGraph | object | Encoder, validator and loader for the packed binary pedestrian walk graph (walk_graph.bin, magic SBWG). |
getCities#
api.utils.getCities(): City[]The city list.
Returns: City[]. the live city array; registerCity pushes onto the same array.
const canadian = api.utils.getCities().filter((c) => c.country === 'CA');
console.log(canadian.map((c) => `${c.name} (${c.code})`).join(', '));getConstants#
api.utils.getConstants(): typeof RULESThe game rules object.
Returns: typeof RULES. the live RULES object; modifyConstants reassigns its top-level keys, replacing nested objects on the override path.
const rules = api.utils.getConstants();
console.log(`Starting money: ${rules.STARTING_MONEY}`);getColors#
api.utils.getColors(): typeof GAME_COLORSThe GAME_COLORS map rendering constants (colors, widths and zoom thresholds).
Returns: typeof GAME_COLORS. the live GAME_COLORS object; modifyColors reassigns its top-level keys, replacing nested objects on the override path.
const colors = api.utils.getColors();
console.log(Object.keys(colors).slice(0, 5));getPathfindingRules#
api.utils.getPathfindingRules(): typeof PATHFINDING_RULESThe pathfinding rules.
Returns: typeof PATHFINDING_RULES. the live PATHFINDING_RULES object; modifyPathfindingRules merges into the same object.
const { MAX_TRANSFERS, MAX_WALK_TO_FROM_STATION } = api.utils.getPathfindingRules();
console.log(`Up to ${MAX_TRANSFERS} transfers, ${MAX_WALK_TO_FROM_STATION / 60} min walk to a station`);getMap#
api.utils.getMap(): any | nullThe MapLibre map instance.
The reference is kept after game end and replaced when the next map finishes loading.
Returns: any | null. the map object once the map has finished loading; null before the first map of the session loads.
// null until the first map loads; inside onMapReady prefer the map argument
const map = api.utils.getMap();
if (map) map.on('moveend', () => console.log(`Zoom ${map.getZoom().toFixed(1)}`));loadCityData#
api.utils.loadCityData(path: string): Promise<T>Load a JSON city-data file and parse it.
An http:// or https:// URL on this machine (host 127.0.0.1 or localhost, any port) is fetched directly. A .gz URL is gunzipped before parsing. A URL on any other host throws. An app-relative path (/data/<CODE>/<file>) is requested from the data server as <path>.gz first, then as <path>. A path already ending in .gz is fetched as is and gunzipped. When called with a mod context, the first call per mod and path shows a warning toast Security notice: mod "<modId>" is reading city data (<path>). 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.
Parameter path (string): app-relative /data/<CODE>/<file> path, or an http:// or https:// URL on this machine.
Returns: Promise<T>. the parsed JSON.
Throws:
- http(s) URL on another host:
Data URLs must point at this machine (127.0.0.1 or localhost): <path>. Serve the file from a local server, or put it in the city data folder and use /data/{CODE}/<file>. - unparseable http(s) URL:
Invalid data URL: <path>. - http(s) URL that answers with a redirect: the fetch rejects (redirects are not followed, so a local server cannot send the request to another host).
- http(s) URL fetch not ok:
Fetch failed: <status>. - outside Electron:
getDataServerPort not available. - absolute filesystem path (drive letter, UNC, or leading
/other than/data/):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. - no data server port:
Data server not available. - data server response not ok:
Server fetch failed: <status>.
Errors: any failure: logs [Modding API] loadCityData failed for path:, then rethrows.
Side effects: warning toast once per mod and path; in development builds logs [DEBUG:loadCityData] Loading: and [DEBUG:loadCityData] Loaded successfully:; on an absolute path, logs [loadData] Absolute file paths are not supported: ... before throwing; on an http(s) URL on another host, logs [loadData] Data URLs must point at this machine ... before throwing.
Changed: http:// and https:// URLs on this machine are fetched directly and URLs on other hosts throw; before, every URL was requested from the data server and failed. An absolute filesystem path now fails with Absolute file paths are not supported; before it failed with Cannot load file from absolute path (PR #1090).
const code = api.utils.getCityCode();
const demand = await api.utils.loadCityData(`/data/${code}/demand_data.json`);
// the server is asked for demand_data.json.gz first, then demand_data.jsongetCityCode#
api.utils.getCityCode(): string | nullThe loaded city's code from the store.
Returns: string | null. the store cityCode; the empty string before a city loads; null on error.
Errors: thrown error: logs [Modding API] getCityCode error:, returns null.
const code = api.utils.getCityCode();
if (code === 'NYC') console.log('Playing New York');icons#
api.utils.icons: typeof LucideIconsThe Lucide icon component set, keyed by icon name.
The full lucide-react export is spread first, then a curated subset.
const { React, icons } = api.utils;
const TrainIcon = () => React.createElement(icons.Train, { size: 16 });
api.ui.registerComponent('bottom-bar', { id: 'my-mod-train-icon', component: TrainIcon });components#
api.utils.components: objectThe game's UI components for mod interfaces.
const { React, components } = api.utils;
const { Card, CardHeader, CardTitle, CardContent, Badge } = components;
const h = React.createElement;
const StatsCard = () =>
h(Card, null,
h(CardHeader, null, h(CardTitle, null, 'Network')),
h(CardContent, null, h(Badge, null, `${api.gameState.getStations().length} stations`)),
);
api.ui.registerComponent('debug-panel', { id: 'my-mod-stats-card', component: StatsCard });components.Button#
api.utils.components.Button: typeof ButtonStyled <button>; props: all button attributes plus variant (default, destructive, outline, secondary, ghost, link), size (default, sm, lg, icon), asChild, silent (no UI click or hover sound).
components.Card#
api.utils.components.Card: typeof CardBordered container <div>; props: div attributes.
components.CardContent#
api.utils.components.CardContent: typeof CardContentCard body <div>; props: div attributes.
components.CardDescription#
api.utils.components.CardDescription: typeof CardDescriptionCard subtitle paragraph; props: paragraph attributes.
components.CardHeader#
api.utils.components.CardHeader: typeof CardHeaderCard header <div>; props: div attributes.
components.CardTitle#
api.utils.components.CardTitle: typeof CardTitleCard heading; props: heading attributes.
components.Progress#
api.utils.components.Progress: typeof ProgressRadix progress bar; props: Radix Progress root props; value 0 to 100 fills the bar.
components.Slider#
api.utils.components.Slider: typeof SliderRange input; props: input attributes except value/onChange, plus value: number[] (default [0]), min (default 0), max (default 100), onValueChange(number[]) per step, onValueCommit(number[]) on pointer release, key release or blur, skipped when the value equals the last committed value (the mount-time value before the first commit).
components.Switch#
api.utils.components.Switch: typeof SwitchRadix toggle switch; props: Radix Switch root props.
components.Label#
api.utils.components.Label: typeof LabelRadix form label; props: Radix Label root props.
components.Input#
api.utils.components.Input: typeof InputStyled <input>; props: input attributes plus prefix and suffix React nodes.
components.Badge#
api.utils.components.Badge: typeof BadgePill <div>; props: div attributes plus variant (default, secondary, destructive, outline).
components.Tooltip#
api.utils.components.Tooltip: typeof TooltipRadix Tooltip root; props: Radix Tooltip root props.
components.TooltipContent#
api.utils.components.TooltipContent: typeof TooltipContentRadix Tooltip content; props: Radix Content props; sideOffset default 4 px.
components.TooltipProvider#
api.utils.components.TooltipProvider: typeof TooltipProviderRadix Tooltip provider; props: Radix Provider props.
components.TooltipTrigger#
api.utils.components.TooltipTrigger: typeof TooltipTriggerRadix Tooltip trigger; props: Radix Trigger props (asChild).
components.SubwayButton#
api.utils.components.SubwayButton: typeof SubwayButtonMenu button with an arrow; props: text, onClick, sideText, size (4xl, lg, default; default default), variant (default, exit; default default), arrowBearing (arrow rotation in degrees; default 0), reverseArrow (default false), className.
components.MainMenuButton#
api.utils.components.MainMenuButton: typeof MainMenuButtonSubwayButton preset for main-menu (size lg, reversed arrow, class w-full h-fit); props: text, onClick, arrowBearing (default 45).
React#
api.utils.React: typeof ReactThe React module the game runs on, for createElement and hooks in mod components.
charts#
api.utils.charts: typeof RechartsThe Recharts module, every export.
const { React, charts } = api.utils;
const { BarChart, Bar, XAxis, YAxis, Tooltip } = charts;
const h = React.createElement;
api.ui.addFloatingPanel({
id: 'my-mod-line-chart',
icon: 'BarChart',
title: 'Riders per line',
render: ({ width, height }) =>
h(BarChart, { width, height: height - 40, data: api.gameState.getLineMetrics() },
h(XAxis, { dataKey: 'routeBullet' }),
h(YAxis),
h(Tooltip),
h(Bar, { dataKey: 'ridersPerHour', fill: '#3b82f6' }),
),
});i18n#
api.utils.i18n: objectMod translations and the game's current language: create, getCurrentLanguage, getSupportedLanguages and translate.
i18n.create#
api.utils.i18n.create(translations: Record<string, Record<string, string>>): (key: string) => stringBuild a lookup function over a mod's translation table.
The returned function reads the current language on each call, tries that code, then its canonical form (es-mx becomes es-MX), then its base language, then en, and returns the key itself when no entry is found. An empty-string entry is skipped like a missing one.
Parameter translations (Record<string, Record<string, string>>): map of language code to map of key to string.
Returns: (key: string) => string. (key) => string.
Changed: lookup walks the full fallback chain (es-MX, es, en); before, only the exact current code and then en were tried (PR #1090).
const t = api.utils.i18n.create({ en: { hi: 'Hello' }, es: { hi: 'Hola' } });
// language es-MX: t('hi') is 'Hola'; language de: t('hi') is 'Hello'; t('missing') is 'missing'i18n.getCurrentLanguage#
api.utils.i18n.getCurrentLanguage(): stringThe i18next language code.
Returns: string. the i18next language code, which can carry a region (for example es-MX); en when i18next has none.
i18n.getSupportedLanguages#
api.utils.i18n.getSupportedLanguages(): string[]A fixed list of language codes.
The language picker offers a different set: en, ar, zh-Hans, zh-Hant, cs, nl, fr, de, hi, hu, id, it, ja, ko, pl, pt, ro, ru, es, sv, th, tr, uk, vi, plus extra codes fetched at runtime; zh is not among them. Bundled locale files ship for en, zh-Hans and zh-Hant; every language whose base is not en is fetched from the translation CDN first, with the bundled file as the fallback.
Returns: string[]. a new array ['en', 'es', 'fr', 'de', 'ja', 'ko', 'zh']; a constant in the API, tied to neither the picker nor the shipped locale files.
i18n.translate#
api.utils.i18n.translate(key: string): stringLook up a key in the game's own i18next strings.
Parameter key (string): a key in the game's strings namespace.
Returns: string. the i18next t(key) result.
Creating Translations#
i18n.create takes one string table per language code and returns a lookup function. Build it once at the top of the script; the function reads the current language on every call, so it follows a language change without being rebuilt.
const t = api.utils.i18n.create({
en: { settings: 'Settings', passengers: 'passengers' },
es: { settings: 'Configuración', passengers: 'pasajeros' },
fr: { settings: 'Paramètres', passengers: 'passagers' },
});
api.ui.addButton('settings-menu', { id: 'my-mod-settings', label: t('settings'), onClick: () => {} });How It Works#
t(key) returns the first non-empty translations[code][key] along this chain, then the key itself:
| Step | Code tried | Example for es-MX | Example for zh-Hant |
|---|---|---|---|
| 1 | the current language as i18next reports it | es-MX | zh-Hant |
| 2 | its canonical form (lowercase language, uppercase region, title-case script) | es-MX | zh-Hant |
| 3 | the base language | es | zh |
| 4 | en | en | en |
An empty string counts as missing. A table keyed by base language (es, zh) covers every regional variant; add a regional table such as zh-Hant only where it differs.
Supported Languages#
The language picker offers these codes, plus extra codes fetched from the translation CDN at runtime. getSupportedLanguages() returns a fixed list (en, es, fr, de, ja, ko, zh) tied to neither this picker nor the shipped strings, so read getCurrentLanguage() instead.
| Code | Language | Code | Language |
|---|---|---|---|
en | English | ja | Japanese |
ar | Arabic | ko | Korean |
zh-Hans | Chinese (Simplified) | pl | Polish |
zh-Hant | Chinese (Traditional) | pt | Portuguese |
cs | Czech | ro | Romanian |
nl | Dutch | ru | Russian |
fr | French | es | Spanish |
de | German | sv | Swedish |
hi | Hindi | th | Thai |
hu | Hungarian | tr | Turkish |
id | Indonesian | uk | Ukrainian |
it | Italian | vi | Vietnamese |
Nested Translations#
Lookups are flat: translations[code][key]. Nested objects are not walked, so { en: { stats: { riders: 'Riders' } } } makes t('stats.riders') return 'stats.riders'. To group keys, put the dot in the property name:
const t = api.utils.i18n.create({
en: { 'stats.riders': 'Total riders', 'stats.revenue': 'Daily revenue' },
de: { 'stats.riders': 'Fahrgäste gesamt', 'stats.revenue': 'Tageseinnahmen' },
});
console.log(t('stats.riders'));buildings#
api.utils.buildings: objectEncoder, validator and loader for the packed binary buildings index (buildings_index.bin, magic SBBI).
api.utils.buildings.load('/data/NYC/buildings_index.bin').then((buffer) => {
const check = api.utils.buildings.validate(buffer);
console.log(check.ok ? `${check.buildingCount} buildings` : check.error);
}).catch((e) => console.log(`No buildings index: ${e.message}`));buildings.encode#
api.utils.buildings.encode(input: BuildingsEncoderInput): ArrayBufferPack a buildings index into the binary format.
A heights section is written only when the building array is not empty and every building has a finite height; an OSM id section only when at least one building lists a finite OSM id. Cells are written sorted by row, then column.
Parameter input (BuildingsEncoderInput): buildings, grid cells and bounds.
Returns: ArrayBuffer. an ArrayBuffer beginning with the SBBI header; the game loads such a file as buildings_index.bin (.gz tried first) from the city data folder, or from the buildingsIndex path given to setCityDataFiles.
buildings.validate#
api.utils.buildings.validate(buffer: ArrayBuffer): BuildingsValidateResultCheck that a buffer is a readable buildings index.
Only the header is checked. Section sizes are not compared with the buffer length.
Parameter buffer (ArrayBuffer): the file bytes.
Returns: BuildingsValidateResult. { ok: true, buildingCount, nonEmptyCells, bbox: [minLon, minLat, maxLon, maxLat] } from the header; { ok: false, error } otherwise.
Errors:
- buffer shorter than 4 bytes or wrong magic:
errorbeginsMissing SBBI magic. - header version is not 1:
errorisUnsupported building index binary version <n>. - buffer shorter than the 88-byte header whose version byte is missing or 1:
erroris the DataView RangeError message.
buildings.load#
api.utils.buildings.load(path: string): Promise<ArrayBuffer>Fetch a binary city-data file and return the raw bytes.
A .gz path or URL is gunzipped. An app-relative path without .gz is requested as <path>.gz first, then as <path>. No security notice toast is shown. 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.
Parameter path (string): app-relative /data/<CODE>/<file> path, or an http:// or https:// URL on this machine (host 127.0.0.1 or localhost, any port).
Returns: Promise<ArrayBuffer>. the file bytes.
Throws:
- http(s) URL on another host:
Data URLs must point at this machine (127.0.0.1 or localhost): <path>. Serve the file from a local server, or put it in the city data folder and use /data/{CODE}/<file>. - unparseable http(s) URL:
Invalid data URL: <path>. - http(s) URL that answers with a redirect: the fetch rejects (redirects are not followed, so a local server cannot send the request to another host).
- http(s) URL fetch not ok:
Fetch failed: <status>. - outside Electron:
getDataServerPort not available. - absolute filesystem path (drive letter, UNC, or leading
/other than/data/):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. - no data server port:
Data server not available. - data server response not ok:
Server fetch failed: <status>.
Side effects: on an absolute path, logs [loadBinaryData] Absolute file paths are not supported: ... before throwing; on an http(s) URL on another host, logs [loadData] Data URLs must point at this machine ... before throwing.
Changed: absolute filesystem paths throw the message above; before, they threw Binary load via absolute path is not supported: <path>. URLs on hosts other than this machine throw; before, any host was fetched (PR #1090).
walkGraph#
api.utils.walkGraph: objectEncoder, validator and loader for the packed binary pedestrian walk graph (walk_graph.bin, magic SBWG).
api.utils.walkGraph.load('/data/NYC/walk_graph.bin.gz').then((buffer) => {
const check = api.utils.walkGraph.validate(buffer);
console.log(check.ok ? `${check.nodeCount} nodes, ${check.edgeCount} edges` : check.error);
}).catch((e) => console.log(`No walk graph: ${e.message}`));walkGraph.encode#
api.utils.walkGraph.encode(input: WalkGraphEncoderInputType): ArrayBufferPack a walk graph into the binary format.
Coordinates are stored as int32 fixed point at 1e-7 degrees. The header bounding box is computed from the nodes. It is all zeros for an empty node array.
Parameter input (WalkGraphEncoderInputType): nodes and undirected edges.
Returns: ArrayBuffer. an ArrayBuffer beginning with the SBWG header; the game loads such a file as walk_graph.bin.gz from the city data folder, or from the walkGraph path given to setCityDataFiles.
Throws: an edge endpoint that is not an integer index into the node array: Walk graph edge references invalid node: <a>-<b> (<n> nodes).
walkGraph.validate#
api.utils.walkGraph.validate(buffer: ArrayBuffer): WalkGraphValidateResultCheck that a buffer is a readable walk graph.
Only the header is checked. Section sizes are not compared with the buffer length.
Parameter buffer (ArrayBuffer): the file bytes.
Returns: WalkGraphValidateResult. { ok: true, nodeCount, edgeCount, bbox: [minLon, minLat, maxLon, maxLat] } from the header; { ok: false, error } otherwise.
Errors:
- buffer shorter than 4 bytes or wrong magic:
errorbeginsMissing SBWG magic. - header version is not 1:
errorisUnsupported walk graph binary version <n>. - buffer shorter than the 56-byte header whose version byte is missing or 1:
erroris the DataView RangeError message.
walkGraph.load#
api.utils.walkGraph.load(path: string): Promise<ArrayBuffer>Fetch a binary city-data file and return the raw bytes.
A .gz path or URL is gunzipped. An app-relative path without .gz is requested as <path>.gz first, then as <path>. No security notice toast is shown. 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.
Parameter path (string): app-relative /data/<CODE>/<file> path, or an http:// or https:// URL on this machine (host 127.0.0.1 or localhost, any port).
Returns: Promise<ArrayBuffer>. the file bytes.
Throws:
- http(s) URL on another host:
Data URLs must point at this machine (127.0.0.1 or localhost): <path>. Serve the file from a local server, or put it in the city data folder and use /data/{CODE}/<file>. - unparseable http(s) URL:
Invalid data URL: <path>. - http(s) URL that answers with a redirect: the fetch rejects (redirects are not followed, so a local server cannot send the request to another host).
- http(s) URL fetch not ok:
Fetch failed: <status>. - outside Electron:
getDataServerPort not available. - absolute filesystem path (drive letter, UNC, or leading
/other than/data/):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. - no data server port:
Data server not available. - data server response not ok:
Server fetch failed: <status>.
Side effects: on an absolute path, logs [loadBinaryData] Absolute file paths are not supported: ... before throwing; on an http(s) URL on another host, logs [loadData] Data URLs must point at this machine ... before throwing.
Changed: absolute filesystem paths throw the message above; before, they threw Binary load via absolute path is not supported: <path>. URLs on hosts other than this machine throw; before, any host was fetched (PR #1090).
BuildingsEncoderInput#
Input for utils.buildings.encode.
buildings: array; index is the building id; each has bounds [minLon, minLat, maxLon, maxLat] in degrees, foundationDepth in m (without a heights section the game uses the larger of foundationDepth times 10 m and the MIN_BUILDING_HEIGHT rule as the building height), polygon as rings of [lon, lat] (first ring outer, later rings holes), optional height in m (a heights section is written when every building has a finite one), optional osmIds. cells: array of { col, row, buildingIds } listing the building ids intersecting each grid cell. cols, rows: grid dimensions. cellSize: grid cell height; degrees latitude; the reader derives the cell width as cellSize divided by cos(mean latitude of the bounding box). maxFoundationDepth: written to the header as given (the encoder does not compute or check it, and the game does not read it). minLon, minLat, maxLon, maxLat: bounding box in degrees.
type BuildingsEncoderInput = EncoderInputBuildingsValidateResult#
Result of utils.buildings.validate.
{ ok: true, buildingCount, nonEmptyCells, bbox } with bbox as [minLon, minLat, maxLon, maxLat] in degrees, or { ok: false, error }.
type BuildingsValidateResult = | { ok: true; buildingCount: number; nonEmptyCells: number; bbox: [number, number, number, number] } | { ok: false; error: string }WalkGraphEncoderInputType#
Input for utils.walkGraph.encode.
nodes: array of [lng, lat] in degrees; the array index is the node id. edges: undirected edges { a, b, lengthM, geometry? }; lengthM in m is stored as the edge's walking length; geometry lists the points between a and b in order (the reader adds the node coordinates at both ends) and is omitted for a straight edge.
type WalkGraphEncoderInputType = WalkGraphEncoderInputWalkGraphValidateResult#
Result of utils.walkGraph.validate.
{ ok: true, nodeCount, edgeCount, bbox } with bbox as [minLon, minLat, maxLon, maxLat] in degrees, or { ok: false, error }.
type WalkGraphValidateResult = | { ok: true; nodeCount: number; edgeCount: number; bbox: [number, number, number, number] } | { ok: false; error: string }