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.

MemberReturnsSummary
getCities()City[]The city list.
getConstants()typeof RULESThe game rules object.
getColors()typeof GAME_COLORSThe GAME_COLORS map rendering constants (colors, widths and zoom thresholds).
getPathfindingRules()typeof PATHFINDING_RULESThe pathfinding rules.
getMap()any or nullThe MapLibre map instance.
loadCityData(path)Promise<T>Load a JSON city-data file and parse it.
getCityCode()string or nullThe loaded city's code from the store.
iconstypeof LucideIconsThe Lucide icon component set, keyed by icon name.
componentsobjectThe game's UI components for mod interfaces.
Reacttypeof ReactThe React module the game runs on, for createElement and hooks in mod components.
chartstypeof RechartsThe Recharts module, every export.
i18nobjectMod translations and the game's current language: create, getCurrentLanguage, getSupportedLanguages and translate.
buildingsobjectEncoder, validator and loader for the packed binary buildings index (buildings_index.bin, magic SBBI).
walkGraphobjectEncoder, 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.

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

The game rules object.

Returns: typeof RULES. the live RULES object; modifyConstants reassigns its top-level keys, replacing nested objects on the override path.

js
const rules = api.utils.getConstants();
console.log(`Starting money: ${rules.STARTING_MONEY}`);

getColors#

api.utils.getColors(): typeof GAME_COLORS

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

js
const colors = api.utils.getColors();
console.log(Object.keys(colors).slice(0, 5));

getPathfindingRules#

api.utils.getPathfindingRules(): typeof PATHFINDING_RULES

The pathfinding rules.

Returns: typeof PATHFINDING_RULES. the live PATHFINDING_RULES object; modifyPathfindingRules merges into the same object.

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

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

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

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

getCityCode#

api.utils.getCityCode(): string | null

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

js
const code = api.utils.getCityCode();
if (code === 'NYC') console.log('Playing New York');

icons#

api.utils.icons: typeof LucideIcons

The Lucide icon component set, keyed by icon name.

The full lucide-react export is spread first, then a curated subset.

js
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: object

The game's UI components for mod interfaces.

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

Styled <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 Card

Bordered container <div>; props: div attributes.

components.CardContent#

api.utils.components.CardContent: typeof CardContent

Card body <div>; props: div attributes.

components.CardDescription#

api.utils.components.CardDescription: typeof CardDescription

Card subtitle paragraph; props: paragraph attributes.

components.CardHeader#

api.utils.components.CardHeader: typeof CardHeader

Card header <div>; props: div attributes.

components.CardTitle#

api.utils.components.CardTitle: typeof CardTitle

Card heading; props: heading attributes.

components.Progress#

api.utils.components.Progress: typeof Progress

Radix progress bar; props: Radix Progress root props; value 0 to 100 fills the bar.

components.Slider#

api.utils.components.Slider: typeof Slider

Range 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 Switch

Radix toggle switch; props: Radix Switch root props.

components.Label#

api.utils.components.Label: typeof Label

Radix form label; props: Radix Label root props.

components.Input#

api.utils.components.Input: typeof Input

Styled <input>; props: input attributes plus prefix and suffix React nodes.

components.Badge#

api.utils.components.Badge: typeof Badge

Pill <div>; props: div attributes plus variant (default, secondary, destructive, outline).

components.Tooltip#

api.utils.components.Tooltip: typeof Tooltip

Radix Tooltip root; props: Radix Tooltip root props.

components.TooltipContent#

api.utils.components.TooltipContent: typeof TooltipContent

Radix Tooltip content; props: Radix Content props; sideOffset default 4 px.

components.TooltipProvider#

api.utils.components.TooltipProvider: typeof TooltipProvider

Radix Tooltip provider; props: Radix Provider props.

components.TooltipTrigger#

api.utils.components.TooltipTrigger: typeof TooltipTrigger

Radix Tooltip trigger; props: Radix Trigger props (asChild).

components.SubwayButton#

api.utils.components.SubwayButton: typeof SubwayButton

Menu 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 MainMenuButton

SubwayButton preset for main-menu (size lg, reversed arrow, class w-full h-fit); props: text, onClick, arrowBearing (default 45).

React#

api.utils.React: typeof React

The React module the game runs on, for createElement and hooks in mod components.

charts#

api.utils.charts: typeof Recharts

The Recharts module, every export.

js
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: object

Mod 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) => string

Build 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).

js
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(): string

The 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): string

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

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

StepCode triedExample for es-MXExample for zh-Hant
1the current language as i18next reports ites-MXzh-Hant
2its canonical form (lowercase language, uppercase region, title-case script)es-MXzh-Hant
3the base languageeszh
4enenen

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.

CodeLanguageCodeLanguage
enEnglishjaJapanese
arArabickoKorean
zh-HansChinese (Simplified)plPolish
zh-HantChinese (Traditional)ptPortuguese
csCzechroRomanian
nlDutchruRussian
frFrenchesSpanish
deGermansvSwedish
hiHindithThai
huHungariantrTurkish
idIndonesianukUkrainian
itItalianviVietnamese

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:

js
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: object

Encoder, validator and loader for the packed binary buildings index (buildings_index.bin, magic SBBI).

js
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): ArrayBuffer

Pack 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): BuildingsValidateResult

Check 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: error begins Missing SBBI magic.
  • header version is not 1: error is Unsupported building index binary version <n>.
  • buffer shorter than the 88-byte header whose version byte is missing or 1: error is 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: object

Encoder, validator and loader for the packed binary pedestrian walk graph (walk_graph.bin, magic SBWG).

js
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): ArrayBuffer

Pack 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): WalkGraphValidateResult

Check 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: error begins Missing SBWG magic.
  • header version is not 1: error is Unsupported walk graph binary version <n>.
  • buffer shorter than the 56-byte header whose version byte is missing or 1: error is 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 = EncoderInput

BuildingsValidateResult#

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 = WalkGraphEncoderInput

WalkGraphValidateResult#

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 }