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:
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.
| Bug | What was wrong | Fix | ||
|---|---|---|---|---|
getCurrentDay() off-by-one | Was 0-indexed, UI clock is 1-indexed | Now +1, matches UI clock | ||
getTicketPrice() wrong on $0 | Used `\ | \ | 0` (falsy for $0), returned 0 instead of default | Uses `?? DEFAULTTICKETCOST` |
addMoney() wrong signature | Passed string reason where addRevenue expects boolean | Now passes false | ||
subtractMoney() wrong signature | Passed string reason where addExpense expects category enum | Now passes 'other' | ||
setMoney() missing hook | Didn't fire onMoneyChanged | Now triggers triggerMoneyChanged | ||
registerLayer() crash on hot reload | Duplicate layer ID throws | Deduplicates by ID | ||
registerSource() crash on hot reload | Duplicate source throws | Checks 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)#
| Method | Description |
|---|---|
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)#
| Method | Returns | Description | |
|---|---|---|---|
gameState.getStationById(stationId) | Station | Get a station by ID | |
gameState.getTrainsByRoute(routeId) | Train[] | All trains on a route | |
gameState.getStationsByRoute(routeId) | Station[] | All stations on a route | |
gameState.getGameMode() | string | Current 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:
{
"memoryLimitMB": 16384
}Chromium flags escape hatch. Power users can pass arbitrary Chromium/V8 flags:
{
"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, notedmodifyConstantsis top-level - hooks: Added full sections for
onStationDeleted,onWarning,onError,onGameEnd; explained mod context preservation and late-fire behavior - game-state: Added
getCompletedCommutes, fixedroute.stations->stNodes, correctedgetRidershipStatsfrom "all time" to "last 15 min rolling window" - stations: Fixed
dwellTime->extraDwellTimeeverywhere, corrected default from 20 to 0 - ui: Added
addFloatingPanelwith full property table - career: Removed undocumented
getMissionsForCity(not implemented) - map: Documented
registerLayerbeforeIdand 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#
.metrotest fixtures moved to R2 cloud storage (npm run test:fixturesto download)- Test mods updated:
test-mods/canary/andtest-mods/api-audit/ - Mock maps now include
.on()forstyledatalistener tests - All
'standard'train type references updated to'heavy-metro'