gameState
Getters for the running game: stations, routes, tracks, trains, fares, demand, clock, money, bonds, statistics, map colors, buildings and streets, blueprint costs, station groups and save identity.
api.gameState. 39 members.
State Summary#
| Member | Returns | Summary |
|---|---|---|
getStations(options?) | Station[] | Every station in the running game. |
getRoutes(options?) | Route[] | Every route in the running game. |
getFareGroups() | FareGroup[] | Every fare group, as a deep copy (structuredClone). |
getJourneyFare(segments) | number | Price a journey's segments ($) with the current fare groups. |
getFareZoneAt(coords) | object[] | The fare zones containing a [lng, lat] point, at most one entry per zone-system fare group. |
getTracks() | Track[] | Every track in the running game. |
getTrains() | Train[] | Every train in the running game, parked trains included. |
getCurrentDay() | number | Current game day, 1-based. |
getCurrentHour() | number | Hour of the game day, 0 to 23. |
getElapsedSeconds() | number | Total elapsed game time in game seconds (timeConfig.elapsedSeconds). |
getBudget() | number | The player's money balance in dollars (money). |
getTicketPrice() | number | The global ticket price in dollars (transitCost). |
getGameSpeed() | 'slow' or 'normal' or 'fast' or 'ultrafast' | Current game speed (timeConfig.timeSpeed). |
isPaused() | boolean | Whether the game clock is paused (timeConfig.paused). |
getMapColors(isDark?) | ModeColors | The complete resolved map color set for one theme. |
getDemandData() | DemandData or null | The store's live demand data: points and popsMap. |
getBuildingsAt(coord) | Promise<object[]> | The buildings whose footprint covers a [lng, lat] coordinate, with OSM tags and demolition state. |
getBuildingTags(osmId) | Promise of Record<string, string> or null | The stored OpenStreetMap tags of a building by OSM id, or null. |
getStreetsAt(coord, radiusMeters?) | Array<Record<string, unknown>> | The road segments within radiusMeters of a [lng, lat] coordinate, nearest first. |
getBonds() | Bond[] | The store's live bonds array. |
getBondTypes() | typeof BOND_TYPES | The BOND_TYPES constant, keyed SMALL, MEDIUM, LARGE. |
getRouteFinancials(routeId?) | RouteFinancials[] | The recorded hourly revenue and expense ledger per route; $. |
getRidershipStats() | RidershipStats | Transit riders who finished a journey in the last 15 game minutes, as a count and an hourly rate. |
getLineMetrics() | LineMetric[] | Per-route train count, trains per hour, riders per hour and an estimated revenue per hour. |
getModeChoiceStats(direction?) | ModeChoiceStats | Citywide commute mode split in people: walking, driving, transit, unknown. |
getStationRidership(stationId?) | StationRidership | Rider counts from the statistics snapshot for one station, or for every station. |
getRouteRidership(routeId?) | RouteRidership | Rider counts from the statistics snapshot for one route, or for every route. |
getCompletedCommutes() | object[] | The store's statistics snapshot of completed transit commutes. |
getRouteReliability(routeId?, period?) | Array<ReliabilityReport> | Punctuality and rider-delay figures for each route over the last game day. |
getNetworkReliability(period?) | ReliabilityStats | Reliability figures pooled across every route for the last game day. |
RELIABILITY_MIN_SAMPLE | number | Completed rides (10) below which the reliability panel leaves a route's rider-delay figure unscored; the perceived time panel and the journey planner's leg hints apply the same minimum. |
getStationGroups() | StationGroup[] | The store's live stationGroups array. |
getStationGroupForStation(stationId) | StationGroup or null | Deep copy (structuredClone) of the first station group whose stationIds include stationId, or null. |
getTransferStationIds() | string[] | Station ids of every station group with two or more members, concatenated in group order. |
getSiblingStationIds(stationId) | string[] | The other member ids of the first station group whose stationIds include stationId. |
getSaveName() | string or null | Name of the loaded save (currentSaveInfo.name), or null. |
getGameSessionId() | string or null | The playthrough's session id (gameSessionId), a UUID. |
calculateBlueprintCost(tracks) | BlueprintCost | Construction cost ($) of the blueprint tracks in tracks. |
getBlueprintTrackLength(tracks?) | object | Blueprint track length (m), as summed rails and as corridor length. |
Stations#
getStations#
api.gameState.getStations(options?: { includeTempRoutes?: boolean; }): Station[]Every station in the running game.
Each entry is the store's live Station object. Mutating one changes game state. With includeTempRoutes false each entry is a shallow copy whose routeIds keeps only the ids of routes with no tempParentId.
| Param | Type | Default | Description |
|---|---|---|---|
options | object | none | option object; default { includeTempRoutes: true }. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
includeTempRoutes | boolean | true | Keep temp-route ids in each station's routeIds; false strips routes whose tempParentId is set and returns shallow copies. |
Returns: Station[]. Station[]; a new array of live store objects, or of shallow copies when includeTempRoutes is false; [] when the store holds no stations and on error.
Errors: any throw: logs [Modding API] Error getting stations:, returns [].
const lines = api.gameState.getStations({ includeTempRoutes: false });
// lines[0].routeIds holds player routes only; lines[0] is a copy, lines[0].nearbyStations is still shared with the storeRoutes#
getRoutes#
api.gameState.getRoutes(options?: { includeTempRoutes?: boolean; }): Route[]Every route in the running game.
With includeTempRoutes true the result is the store's own routes array. Mutating it changes game state. With includeTempRoutes false the result is a new array holding the routes with no tempParentId.
| Param | Type | Default | Description |
|---|---|---|---|
options | object | none | option object; default { includeTempRoutes: true }. |
options fields:
| Field | Type | Default | Description |
|---|---|---|---|
includeTempRoutes | boolean | true | Include routes with a non-null tempParentId; false filters them out into a new array. |
Returns: Route[]. Route[]; the live store array, or a new filtered array; [] on error.
Errors: any throw: logs [Modding API] Error getting routes:, returns [].
const routes = api.gameState.getRoutes({ includeTempRoutes: false });
for (const r of routes) console.log(`${r.bullet}: ${r.stNodes?.length ?? 0} stops, ${r.idealTrainCount ?? 0} trains`);getFareGroups#
api.gameState.getFareGroups(): FareGroup[]Every fare group, as a deep copy (structuredClone).
Mutating the result changes nothing in the store.
Returns: FareGroup[]. FareGroup[]; deep copies; [] when the store has no fare groups and on error.
Errors: any throw: logs [Modding API] Error getting fare groups:, returns [].
for (const g of api.gameState.getFareGroups()) {
console.log(`${g.name || g.id}: ${g.fareSystem}, ${g.routeIds.length} routes`);
}getJourneyFare#
api.gameState.getJourneyFare(segments: Array<{…}>): numberPrice a journey's segments ($) with the current fare groups.
Segments with isWalking, isDriving or routeId equal to 'walking' add nothing. Segments whose routeId belongs to no fare group share one implicit flat group charged once at the current ticket price. A temp route is priced as its parent route. Zone pricing reads fromStopId and toStopId (station ids); a stop inside no zone adds no zone. Distance pricing in 'track' mode reads fromStopId and toStopId. Every other case measures fromStopCoords to toStopCoords straight-line in km, and 0 km when either coordinate is missing. Transfer credits, boarding charges and caps follow each group's transferPolicy and fareCap. The total is rounded to cents.
| Param | Type | Default | Description |
|---|---|---|---|
segments | object[] | required | journey legs in riding order. |
segments fields:
| Field | Type | Default | Description |
|---|---|---|---|
routeId | string | required | Route ridden on this leg; 'walking' for a walk leg. |
isWalking | boolean | none | True for a walk leg, which adds nothing. |
isDriving | boolean | none | True for a driving access leg, which adds nothing. |
fromStopCoords | [number, number] | none | [lng, lat] where the leg boards; straight-line distance pricing. |
toStopCoords | [number, number] | none | [lng, lat] where the leg alights. |
fromStopId | string | none | Station id where the leg boards; zone pricing and 'track' distance pricing. |
toStopId | string | none | Station id where the leg alights. |
Returns: number. number; dollars; 0 when no leg is charged.
Errors: segments is no array: logs [Modding API] getJourneyFare requires an array of segments, returns 0; any throw: logs [Modding API] Error computing journey fare:, returns 0.
const fare = api.gameState.getJourneyFare([
{ routeId: 'r1', fromStopId: 'stA', toStopId: 'stB', fromStopCoords: [-73.98, 40.75], toStopCoords: [-73.95, 40.78] },
{ routeId: 'walking', isWalking: true },
{ routeId: 'r2', fromStopId: 'stB', toStopId: 'stC', fromStopCoords: [-73.95, 40.78], toStopCoords: [-73.93, 40.8] },
]);getFareZoneAt#
api.gameState.getFareZoneAt(coords: [number, number]): Array<{ groupId: string; zoneId: string; zoneNumber: number }>The fare zones containing a [lng, lat] point, at most one entry per zone-system fare group.
Groups are visited in store order and groups whose fareSystem is anything but 'zone' are skipped. Within a group the zones are tested in zones order and the first containing zone is reported. zoneNumber is the zone's 1-based index in the group's zones array.
Parameter coords ([number, number]): [lng, lat]; both finite numbers.
Returns: Array<{ groupId: string; zoneId: string; zoneNumber: number }>. Array of { groupId, zoneId, zoneNumber }; a fresh array; [] when no zone contains the point.
Errors: invalid coords: logs [Modding API] getFareZoneAt requires [lng, lat] coordinates, returns []; any throw: logs [Modding API] Error resolving fare zone:, returns [].
const hits = api.gameState.getFareZoneAt([-122.42, 37.77]);
// one entry per zone-system fare group: { groupId, zoneId, zoneNumber }
for (const h of hits) console.log(`Zone ${h.zoneNumber} of group ${h.groupId}`);Tracks#
getTracks#
api.gameState.getTracks(): Track[]Every track in the running game.
The result is a new array of the store's live Track objects, whatever their displayType.
Returns: Track[]. Track[]; new array, live objects; [] when the store holds no tracks and on error.
Errors: any throw: logs [Modding API] Error getting tracks:, returns [].
const tracks = api.gameState.getTracks();
const built = tracks.filter((t) => t.displayType === 'constructed');
const km = built.reduce((sum, t) => sum + t.length, 0) / 1000;
console.log(`${built.length} built tracks, ${km.toFixed(1)} km`);Trains#
getTrains#
api.gameState.getTrains(): Train[]Every train in the running game, parked trains included.
The result is a new array of the store's live Train objects.
Returns: Train[]. Train[]; new array, live objects; [] when the store holds no trains and on error.
Errors: any throw: logs [Modding API] Error getting trains:, returns [].
const perRoute = {};
for (const t of api.gameState.getTrains()) perRoute[t.routeId] = (perRoute[t.routeId] || 0) + 1;
console.log(perRoute);Time & Money#
getCurrentDay#
api.gameState.getCurrentDay(): numberCurrent game day, 1-based.
Computed as floor(elapsedSeconds / 86400) + 1; a game day is 86400 game seconds.
Returns: number. number; game days; 1 with no elapsed time and on error.
Errors: any throw: logs [Modding API] Error getting current day:, returns 1.
const day = api.gameState.getCurrentDay();
console.log(`Day ${day}`);getCurrentHour#
api.gameState.getCurrentHour(): numberHour of the game day, 0 to 23.
Computed as floor((elapsedSeconds % 86400) / 3600).
Returns: number. number; 0 on error.
Errors: any throw: logs [Modding API] Error getting current hour:, returns 0.
const hour = api.gameState.getCurrentHour();
const rush = (hour >= 7 && hour < 10) || (hour >= 16 && hour < 19);
console.log(rush ? 'Rush hour' : 'Off peak');getElapsedSeconds#
api.gameState.getElapsedSeconds(): numberTotal elapsed game time in game seconds (timeConfig.elapsedSeconds).
Returns: number. number; game seconds; 0 for a fresh game and on error.
Errors: any throw: logs [Modding API] Error getting elapsed seconds:, returns 0.
const t = api.gameState.getElapsedSeconds();
const day = Math.floor(t / 86400) + 1;
const clock = new Date((t % 86400) * 1000).toISOString().slice(11, 16);
console.log(`Day ${day}, ${clock}`);getBudget#
api.gameState.getBudget(): numberThe player's money balance in dollars (money).
Returns: number. number; $; 0 on error.
Errors: any throw: logs [Modding API] Error getting budget:, returns 0.
const money = api.gameState.getBudget();
if (money < 0) api.ui.showNotification('You are in debt', 'warning');getTicketPrice#
api.gameState.getTicketPrice(): numberThe global ticket price in dollars (transitCost).
Falls back to RULES.DEFAULT_TICKET_COST (3) when the store holds none.
Returns: number. number; $; RULES.DEFAULT_TICKET_COST on error.
Errors: any throw: logs [Modding API] Error getting ticket price:, returns RULES.DEFAULT_TICKET_COST.
const fare = api.gameState.getTicketPrice();
console.log(`Base fare: ${fare}`);Game Controls#
getGameSpeed#
api.gameState.getGameSpeed(): 'slow' | 'normal' | 'fast' | 'ultrafast'Current game speed (timeConfig.timeSpeed).
Returns: 'slow' | 'normal' | 'fast' | 'ultrafast'. 'slow' | 'normal' | 'fast' | 'ultrafast'; 'normal' when unset and on error.
Errors: any throw: logs [Modding API] Error getting game speed:, returns 'normal'.
if (api.gameState.getGameSpeed() === 'ultrafast') console.log('Running at top speed');isPaused#
api.gameState.isPaused(): booleanWhether the game clock is paused (timeConfig.paused).
A new game starts paused.
Returns: boolean. boolean; false on error.
Errors: any throw: logs [Modding API] Error getting pause state:, returns false.
if (!api.gameState.isPaused()) api.actions.setPause(true);getMapColors#
api.gameState.getMapColors(isDark?: boolean): ModeColorsThe complete resolved map color set for one theme.
isDark defaults to whether the <html> element carries the dark class. The result is a fresh object: the theme's base colors overlaid by the selected built-in preset, or by the player's custom colors when no built-in preset is selected. Every key of the base color set is present.
Parameter isDark (boolean): true for the dark theme, false for light; default the current document theme.
Returns: ModeColors. ModeColors; fresh object each call.
Errors: any throw: logs [Modding API] Error getting map colors:, returns the light theme's resolved colors.
const dark = api.gameState.getMapColors(true);
const light = api.gameState.getMapColors(false);
console.log(Object.keys(dark).length === Object.keys(light).length);Demand Data#
getDemandData#
api.gameState.getDemandData(): DemandData | nullThe store's live demand data: points and popsMap.
Mutating the result changes game state. A new game starts with two empty Maps, so null comes back only when the store holds no demand object and on error.
Returns: DemandData | null. DemandData; live object; null on error.
Errors: any throw: logs [Modding API] Error getting demand data:, returns null.
const demand = api.gameState.getDemandData();
if (demand) {
let residents = 0;
for (const point of demand.points.values()) residents += point.residents;
console.log(`${demand.points.size} demand points, ${residents} residents, ${demand.popsMap.size} pops`);
}Buildings & Streets#
OSM Tag Keys#
The building tags the game keeps from OpenStreetMap, as returned by getBuildingsAt and getBuildingTags. A building is stored only when it carries one of the tags marked in the second column, so a plain building=yes has no stored tags.
| Tag | Makes a building stored |
|---|---|
building | no |
name | yes |
amenity | yes |
shop | yes |
office | yes |
tourism | yes |
leisure | yes |
historic | yes |
man_made | yes |
operator | yes |
brand | yes |
website | no |
wikidata | yes |
start_date | no |
addr:housenumber | no |
addr:street | no |
addr:postcode | no |
getBuildingsAt#
api.gameState.getBuildingsAt(coord: [number, number]): Promise<Array<{ osmId: number; tags: Record<string, string>; demolished: boolean }>>The buildings whose footprint covers a [lng, lat] coordinate, with OSM tags and demolition state.
Reads the building collision index that construction checks. Each entry holds osmId, tags (stored OSM tags, or {} when none) and demolished (true when this playthrough already demolished it). Resolves [] with no city or collision index loaded, and when no footprint contains the point.
Parameter coord ([number, number]): [lng, lat].
Returns: Promise<Array<{ osmId: number; tags: Record<string, string>; demolished: boolean }>>. Promise of a fresh array of { osmId, tags, demolished }.
Errors: any throw: logs [Modding API] Error getting buildings at coordinate:, resolves [].
Side effects: a call that finds a building loads the city's building tag table when it is not cached yet.
api.hooks.onStationSelected((station) => {
api.gameState.getBuildingsAt(station.coords).then((buildings) => {
for (const b of buildings) console.log(b.osmId, b.tags.name || b.tags.building, b.demolished ? 'demolished' : '');
});
});getBuildingTags#
api.gameState.getBuildingTags(osmId: number): Promise<Record<string, string> | null>The stored OpenStreetMap tags of a building by OSM id, or null.
Positive ids are OSM ways, negative ids are relations. The city's tag table loads once and stays cached until the next city load. Resolves null with no city loaded, with no tag table for the city, or for a building with no stored tags. The returned record is the cached object itself.
Parameter osmId (number): OSM id of the building.
Returns: Promise<Record<string, string> | null>. Promise of the tag record or null.
Errors: any throw: logs [Modding API] Error getting building tags:, resolves null.
Side effects: a call made before the table is cached loads it; logs [OSM tags] Loaded tags for ${table.size} buildings in ${cityCode} (development builds only) on success or warns with the prefix [OSM tags] No building tags for ${cityCode} (${path}) when the load fails.
api.gameState.getBuildingTags(123456789).then((tags) => {
// OSM keys such as building, amenity, shop, tourism, name
if (tags) console.log(tags.name, tags.building, tags.amenity);
});getStreetsAt#
api.gameState.getStreetsAt(coord: [number, number], radiusMeters?: number): Array<Record<string, unknown>>The road segments within radiusMeters of a [lng, lat] coordinate, nearest first.
Each entry is the road feature's properties plus distanceMeters, the distance from the coordinate to the segment. Synchronous: roads load with the city. Returns [] when no road data is loaded.
| Param | Type | Default | Description |
|---|---|---|---|
coord | [number, number] | required | [lng, lat]. |
radiusMeters | number | none | search radius; m; default 25. |
Returns: Array<Record<string, unknown>>. fresh array of property records, each with distanceMeters; [] on error.
Errors: any throw: logs [Modding API] Error getting streets at coordinate:, returns [].
const [nearest] = api.gameState.getStreetsAt([-73.9857, 40.7484], 50);
if (nearest) console.log(`${nearest.name ?? 'unnamed road'}, ${Math.round(nearest.distanceMeters)} m away`);Financial Data#
getBonds#
api.gameState.getBonds(): Bond[]The store's live bonds array.
A bond leaves the array when its remainingPrincipal reaches 0.
Returns: Bond[]. Bond[]; the live store array; [] when none and on error.
Errors: any throw: logs [Modding API] Error getting bonds:, returns [].
for (const b of api.gameState.getBonds()) {
console.log(`${b.bondTypeId}: ${b.remainingPrincipal} of ${b.principal} left`);
}getBondTypes#
api.gameState.getBondTypes(): typeof BOND_TYPESThe BOND_TYPES constant, keyed SMALL, MEDIUM, LARGE.
Each entry has a fixed id ('small', 'medium', 'large') and getters displayName, principal, interestRate, principalPaymentRate, requiredDailyRevenue that read RULES.BOND_TYPES live, so modifyConstants overrides show without a reload. The same object comes back on every call.
Returns: typeof BOND_TYPES. typeof BOND_TYPES; the shared constant.
const types = api.gameState.getBondTypes();
for (const key of Object.keys(types)) {
const t = types[key];
// pass t.id (not the key) to actions.issueBond
console.log(`${t.id}: ${t.principal} at ${t.interestRate}, needs ${t.requiredDailyRevenue} revenue/day`);
}getRouteFinancials#
api.gameState.getRouteFinancials(routeId?: string | null): RouteFinancials[]The recorded hourly revenue and expense ledger per route; $.
Temp routes are excluded. With routeId only that route is returned. hourly holds completed game hours, oldest first, capped at 336 entries (14 game days); each entry is { timestamp, revenue, expenses } with timestamp the hour's start in game seconds. Hours with no revenue and no expenses are not recorded, so hourly has gaps. expenses are train operating costs charged to the route. currentHour holds the amounts not yet moved into hourly. last24Hours sums the last 24 entries of the list formed by hourly plus currentHour (appended when nonzero) and reports profit as revenue minus expenses. Revenue is attributed per boarding from the fare-group breakdown of each journey, scaled to the fare the journey paid and multiplied by RULES.FARE_MULTIPLIER (365); when current fares price the journey at $0 the paid fare splits equally across its legs. Amounts recorded on a temp route fold into its parent route. Every returned object is a copy.
Parameter routeId (string | null): route id to restrict to; null or omitted returns every route.
Returns: RouteFinancials[]. fresh array; [] for an unknown routeId and on error.
Fields: Route Financials.
Errors: any throw: logs [Modding API] Error getting route financials:, returns [].
for (const r of api.gameState.getRouteFinancials()) {
const { revenue, expenses, profit } = r.last24Hours;
console.log(`${r.routeBullet}: +${revenue} -${expenses} = ${profit}`);
}Performance Metrics#
getRidershipStats#
api.gameState.getRidershipStats(): RidershipStatsTransit riders who finished a journey in the last 15 game minutes, as a count and an hourly rate.
Reads the live completed-commute list and keeps commutes whose journeyEnd lies within 900 game seconds of the current elapsedSeconds. totalRiders sums size (people) over those commutes; totalRidersPerHour is totalRiders / 0.25; timeWindowHours is the constant 0.25.
Returns: RidershipStats. fresh { totalRidersPerHour, totalRiders, timeWindowHours }; zeros with timeWindowHours 0.25 on error.
Fields: Ridership Stats.
Errors: any throw: logs [Modding API] Error getting ridership stats:, returns the zero object.
const stats = api.gameState.getRidershipStats();
console.log(`${stats.totalRidersPerHour} riders/hour over the last ${stats.timeWindowHours} h`);getLineMetrics#
api.gameState.getLineMetrics(): LineMetric[]Per-route train count, trains per hour, riders per hour and an estimated revenue per hour.
Temp routes (non-null tempParentId) are excluded. trainCount counts trains whose routeId is the route. trainsPerHour is 3600 / (the route's last stComboTimings departureTime / trainCount); null with no trains, no timings or a zero final departureTime. ridersPerHour sums size over commutes finished in the last 900 game seconds that rode the route, divided by 0.25. revenuePerHour is ridersPerHour times the global ticket price times RULES.FARE_MULTIPLIER (365); fare groups are ignored.
Returns: LineMetric[]. fresh array of { routeId, routeBullet, routeColor, trainCount, trainsPerHour, ridersPerHour, revenuePerHour }; [] on error.
Fields: Line Metrics.
Errors: any throw: logs [Modding API] Error getting line metrics:, returns [].
const lines = api.gameState.getLineMetrics();
lines.sort((a, b) => b.ridersPerHour - a.ridersPerHour);
for (const l of lines) console.log(`${l.routeBullet}: ${l.ridersPerHour} riders/h, ${l.trainCount} trains, ${l.revenuePerHour}/h`);getModeChoiceStats#
api.gameState.getModeChoiceStats(direction?: 'homeToWork' | 'workToHome'): ModeChoiceStatsCitywide commute mode split in people: walking, driving, transit, unknown.
Without direction each pop's home-to-work commute is read, falling back to its work-to-home commute, then to the legacy single commute summary. With direction only that leg is read. A pop with no commute in that direction adds its whole size to unknown. Each pop adds its mode split to the four counters. A pop with no commute at all adds its size to unknown.
Parameter direction ('homeToWork' | 'workToHome'): 'homeToWork' or 'workToHome'; default none (home-to-work with fallbacks).
Returns: ModeChoiceStats. fresh { walking, driving, transit, unknown }; people; zeros on error.
Fields: Mode Choice Stats.
Errors: no pops map in the store: logs debug [Modding API] getModeChoiceStats: popsMap is undefined (development builds only), returns zeros; any throw: logs [Modding API] Error getting mode choice stats:, returns zeros.
Side effects: logs debug [Modding API] getModeChoiceStats: (development builds only) with pop counts on every successful call.
const m = api.gameState.getModeChoiceStats('homeToWork');
const total = m.walking + m.driving + m.transit + m.unknown;
if (total) console.log(`Transit share: ${((m.transit / total) * 100).toFixed(1)}%`);getStationRidership#
api.gameState.getStationRidership(stationId?: string | null): StationRidershipRider counts from the statistics snapshot for one station, or for every station.
Counts snapshot commutes with a leg whose stationIds include stationId. null counts every commute. total sums size. transfers sums size of commutes in which the station appears in more than one leg, boards a leg after the first or alights a leg before the last; 0 when stationId is null. byRoute counts each route once per commute, legs touching the station only when stationId is set, and sorts by popCount descending, with percent relative to total. Same snapshot as getCompletedCommutes.
Parameter stationId (string | null): station id; null or omitted counts every station.
Returns: StationRidership. fresh { total, transfers, byRoute }; people; { total: 0, transfers: 0, byRoute: [] } on error.
Fields: Station Ridership.
Errors: any throw: logs [Modding API] Error getting station ridership:, returns the zero object.
const [station] = api.gameState.getStations();
if (station) {
const r = api.gameState.getStationRidership(station.id);
console.log(`${station.name}: ${r.total} riders, ${r.transfers} transfers`);
}getRouteRidership#
api.gameState.getRouteRidership(routeId?: string | null): RouteRidershipRider counts from the statistics snapshot for one route, or for every route.
Keeps legs whose routeId matches (every leg when null). byStation adds size to both stations of each kept leg, once per station per commute. total counts each commute once. Sorted by popCount descending. Same snapshot as getCompletedCommutes.
Parameter routeId (string | null): route id; null or omitted counts every route.
Returns: RouteRidership. fresh { total, byStation }; people; { total: 0, byStation: [] } on error.
Fields: Route Ridership.
Errors: any throw: logs [Modding API] Error getting route ridership:, returns the zero object.
const [route] = api.gameState.getRoutes({ includeTempRoutes: false });
if (route) {
const r = api.gameState.getRouteRidership(route.id);
const top = [...r.byStation].sort((a, b) => b.popCount - a.popCount)[0];
console.log(`${route.bullet}: ${r.total} riders, busiest stop ${top?.stationId ?? 'none'}`);
}getCompletedCommutes#
api.gameState.getCompletedCommutes(): Array<{ size: number; origin: 'home' | 'work'; stationRoutes: Array<{ routeId: string; stationIds: string[] }>; }>The store's statistics snapshot of completed transit commutes.
The snapshot is the whole completed-commute list, taken by reference when 900 or more game seconds have passed since the last snapshot. The underlying list is pruned at most once per RULES.COMMUTE_DATA_FILTER_INTERVAL (3600 game seconds) to commutes whose journeyEnd lies within RULES.TIME_TO_KEEP_COMMUTE_DATA (86400 game seconds), so the snapshot spans about one game day. Each entry holds popId, size (people riding transit in the pop), stationRoutes (legs in order, each { routeId, stationIds: [boarded, alighted] }), journeyStart and journeyEnd (game seconds) and origin. The result is the live snapshot array.
Returns: Array<{ size: number; origin: 'home' | 'work'; stationRoutes: Array<{ routeId: string; stationIds: string[] }>; }>. the live snapshot array; [] on error.
Errors: any throw: logs [Modding API] Error getting completed commutes:, returns [].
const commutes = api.gameState.getCompletedCommutes();
const people = commutes.reduce((sum, c) => sum + c.size, 0);
const transfers = commutes.filter((c) => c.stationRoutes.length > 1).length;
console.log(`${people} people arrived; ${transfers} groups transferred`);getRouteReliability#
api.gameState.getRouteReliability(routeId?: string | null, period?: string): Array<ReliabilityReport>Punctuality and rider-delay figures for each route over the last game day.
Temp routes are excluded. routeId restricts the result to one route. Records are hourly buckets: up to 24 completed hours per section or boarding station, plus the in-progress hour. Buckets stamped more than 86400 game seconds before the in-progress hour's start are dropped. period is 'all' or a key of the active commute time-of-day ranges (popTiming overrides included). An unknown key reads as 'all'. A range keeps only buckets whose hour of day falls inside it. Punctuality pools each physical section once, so a route running a section twice counts it once in sampleCount. sections lists every stCombo hop in route order, hops with no arrivals included. A hop whose start or end node belongs to no station is skipped. stations lists boarding stations with records, sorted by averageExcessRideSeconds descending. A null field means nothing was measured in the window. averageDelaySeconds reads 0 then. perceivedExtraSeconds is averageExcessRideSeconds times PATHFINDING_RULES.PERCEIVED_TIME.LATE_MULTIPLIER (3.01).
| Param | Type | Default | Description |
|---|---|---|---|
routeId | string or null | none | route id; null or omitted returns every route. |
period | string | none | 'all' or a time-of-day key; default 'all'. |
Returns: Array<ReliabilityReport>. fresh array of ReliabilityReport; [] on error.
Errors: any throw: logs [Modding API] Error getting route reliability:, returns [].
Changed: period resolves against the active commute ranges, so keys set by popTiming.setCommuteTimeRanges are accepted; before, only the default keys were (PR #1090).
for (const report of api.gameState.getRouteReliability(null, 'all')) {
const late = report.stations[0];
if (late) console.log(`${report.routeBullet}: worst boarding station ${late.stationId}`);
}getNetworkReliability#
api.gameState.getNetworkReliability(period?: string): ReliabilityStatsReliability figures pooled across every route for the last game day.
Punctuality pools the per-route rollups (temp routes excluded). Rider delay pools every recorded route and boarding station. period is 'all' or a key of the active commute time-of-day ranges (popTiming overrides included). An unknown key reads as 'all'. A range keeps only buckets whose hour of day falls inside it. A null field means nothing was measured. sampleCount, riders, rideRiders and averageDelaySeconds read 0 then.
Parameter period (string): 'all' or a time-of-day key; default 'all'.
Returns: ReliabilityStats. fresh ReliabilityStats; the all-zero, all-null object on error.
Errors: any throw: logs [Modding API] Error getting network reliability:, returns the empty object.
Changed: period resolves against the active commute ranges, so keys set by popTiming.setCommuteTimeRanges are accepted; before, only the default keys were (PR #1090).
const net = api.gameState.getNetworkReliability('all');
if (net.sampleCount >= api.gameState.RELIABILITY_MIN_SAMPLE && net.onTimePercent !== null) {
console.log(`Network on time: ${net.onTimePercent.toFixed(1)}%`);
}RELIABILITY_MIN_SAMPLE#
api.gameState.RELIABILITY_MIN_SAMPLE: numberCompleted rides (10) below which the reliability panel leaves a route's rider-delay figure unscored; the perceived time panel and the journey planner's leg hints apply the same minimum.
Equal to the rider-wait helper's MIN_RIDER_WAIT_SAMPLE.
const [report] = api.gameState.getRouteReliability();
const enough = (s) => s.sampleCount >= api.gameState.RELIABILITY_MIN_SAMPLE;
if (report) console.log(report.sections.filter(enough).length, 'sections with a usable sample');Station Groups & Transfers#
How Station Groups Work#
A station group is one named interchange: several stations that sit together and read as one place.
| Rule | Detail |
|---|---|
| Groups are derived | Stations whose centers lie within 150 m of each other cluster into a group (GAME_COLORS.STATION_GROUP_DISTANCE_THRESHOLD). The game rebuilds the groups when stations, station names or route geometry change. Mods do not create or delete groups. |
| The link points one way | A StationGroup lists its members in stationIds; a Station has no group field. Use getStationGroupForStation for one lookup, or build a map from getStationGroups() for many. |
| Group ids can change | A rebuild keeps the id of the matching previous group, else it mints a new one, so ids change when groups merge or split. Persist station ids and look the group up again. |
| Players edit groups | manualAdditions and manualRemovals record hand edits. A station removed by hand does not merge back into that group until the group is reset, so distance alone does not predict membership. |
| Groups do not route riders | Rider pathfinding transfers on foot between Station.nearbyStations, not by group. Groups drive naming, the interchange UI, ridership aggregation, getTransferStationIds and getSiblingStationIds. |
const groupByStation = new Map();
for (const group of api.gameState.getStationGroups()) {
for (const id of group.stationIds) groupByStation.set(id, group);
}getStationGroups#
api.gameState.getStationGroups(): StationGroup[]The store's live stationGroups array.
Groups are derived: stations whose centers (the visual center when the map has one) lie within GAME_COLORS.STATION_GROUP_DISTANCE_THRESHOLD (150 m) of each other cluster, with the player's manual additions and removals applied. Rebuilding the groups leaves out stations with noBoarding; adding a station to a group by hand does not check it. Each setStations call, station rename and route geometry recalculation, and clearing a group's manual center or custom name, rebuild the array sorted by member count ascending; adding a station to a group, removing one, resetting a group, and setting a manual center or custom name write the array without re-sorting.
Returns: StationGroup[]. StationGroup[]; the live store array; [] when none and on error.
Errors: any throw: logs [Modding API] Error getting station groups:, returns [].
for (const g of api.gameState.getStationGroups()) {
console.log(`${g.name}: ${g.stationIds.length} stations`);
}getStationGroupForStation#
api.gameState.getStationGroupForStation(stationId: string): StationGroup | nullDeep copy (structuredClone) of the first station group whose stationIds include stationId, or null.
Parameter stationId (string): station id.
Returns: StationGroup | null. StationGroup copy; null for an ungrouped or unknown id and on error.
Errors: any throw: logs [Modding API] Error getting station group for station:, returns null.
api.hooks.onStationSelected((station) => {
const group = api.gameState.getStationGroupForStation(station.id);
if (group) console.log(`${station.name} is part of ${group.name}`);
});getTransferStationIds#
api.gameState.getTransferStationIds(): string[]Station ids of every station group with two or more members, concatenated in group order.
Returns: string[]. string[]; fresh array; [] when no group has two members and on error.
Errors: any throw: logs [Modding API] Error getting transfer station IDs:, returns [].
const transfers = new Set(api.gameState.getTransferStationIds());
const names = api.gameState.getStations().filter((s) => transfers.has(s.id)).map((s) => s.name);
console.log(`Transfer stations: ${names.join(', ') || 'none'}`);getSiblingStationIds#
api.gameState.getSiblingStationIds(stationId: string): string[]The other member ids of the first station group whose stationIds include stationId.
Parameter stationId (string): station id.
Returns: string[]. string[]; fresh array; [] for an ungrouped or unknown id and on error.
Errors: any throw: logs [Modding API] Error getting sibling station IDs:, returns [].
const [station] = api.gameState.getStations();
if (station) {
const siblings = api.gameState.getSiblingStationIds(station.id);
console.log(`${station.name} shares a complex with ${siblings.length} other stations`);
}Save State#
getSaveName#
api.gameState.getSaveName(): string | nullName of the loaded save (currentSaveInfo.name), or null.
The store's currentSaveInfo starts null. Loading a save, and writing one through the keyboard save handler, set it through setCurrentSaveInfo.
Returns: string | null. string | null; null with no save info and on error.
Errors: any throw: logs [Modding API] Error getting save name:, returns null.
const name = api.gameState.getSaveName();
console.log(name ? `Playing save ${name}` : 'Unsaved game');getGameSessionId#
api.gameState.getGameSessionId(): string | nullThe playthrough's session id (gameSessionId), a UUID.
A new game sets a fresh UUID. Loading a save restores the save's id, or sets a fresh one for a save without it. Every save writes it.
Returns: string | null. string | null; null on error.
Errors: any throw: logs [Modding API] Error getting game session ID:, returns null.
// stable across save/load of one playthrough: key per-playthrough data by it
const sessionId = api.gameState.getGameSessionId();
if (sessionId) api.storage.set(`stats:${sessionId}`, { startedDay: api.gameState.getCurrentDay() });Blueprint Cost Calculation#
calculateBlueprintCost#
api.gameState.calculateBlueprintCost(tracks: Track[]): BlueprintCostConstruction cost ($) of the blueprint tracks in tracks.
Only tracks with displayType equal to 'blueprint' count. The result is all zeros when there are none. Building collisions come from the store's collision index and skip buildings this playthrough already demolished. With no collision index loaded, buildingDemolitionCost is 0. breakdown.trackCost includes yard trackage. totalCost also includes building-overpass, TBM mobilization, portal, ramp and scissors-crossover costs that the breakdown omits, so the three breakdown fields sum to at most totalCost. Grade crossings and staged track conversions are not passed on this path and add 0.
Parameter tracks (Track[]): tracks to price; entries with another displayType are ignored.
Returns: BlueprintCost. fresh { totalCost, breakdown: { trackCost, stationCost, buildingDemolitionCost } }; $; zeros on error.
Fields: Blueprint Cost.
Errors: any throw: logs [Modding API] Error calculating blueprint cost:, returns zeros.
const blueprints = api.gameState.getTracks().filter((t) => t.displayType === 'blueprint');
const { totalCost, breakdown } = api.gameState.calculateBlueprintCost(blueprints);
console.log(`Build all: ${totalCost} (track ${breakdown.trackCost}, stations ${breakdown.stationCost})`);getBlueprintTrackLength#
api.gameState.getBlueprintTrackLength(tracks?: Track[]): { trackMeters: number; routeMeters: number; }Blueprint track length (m), as summed rails and as corridor length.
With tracks omitted every store track is measured. Only tracks with displayType equal to 'blueprint' count. trackMeters sums length over distinct track ids. routeMeters adds, per track group, the mean length of its measured tracks, skipping yard groups and scissors-crossover tracks. A measured track in no group adds its own length unless its type is 'yard' or 'scissors-crossover'.
Parameter tracks (Track[]): tracks to measure; default the store's tracks.
Returns: object. fresh { trackMeters, routeMeters }; m; zeros on error.
| Field | Type | Description |
|---|---|---|
trackMeters | number | Sum of length over the measured blueprint tracks; m. |
routeMeters | number | Corridor length: one lane per track group, no yard or scissors-crossover track; m. |
Errors: any throw: logs [Modding API] Error measuring blueprint track length:, returns zeros.
Changed: added (PR #1090).
const { trackMeters, routeMeters } = api.gameState.getBlueprintTrackLength();
// a 1 km double-track blueprint: trackMeters 2000, routeMeters 1000ModeColors#
The map's resolved color set. Every key of the light theme's base colors, each a CSS color string.
type ModeColors = typeof COLORS.LIGHTReliabilityReport#
ReliabilityStats for one route plus its identity, per-section punctuality and per-station rider delay.
| Field | Type | Default | Description |
|---|---|---|---|
routeId | string | required | Route id. |
routeBullet | string | required | The route's bullet. |
routeColor | string | required | The route's color; CSS color string. |
sections | Array<{ fromStationId: string; toStationId: string; sampleCount: number; onTimePercent: number | null; averageDelaySeconds: number; averageAddedDelaySeconds: number; }> | required | One entry per stCombo hop in route order: fromStationId, toStationId, sampleCount, onTimePercent, averageDelaySeconds, averageAddedDelaySeconds (mean running time lost on that hop against its booked time). A hop whose start or end node belongs to no station is skipped. |
stations | Array<{ stationId: string; riders: number; averageExcessWaitSeconds: number | null; delayedPercent: number | null; rideRiders: number; averageExcessRideSeconds: number | null; rideDelayedPercent: number | null; }> | required | Boarding stations with records: stationId plus riders, averageExcessWaitSeconds, delayedPercent, rideRiders, averageExcessRideSeconds, rideDelayedPercent; sorted by averageExcessRideSeconds descending. |
ReliabilityStats#
Punctuality and rider-delay figures for a route or the whole network over the last game day.
| Field | Type | Default | Description |
|---|---|---|---|
sampleCount | number | required | Train arrivals recorded in the window. |
onTimePercent | number or null | required | Share (0 to 100) of arrivals whose accumulated lost running time is at most RULES.IS_DELAYED_THRESHOLD (300 s); null with no arrivals. |
averageDelaySeconds | number | required | Mean seconds of running time lost against the booked running time, accumulated over the lap up to the arrival; 0 with no arrivals. |
riders | number | required | Boardings measured; people. |
averageExcessWaitSeconds | number or null | required | Mean extra platform seconds per boarding beyond the planned wait; null with no boardings. |
delayedPercent | number or null | required | Share (0 to 100) of boardings whose extra wait exceeded 300 s; null with no boardings. |
rideRiders | number | required | Completed rides measured; people. |
averageExcessRideSeconds | number or null | required | Mean extra on-board seconds per ride beyond the booked ride time; null with no rides. |
rideDelayedPercent | number or null | required | Share (0 to 100) of rides whose extra on-board time exceeded 300 s; null with no rides. |
perceivedExtraSeconds | number or null | required | averageExcessRideSeconds times PATHFINDING_RULES.PERCEIVED_TIME.LATE_MULTIPLIER (3.01); null with no rides. |