Game State
Read current game state (read-only)
The game state API provides read-only access to current game data for analysis, UI mods, or debugging.
For complete property listings of every returned object, see the Type Reference.
Stations#
getStations#
Get all stations in the current game.
const stations = window.SubwayBuilderAPI.gameState.getStations();
console.log('Total stations:', stations.length);
// Station data includes: id, name, coords, routeIds, etc.
// Note: stations do NOT carry a group field β see Station Groups below
stations.forEach((station) => {
console.log(`${station.name} at [${station.coords}]`);
});Routes#
getRoutes#
Get all routes (subway lines).
const routes = window.SubwayBuilderAPI.gameState.getRoutes();
routes.forEach((route) => {
console.log(`Route ${route.bullet}: ${route.stNodes.length} stops, color ${route.color}`);
});getFareGroups#
Get all fare groups as deep copies β routes are partitioned into groups, each pricing journeys with one fare system ('flat', 'route', 'distance', or 'zone') and a transfer policy. Mutating the returned objects does nothing; use actions.updateFareGroup and friends to change them.
const groups = window.SubwayBuilderAPI.gameState.getFareGroups();
// [{ id, name, fareSystem, flatFare, routeFares, routeIds,
// transferPolicy, boardingCharge, perKmRate, fareCap,
// zones, zonePricingMode, zoneBaseFare, zonePerZoneFare }]getJourneyFare#
Price a journey's transit segments with the current fare groups. Accepts segments shaped like a pathfinding result's segments (each with routeId, plus fromStopCoords/toStopCoords for distance-based pricing and fromStopId/toStopId station ids for zone-based pricing); walking and driving legs are free. Returns dollars.
const fare = window.SubwayBuilderAPI.gameState.getJourneyFare(path.segments);getFareZoneAt#
Which fare zones contain a [lng, lat] point β one entry per zone-system fare group whose zones cover it (zones within a group never overlap). zoneNumber is the 1-based position in the group's zone order, matching the number shown on the map.
const hits = window.SubwayBuilderAPI.gameState.getFareZoneAt([-122.42, 37.77]);
// [{ groupId, zoneId, zoneNumber }]Tracks#
getTracks#
Get all tracks with elevation/depth data.
const tracks = window.SubwayBuilderAPI.gameState.getTracks();
// Each track has startElevation and endElevation
// Negative values = underground, positive = elevated
tracks.forEach((track) => {
console.log(`Track ${track.id}: ${track.startElevation}m to ${track.endElevation}m`);
});
// Find deepest track
const deepestTrack = tracks.reduce((deepest, track) => {
const minElev = Math.min(track.startElevation, track.endElevation);
const deepestElev = Math.min(deepest.startElevation, deepest.endElevation);
return minElev < deepestElev ? track : deepest;
}, tracks[0]);
console.log('Deepest track:', deepestTrack?.startElevation, 'm');Trains#
getTrains#
Get all active trains.
const trains = window.SubwayBuilderAPI.gameState.getTrains();
console.log('Active trains:', trains.length);Time & Money#
getCurrentDay#
Get the current in-game day.
const day = window.SubwayBuilderAPI.gameState.getCurrentDay();getCurrentHour#
Get the current in-game hour (0-23).
const hour = window.SubwayBuilderAPI.gameState.getCurrentHour();getElapsedSeconds#
Get total elapsed in-game time in seconds.
const elapsedSeconds = window.SubwayBuilderAPI.gameState.getElapsedSeconds();getBudget#
Get current money balance.
const budget = window.SubwayBuilderAPI.gameState.getBudget();
console.log(`$${budget.toLocaleString()} remaining`);getTicketPrice#
Get current ticket price.
const ticketPrice = window.SubwayBuilderAPI.gameState.getTicketPrice();Game Controls#
getGameSpeed#
Get current game speed.
const speed = window.SubwayBuilderAPI.gameState.getGameSpeed();
// Returns: 'slow', 'normal', 'fast', or 'ultrafast'isPaused#
Check if game is paused.
const paused = window.SubwayBuilderAPI.gameState.isPaused();Demand Data#
getDemandData#
Get population/commuter demand data.
const demandData = window.SubwayBuilderAPI.gameState.getDemandData();
if (demandData) {
console.log('Demand points:', demandData.points.size);
console.log('Population groups:', demandData.popsMap.size);
// Iterate over demand points
demandData.points.forEach((point, id) => {
console.log(`Point ${id}: ${point.jobs} jobs, ${point.residents} residents`);
});
}Buildings & Streets#
These methods answer "what is at this coordinate?" using the same OpenStreetMap data the map renders and construction collides against. Coordinates are [longitude, latitude], matching everything else in the API.
getBuildingsAt#
Get the buildings whose footprint covers a coordinate, with their OSM tags. This reads the construction collision index, so the answer matches what a track built there would demolish β usually zero or one building.
const buildings = await window.SubwayBuilderAPI.gameState.getBuildingsAt([-73.98, 40.75]);
buildings.forEach((b) => {
console.log(`OSM ${b.osmId}: ${b.tags.name ?? '(unnamed)'}${b.demolished ? ' [demolished]' : ''}`);
});Each entry contains:
osmIdβ the building's OpenStreetMap id (positive = way, negative = relation)tagsβ its stored OSM tags, or{}if it carries none (seegetBuildingTagsbelow)demolishedβtrueif construction in this playthrough has already demolished it
getBuildingTags#
Get the OSM tags for a building by its OSM id β the same ids getBuildingsAt returns. Async because the city's tag table loads lazily on first use.
const tags = await window.SubwayBuilderAPI.gameState.getBuildingTags(123456789);
if (tags) {
console.log(tags.name, tags.amenity, tags['addr:street']);
}Returns null for buildings with nothing worth reporting: only buildings carrying a name, amenity, shop, office, or similar tag are stored β a few percent of any city. A plain untagged building=yes returns null, as does any lookup in a city without a tag table (custom cities can provide one via the buildingTags city data file).
Stored tag keys: building, name, amenity, shop, office, tourism, leisure, historic, man_made, operator, brand, website, wikidata, start_date, addr:housenumber, addr:street, addr:postcode.
getStreetsAt#
Get the streets within a radius of a coordinate (default 25 m), nearest first. Roads are already loaded with the city, so this one is synchronous.
const streets = window.SubwayBuilderAPI.gameState.getStreetsAt([-73.98, 40.75], 50);
streets.forEach((s) => {
console.log(`${s.name ?? s.highway} β ${s.distanceMeters.toFixed(1)} m away`);
});Each entry is the road feature's properties (including OSM tags like highway, name, ref, lanes, oneway, maxspeed, bridge, tunnel where present) plus distanceMeters, the distance from your coordinate to the street.
Financial Data#
getBonds#
Get all active bonds.
const bonds = window.SubwayBuilderAPI.gameState.getBonds();
bonds.forEach((bond) => {
console.log(`Bond: $${bond.principal} at ${bond.interestRate}%`);
});getBondTypes#
Get available bond type definitions.
const bondTypes = window.SubwayBuilderAPI.gameState.getBondTypes();
console.log('Available bond types:', Object.keys(bondTypes));Performance Metrics#
getRidershipStats#
Get ridership statistics.
const ridership = window.SubwayBuilderAPI.gameState.getRidershipStats();
console.log('Riders per hour:', ridership.totalRidersPerHour);
console.log('Riders in last 15 min:', ridership.totalRiders);
// Note: stats are based on a 15-minute rolling window, not cumulative all-timeThe window rolls continuously with game time, so the rates hold steady between engine ticks. (Older builds read a snapshot that only refreshed every 15 game-minutes, so totalRidersPerHour and getLineMetrics's revenuePerHour decayed to 0 right before each refresh.)
getLineMetrics#
Get per-line performance metrics. Revenue values include the fare multiplier and match what you see in the financial dashboard. Like getRidershipStats, rates come from a continuously rolling 15-minute window and no longer pulse to 0 between engine ticks.
const lineMetrics = window.SubwayBuilderAPI.gameState.getLineMetrics();
lineMetrics.forEach((line) => {
console.log(`Route ${line.routeBullet}: ${line.ridersPerHour} riders/hr, $${line.revenuePerHour}/hr`);
});
// Each metric object contains:
// { routeId, routeBullet, routeColor, trainCount, trainsPerHour, ridersPerHour, revenuePerHour }getRouteFinancials#
Get recorded revenue and expense history per route β the same data shown in the Financials tab of the route details panel. Unlike getLineMetrics (which estimates revenue from a 15-minute ridership window and credits a transferring rider fully to every route they touch), this is the actual recorded ledger: each rider's fare is split equally among the trains they rode, and expenses are the train operating costs charged to the route.
// All routes
const financials = window.SubwayBuilderAPI.gameState.getRouteFinancials();
financials.forEach((route) => {
console.log(`Route ${route.routeBullet}: $${route.last24Hours.profit} profit (24h)`);
});
// A single route
const [line] = window.SubwayBuilderAPI.gameState.getRouteFinancials('route-123');
line.hourly.forEach((entry) => {
// entry = { timestamp, revenue, expenses } β one per completed game hour, oldest first
console.log(`Hour starting ${entry.timestamp}: +$${entry.revenue} / -$${entry.expenses}`);
});
console.log('In-progress hour so far:', line.currentHour);Hourly history is kept for up to 14 game days per route. last24Hours includes the in-progress hour.
getModeChoiceStats#
Get commuter mode choice breakdown.
const modes = window.SubwayBuilderAPI.gameState.getModeChoiceStats();
// { walking: 1200, driving: 8500, transit: 3200, unknown: 100 }
const total = modes.walking + modes.driving + modes.transit;
const transitShare = ((modes.transit / total) * 100).toFixed(1);
console.log(`Transit mode share: ${transitShare}%`);Counts the homeβwork commute β the standard journey-to-work mode share. It doesn't move with the clock, so polling it every tick gives a stable number you can chart or compare across days.
Pass a direction to ask for one leg on its own. The evening commute usually shows a different split, since traffic and departure times differ:
const morning = api.gameState.getModeChoiceStats('homeToWork');
const evening = api.gameState.getModeChoiceStats('workToHome');
console.log(`Transit riders β AM: ${morning.transit}, PM: ${evening.transit}`);<Callout type="warning"> Pops with no commute recorded in the requested direction count as unknown. Right after loading a save written before per-direction data existed, an explicit direction reports everything as unknown until the next commute cycle runs β the no-argument form has no such gap. Divide by walking + driving + transit rather than by the total if you want a share that ignores unknowns. </Callout>
getStationRidership#
Get ridership data for stations.
// All stations
const allStats = window.SubwayBuilderAPI.gameState.getStationRidership();
console.log('Total system ridership:', allStats.total);
// Specific station
const stationStats = window.SubwayBuilderAPI.gameState.getStationRidership('station-123');
console.log(`Riders: ${stationStats.total}, transfers: ${stationStats.transfers}`);
stationStats.byRoute.forEach((r) => {
console.log(` Route ${r.routeId}: ${r.popCount} riders`);
});getRouteRidership#
Get ridership data for a route.
const routeStats = window.SubwayBuilderAPI.gameState.getRouteRidership('route-456');
routeStats.byStation.forEach((s) => {
console.log(` ${s.stationId}: ${s.popCount} riders`);
});getCompletedCommutes#
Get all completed commutes from the last 15 minutes of game time. Useful for advanced analytics mods that need raw commute data.
const commutes = window.SubwayBuilderAPI.gameState.getCompletedCommutes();
console.log(`${commutes.length} commutes in last 15 min`);
commutes.forEach((c) => {
console.log(`Group of ${c.size} from ${c.originId} to ${c.destId}`);
});Station Groups & Transfers#
How station groups work#
A station group represents one named interchange complex β think "Union Square" as a single place, even though three separate stations sit there. Understanding four things will save you debugging time:
1. Groups are derived, not authored. The game automatically clusters stations whose (visual) centers are within ~150 m of each other, and recomputes the clustering whenever stations or tracks change. You never create or destroy groups directly.
2. The relationship points one way. A StationGroup lists its members in stationIds β but a Station has no group field. To go from a station to its group, use `getStationGroupForStation()` (or build your own reverse map if you're doing many lookups):
const groups = api.gameState.getStationGroups();
const groupByStationId = new Map();
groups.forEach((group) => {
group.stationIds.forEach((sid) => groupByStationId.set(sid, group));
});3. Don't persist group IDs. Group IDs are kept stable across recomputes on a best-effort basis, but when groups merge or split (a new station built between two hubs, a station demolished), IDs can change. Station IDs are the durable handle β store those, and re-derive the group when you need it.
4. Players can hand-edit groups. The manualAdditions / manualRemovals fields record stations the player explicitly added to or removed from a group. Manually separated stations will never be automatically re-merged, so don't assume proximity alone predicts membership.
<Callout type="info"> Groups are not what makes transfers work. Rider pathfinding uses walking time between stations (station.nearbyStations), not group membership. Groups drive naming, the interchange UI, and ridership aggregation β getTransferStationIds() and getSiblingStationIds() below are defined in group terms. Two stations can be walk-transferable without sharing a group, and vice versa. </Callout>
getStationGroups#
Get all station groups.
const groups = window.SubwayBuilderAPI.gameState.getStationGroups();
groups.forEach((group) => {
console.log(`Group ${group.id}: ${group.stationIds.length} stations`);
});getStationGroupForStation#
Get the group a station belongs to (the reverse lookup described above), or null if the station is ungrouped or the ID is unknown. Returns a copy β mutating it won't affect game state.
const group = window.SubwayBuilderAPI.gameState.getStationGroupForStation('station-123');
if (group) {
console.log(`Station is part of ${group.name} (${group.stationIds.length} stations)`);
}getTransferStationIds#
Get IDs of all stations that are part of a transfer (in groups with 2+ stations).
const transferIds = window.SubwayBuilderAPI.gameState.getTransferStationIds();
console.log(`${transferIds.length} stations serve as transfers`);getSiblingStationIds#
Get sibling station IDs for a given station (other stations in the same transfer group).
const siblings = window.SubwayBuilderAPI.gameState.getSiblingStationIds('station-123');
console.log(`Station has ${siblings.length} transfer siblings`);Save State#
getSaveName#
Get the name of the currently loaded save, or null if no save is loaded.
const saveName = window.SubwayBuilderAPI.gameState.getSaveName();
if (saveName) {
console.log(`Playing save: ${saveName}`);
}<Callout type="warning"> The save name is not a stable identifier: it's null until the player saves for the first time, it changes on rename or "save as", and two different playthroughs can use the same name. For a stable ID, use getGameSessionId(). </Callout>
getGameSessionId#
Get the stable ID of the current playthrough β a UUID generated when a new game starts, written into every save file, and preserved across save, load, and rename. Use this (not the save name) to key per-playthrough mod data.
const sessionId = window.SubwayBuilderAPI.gameState.getGameSessionId();
console.log(`Current playthrough: ${sessionId}`);Typical use β scoping stored data to one playthrough (see Mod Storage for details):
const api = window.SubwayBuilderAPI;
const storage = api.storage.scoped();
api.hooks.onGameLoaded(async () => {
const runId = api.gameState.getGameSessionId();
const history = await storage.get(`run:${runId}:history`, []);
console.log(`This playthrough has ${history.length} recorded entries`);
});<Callout type="info"> Saves created before this ID existed are assigned one the first time they're loaded; it becomes permanent when the game is next saved. </Callout>
Blueprint Cost Calculation#
calculateBlueprintCost#
Calculate cost of blueprint tracks before construction.
const tracks = window.SubwayBuilderAPI.gameState.getTracks();
const blueprintTracks = tracks.filter((t) => t.displayType === 'blueprint');
const cost = window.SubwayBuilderAPI.gameState.calculateBlueprintCost(blueprintTracks);
console.log('Total cost:', cost.totalCost.toLocaleString());
console.log('Breakdown:');
console.log(' Track cost:', cost.breakdown.trackCost.toLocaleString());
console.log(' Station cost:', cost.breakdown.stationCost.toLocaleString());
console.log(' Demolition:', cost.breakdown.buildingDemolitionCost.toLocaleString());Buildings already demolished earlier in the playthrough are excluded β they don't collide and aren't charged again.
State Summary#
| Method | Returns | Description | |
|---|---|---|---|
getStations() | Station[] | All stations | |
getRoutes() | Route[] | All routes | |
getFareGroups() | FareGroup[] | All fare groups (deep copies) | |
getJourneyFare(segments) | number | Price a journey with the current fare groups | |
getFareZoneAt(coords) | Array<{...}> | Fare zones containing a [lng, lat] point | |
getTracks() | Track[] | All tracks | |
getTrains() | Train[] | All trains | |
getCurrentDay() | number | Current day | |
getCurrentHour() | number | Current hour (0-23) | |
getElapsedSeconds() | number | Total elapsed seconds | |
getBudget() | number | Current money | |
getTicketPrice() | number | Ticket price | |
getGameSpeed() | string | Game speed | |
isPaused() | boolean | Pause state | |
getDemandData() | DemandData | Demand/population data | |
getBuildingsAt(coord) | Promise<object[]> | Buildings covering a coordinate, with OSM tags | |
getBuildingTags(osmId) | `Promise<object\ | null>` | OSM tags for a building by OSM id |
getStreetsAt(coord, radius?) | object[] | Streets near a coordinate, nearest first | |
getBonds() | Bond[] | Active bonds | |
getBondTypes() | object | Bond type definitions | |
getRidershipStats() | object | Ridership statistics | |
getLineMetrics() | object[] | Per-line metrics | |
getRouteFinancials(id?) | object[] | Recorded per-route revenue/expense history | |
getModeChoiceStats(direction?) | object | Mode choice breakdown (homeβwork by default) | |
getStationRidership(id?) | object | Station ridership | |
getRouteRidership(id) | object | Route ridership | |
getCompletedCommutes() | object[] | Raw commutes (last 15 min) | |
getStationGroups() | StationGroup[] | All station groups | |
getStationGroupForStation(id) | `StationGroup \ | null` | Group containing a station (reverse lookup) |
getTransferStationIds() | string[] | Station IDs in transfer groups | |
getSiblingStationIds(id) | string[] | Sibling stations in same group | |
getSaveName() | `string \ | null` | Current save name |
getGameSessionId() | `string \ | null` | Stable per-playthrough ID (survives load/rename) |
calculateBlueprintCost(tracks) | object | Cost breakdown |