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.

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

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

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

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

javascript
const hits = window.SubwayBuilderAPI.gameState.getFareZoneAt([-122.42, 37.77]);
// [{ groupId, zoneId, zoneNumber }]

Tracks#

getTracks#

Get all tracks with elevation/depth data.

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

javascript
const trains = window.SubwayBuilderAPI.gameState.getTrains();
console.log('Active trains:', trains.length);

Time & Money#

getCurrentDay#

Get the current in-game day.

javascript
const day = window.SubwayBuilderAPI.gameState.getCurrentDay();

getCurrentHour#

Get the current in-game hour (0-23).

javascript
const hour = window.SubwayBuilderAPI.gameState.getCurrentHour();

getElapsedSeconds#

Get total elapsed in-game time in seconds.

javascript
const elapsedSeconds = window.SubwayBuilderAPI.gameState.getElapsedSeconds();

getBudget#

Get current money balance.

javascript
const budget = window.SubwayBuilderAPI.gameState.getBudget();
console.log(`$${budget.toLocaleString()} remaining`);

getTicketPrice#

Get current ticket price.

javascript
const ticketPrice = window.SubwayBuilderAPI.gameState.getTicketPrice();

Game Controls#

getGameSpeed#

Get current game speed.

javascript
const speed = window.SubwayBuilderAPI.gameState.getGameSpeed();
// Returns: 'slow', 'normal', 'fast', or 'ultrafast'

isPaused#

Check if game is paused.

javascript
const paused = window.SubwayBuilderAPI.gameState.isPaused();

Demand Data#

getDemandData#

Get population/commuter demand data.

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

javascript
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 (see getBuildingTags below)
  • demolished β€” true if 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.

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

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

javascript
const bonds = window.SubwayBuilderAPI.gameState.getBonds();
bonds.forEach((bond) => {
    console.log(`Bond: $${bond.principal} at ${bond.interestRate}%`);
});

getBondTypes#

Get available bond type definitions.

javascript
const bondTypes = window.SubwayBuilderAPI.gameState.getBondTypes();
console.log('Available bond types:', Object.keys(bondTypes));

Performance Metrics#

getRidershipStats#

Get ridership statistics.

javascript
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-time

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

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

javascript
// 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.

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

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

javascript
// 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.

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

javascript
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):

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

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

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

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

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

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

javascript
const sessionId = window.SubwayBuilderAPI.gameState.getGameSessionId();
console.log(`Current playthrough: ${sessionId}`);

Typical use β€” scoping stored data to one playthrough (see Mod Storage for details):

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

javascript
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#

MethodReturnsDescription
getStations()Station[]All stations
getRoutes()Route[]All routes
getFareGroups()FareGroup[]All fare groups (deep copies)
getJourneyFare(segments)numberPrice 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()numberCurrent day
getCurrentHour()numberCurrent hour (0-23)
getElapsedSeconds()numberTotal elapsed seconds
getBudget()numberCurrent money
getTicketPrice()numberTicket price
getGameSpeed()stringGame speed
isPaused()booleanPause state
getDemandData()DemandDataDemand/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()objectBond type definitions
getRidershipStats()objectRidership statistics
getLineMetrics()object[]Per-line metrics
getRouteFinancials(id?)object[]Recorded per-route revenue/expense history
getModeChoiceStats(direction?)objectMode choice breakdown (home→work by default)
getStationRidership(id?)objectStation ridership
getRouteRidership(id)objectRoute 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)objectCost breakdown