Changelog

Modding API changes, fixes, and new features

v1.0.0-rc1 β€” Modding API Fixes#

PR #463 β€” `fix/modding-api-getter-gaps`


Critical Lifecycle Fixes#

`onGameInit` / `onGameLoaded` / `onBeforeSaveLoad` now fire every session. Previously, StoreInitializer.runInitOnce() cached initialization state across game sessions. If you started a game, went back to the menu, and started another game, your onGameInit callback never fired the second time. Fixed by adding cleanup on unmount so lifecycle hooks reset between sessions.

`onGameLoaded` fires on every save load (not just the first). The monolithic API version wasn't tracking currentSaveName in lifecycle state. Now tracked properly:

  • Resets in triggerGameEnd() so the next load fires correctly
  • Late-registration replay: if you register `onGameLoaded` after a save has already loaded, it fires immediately with the current save name
  • Hot-reload replay: triggerPostReloadLifecycle() replays it too

Bug Fixes#

`setSpeed()` was broken. `MSPERTICK` was computed once at module load time, so calling `setSpeedMultiplier()` to mutate `GAMESECONDSPERSECOND` had no effect on the game loop. Now computed dynamically via `MSPER_UPDATE()` on every tick.

Custom map layers killed on rotation/pitch. When the map style reloads (rotation, pitch change, style swap), all custom sources and layers were destroyed. Added a styledata event listener that re-adds your registered sources and layers automatically. You no longer need to handle styledata yourself.

`registerLayer()` now accepts `beforeId` parameter. Control z-order:

javascript
api.map.registerLayer({
    id: 'my-layer',
    type: 'fill',
    source: 'my-source',
    paint: { 'fill-color': '#088' }
}, 'road-labels');  // renders below road-labels

`getStationTypes()` always returned `"standard"`. getStations.ts never preserved the stationType field from the previous station object. Now preserved across save/load.

License key exfiltration blocked. Mods could previously read the license key from localStorage.getItem('L_CHECK'). Now returns null when executing in mod context. Triggers a security alert on access attempt.

`buyTrains()` default type `'standard'` changed to `'heavy-metro'`. 'standard' is not a valid built-in train type. The actual types are 'heavy-metro', 'light-metro', 'commuter-rail'. Calling buyTrains(5) with no type was silently using a nonexistent type.

Hot-reload speed multiplier compounding. `resetOriginalSpeedValues()` was nulling the cached originals without restoring `GAME_SECONDS_PER_SECOND` first. On next mod execution, `setOriginalSpeedValues` captured the already-mutated values, so each hot-reload compounded the multiplier exponentially (600 -> 1200 -> 2400 -> ...). Multiplier now always applies to original base values. (Reported by Steno)

`getLineMetrics().revenuePerHour` was 365x too low. Revenue was calculated as `ridersPerHour transitCost`, but the game applies `RULES.FARE_MULTIPLIER` (365x) to all fare revenue because each game day represents a year. The API was returning values ~365x lower than the financial dashboard. (Reported by Steno)*


7 API Parity Fixes#

Inconsistencies between the modular and monolithic API versions. Both are now in sync.

BugWhat was wrongFix
getCurrentDay() off-by-oneWas 0-indexed, UI clock is 1-indexedNow +1, matches UI clock
getTicketPrice() wrong on $0Used `\\0` (falsy for $0), returned 0 instead of defaultUses `?? DEFAULTTICKETCOST`
addMoney() wrong signaturePassed string reason where addRevenue expects booleanNow passes false
subtractMoney() wrong signaturePassed string reason where addExpense expects category enumNow passes 'other'
setMoney() missing hookDidn't fire onMoneyChangedNow triggers triggerMoneyChanged
registerLayer() crash on hot reloadDuplicate layer ID throwsDeduplicates by ID
registerSource() crash on hot reloadDuplicate source throwsChecks getSource() first

Deferred execution parity: addMoney, subtractMoney, setMoney, setTicketPrice in the monolithic version were executing synchronously. Now wrapped in queueMicrotask to match the modular version's hook safety contract.

UI deduplication: All 13 add* UI methods (addMainMenuButton, addStyledButton, addStyledToggle, addStyledSlider, addToolbarButton, addToolbarPanel, addFloatingPanel, etc.) now deduplicate by ID via upsertComponent() instead of blindly pushing. No more duplicate UI elements on hot reload.


New API Methods#

Actions (write operations)#

MethodDescription
actions.deleteTrain(trainId)Delete a train by ID
actions.addTrainToRoute(routeId, stationIndex?)Spawn a train on a route
actions.getRouteById(routeId)Get a route by ID
actions.getTrainById(trainId)Get a train by ID

Game State (read-only)#

MethodReturnsDescription
gameState.getStationById(stationId)StationGet a station by ID
gameState.getTrainsByRoute(routeId)Train[]All trains on a route
gameState.getStationsByRoute(routeId)Station[]All stations on a route
gameState.getGameMode()stringCurrent game mode (normal/sandbox)
gameState.getStationGroups()StationGroup[]All station groups (transfer hubs)
gameState.getTransferStationIds()string[]Station IDs in transfer groups
gameState.getSiblingStationIds(stationId)string[]Sibling stations in same group
gameState.getSaveName()`string \null`Current save name

These were added because mods were bypassing the API to access this data by scanning window for the Zustand store directly.


Performance & Memory#

User-configurable memory limit. Large maps (Osaka, Greater LA, Nagoya) can now override the V8 heap size via settings.json:

json
{
    "memoryLimitMB": 16384
}

Chromium flags escape hatch. Power users can pass arbitrary Chromium/V8 flags:

json
{
    "chromiumFlags": ["--disable-gpu", "--enable-logging"]
}

Both settings require an app restart. See the Performance guide for details, platform-specific paths, and troubleshooting.


Documentation#

New: [Type Reference](/docs/v1.0.0/api-reference/types) page. Every property on Station, Route, Track, Train, Bond, StationGroup, DemandData, RidershipStats, LineMetric, ModeChoiceStats, StationRidership, RouteRidership, and BlueprintCost documented with TypeScript types and usage examples.

(h/t muffintime for pushing us to document this properly)

Documentation fixes across 8 API reference pages:

  • actions: Documented deferred execution (queueMicrotask), fixed misleading param examples, added return types, noted modifyConstants is top-level
  • hooks: Added full sections for onStationDeleted, onWarning, onError, onGameEnd; explained mod context preservation and late-fire behavior
  • game-state: Added getCompletedCommutes, fixed route.stations -> stNodes, corrected getRidershipStats from "all time" to "last 15 min rolling window"
  • stations: Fixed dwellTime -> extraDwellTime everywhere, corrected default from 20 to 0
  • ui: Added addFloatingPanel with full property table
  • career: Removed undocumented getMissionsForCity (not implemented)
  • map: Documented registerLayer beforeId and layer persistence
  • index: Fixed API overview to match actual exports

New: [Performance & Memory](/docs/v1.0.0/guides/performance) guide. Covers memory settings, Chromium flags, verification, and large-map troubleshooting.


Testing & Infrastructure#

  • .metro test fixtures moved to R2 cloud storage (npm run test:fixtures to download)
  • Test mods updated: test-mods/canary/ and test-mods/api-audit/
  • Mock maps now include .on() for styledata listener tests
  • All 'standard' train type references updated to 'heavy-metro'