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#

MemberReturnsSummary
placeBlueprintTracks(tracks, trackGroups?)PlaceBlueprintResultPlace 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()voidRemove every blueprint track from the store without building it.
undoBlueprint()voidUndo the most recent entry of the store's blueprint history.
redoBlueprint()voidRe-apply the most recently undone entry of the store's blueprint history.
getBlueprintCost()numberThe store's current construction cost estimate for the placed blueprints and staged Electrify conversions; $.
getBlueprintCount()numberNumber of tracks in the store with buildType: 'blueprint'.
createRoute(options?)CreateRouteResultCreate a route with no stations through the store's route generator.
deleteRoute(routeId)booleanDelete a route and every train on it through the store.
buyTrains(count, trainType?)BuyTrainsResultBuy trains into the owned fleet through the store's purchase action.
addTrainToRoute(routeId, stationIndex?)AddTrainResultSpawn a train on a route at one of its stations through the store's spawn action.
deleteTrain(trainId)booleanDelete a train through the store's train deletion.

Blueprints#

placeBlueprintTracks#

api.build.placeBlueprintTracks(tracks: BlueprintTrackInput[], trackGroups?: BlueprintTrackGroupInput[]): PlaceBlueprintResult

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

ParamTypeDefaultDescription
tracksBlueprintTrackInput[]requiredtrack definitions to place; coordinates are [lng, lat] pairs.
trackGroupsBlueprintTrackGroupInput[]nonetrack 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 trackType on any track or group that is not a registered train type id: returns error: 'Unknown track type: <id>' and places nothing.
  • exception from the store: logs [Modding API] Error placing blueprint tracks:, returns success: false with the exception message as error.

Hooks fired:

  • onBlueprintPlaced (with the newly placed blueprint tracks, from the store's track setter).
  • onTrackChange ('add' and the new total track count, when tracks is 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:

  • length is now meters along the ground; before it was the sum of straight-line segment distances in degrees of lng/lat (PR #1090).
  • A trackType that is not a registered train type id fails the call with Unknown track type: <id> and places nothing (PR #1090).
js
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': returns error: 'No blueprint tracks to build'.
  • the store refused the build and showed a toast: returns error starting with Build was refused and ending with the toast quoted as game said: "<title>", or game said: "<title>: <description>" when the toast has a description (for example Can'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:, returns success: false with the exception message as error.

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

js
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(): void

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

js
if (api.build.getBlueprintCost() > api.gameState.getBudget()) api.build.eraseBlueprints();

Undo/Redo Blueprints#

undoBlueprint#

api.build.undoBlueprint(): void

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

js
// same as the player's undo: steps back one entry of the blueprint history
api.build.undoBlueprint();

redoBlueprint#

api.build.redoBlueprint(): void

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

js
api.build.undoBlueprint();
api.build.redoBlueprint(); // re-applies the entry just undone

Get Blueprint Info#

getBlueprintCost#

api.build.getBlueprintCost(): number

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

js
const cost = api.build.getBlueprintCost();
console.log(`Blueprints will cost ${cost.toLocaleString()}`);

getBlueprintCount#

api.build.getBlueprintCount(): number

Number of tracks in the store with buildType: 'blueprint'.

Returns: number. count of blueprint tracks.

js
if (api.build.getBlueprintCount() === 0) console.log('Nothing planned yet');

Routes#

createRoute#

api.build.createRoute(options?: CreateRouteOptions = {}): CreateRouteResult

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

js
const { route } = api.build.createRoute({ bullet: 'A', color: '#FF5733', shape: 'diamond' });
// route.stNodes.length === 0 until the player adds stops

deleteRoute#

api.build.deleteRoute(routeId: string): boolean

Delete 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>, returns false.
  • exception: logs [Modding API] Error deleting route:, returns false.

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

js
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): BuyTrainsResult

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

ParamTypeDefaultDescription
countnumberrequiredtrains to buy; must be greater than 0.
trainTypeTrainTypeIdDEFAULT_TRAIN_TYPE_IDtrain type id; default 'heavy-metro'.

Returns: BuyTrainsResult. { success: true, totalPurchased: count, totalCost }; { success: false, totalPurchased: 0, totalCost: 0, error } on failure.

Errors:

  • count of 0 or less, NaN, infinite or not a number: returns error: '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:, returns success: false with the exception message as error.

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

js
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): AddTrainResult

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

ParamTypeDefaultDescription
routeIdstringrequiredid of the route to spawn on.
stationIndexnumber0zero-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 as Cannot add more trains. You own <n> trains. Purchase more trains to expand service. (train when <n> is 1).
  • exception: logs [Modding API] Error adding train to route:, returns success: false with the exception message as error.

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

js
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 train

deleteTrain#

api.build.deleteTrain(trainId: string): boolean

Delete 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>, returns false.
  • exception: logs [Modding API] Error deleting train:, returns false.

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.

js
const [train] = api.gameState.getTrains();
if (train) api.build.deleteTrain(train.id);

BlueprintTrackInput#

One blueprint track for build.placeBlueprintTracks.

FieldTypeDefaultDescription
idstringnoneTrack id; generated as mod-blueprint-<timestamp>-<index> when omitted.
coords[number, number][]requiredTrack geometry as [lng, lat] pairs in order; degrees.
trackTypestringundefinedTrack type id; checked with in against the train type registry.
startElevationnumber0Elevation at the first coordinate; m, negative underground.
endElevationnumber0Elevation at the last coordinate; m, negative underground.

BlueprintTrackGroupInput#

One track group for build.placeBlueprintTracks. Binds placed tracks into a single, parallel or quad corridor.

FieldTypeDefaultDescription
idstringnoneGroup id; generated as mod-tg-<timestamp>-<index> when omitted.
trackIdsstring[]requiredIds of the member tracks, in lane order.
trackLanesType'single' or 'parallel' or 'quad''parallel'Lane layout of the corridor.
lanes('forward' | 'backward')[]nonePer-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][]requiredCorridor centerline as [lng, lat] pairs; degrees.
type'station' or 'scissors-crossover' or nullnullGroup kind: 'station' for a platform group, 'scissors-crossover' for a crossover, null for plain track.
trackTypestringundefinedTrack type id for the group; checked with in against the train type registry.

PlaceBlueprintResult#

Result of build.placeBlueprintTracks.

FieldTypeDefaultDescription
successbooleanrequiredtrue when every track and group was placed.
trackIdsstring[]requiredIds of the placed tracks in input order; empty on failure.
trackGroupIdsstring[]requiredIds of the placed track groups in input order; empty on failure.
errorstringnoneFailure reason: Unknown track type: <id> or the exception message.

BuildResult#

Result of build.buildBlueprints.

FieldTypeDefaultDescription
successbooleanrequiredtrue when at least one track was constructed.
builtTrackCountnumberrequiredTracks this call constructed; 0 on failure.
totalCostnumberrequiredConstruction cost estimate as it stood when the call started; $; 0 on failure.
errorstringnoneFailure 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.

FieldTypeDefaultDescription
bulletstringnoneBullet text; read only when color is set (an empty string is kept); a random bullet is drawn when bullet or color is omitted.
fullNamestringnoneLong route name; stored as undefined when omitted or empty.
colorstringnoneBullet background color as a CSS color string; a random palette color is used when omitted.
textColorstringnoneBullet 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.
borderedbooleanundefinedTwo-tone bullet: a ring of the route color around a center filled with the inverse of the text color.
shapeRouteShape'circle'Bullet shape: 'circle', 'diamond', 'square', 'rounded-square' or 'triangle'.
fontRouteBulletFontnoneBullet text styling: 'default', 'mono', 'bolder', 'lighter' or 'italic'; 'default' is stored as no font.
textSizeRouteBulletTextSizenoneBullet label size: 'default' or 'small'; 'default' is stored as no textSize.
trainTypeTrainTypeId'heavy-metro'Train type id the route runs.

CreateRouteResult#

Result of build.createRoute.

FieldTypeDefaultDescription
successbooleanrequiredtrue when the route was created.
routeRoutenoneThe Route object as inserted into the store; absent on failure.
errorstringnoneException message; absent on success.

AddTrainResult#

Result of build.addTrainToRoute.

FieldTypeDefaultDescription
successbooleanrequiredtrue when a train was spawned or the sim host accepted the spawn.
trainIdstringnoneId of the spawned train; absent when the sim host creates the train on a later frame.
errorstringnoneFailure reason; absent on success.

BuyTrainsResult#

Result of build.buyTrains.

FieldTypeDefaultDescription
successbooleanrequiredtrue when the purchase went through.
totalPurchasednumberrequiredTrains bought; equals count on success, 0 on failure.
totalCostnumberrequiredPurchase cost; $; count times the type's carCost; 0 on failure.
errorstringnoneFailure 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.

js
(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();
})();