Yanyang Extensions

Extensions at a glance

ExtensionEntry pointIn one sentence
Mixed JS + C++ entriesDeclaration entriesFreely mix JS and C++ modules within one entry; both engines render side by side
Door-opening direction memoryVehicleWrapper.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 subdivisionsVehicleWrapper.getTransportType() and friendsFine-grained route type ids (interworking with MAGIC MTR) plus interchange extension fields
Raw pixel uploadsGraphicsTexture.uploadRawABGR()Raw byte-row writes + dirty-region-only uploads โ€” the bandwidth-saving channel for high-frequency per-pixel updates
Compatibility aliasesDeclaration entriesAccepts 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:

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

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

Relation to pure JS entries

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 canOpenDoors block 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.
Example: door-side indicator lamps
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); unknown when 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), and getRouteId(): 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 siteSideNotes
vehicle.getTransportType()JS ยท vehicle scriptsThe 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 scriptsPer-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.
  • ppm is 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:

JSC++ (SDK)Introduced
vehicle.getTransportType()mtr::Vehicle::getTransportType()ABI 9
Stop.routeInterchanges[i].getTransportType() and friendsmtr::Stop::interchange(i)'s type fieldsABI 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.