Yanyang Extensions
What this fork (the Yanyang Tech Team's modified build) adds on top of upstream JCM 2.3.0-prerelease.3's JS API. Everything upstream already provides keeps working as usual; this page covers only the additions.
Extensions at a glance
| Extension | Entry point | In one sentence |
|---|---|---|
| Mixed JS + C++ entries | Declaration entries | Freely mix JS and C++ modules within one entry; both engines render side by side |
| Door-opening direction memory | VehicleWrapper.getDirectionOfDoorOpening() | Which side the doors will open on โ during platform stops, predicted on the main line, and remembered server-side beyond view distance |
| Transport type subdivisions | VehicleWrapper.getTransportType() and friends | Fine-grained route type ids (interworking with MAGIC MTR) plus interchange extension fields |
| Raw pixel uploads | GraphicsTexture.uploadRawABGR() | Raw byte-row writes + dirty-region-only uploads โ the bandwidth-saving channel for high-frequency per-pixel updates |
| Compatibility aliases | Declaration entries | Accepts the script_input key name used in the NTE docs' examples; nativeLibrary / nativeLibraries mixed with .js |
Mixed JS + C++ entries
In the upstream format, a script entry is all JS. Yanyang delegates the decision to file characteristics: each scriptLocations entry is judged on its own โ *.js (case-insensitive) goes to 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('The JS side still runs')"],
"input": { "era": 2004 }
}
]
C++ declarations come in three forms:
| Form | Spelling | Resolution |
|---|---|---|
| Platform-agnostic 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 C++ 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 beating generic ones; each value is itself a stem or a full path |
Each engine draws its own thing: the JS side (together with prependExpressions and input) is 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 its own draw calls without crossing wires; one side missing or failing to load just means that side doesn't draw โ it never drags the other side down.
The language field (a fallback switch)
| Value | Effect |
|---|---|
| Omitted (recommended) | Auto-detected from file characteristics โ zero configuration for the vast majority of cases |
"cpp" | A pure native entry (the old semantics): .js files inside scriptLocations are ignored with a log note; used together with nativeLibrary / nativeLibraries |
"javascript" / "js" | Forces everything to be parsed as JS โ the escape hatch for extension-less JS file names |
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 they can coexist with .js in the same entry.
The JS fields โ prependExpressions / input / rateLimit / dataFetchMode and friends โ keep their meanings; pure JS entries (all .js) behave exactly like upstream. For how the C++ side reads input, see the C++ docs.
Door-opening direction memory
VehicleWrapper.getDirectionOfDoorOpening(): String answers "which side will this train's doors open on", with values "not" (neither side) / "left" (left of the front of the train) / "right" (right of the front) / "all" (both sides):
- While stopped at a platform: whether the doors are still closed, half open, closing, or fully closed again, the answer matches the fully open state โ whichever side has platform blocks (or unlocked PSD / APG doors) counts as the side that "will open". When the door value is > 0, the per-car verdict for the current frame is read directly.
- While running on the main line: predicts the door side at the next stop โ the train is placed at its stop point on its own path and scanned with the same
canOpenDoorsblock check MTR uses. Available immediately when the stop point's chunks are loaded; beyond view distance, the server answers instead: it has scanned the whole map once and persisted the results (adding, removing, or editing platform blocks triggers an automatic incremental rescan) and answers requests from a per-route cache of both sides' answers โ joining mid-route, restarting the game, or visiting for the first time all work as usual. - Server without this mod, or no answer: falls back to the client's own within-view-distance scan; predictions for far-away platforms may then be unavailable.
- Aggregation rule: if any single car has a platform block beside a door on one side, that whole side counts as opening (aggregated per side, not per door). Left/right are relative to the front of the train (the bogie 1 end): each car is evaluated in its own coordinate system (-X = left of the car front, the MTR model convention), and travel direction and turnbacks never change the reading.
- Purely a query: it serves display logic only (door-side indicators, in-car screen arrows) and never interferes with MTR actually opening the doors.
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);
}
Transport type subdivisions
MTR's TransportMode has only four base modes (train / boat / cable_car / airplane), which isn't enough for display purposes. The MAGIC MTR add-on lets players subdivide the "train" mode into display types (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 subdivision (train_normal/train_light_rail/train_high_speed/train_metro/train_bus/train_tram/train_sbahn) when one is assigned, otherwise falls back to the base mode id (train/boat/cable_car/airplane);unknownwhen unknown, and never null.Stop.routeInterchanges[i](RouteInterchange) gains:getTransportMode(): TransportMode?(the interchange route's transport mode; null for old data),getTransportType(): String(the canonical id as above), andgetRouteId(): long(the source route's id; 0 when the server data is missing) โ interchange signs picking icons / color schemes by type no longer have to rely on color alone.- Without MAGIC installed, or for routes without an assignment, everything falls back to the base mode as before โ scripts need not probe for the add-on. Type lookups are cached briefly per route (about 1 second), so changing a type in MTR shows up 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.
Raw pixel uploads
GraphicsTexture.uploadRawABGR(source: byte[], dstOffsetX: int, dstOffsetY: int, width: int, height: int, ppm: int[]): void
Writes a width ร height ร 4-byte, row-major, BGRA-ordered array of pixel rows straight into the texture buffer and uploads only that rectangle to VRAM:
- Where it fits: the bandwidth-saving channel for high-frequency per-pixel updates. The regular AWT route's
upload()moves the whole image; a 4 Mpx screen fully uploaded every frame is a ten-plus-megabyte transfer, while point-wise animation (progress bars, ticking digits) usually changes only a thin strip โ a few hundred KB suffice with this method. ppmis a 4 ร 256-entry premultiplied lookup table (four runs of 256 entries โ B / G / R / A in order), held by the caller; bytes are expanded through the table on write, avoiding a multiply per pixel.- After the write, only that rectangle is uploaded and the rest of the texture is left alone โ it saves the "first edit it on the AWT canvas" step that
upload(x, y, w, h)still needs. - Usable from JS (assemble the byte array with
Java.newArray), but it was designed for the C++ bridge's frame-pixel return path; pure JS authors can stick with AWT + sub-region uploads.
JS and C++ vocabulary parity
The same data goes by the same name in both languages โ no split-brain when the two sides of a mixed entry draw the same thing:
| JS | C++ (SDK) | Introduced |
|---|---|---|
vehicle.getTransportType() | mtr::Vehicle::getTransportType() | ABI 9 |
Stop.routeInterchanges[i].getTransportType() and friends | mtr::Stop::interchange(i)'s type fields | ABI 9 |
vehicle.getDirectionOfDoorOpening() | mtr::Vehicle::doorDirection() / getDirectionOfDoorOpening() | ABI 11 |
Pure JS readers only need to remember one thing: every extension on this page is available on the C++ side too (as direct snapshot field reads, with zero marshaling cost) โ see Data snapshots.
Back to: JS Script Types ยท JS Common APIs; want the same data with zero JVM overhead? See C++ native scripting.