Type Reference
Complete return types and object shapes for the Modding API
{/ shoutout muffintime for keeping us honest about documentation β you were right, we should've had this from the start /}
_"return types and values or object properties are just not documented for a good chunk of the functions"_ β a wise and extremely patient modder who will remain unnamed (thanks muffintime)
Every getter in the Modding API returns plain JavaScript objects. This page documents every property so you know exactly what you're working with β no more guessing.
Station#
Returned by gameState.getStations(), hooks.onStationBuilt(), hooks.onStationSelected(), and hooks.onTrainArrivedAtStation().
type Station = {
/** Unique station identifier */
id: string;
/** Display name (e.g. "Times Square") */
name: string;
/** [longitude, latitude] */
coords: [number, number];
/** IDs of tracks at this station */
trackIds: string[];
/** Track group this station belongs to */
trackGroupId: string;
/** Station-track node IDs (for routing) */
stNodeIds: string[];
/** 'constructed' | 'blueprint' | 'preview' */
buildType: BuildType;
/** Route IDs serving this station */
routeIds: string[];
/** Train type constraint (default: 'heavy-metro') */
trainType?: string;
/** Station type (default: 'standard', or custom registered type) */
stationType?: string;
/** Nearby stations with walking times */
nearbyStations?: { stationId: string; walkingTime: number }[];
/** Unix timestamp (ms) when station was created */
createdAt?: number;
};Example: Accessing Station Data#
const stations = api.gameState.getStations();
stations.forEach((station) => {
// All these properties are guaranteed to exist:
console.log(station.id); // "station-abc123"
console.log(station.name); // "Grand Central"
console.log(station.coords); // [-73.977, 40.752]
console.log(station.buildType); // "constructed"
console.log(station.routeIds); // ["route-1", "route-2"]
// Optional properties β check before using:
if (station.stationType) {
console.log(station.stationType); // "express" or "standard"
}
if (station.nearbyStations) {
station.nearbyStations.forEach((ns) => {
console.log(`${ns.stationId} is ${ns.walkingTime}s walk away`);
});
}
});Route#
Returned by gameState.getRoutes() and hooks.onRouteCreated().
type Route = {
/** Unique route identifier */
id: string;
/** Parent route ID for temporary redirect routes, or null */
tempParentId: string | null;
/** Route bullet/name (e.g. "A", "7", "L") */
bullet: string;
/** Optional full route name (e.g. "8th Avenue Express") β shown next to the bullet on departure boards */
fullName?: string;
/** Station-track nodes on this route */
stNodes: StationTrackNode[];
/** Route color (hex, e.g. "#0039A6") */
color: string;
/** Text color for the route bullet (hex) */
textColor: string;
/** Two-tone bullet: a ring of the route color around a center filled with the inverse of the text color */
bordered?: boolean;
/** Ideal number of trains for this route */
idealTrainCount: number;
/** Demand-tiered train counts β if not set, idealTrainCount is used for all times */
trainSchedule?: RouteTrainSchedule;
/** Timetable-based scheduling with per-time-period headways */
timetableSchedule?: RouteTimetableSchedule;
/** Route shape icon: 'circle' | 'diamond' | 'square' | 'rounded-square' | 'triangle' */
shape?: string;
/** Bullet text styling: 'default' | 'mono' | 'bolder' | 'lighter' | 'italic'. Undefined = bold sans */
font?: string;
/** Cars per train on this route */
carsPerTrain?: number;
/** Train type for this route */
trainType?: string;
/** Unix timestamp (ms) */
createdAt?: number;
};FareGroup#
Returned by gameState.getFareGroups() and hooks.onFareGroupsChanged(); edited via actions.updateFareGroup and friends. Every non-temporary route belongs to exactly one fare group.
type FareGroup = {
/** Unique fare group identifier */
id: string;
/** Player-editable display name. Empty string = the localized default ("Fare group N") */
name: string;
/** How this group prices journeys */
fareSystem: 'flat' | 'route' | 'distance' | 'zone';
/** Price in dollars of one journey within this group when fareSystem is 'flat' */
flatFare: number;
/** Per-route prices (dollars, keyed by route id) when fareSystem is 'route'; missing routes fall back to flatFare */
routeFares: Record<string, number>;
/** Ordered route ids in this group */
routeIds: string[];
/**
* How transfers are priced when boarding this group:
* - 'all-paid': every boarding charges again
* - 'count-within-group': legs in this group combine into one fare (default)
* - 'count-all-groups': also credits the first charge for journeys that already paid in another group
* - 'free-within-group': deprecated β legacy saves only, normalized to 'count-within-group'
*/
transferPolicy: 'all-paid' | 'count-within-group' | 'count-all-groups' | 'free-within-group';
/** Distance system: boarding charge in dollars (flag fall) */
boardingCharge: number;
/** Distance system: dollars per km (straight-line between boarding and alighting stations) */
perKmRate: number;
/** Distance/zone systems: maximum fare in dollars per trip. 0 = no cap. Fares round to the nearest $0.05 */
fareCap: number;
/** Zone system: ordered fare zones drawn on the map (index = displayed zone number and ring order) */
zones: FareZone[];
/**
* Zone system: how zones are counted for pricing:
* - 'count': pay per distinct zone the journey touches (London-style)
* - 'combination': nested-ring tickets (Berlin-style) β pay for the contiguous
* span from the innermost to the outermost zone touched, with a two-zone
* minimum when the group has 2+ zones (a single-zone group is a flat city zone)
*/
zonePricingMode: 'count' | 'combination';
/** Zone system: price in dollars of a trip touching a single zone (or no zone at all) */
zoneBaseFare: number;
/** Zone system: dollars added per additional zone the journey is charged for */
zonePerZoneFare: number;
};FareZone#
One fare zone of a zone-based fare group β a polygon drawn on the map (or created via actions.addFareZone). Zones in a group never overlap: new shapes are clipped against the group's existing zones, so a shape drawn around an existing zone becomes a concentric ring.
type FareZone = {
/** Unique zone identifier */
id: string;
/**
* MultiPolygon coordinates ([lng, lat]): polygons β rings, where a polygon's
* first ring is its outer boundary and later rings are holes (holes appear
* when a zone is clipped against another zone inside it)
*/
polygons: [number, number][][][];
};A journey's zone fare is zoneBaseFare + (zonesCharged - 1) Γ zonePerZoneFare, capped by fareCap, where zonesCharged comes from the boarding/alighting stations of its transit legs under the group's zonePricingMode. Stations outside every zone count as a single-zone trip.
Route Schedules#
Schedule types used by Route and by hooks.onScheduleChange().
type RouteTrainSchedule = {
/** Number of trains for high demand periods */
highDemand: number;
/** Number of trains for medium demand periods */
mediumDemand: number;
/** Number of trains for low demand periods */
lowDemand: number;
/** Number of trains for very low demand periods (falls back to lowDemand when absent) */
veryLowDemand?: number;
};
type ScheduleTimePeriod = {
id: string;
name: string;
startHour: number;
endHour: number;
/** Target headway in seconds. 0 = no service. */
headwaySeconds: number;
};
type RouteTimetableSchedule = {
mode: 'frequency' | 'timetable';
periods: ScheduleTimePeriod[];
};
/** Passed to onScheduleChange callbacks. Copies β safe to mutate. */
type RouteScheduleSnapshot = {
idealTrainCount: number;
trainSchedule?: RouteTrainSchedule;
timetableSchedule?: RouteTimetableSchedule;
};Track#
Returned by gameState.getTracks(), hooks.onTrackBuilt(), and hooks.onBlueprintPlaced().
type Track = {
/** Unique track identifier */
id: string;
/** Array of [longitude, latitude] coordinate pairs */
coords: [number, number][];
/** Elevation at start of track (negative = underground, meters) */
startElevation: number;
/** Elevation at end of track (negative = underground, meters) */
endElevation: number;
/** 'constructed' | 'blueprint' | 'preview' */
buildType: string;
/** Display type for rendering */
displayType: 'constructed' | 'blueprint' | 'preview' | 'preview-center' | 'preview-connected';
/** Track feature type */
type: 'station' | 'scissors-crossover' | 'express-station' | null;
/** Whether trains can reverse on this track */
reversable: boolean;
/** Whether this track can be manually deleted/snapped */
interactable: boolean;
/** Track length in meters */
length: number;
/** Track type (determines compatible trains) */
trackType?: string;
/** Percentage of track over water (0-100) */
waterIntersectionPercentage?: number;
/** Unix timestamp (ms) */
createdAt?: number;
/** Curve type for rendering */
curveType?: 'straight' | 'quadratic-start' | 'quadratic-end' | 'cubic';
};Example: Elevation Analysis#
const tracks = api.gameState.getTracks();
const constructed = tracks.filter((t) => t.buildType === 'constructed');
// Find deepest tunnel
const deepest = constructed.reduce((min, track) => {
const depth = Math.min(track.startElevation, track.endElevation);
return depth < min ? depth : min;
}, 0);
console.log(`Deepest tunnel: ${deepest}m`);
// Total track length
const totalKm = constructed.reduce((sum, t) => sum + t.length, 0) / 1000;
console.log(`Total track: ${totalKm.toFixed(1)}km`);
// Underwater tracks
const underwater = constructed.filter((t) => (t.waterIntersectionPercentage ?? 0) > 0);
console.log(`${underwater.length} tracks cross water`);Train#
Returned by gameState.getTrains(), hooks.onTrainSpawned(), hooks.onTrainSelected(), and hooks.onTrainArrivedAtStation().
type Train = {
/** Unique train identifier */
id: string;
/** Route this train runs on */
routeId: string;
/** Train length in meters */
length: number;
/** Number of cars */
cars?: number;
/** Train type ID */
trainType?: string;
/** Current position on route */
currentStComboInfo: {
/** Current station-track combo index */
index: number;
/** Gap from head of train to end of route segment */
gapFromHeadToEndOfRoute: number;
/** Game time (ms) train stopped at station, or null */
timeAtStop: number | null;
/** Game time (ms) train will depart station, or null */
timeAtStopEnd: number | null;
};
/** Current motion state */
motion: {
/** Current speed in meters per second */
speed: number;
/** Current acceleration in m/sΒ² */
acceleration: number;
};
/** Train capacity */
specs: {
/** Maximum passenger capacity */
maxCapacity: number;
};
/** Operational time tracking */
operationalTime: {
/** Total seconds in service */
totalSeconds: number;
/** Last time operational costs were charged */
lastChargedAt: number;
};
/** When this train runs (by demand level) */
operatingSchedule?: {
highDemand: boolean;
mediumDemand: boolean;
lowDemand: boolean;
};
};Example: Train Speed Monitor#
const trains = api.gameState.getTrains();
trains.forEach((train) => {
const speedKmh = (train.motion.speed * 3.6).toFixed(1);
const isAtStation = train.currentStComboInfo.timeAtStop !== null;
console.log(`Train ${train.id}: ${speedKmh} km/h` + (isAtStation ? ' (at station)' : ''));
});
// Average speed across fleet
const avgSpeed = trains.reduce((s, t) => s + t.motion.speed, 0) / trains.length;
console.log(`Fleet avg: ${(avgSpeed * 3.6).toFixed(1)} km/h`);Bond#
Returned by gameState.getBonds() and actions.getBonds().
type Bond = {
/** Unique bond identifier */
id: string;
/** Reference to bond type (e.g. 'SMALL', 'MEDIUM', 'LARGE') */
bondTypeId: string;
/** Original loan amount in dollars */
principal: number;
/** Amount still owed */
remainingPrincipal: number;
/** Interest rate (e.g. 0.06 = 6%) */
interestRate: number;
/** Hourly principal payment amount */
monthlyPayment: number;
/** Game timestamp when bond was issued */
issuedAt: number;
/** Game timestamp of last interest charge */
lastPaymentAt: number;
};StationGroup#
Returned by gameState.getStationGroups(), gameState.getStationGroupForStation(), and hooks.onStationGroupSelected().
Station groups are derived data: the game recomputes them automatically (by station proximity) whenever stations or tracks change. Group IDs are kept stable when possible, but can change when groups merge or split β persist station IDs, not group IDs. See Station Groups & Transfers for the full model.
type StationGroup = {
/** Unique group identifier (best-effort stable β do not persist across sessions) */
id: string;
/** Display name (usually the primary station name) */
name: string;
/** Station IDs in this group. Stations do NOT have a back-pointer to their group. */
stationIds: string[];
/** Center point [longitude, latitude] */
center: [number, number];
/** Geographic bounds of the group */
bounds: {
minLng: number;
maxLng: number;
minLat: number;
maxLat: number;
};
/** Stations the player manually added to this group */
manualAdditions?: string[];
/** Stations the player manually removed β removed pairs are never auto-re-merged */
manualRemovals?: string[];
/** Player-picked marker position that overrides the computed center (saved with the map) */
manualCenter?: [number, number];
/** User-given name β overrides the name derived from station names. Saved with the map. */
customName?: string;
};DemandData#
Returned by gameState.getDemandData().
type DemandData = {
/** Map of demand point ID β demand point data */
points: Map<string, DemandPoint>;
/** Map of pop group ID β pop data */
popsMap: Map<string, Pop>;
};
type DemandPoint = {
id: string;
/** [longitude, latitude] */
location: [number, number];
/** Number of jobs at this point */
jobs: number;
/** Number of residents at this point */
residents: number;
/** IDs of the pops that live or work here */
popIds: string[];
/** Mode split of the pops living here (homeβwork leg) */
residentModeShare?: ModeChoiceStats;
/** Mode split of the pops working here (homeβwork leg) */
workerModeShare?: ModeChoiceStats;
};
type Pop = {
id: string;
/** How many people this pop represents */
size: number;
residenceId: string;
jobId: string;
drivingSeconds: number;
drivingDistance: number;
/** Seconds from midnight when this pop leaves home / leaves work */
homeDepartureTime: number;
workDepartureTime: number;
/** The trip currently underway, or the most recent one, in either direction */
lastCommute?: {
transitPaths: RaptorPath[];
walking: { distance: number; time: number };
modeChoice: ModeChoiceStats;
};
/** Each leg's mode split, kept separately β see below */
commutes?: {
homeToWork?: CommuteSummary;
workToHome?: CommuteSummary;
};
};
type CommuteSummary = {
modeChoice: ModeChoiceStats;
/** Perceived transit time this split was priced against. Absent = no transit option. */
transitTime?: number;
/** Fare charged for the chosen journey */
transitCost?: number;
/** Traffic multiplier for this leg's departure hour */
drivingTimeMultiplier: number;
};<Callout type="info"> Read mode share from commutes, not lastCommute. lastCommute holds whichever direction ran most recently β it flips at the morning and evening peaks, and saves drop its transitPaths for pops not currently travelling. commutes keeps each leg separately and survives saving intact. lastCommute is still the right thing to read when you care about the journey a pop is actually on.
Both are absent on pops that haven't commuted yet. commutes is also absent on saves written before it was introduced β those fill in one commute cycle after the save is loaded, so guard with ?. rather than assuming it's there. </Callout>
Ridership Stats#
Returned by gameState.getRidershipStats().
type RidershipStats = {
/** Passengers per hour (current window) */
totalRidersPerHour: number;
/** Total passengers all time */
totalRiders: number;
/** Time window used for per-hour calculation (hours) */
timeWindowHours: number;
};Line Metrics#
Returned by gameState.getLineMetrics().
type LineMetric = {
/** Route ID */
routeId: string;
/** Route bullet/name */
routeBullet: string;
/** Route color (hex) */
routeColor: string;
/** Number of active trains */
trainCount: number;
/** Trains per hour, or null if not enough data */
trainsPerHour: number | null;
/** Passengers per hour on this route */
ridersPerHour: number;
/** Revenue per hour from this route */
revenuePerHour: number;
};Route Financials#
Returned by gameState.getRouteFinancials().
type RouteFinancials = {
/** Route ID */
routeId: string;
/** Route bullet/name */
routeBullet: string;
/** Route color (hex) */
routeColor: string;
/** Completed hourly entries, oldest first (up to 14 game days) */
hourly: Array<{
/** Game seconds at the start of the recorded hour */
timestamp: number;
revenue: number;
expenses: number;
}>;
/** Totals accumulated so far in the in-progress hour */
currentHour: { revenue: number; expenses: number };
/** Rolling totals over the last 24 recorded hours (includes the in-progress hour) */
last24Hours: { revenue: number; expenses: number; profit: number };
};Mode Choice Stats#
Returned by gameState.getModeChoiceStats(), and used for the per-leg splits on Pop and the resident/worker splits on DemandPoint.
type ModeChoiceStats = {
/** Number of commuters walking */
walking: number;
/** Number of commuters driving */
driving: number;
/** Number of commuters using transit */
transit: number;
/** Commuters without a determined mode */
unknown: number;
};Station Ridership#
Returned by gameState.getStationRidership().
type StationRidership = {
/** Total riders at this station (or system-wide if no ID given) */
total: number;
/** Number of transfer passengers */
transfers: number;
/** Breakdown by route */
byRoute: Array<{
routeId: string;
popCount: number;
/** Percentage of total (0-100) */
percent: number;
}>;
};Route Ridership#
Returned by gameState.getRouteRidership().
type RouteRidership = {
/** Total riders on this route */
total: number;
/** Breakdown by station */
byStation: Array<{
stationId: string;
popCount: number;
/** Percentage of total (0-100) */
percent: number;
}>;
};Blueprint Cost#
Returned by gameState.calculateBlueprintCost().
type BlueprintCost = {
/** Total cost in dollars */
totalCost: number;
/** Cost breakdown */
breakdown: {
/** Track construction cost */
trackCost: number;
/** Station construction cost */
stationCost: number;
/** Building demolition cost */
buildingDemolitionCost: number;
};
};Common Enums#
These string values appear across multiple types:
/** Track/station construction state */
type BuildType = 'constructed' | 'blueprint' | 'preview';
/** Game speed settings */
type TimeSpeed = 'slow' | 'normal' | 'fast' | 'ultrafast';
/** Built-in train types */
type TrainTypeId = 'heavy-metro' | 'light-metro' | 'light-rail' | 'commuter-rail';
/** Station type (extensible via registerStationType) */
type StationTypeId = 'standard' | string;