Data snapshots
Read-only data marshaled by the host once per frame. Every accessor is a plain memory read โ zero JNI, zero reflection, zero boxing.
How snapshots work
At the start of each frame, the host marshals the instance's data in one shot into a POD snapshot (fixed-length header + variable-length array pools + a UTF-8 string pool) and passes it, together with the state block pointer, to mtrRender. The SDK's typed wrappers (mtr::Vehicle, mtr::Pids, mtr::Lift, mtr::EyeCandy) provide same-named accessors over that memory as the JS Wrappers:
- Strings come back as
mtr::StrView(data+len); call.str()to convert tostd::stringand.empty()to test for empty; views are valid only within the currentrendercall. - Out-of-range indexes in ordinary lists throw
std::out_of_range(matching the Java side'sIndexOutOfBoundssemantics). - Do not keep snapshot pointers or wrappers across frames.
Snapshots cover the fields read at high frequency. Data that is not in the snapshot (networking, files, deep interfaces of the render managers, etc.) is reached through mtr::Api โ that is the low-frequency channel; keep it off the hot path.
Vehicle
MTR 4 uniformly calls these vehicles (the resource pack declaration key is vehicleScripts, the Java side is VehicleWrapper), so this SDK's data wrapper class is named mtr::Vehicle. Earlier SDK releases named it mtr::Train after the MTR 3 convention; that name is now kept as a source-compatible alias โ both spellings compile and behave identically.
Note: the mtr::Vehicle name is in the repository's SDK sources and ships with the next release's zip; if your project uses the 2.3.0-beta-2.yanyang.7 (or earlier) SDK zip, refer to this class as mtr::Train (both spellings compile against the new SDK).
Provided by mtr::Vehicle, mirroring the JS VehicleWrapper + NTETrainWrapper:
| Accessor | Type | Description |
|---|---|---|
id() / siding_id() / this_route_id() / departure_index() | int64_t | Vehicle / siding / current route ids, departure index (64-bit ids keep full precision end to end) |
car_count() / car(i) | int32_t / Car | Car count and per-car data (see below) |
speed_kmh() / speed_ms() | double | Vehicle speed |
rail_progress() / rail_progress(car) | double | Rail progress; the parameterized version derives the position of car i from car lengths |
door_value() | double | Door opening 0..1 |
notch_level() / notch_position() | int32_t / double | Notch level 0..5 and normalized position |
reversed() / on_route() / door_opening() | bool | Reversed / on a route / doors currently opening |
currently_manual() / manual_allowed() | bool | Manual driving state |
client_player_riding() / rendered() | bool | Client player riding / any car visible |
total_dwell_time_millis() / elapsed_dwell_time_millis() | double | Total dwell time / elapsed dwell time |
game_time_millis() / in_game_time() | int64_t | Host wall clock (server-offset corrected) / in-game tick 0..24000 |
stops() / this_route_stops() / next_route_stops() | StopList | Stop lists for all stops / the current route / the next route (see below) |
next_stop_index() | int32_t | Next-stop index within the current route (precomputed by the host โ don't recompute it yourself) |
route_name() / route_color() / circular_state() / circular() | StrView / uint32_t / uint8_t / bool | Current route name / color ARGB / circular state (0 none, 1 clockwise, 2 counterclockwise) |
getTransportMode() / getTransportType() | TransportMode / StrView | See Transport modes and types |
doorDirection() / getDirectionOfDoorOpening() | DoorDirection / StrView | Door-opening side: NOT / "not" (neither side), LEFT / "left" (head-left), RIGHT / "right" (head-right), ALL / "all" (both sides). While dwelling at a platform the value is reported the same regardless of the current door phase (not-yet-open / partial / closing / closed); while running on the mainline it predicts the next stop โ laying the vehicle out at its stopping point and running MTR's own canOpenDoors block scan per car. The prediction prefers the server-side per-route persistent memory (persisted with the save when the server runs this mod โ valid beyond view distance, incrementally refreshed as platform blocks change); when the server has no answer it falls back to the client's own view-distance scan. If any one car on a side can open, that whole side counts as openable; left/right are relative to the vehicle head (bogie-1 end) โ travel direction and reversing never flip them. Purely informational โ MTR still owns which doors actually open. Identical to the JS vehicle.getDirectionOfDoorOpening() (ABI 11) |
siding_name() | StrView | Siding display name (user-defined name / description) |
raw() | JcmVehicleSnapshot& | Raw snapshot (ABI details in C ABI) |
Car
| Accessor | Description |
|---|---|
length() / width() | Car length / width |
left_door_open() / right_door_open() | Whether the left / right doors are fully open |
rendered() | Whether this car is still visible after culling this frame |
vehicle_type() | Car type id (e.g. mtr_custom_train_demo_cab_1) |
StopList
| Accessor | Description |
|---|---|
size() / empty() / at(i) / operator[](i) | Size and random access (out-of-range throws) |
next_stop_index(rail_progress, tolerance=0.5) | Derives the next stop from rail progress; for the current route's list use Vehicle::next_stop_index() directly (already precomputed by the host) |
Stop / interchanges / exits
mtr::Stop mirrors the JS VehicleWrapper.Stop:
| Accessor | Description |
|---|---|
name() | Stop name |
destination_name() | Destination display name for this platform |
has_custom_destination() / custom_destination() | Custom destination (JS returns null when unset; in C++ test with has_custom_destination()) |
distance() | Rail progress (-1 when unavailable) |
dwell_time_millis() | Dwell time at this stop |
route_id() / station_id() / platform_id() | Route / station / platform ids (64-bit) |
is_route_switchover() | Whether this is a route switchover stop |
route_circular_state() | Circular state of this stop's route (distinguishes direction changes after crossing routes) |
interchange_count() / interchange(i) | Interchange info: each has color (ARGB), name (route name), getTransportMode(), getTransportType() |
exit_count() / exit(i) | Station exits: each has name (the exit letter, e.g. "A") and destination(j) (a list of landmark strings) |
Transport modes and types
Two complementary views: the JS-side getTransportMode() / getTransportType() return exactly the same values, so scripts across languages can share layout logic:
- Mode (the
mtr::TransportModeenum):UNKNOWN / TRAIN / BOAT / CABLE_CAR / AIRPLANE, covering the four basic vehicles. - Type (a string id, the
mtr::transport_typeconstants): the lowercase names of the basic modes ("train","boat","cable_car","airplane") plus the extended route types of the MAGIC MTR Addon โtrain_normal / train_light_rail / train_high_speed / train_metro / train_bus / train_tram / train_sbahn; unresolvable values come back as"unknown"or an empty view (older server data).
if (vehicle.getTransportType() == mtr::transport_type::TRAIN_METRO) { /* ... */ }
for (int i = 0; i < stop.interchange_count(); i++) {
mtr::Stop::Interchange ic = stop.interchange(i);
if (ic.getTransportType() == mtr::transport_type::TRAIN_HIGH_SPEED) { /* ... */ }
}
MAGIC types resolve from local configuration when the client has the addon installed; when the server has it but the client doesn't, they arrive through the synchronized data set; older servers that don't send route metadata yield empty / UNKNOWN โ servers running this mod can supply full metadata for remote stations.
Pids (arrival display)
mtr::Pids mirrors the JS PIDSWrapper:
| Accessor | Description |
|---|---|
width() / height() | Display width / height (1.0 unit = 16 px, screen coordinate system) |
rows() / is_row_hidden(i) | Number of text rows / whether row i is hidden (up to 32 rows) |
station_id() | Id of the station the PIDS is at |
is_key_block() / is_platform_number_hidden() | Whether it is a key block (controls hiding other screens) / whether the platform number is hidden |
get_custom_message(i) | Custom message for row i; empty when unset (JS returns null) |
arrivals() | Arrivals: the arrival list (see below) |
game_time_millis() / in_game_time() | Clocks (server-offset corrected) |
Arrivals / Arrival
| Accessor | Description |
|---|---|
arrivals().size() / .at(i) | Arrival entry count and access |
arrivals().next_for_platform(platform_id, out, cursor) | Iterates in platform order (mirrors the JS forEach(platformId, cb)); the cursor is reused, zero allocations |
arrivals().mixed_car_length() | Whether entries disagree on car count (mirrors mixedCarLength()) |
destination() / route_name() / route_number() / platform_name() | Destination / route name / route number / platform name |
arrival_time() / departure_time() / deviation() | Arrival / departure times and deviation (epoch milliseconds, corrected) |
arrived() / departed() | Whether it has arrived / departed (compared against the current time) |
realtime() / terminating() / circular_state() | Realtime data / terminating this trip / circular state |
route_id() / platform_id() / route_color() / car_count() / departure_index() | Route / platform ids, route color ARGB, car count, departure index |
Psd (platform screen door)
mtr::Psd mirrors the JS PSDWrapper (a psdScripts entry โ see JS script types ยท PSD scripts):
| Accessor | Description |
|---|---|
type() / is_glass() / is_top() | Binding surface: Psd::GLASS or Psd::TOP |
width() / height() | Canvas size (PIDS pixels): glass โ the whole segment's transparent window (96 px per block, full-height columns merge vertically); top panel โ ~96 px per block wide (minus padding) ร 42 high |
platform_id() / has_platform() / station_id() | Served platform and station (ids are 0 without a platform) |
platform_number() / station_name() | Platform number (e.g. "1") and station name |
door_value() / is_door_open() | Wall-wide current open amount (0..1) and whether any door is open |
doors() | PsdDoorList: the door units (below) |
arrivals() | PsdArrivalList: upcoming trains (below) |
routes() | PsdRouteList: routes calling at the platform (below; stop lists only with dataFetchMode: "ALL") |
stop() / has_station() | Complete station info of the served station (ABI 13): the mtr::PsdStation accessors below; has_station() is false (and stop() empty) when the wall has no platform / station |
game_time_millis() / in_game_time() | Clocks (server-offset corrected) |
PsdDoor (door unit)
| Accessor | Description |
|---|---|
number() | 1-based unit number along the wall โ one sliding panel group (a LEFT+RIGHT pair, or the 3-block odd style) within one contiguous wall run |
is_open() / is_locked() | Live open / locked state (read straight off the door block entities every frame) |
x() / y() / z() | Block position of the unit's first door block |
PsdArrival (upcoming train)
Carries every Pids Arrival accessor (destination / route_name / route_number / platform_name / arrival_time / departure_time / deviation / realtime / terminating / circular_state / route_id / platform_id / route_color / car_count / departure_index โ see Pids), plus:
| Accessor | Description |
|---|---|
door_count() / door_number(i) | The door units this train will open: the actually-open units while the train dwells (falling back to the prediction during the door-animation gaps); before arrival, an approximation โ the train stops centered on the platform midpoint, its length summed from the per-car vehicle resources |
PsdRoute / PsdStop (routes and stations)
| Accessor | Description |
|---|---|
id() / name() / route_number() / color() / circular_state() / getTransportType() | Route id / name / number / ARGB color / circular state / canonical transport type id |
stops() | PsdStopList: the ordered stops, laid out like the vehicle Stop (name / destination_name / custom_destination / dwell_time_millis / route_id / station_id / platform_id / route_circular_state / interchange(i) / exit(i) โ see Stop / interchanges / exits) |
PsdStation (the station record, ABI 13)
JcmPsdSnapshot.station_offset (byte offset 124; -1 = no station โ the field was padding in ABI 12) points at the 96-byte JcmPsdStation record โ the same data the JS psd.stop() object exposes:
PsdStation and its nested station accessors return absent objects, empty strings or defaults for invalid indices or records instead of throwing index exceptions. When constructing a snapshot outside the host, pass the actual buffer size to mtr::Psd(snapshot, byteLength). The compatible pointer-only constructor requires a fully readable header and its declared backing buffer; a raw pointer cannot reveal the allocation size.
| Field | Description |
|---|---|
station_id / name | Station id and name |
color / color_hex | Station color โ ARGB integer and #RRGGBB string |
zone1 / zone2 / zone3 | Fare zones 1โ3 |
interchanges | Interchange routes: route_id / color / name / transport_mode / type |
connecting_groups | Connecting-station routes grouped by station name (each group = the station name + its routes) |
connected_station_names | Physically connected station names |
exits | Exits: name + destinations |
platform_numbers | The station's platform numbers (the JS side adds the count) |
ABI 12 โ 13 is purely additive (a padding field promoted in place, every pre-existing struct unchanged), so ABI 6โ12 modules keep loading.
Lift
mtr::Lift (a snapshot + Java wrapper combination; the snapshot part):
| Accessor | Description |
|---|---|
id() | Lift instance id |
should_render() / is_double_sided() / is_client_player_riding() | Should render / double-sided / client player riding |
width() / height() / depth() | Dimensions |
door_value() | Door opening 0..1 |
angle_degrees() / angle_radians() | Heading angle |
direction() | Travel direction |
floor_count() / floor(i) | Floor list: each floor has index, current_floor / target_floor flags, position[3], and floor-number / description strings (fetched via string(offset,len)) |
game_time_millis() / in_game_time() | Clocks |
Deeper interfaces โ render manager, sound manager, visibility, and so on โ are reached through Api's wrapper() (lifts have no standalone JS documentation page; the Java wrapper is authoritative).
EyeCandy
mtr::EyeCandy mirrors EyecandyBlockEntityWrapper:
| Accessor | Description |
|---|---|
model_id() | Current model id |
translate_x() / _y() / _z() / rotate_x() / _y() / _z() | User-configured translation / rotation |
facing() | Facing (MC Direction.ordinal(), 0..5) |
facing_angle() | Facing angle in degrees (Direction.asRotation(): S 0 / W 90 / N 180 / E 270) โ same as the JS facingAngle() |
full_brightness() | Full-brightness rendering |
redstone_level() | Redstone level 0..15 |
is_crosshair_target() | Whether the crosshair points at it |
block_pos(i) | Block position component (i = 0..2) |
consume_block_use() | Consumes one onBlockUse event accumulated this frame (at most once per frame; multiple clicks within a frame are merged โ same behavior as the JS event queue) |
game_time_millis() / in_game_time() | Clocks |
Event details (player, held item, etc.) are read through Api:
mtr::Api api(ctx.input());
auto event = api.context().call("events").get("onBlockUse");
if (event.call("occurred").as<bool>()) {
auto detail = event.call("detail");
auto player = detail.call("player");
auto item = detail.call("item");
}
The eyecandy draw context EyeCandyContext provides draw_model(model, matrices), play_sound(sound, volume, pitch), set_outline_shape(boxes, count), and set_collision_shape(boxes, count) (6 floats per box; collision boxes keep the Java side's 1.5-block height cap, with overflow validated and logged by the host). Lifts reuse the same context type (LiftContext = EyeCandyContext).
Helper types
<mtr/util.hpp> provides hot-path-friendly cycling and state-tracking helpers (zero allocation):
// "A|B|C" rotates one segment every 40 ticks; switching views is pure offset math
mtr::CycleTracker dest{"KOWLOON|TSUEN WAN"};
StrView current = dest.value(vehicle.in_game_time());
| Member | Description |
|---|---|
CycleTracker(packed, interval_ticks=40) | Constructs a cycler (mirrors TextUtil.cycleString) |
value(game_tick) / value_str(game_tick) | The currently active segment (view / string) |
interval() / part_count() | Rotation interval and segment count |
StrView | A {data, len} string view: .str(), .empty(), comparable with == |
State-machine tracking mtr::StateTracker<T> (mirrors the upstream StateTrackerJS; the clock is passed explicitly โ the snapshot's game_time_millis() is the natural equivalent): keep the tracker field in State and do a single transition check per render:
struct State {
mtr::StateTracker<Phase> phase; // persists across frames
};
// in render():
if (s.phase.set_state(next, vehicle.game_time_millis())) {
// returned true โ first frame in this state (new upstream prerelease.2 semantics)
}
double staying = s.phase.state_now_duration(vehicle.game_time_millis());
if (s.phase.changed_from_to(PHASE_IDLE, PHASE_ARRIVING)) { /* play arrival animation */ }
| Member | Description |
|---|---|
set_state(value, now_ms) | Update the state; returns whether this is the first frame in the state (setState returns a boolean since upstream v2.3.0-prerelease.2) |
state_now() / state_last() / state_now_first() | Current / previous state; first frame flag |
state_now_duration(now_ms) | How long the current state has been active (milliseconds) |
changed_to(state) / changed_from_to(old, new) | Whether this frame entered the given state / transitioned from one given state to another |
Up next: Drawing and canvases โ turning this data into pixels.