Yanyang extensions
The capabilities this fork (the Yanyang technical team's modified JCM) adds on top of the upstream JCM 2.3.0-prerelease.3 JS API. Everything upstream already provides keeps working as before; this page covers only the additions.
Extension overview
| Extension | Entry point | In one line |
|---|---|---|
| Mixed JS + C++ entries | Declaration entries | Mix JS and C++ modules freely within one entry; both engines render at the same time |
| Door-opening side memory | VehicleWrapper.getDirectionOfDoorOpening() | Which side the doors will open at a stop—dwell-time answers, mainline prediction, and beyond-view-distance answers remembered by the server |
| Fine-grained transport types | VehicleWrapper.getTransportType() and friends | Fine-grained route type ids (interworking with MAGIC MTR) and the interchange extension fields |
| Direct-write pixel upload | GraphicsTexture.uploadRawABGR() | Direct byte-array writes + uploading only the dirty region: the bandwidth-saving channel for high-frequency per-pixel updates |
| Compatibility aliases | Declaration entries | Accepts the script_input key name used by the NTE documentation's examples; nativeLibrary / nativeLibraries mixed with .js |
Mixed JS + C++ entries
In the upstream format, a script entry is all JS or nothing. Yanyang hands the decision to the file characteristics: each entry in scriptLocations is examined on its own—*.js (case-insensitive) is treated as JavaScript, everything else is parsed as a C++ module. Mix the two freely within one entry, in any quantity:
"vehicleScripts": [
{
"id": "demo:sp1900",
"scriptLocations": [
"demo:js/sp1900_ui.js",
"demo:natives/sp1900_motor",
{"windows": "demo:natives/win-x64/libmotor.dll",
"linux": "demo:natives/linux-x64/libmotor.so",
"macos": "demo:natives/macos-arm64/libmotor.dylib"}
],
"prependExpressions": ["print('JS 侧照常执行')"],
"input": { "era": 2004 }
}
]
C++ declarations come in three forms:
| Form | Written as | Resolution |
|---|---|---|
| Platform-independent stem | "demo:natives/sp1900_motor" | Expands per platform: natives/linux-x64/libsp1900_motor.so, natives/windows-x64/sp1900_motor.dll, natives/macos-arm64/libsp1900_motor.dylib and so on (see Quick start for the directory layout) |
| Full library path | "demo:natives/linux-x64/libfoo.so" | Binds to the system that extension maps to; other systems skip the entry |
| Per-platform mapping | {"windows": ..., "linux": ..., "macos": ...} | Keys can be architecture-precise (macos-arm64, linux-x64, …), with precise keys taking precedence over generic ones; each value is itself a stem or a full path |
The two engines each draw their own: the JS parts (together with prependExpressions and input) are assembled into one script and parsed; all the C++ parts hang under the same script id and are loaded by the native manager. At render time each side captures and replays on its own, with no cross-talk between their draw commands; a missing or failed side simply doesn't draw—it never drags the other side down.
The language field (the fallback switch)
| Value | Effect |
|---|---|
| Omitted (recommended) | Detection from file characteristics—zero configuration for the vast majority of cases |
"cpp" | Pure native entry (the old semantics): .js entries in scriptLocations are ignored with a log note left behind; used together with nativeLibrary / nativeLibraries |
"javascript" / "js" | Forces the whole entry to be parsed as JS—the escape hatch for JS filenames without an extension |
The field name depends on the format: the MTR 4 key is language, the legacy formats use scriptLang (camelCase) / script_lang (snake_case). nativeLibrary / nativeLibraries (the latter lets one id carry several libraries) are always treated as C++, and can coexist with .js in the same entry.
JS fields such as prependExpressions / input / rateLimit / dataFetchMode keep their meaning; pure JS entries (all .js) behave exactly as upstream. For how the C++ side reads input, see the C++ documentation.
Door-opening side memory
VehicleWrapper.getDirectionOfDoorOpening(): String answers 'which side will this train's doors open on', with values "not" (neither side) / "left" (head-left) / "right" (head-right) / "all" (both sides):
- While dwelling at a platform: whether the doors are not-yet-open / partially open / closing / closed, the answer matches the fully open state—whichever side the platform blocks (or the unlocked PSD / APG doors) sit on counts as 'will open'. When the door value is > 0 it reads this frame's per-car determination directly.
- Whilst running on the mainline: it predicts the opening side at the next stop—laying the train out at its stopping point on its own path and running MTR's own
canOpenDoorsblock scan. Immediately available once the stopping point's chunks are loaded; beyond view distance the server answers instead: it has scanned the whole map once and persists the result (platform block changes trigger incremental rescans automatically), caching both sides' answers per route to serve requests—boarding mid-journey, restarting the game or a first visit all keep working. - Server without this mod, or no answer: falls back to the client's own view-distance scan; predictions for far-away platforms may then be unavailable.
- Aggregation rule: if any single car on a side has a platform block beside its doors, the whole side counts as openable (aggregated per side, not per door). Left/right are relative to the vehicle head (the bogie-1 end): each car is evaluated in its own coordinate frame (-X = head-left, the MTR model convention), and travel direction or reversing never changes the reading.
- Purely informational: it serves display logic only (door-side indicators, in-car screen arrows) and never interferes with which doors MTR actually opens.
function render(ctx, state, vehicle) {
let dir = vehicle.getDirectionOfDoorOpening(); // "not"/"left"/"right"/"all"
if (dir == "left" || dir == "all") drawLamp(ctx, state, "left", 0x00FF00);
if (dir == "right" || dir == "all") drawLamp(ctx, state, "right", 0x00FF00);
}
Fine-grained transport types
MTR's TransportMode has only the four basic modes (train / boat / cable_car / airplane)—not enough for display purposes. The MAGIC MTR add-on lets players subdivide the display type of a 'train' further (stored in its route_types.json, keyed by route id). This fork threads those types through to scripts:
VehicleWrapper.getTransportType(): String— the current route's canonical type id: uses the MAGIC-assigned refinement (train_normal/train_light_rail/train_high_speed/train_metro/train_bus/train_tram/train_sbahn) when one exists, otherwise falls back to the basic mode id (train/boat/cable_car/airplane); unresolvable values becomeunknown, never null.Stop.routeInterchanges[i](RouteInterchange) gains:getTransportMode(): TransportMode?(the interchange route's transport mode, possibly null with old data),getTransportType(): String(the same canonical id) andgetRouteId(): long(the source route id, 0 when server data is missing)—interchange signs picking icons / colour schemes by type no longer have only the colour to read.- With MAGIC absent, or with no assignment for the route, everything falls back to the basic mode as before—scripts need not probe for the add-on. Type lookups are cached briefly per route (about 1 second), so a type change in MTR becomes visible quickly.
Other call sites
The canonical type id isn't consumed by vehicle.getTransportType() alone — the host itself calls it, and scripts have sibling entry points:
| Call site | Side | Notes |
|---|---|---|
vehicle.getTransportType() | JS · vehicle scripts | The primary entry point above; the host's snapshot builder also calls it — once per vehicle per frame — whenever the vehicle carries a C++ script, pooling the string into the snapshot's string pool |
stop.routeInterchanges[i].getTransportType()stop.connectingInterchanges[connectedStationName][i].getTransportType() | JS · vehicle scripts | Per-interchange copies of the same id; the type arrives pre-resolved with the synced interchange data (the server's assignment wins — received whether or not the client runs MAGIC), with a client-side getRouteId() lookup only as the fallback for older data |
mtr::Vehicle::getTransportType() (legacy name mtr::Train)mtr::Stop::interchange(i).getTransportType() | C++ snapshot · ABI 9+ | Read the string the host pre-pooled into the snapshot — identical to the JS result; see Data snapshots |
Every site above reads the same per-route resolution — the id seen at one call site is exactly the id seen at the others.
Direct-write pixel upload
GraphicsTexture.uploadRawABGR(source: byte[], dstOffsetX: int, dstOffsetY: int, width: int, height: int, ppm: int[]): void
Writes a row-major, BGRA-ordered pixel block of width × height × 4 bytes straight into the texture buffer, and uploads only that rectangle to the GPU:
- Where it fits: the bandwidth-saving channel for high-frequency per-pixel updates. The ordinary AWT route's
upload()hauls the whole image; a 4 Mpx screen shipped in full every frame means a transfer of a dozen-plus MB, whilst a per-point animation (progress bars, jumping digits) usually changes only a thin strip—this method gets by with a few hundred KB. ppmis a 4 × 256-entry premultiplied lookup table (four 256-entry segments for B / G / R / A in order), held by the caller; bytes are expanded through the table as they are written, avoiding a per-pixel multiply.- After the write, only that rectangle is uploaded automatically; the rest of the texture stays as it was—saving the 'modify it on the AWT canvas first' step that
upload(x, y, w, h)requires. - Usable from JS (pair it with
Java.newArrayto assemble the byte array), but its design purpose is serving the C++ bridge's per-frame pixel writeback; pure JS authors will do fine with AWT + sub-region uploads.
JS and C++ vocabulary parity
The same data goes by the same name in both languages—so the two sides of a mixed entry drawing the same thing stay perfectly in step:
| JS | C++ (SDK) | Introduced |
|---|---|---|
vehicle.getTransportType() | mtr::Vehicle::getTransportType() | ABI 9 |
Stop.routeInterchanges[i].getTransportType() and friends | the type fields of mtr::Stop::interchange(i) | ABI 9 |
vehicle.getDirectionOfDoorOpening() | mtr::Vehicle::doorDirection() / getDirectionOfDoorOpening() | ABI 11 |
Pure JS readers need remember only one thing: the extensions on this page are available on the C++ side as well (direct snapshot field reads, zero marshalling cost)—see Data snapshots.
Back to: JS script types · JS common APIs; want the same data with zero JVM overhead? See C++ native scripting.