build
Build automation: blueprint track placement, construction, routes and trains, through the store actions the game itself uses.
api.build. 12 members. Every member catches its own exceptions and logs them; members that return a result object also return the exception message as error.
API Summary#
| Member | Returns | Summary |
|---|---|---|
placeBlueprintTracks(tracks, trackGroups?) | PlaceBlueprintResult | Place blueprint tracks, and optionally track groups, into the store without constructing them. |
buildBlueprints() | Promise<BuildResult> | Construct every blueprint track through the store's build action. |
eraseBlueprints() | void | Remove every blueprint track from the store without building it. |
undoBlueprint() | void | Undo the most recent entry of the store's blueprint history. |
redoBlueprint() | void | Re-apply the most recently undone entry of the store's blueprint history. |
getBlueprintCost() | number | The store's current construction cost estimate for the placed blueprints and staged Electrify conversions; $. |
getBlueprintCount() | number | Number of tracks in the store with buildType: 'blueprint'. |
createRoute(options?) | CreateRouteResult | Create a route with no stations through the store's route generator. |
deleteRoute(routeId) | boolean | Delete a route and every train on it through the store. |
buyTrains(count, trainType?) | BuyTrainsResult | Buy trains into the owned fleet through the store's purchase action. |
addTrainToRoute(routeId, stationIndex?) | AddTrainResult | Spawn a train on a route at one of its stations through the store's spawn action. |
deleteTrain(trainId) | boolean | Delete a train through the store's train deletion. |
Blueprints#
placeBlueprintTracks#
api.build.placeBlueprintTracks(tracks: BlueprintTrackInput[], trackGroups?: BlueprintTrackGroupInput[]): PlaceBlueprintResultPlace blueprint tracks, and optionally track groups, into the store without constructing them.
Every trackType on a track or group is checked against the train type registry before anything is placed. A track without id gets mod-blueprint-<timestamp>-<index>; a group without id gets mod-tg-<timestamp>-<index>. Placed tracks have buildType: 'blueprint', displayType: 'blueprint', type: null, reversable: false, interactable: true and cantEnabled: true. Each placed track's length is its length along the ground in meters, computed the same way as for drawn track. Loading a save does not recompute length. A group without trackLanesType gets 'parallel'; a group without type gets null. The tracks and groups are appended to the existing ones through the store's track setter with station regeneration on. Logs [Modding API] Placed <n> blueprint tracks, <m> track groups (development builds only) on success.
| Param | Type | Default | Description |
|---|---|---|---|
tracks | BlueprintTrackInput[] | required | track definitions to place; coordinates are [lng, lat] pairs. |
trackGroups | BlueprintTrackGroupInput[] | none | track group definitions linking placed track ids; default none. |
Returns: PlaceBlueprintResult. { success: true, trackIds, trackGroupIds } with the ids as placed; { success: false, trackIds: [], trackGroupIds: [], error } on failure.
Errors:
- a
trackTypeon any track or group that is not a registered train type id: returnserror: 'Unknown track type: <id>'and places nothing. - exception from the store: logs
[Modding API] Error placing blueprint tracks:, returnssuccess: falsewith the exception message aserror.
Hooks fired:
- onBlueprintPlaced (with the newly placed blueprint tracks, from the store's track setter).
- onTrackChange (
'add'and the new total track count, whentracksis not empty).
Side effects: store tracks and track groups replaced with appended arrays; stations regenerated; construction costs recomputed; blueprint undo history entry added when tracks is not empty.
Changed:
lengthis now meters along the ground; before it was the sum of straight-line segment distances in degrees of lng/lat (PR #1090).- A
trackTypethat is not a registered train type id fails the call withUnknown track type: <id>and places nothing (PR #1090).
const r = api.build.placeBlueprintTracks(
[{ id: 't1', coords: [[-73.98, 40.76], [-73.97, 40.755]], trackType: 'heavy-metro', startElevation: -15, endElevation: -15 }],
[{ trackIds: ['t1'], centerLine: [[-73.98, 40.76], [-73.97, 40.755]], trackLanesType: 'single' }]
);
// r.trackIds[0] === 't1'; r.trackGroupIds[0] starts with 'mod-tg-'buildBlueprints#
api.build.buildBlueprints(): Promise<BuildResult>Construct every blueprint track through the store's build action.
The store prices the blueprints, asks the Electron main process to build them, charges the construction cost as an expense and swaps the constructed tracks in. Outside sandbox mode the main process refuses a build that costs more than the available money, and the expense lowers money. In sandbox mode neither happens; the expense is still recorded. A refused build shows a destructive toast titled Can't build blueprints and constructs nothing. builtTrackCount is the number of tracks this call constructed. totalCost is the store's construction cost estimate as it stood when the call started; $.
Returns: Promise<BuildResult>. { success: true, builtTrackCount, totalCost }; { success: false, builtTrackCount: 0, totalCost: 0, error } when nothing was built.
Errors:
- no track with
buildType: 'blueprint': returnserror: 'No blueprint tracks to build'. - the store refused the build and showed a toast: returns
errorstarting withBuild was refusedand ending with the toast quoted asgame said: "<title>", orgame said: "<title>: <description>"when the toast has a description (for exampleCan't build blueprints: Insufficient funds). - the store refused the build without a toast: returns
error: 'Build was refused (e.g. not enough money or a blocked track); see the in-game message'. - exception: logs
[Modding API] Error building blueprints:, returnssuccess: falsewith the exception message aserror.
Hooks fired:
- onTrackBuilt (with the tracks this build constructed).
- onStationBuilt (once per constructed station touching a built track).
- onMoneyChanged (from the construction expense).
- onTrackChange (when the build changes the track count).
Side effects: store tracks, signals and stations replaced; construction cost charged through the store expense action; routes touching changed tracks regenerated; staged Electrify conversions applied and cleared; blueprint undo history cleared; built blueprints pruned from the blueprint library.
Changed: builtTrackCount counts only the tracks this call constructed, and a refused build returns success: false with the game's toast quoted in error (PR #1090).
api.build.buildBlueprints().then((r) => {
if (r.success) console.log(`Built ${r.builtTrackCount} tracks for ${r.totalCost}`);
else console.log(`Not built: ${r.error}`);
});eraseBlueprints#
api.build.eraseBlueprints(): voidRemove every blueprint track from the store without building it.
Track groups lose the erased track ids. A group left with no tracks is removed. Logs [Modding API] Erased all blueprint tracks (development builds only).
Errors: exception: logs [Modding API] Error erasing blueprints:, does nothing else.
Hooks fired: onTrackChange ('delete' and the new total track count, when any blueprint track was removed).
Side effects: store tracks, track groups and signals replaced through the store's erase action (blueprint signals removed); stations regenerated; staged Electrify conversions cleared; construction costs recomputed; blueprint undo history entry added when any blueprint track was removed.
if (api.build.getBlueprintCost() > api.gameState.getBudget()) api.build.eraseBlueprints();Undo/Redo Blueprints#
undoBlueprint#
api.build.undoBlueprint(): voidUndo the most recent entry of the store's blueprint history.
Changes nothing when the history has no past entry; the log line still prints. Logs [Modding API] Undid last blueprint (development builds only).
Errors: exception: logs [Modding API] Error undoing blueprint:, does nothing else.
Hooks fired: onTrackChange (when the restored snapshot changes the track count).
Side effects: for a track entry: store tracks, track groups, signals and staged Electrify conversions restored to the previous blueprint snapshot and stations regenerated; for an Electrify staging entry: only the staged conversions restored and construction costs recomputed. The entry moves to the redo stack.
// same as the player's undo: steps back one entry of the blueprint history
api.build.undoBlueprint();redoBlueprint#
api.build.redoBlueprint(): voidRe-apply the most recently undone entry of the store's blueprint history.
Changes nothing when the history has no future entry; the log line still prints. Logs [Modding API] Redid blueprint (development builds only).
Errors: exception: logs [Modding API] Error redoing blueprint:, does nothing else.
Hooks fired: onTrackChange (when the restored snapshot changes the track count).
Side effects: for a track entry: store tracks, track groups, signals and staged Electrify conversions restored to the redo snapshot and stations regenerated; for an Electrify staging entry: only the staged conversions restored and construction costs recomputed. The entry moves back to the undo stack.
api.build.undoBlueprint();
api.build.redoBlueprint(); // re-applies the entry just undoneGet Blueprint Info#
getBlueprintCost#
api.build.getBlueprintCost(): numberThe store's current construction cost estimate for the placed blueprints and staged Electrify conversions; $.
Returns: number. constructionCosts.totalCost from the store, or 0 when there is no estimate.
const cost = api.build.getBlueprintCost();
console.log(`Blueprints will cost ${cost.toLocaleString()}`);getBlueprintCount#
api.build.getBlueprintCount(): numberNumber of tracks in the store with buildType: 'blueprint'.
Returns: number. count of blueprint tracks.
if (api.build.getBlueprintCount() === 0) console.log('Nothing planned yet');Routes#
createRoute#
api.build.createRoute(options?: CreateRouteOptions = {}): CreateRouteResultCreate a route with no stations through the store's route generator.
Omitted shape becomes 'circle'. Omitted color picks a random palette color, text color and bullet; bullet and textColor are ignored in that case. A supplied color with no bullet draws a random bullet. With color set and textColor omitted, the text color comes from the palette entry for color, or from Color(color).isLight() (#000000 when true, #ffffff when false or when the color string does not parse). Omitted trainType becomes 'heavy-metro'. font: 'default' is stored as no font. Logs [Modding API] Created route: <bullet> (development builds only).
Parameter options (CreateRouteOptions): route appearance and train type; default {}.
Returns: CreateRouteResult. { success: true, route } where route is the Route object as inserted into the store; { success: false, error } on exception.
Errors: exception from the generator: logs [Modding API] Error creating route:, returns success: false with the exception message as error.
Hooks fired: onRouteCreated (with the new route).
Side effects: store routes replaced with the appended array.
const { route } = api.build.createRoute({ bullet: 'A', color: '#FF5733', shape: 'diamond' });
// route.stNodes.length === 0 until the player adds stopsdeleteRoute#
api.build.deleteRoute(routeId: string): booleanDelete a route and every train on it through the store.
Each train on the route is deleted first through the store's train deletion. The store's route deletion also removes the route's temporary child routes, strips the route id from every station and drops the route's financial and reliability history. Logs [Modding API] Deleted route: <bullet> (development builds only) on success.
Parameter routeId (string): id of the route to delete.
Returns: boolean. true when the route existed and was deleted; false otherwise.
Errors:
- unknown id: logs
[Modding API] Route not found: <id>, returnsfalse. - exception: logs
[Modding API] Error deleting route:, returnsfalse.
Hooks fired:
- onTrainDeleted (once per train that was on the route; with the sim host active, from a later frame's train diff).
- onRouteDeleted (with the route id and bullet).
Side effects: store trains, routes and stations replaced; route financials, reliability history and rider wait history for the route removed.
Changed: Deletion goes through the store's own route deletion, so onRouteDeleted fires and temporary child routes and station references are cleaned up (PR #1090).
const { success, route } = api.build.createRoute({ bullet: 'T', color: '#888888' });
// deletes the route's trains too
if (success) api.build.deleteRoute(route.id);Trains#
buyTrains#
api.build.buyTrains(count: number, trainType?: TrainTypeId = DEFAULT_TRAIN_TYPE_ID): BuyTrainsResultBuy trains into the owned fleet through the store's purchase action.
The cost is count times the train type's carCost; $. The store records it as a trainPurchase expense, deducts it from money outside sandbox mode, and raises the owned train count and the per-type owned car count. Logs [Modding API] Purchased <count> <trainType> trains (development builds only) on success.
| Param | Type | Default | Description |
|---|---|---|---|
count | number | required | trains to buy; must be greater than 0. |
trainType | TrainTypeId | DEFAULT_TRAIN_TYPE_ID | train type id; default 'heavy-metro'. |
Returns: BuyTrainsResult. { success: true, totalPurchased: count, totalCost }; { success: false, totalPurchased: 0, totalCost: 0, error } on failure.
Errors:
countof 0 or less,NaN, infinite or not a number: returnserror: 'Count must be positive'.- cost above the available money: returns
error: 'Insufficient funds. Need $<x>M, have $<y>M'. - exception: logs
[Modding API] Error buying trains:, returnssuccess: falsewith the exception message aserror.
Hooks fired: onMoneyChanged (from the purchase expense).
Side effects: store money reduced by the cost (unchanged in sandbox mode); ownedTrainCount and ownedCarsByType[trainType] raised by count.
Changed: totalCost is the purchase cost instead of 0 (PR #1090).
const r = api.build.buyTrains(3, 'heavy-metro');
console.log(r.success ? `Bought ${r.totalPurchased} trains for ${r.totalCost}` : r.error);addTrainToRoute#
api.build.addTrainToRoute(routeId: string, stationIndex?: number = 0): AddTrainResultSpawn a train on a route at one of its stations through the store's spawn action.
While the sim host worker runs (always, except a multiplayer guest never starts it) the store accepts the spawn and the worker creates the train on a later frame, so the result has no trainId. Otherwise the train exists when the call returns and trainId is its id. Logs [Modding API] Spawned train <id> on route <bullet> (development builds only) when the train id is known.
| Param | Type | Default | Description |
|---|---|---|---|
routeId | string | required | id of the route to spawn on. |
stationIndex | number | 0 | zero-based index into the route's stations; default 0. |
Returns: AddTrainResult. { success: true, trainId } when the train exists; { success: true } when the sim host accepted the spawn; { success: false, error } otherwise.
Errors:
- unknown route: returns
error: 'Route not found: <id>'. - route with no stations: returns
error: 'Route has no stations'. - index out of range: returns
error: 'Invalid station index <i>. Route has <n> stations.'. - store rejected the spawn (for example every owned train is in service): returns the store's message as
error, such asCannot add more trains. You own <n> trains. Purchase more trains to expand service.(trainwhen <n> is 1). - exception: logs
[Modding API] Error adding train to route:, returnssuccess: falsewith the exception message aserror.
Hooks fired: onTrainSpawned (immediately on the local path; on a later frame on the sim host path).
Side effects: store trains and signals replaced and a Train spawned toast shown on the local path; a spawn-train-at-station op sent to the sim host worker otherwise.
Changed: A spawn the store rejects returns its message as error, and a spawn the sim host accepted returns success: true without trainId (PR #1090).
api.hooks.onTrainSpawned((train) => console.log(train.id));
const routeId = api.gameState.getRoutes()[0].id;
const r = api.build.addTrainToRoute(routeId, 0);
// r.trainId is undefined under the sim host; the hook delivers the traindeleteTrain#
api.build.deleteTrain(trainId: string): booleanDelete a train through the store's train deletion.
With the sim host active the deletion is sent to the worker and the hook fires from a later frame's train diff. Logs [Modding API] Deleted train: <id> (development builds only) on success.
Parameter trainId (string): id of the train to delete.
Returns: boolean. true when the train existed in the store; false otherwise.
Errors:
- unknown id: logs
[Modding API] Train not found: <id>, returnsfalse. - exception: logs
[Modding API] Error deleting train:, returnsfalse.
Hooks fired: onTrainDeleted (with the train id and its route id).
Side effects: without the sim host: store trains replaced, temporary routes and rider movements of the train cleaned up; with the sim host: a delete-train op sent to the worker.
const [train] = api.gameState.getTrains();
if (train) api.build.deleteTrain(train.id);BlueprintTrackInput#
One blueprint track for build.placeBlueprintTracks.
| Field | Type | Default | Description |
|---|---|---|---|
id | string | none | Track id; generated as mod-blueprint-<timestamp>-<index> when omitted. |
coords | [number, number][] | required | Track geometry as [lng, lat] pairs in order; degrees. |
trackType | string | undefined | Track type id; checked with in against the train type registry. |
startElevation | number | 0 | Elevation at the first coordinate; m, negative underground. |
endElevation | number | 0 | Elevation at the last coordinate; m, negative underground. |
BlueprintTrackGroupInput#
One track group for build.placeBlueprintTracks. Binds placed tracks into a single, parallel or quad corridor.
| Field | Type | Default | Description |
|---|---|---|---|
id | string | none | Group id; generated as mod-tg-<timestamp>-<index> when omitted. |
trackIds | string[] | required | Ids of the member tracks, in lane order. |
trackLanesType | 'single' or 'parallel' or 'quad' | 'parallel' | Lane layout of the corridor. |
lanes | ('forward' | 'backward')[] | none | Per-lane travel direction relative to the centerline direction; stored only when supplied; the default directions are used when its length differs from the group's lane count or an entry is not 'forward' or 'backward'. |
centerLine | [number, number][] | required | Corridor centerline as [lng, lat] pairs; degrees. |
type | 'station' or 'scissors-crossover' or null | null | Group kind: 'station' for a platform group, 'scissors-crossover' for a crossover, null for plain track. |
trackType | string | undefined | Track type id for the group; checked with in against the train type registry. |
PlaceBlueprintResult#
Result of build.placeBlueprintTracks.
| Field | Type | Default | Description |
|---|---|---|---|
success | boolean | required | true when every track and group was placed. |
trackIds | string[] | required | Ids of the placed tracks in input order; empty on failure. |
trackGroupIds | string[] | required | Ids of the placed track groups in input order; empty on failure. |
error | string | none | Failure reason: Unknown track type: <id> or the exception message. |
BuildResult#
Result of build.buildBlueprints.
| Field | Type | Default | Description |
|---|---|---|---|
success | boolean | required | true when at least one track was constructed. |
builtTrackCount | number | required | Tracks this call constructed; 0 on failure. |
totalCost | number | required | Construction cost estimate as it stood when the call started; $; 0 on failure. |
error | string | none | Failure reason: No blueprint tracks to build, a Build was refused message, or the exception message. |
CreateRouteOptions#
Appearance and train type for build.createRoute. Every field is optional.
| Field | Type | Default | Description |
|---|---|---|---|
bullet | string | none | Bullet text; read only when color is set (an empty string is kept); a random bullet is drawn when bullet or color is omitted. |
fullName | string | none | Long route name; stored as undefined when omitted or empty. |
color | string | none | Bullet background color as a CSS color string; a random palette color is used when omitted. |
textColor | string | none | Bullet text color; read only when color is set; otherwise derived from the palette entry or from Color(color).isLight() (#000000 on a light color, #ffffff otherwise), or taken from the random palette pick when color is omitted. |
bordered | boolean | undefined | Two-tone bullet: a ring of the route color around a center filled with the inverse of the text color. |
shape | RouteShape | 'circle' | Bullet shape: 'circle', 'diamond', 'square', 'rounded-square' or 'triangle'. |
font | RouteBulletFont | none | Bullet text styling: 'default', 'mono', 'bolder', 'lighter' or 'italic'; 'default' is stored as no font. |
textSize | RouteBulletTextSize | none | Bullet label size: 'default' or 'small'; 'default' is stored as no textSize. |
trainType | TrainTypeId | 'heavy-metro' | Train type id the route runs. |
CreateRouteResult#
Result of build.createRoute.
| Field | Type | Default | Description |
|---|---|---|---|
success | boolean | required | true when the route was created. |
route | Route | none | The Route object as inserted into the store; absent on failure. |
error | string | none | Exception message; absent on success. |
AddTrainResult#
Result of build.addTrainToRoute.
| Field | Type | Default | Description |
|---|---|---|---|
success | boolean | required | true when a train was spawned or the sim host accepted the spawn. |
trainId | string | none | Id of the spawned train; absent when the sim host creates the train on a later frame. |
error | string | none | Failure reason; absent on success. |
BuyTrainsResult#
Result of build.buyTrains.
| Field | Type | Default | Description |
|---|---|---|---|
success | boolean | required | true when the purchase went through. |
totalPurchased | number | required | Trains bought; equals count on success, 0 on failure. |
totalCost | number | required | Purchase cost; $; count times the type's carCost; 0 on failure. |
error | string | none | Failure reason: Count must be positive, the store's Insufficient funds. ... message, or the exception message. |
Complete Example: Automated Line Builder#
Place a tunnel, check the cost against the budget, build it, then create a route and buy its trains. The build API places no stations and adds no stops: the player adds stops to the route, and addTrainToRoute fails with Route has no stations until then.
(function () {
const api = window.SubwayBuilderAPI;
async function buildSimpleLine() {
const placed = api.build.placeBlueprintTracks([
{
coords: [[-73.985, 40.758], [-73.98, 40.755], [-73.975, 40.752]],
trackType: 'heavy-metro',
startElevation: -15, // underground
endElevation: -15,
},
]);
if (!placed.success) return console.log(`Placement failed: ${placed.error}`);
const cost = api.build.getBlueprintCost();
if (cost > api.gameState.getBudget()) {
api.build.eraseBlueprints();
return console.log(`Not enough money for ${cost.toLocaleString()}`);
}
const built = await api.build.buildBlueprints();
if (!built.success) return console.log(`Build failed: ${built.error}`);
const created = api.build.createRoute({ bullet: '7', color: '#B933AD', shape: 'circle' });
if (!created.success) return console.log(`Route failed: ${created.error}`);
api.build.buyTrains(3, 'heavy-metro');
const spawn = api.build.addTrainToRoute(created.route.id, 0);
if (!spawn.success) console.log(`No train yet: ${spawn.error}`);
}
buildSimpleLine();
})();