Data snapshots

How snapshots work

At the start of each frame the host marshals the instance's data in one go into a POD snapshot (fixed-length header + variable-length array pools + a UTF-8 string pool) and passes it to mtrRender together with the state block pointer. On top of that memory, the SDK's typed wrappers (mtr::Vehicle, mtr::Pids, mtr::Lift, mtr::EyeCandy) provide accessors with the same names as their JS counterparts:

  • Strings come back as mtr::StrView (data + len); convert with .str() to a std::string and test with .empty(); the view is valid only within the current render call.
  • Out-of-range indices in ordinary lists throw std::out_of_range (matching the semantics of the Java side's IndexOutOfBounds).
  • Do not keep snapshot pointers or wrappers across frames.
Coverage

Snapshots cover the fields that are read at high frequency. Data that is not in the snapshot (networking, files, deep interfaces of the render manager and so on) is reached through mtr::Api—that is a low-frequency channel; do not use it on the hot path.

Vehicle

Terminology: MTR 3's train → MTR 4's vehicle

MTR 4 uniformly uses vehicle as its term (the resource pack declaration key vehicleScripts, the Java-side VehicleWrapper), so this SDK's data wrapper class is named mtr::Vehicle. Earlier SDK versions followed the MTR 3 convention and named it mtr::Train; that name is now retained as a source-compatible alias—either name compiles, and the behaviour is completely identical.

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:

AccessorTypeDescription
id() / siding_id() / this_route_id() / departure_index()int64_tVehicle / siding / current route ids and the departure index (64-bit ids keep full precision throughout)
car_count() / car(i)int32_t / CarCar count and per-car data (see below)
speed_kmh() / speed_ms()doubleVehicle speed
rail_progress() / rail_progress(car)doubleRail progress; the parameterised version works out the position of car i from the car lengths
door_value()doubleDoor opening 0..1
notch_level() / notch_position()int32_t / doubleNotch level 0..5 and its normalised position
reversed() / on_route() / door_opening()boolReversed / on a route / doors currently opening
currently_manual() / manual_allowed()boolManual driving state
client_player_riding() / rendered()boolClient player riding / any car visible
total_dwell_time_millis() / elapsed_dwell_time_millis()doubleTotal dwell time / elapsed dwell time
game_time_millis() / in_game_time()int64_tHost wall clock (server offset already corrected) / in-game tick 0..24000
stops() / this_route_stops() / next_route_stops()StopListAll / current-route / next-route stop lists (see below)
next_stop_index()int32_tNext-stop index on the current route (precomputed by the host—don't recompute it yourself)
route_name() / route_color() / circular_state() / circular()StrView / uint32_t / uint8_t / boolCurrent route name / colour ARGB / circular state (0 none, 1 clockwise, 2 anticlockwise)
getTransportMode() / getTransportType()TransportMode / StrViewSee transport modes and types
doorDirection() / getDirectionOfDoorOpening()DoorDirection / StrViewDoor-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); whilst 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()StrViewSiding display name (user-defined name / description)
raw()JcmVehicleSnapshot&Raw snapshot (see C ABI for ABI details)

Car

AccessorDescription
length() / width()Car length / width
left_door_open() / right_door_open()Whether the left / right doors are fully open
rendered()Whether this car survived culling and is visible this frame
vehicle_type()Car type id (e.g. mtr_custom_train_demo_cab_1)

StopList

AccessorDescription
size() / empty() / at(i) / operator[](i)Size and random access (out-of-range access throws)
next_stop_index(rail_progress, tolerance=0.5)Works out the next stop from rail progress; for the current route's list, just use Vehicle::next_stop_index() (already precomputed by the host)

Stop / interchanges / exits

mtr::Stop mirrors the JS VehicleWrapper.Stop:

AccessorDescription
name()Station 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 the route this stop belongs to (to distinguish direction changes after crossing routes)
interchange_count() / interchange(i)Interchange information: each carries color (ARGB), name (route name), getTransportMode() and getTransportType()
exit_count() / exit(i)Station exits: each carries name (the exit letter, e.g. "A") and destination(j) (a list of landmark strings)

Transport modes and types

Two complementary classifications, for which the JS-side getTransportMode() / getTransportType() return exactly the same values, so scripts across languages can share layout logic:

  • Mode (the mtr::TransportMode enum): UNKNOWN / TRAIN / BOAT / CABLE_CAR / AIRPLANE, covering the four basic vehicle kinds.
  • Type (string ids, the mtr::transport_type constants): the lower-case names of the basic modes ("train", "boat", "cable_car", "airplane"), plus the extended route types from the MAGIC MTR Addon: train_normal / train_light_rail / train_high_speed / train_metro / train_bus / train_tram / train_sbahn; values that cannot be resolved become "unknown" or an empty view (old 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) { /* ... */ }
}
Data source

MAGIC types are resolved from local configuration when the client has the addon installed; when the server has it but the client does not, they arrive via a synchronised data set; old servers that send no route metadata yield empty / UNKNOWN—servers running this mod can supply complete metadata for remote stations.

Pids (arrival display)

mtr::Pids mirrors the JS PIDSWrapper:

AccessorDescription
width() / height()Display width and height (1.0 unit = 16 px, screen coordinate system)
rows() / is_row_hidden(i)Number of text rows / whether row i is hidden (32 rows at most)
station_id()Id of the station the block is in
is_key_block() / is_platform_number_hidden()Whether this is a key block (controls hiding the 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 already corrected)

Arrivals / Arrival

AccessorDescription
arrivals().size() / .at(i)Arrival entry count and access
arrivals().next_for_platform(platform_id, out, cursor)Iterates in platform order (mirroring the JS forEach(platformId, cb)); the cursor is reused, zero allocation
arrivals().mixed_car_length()Whether the 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, already corrected)
arrived() / departed()Whether the vehicle has arrived / departed (compared against the current time)
realtime() / terminating() / circular_state()Real-time data / terminating here / circular state
route_id() / platform_id() / route_color() / car_count() / departure_index()Route / platform ids, route colour ARGB, car count, departure index

Psd (platform screen door)

mtr::Psd mirrors the JS PSDWrapper (a psdScripts entry — see JS script types · PSD scripts):

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

AccessorDescription
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:

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

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

FieldDescription
station_id / nameStation id and name
color / color_hexStation colour — ARGB integer and #RRGGBB string
zone1 / zone2 / zone3Fare zones 1–3
interchangesInterchange routes: route_id / colour / name / transport_mode / type
connecting_groupsConnecting-station routes grouped by station name (each group = the station name + its routes)
connected_station_namesPhysically connected station names
exitsExits: name + destinations
platform_numbersThe 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):

AccessorDescription
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()Facing angle
direction()Travel direction
floor_count() / floor(i)Floor list: each floor carries index, the current_floor / target_floor flags, position[3], and its floor number and description strings (fetched via string(offset,len))
game_time_millis() / in_game_time()Clocks

Deeper interfaces—the render manager, the sound manager, visibility and so on—are reached through Api's wrapper() (lifts have no dedicated JS documentation page; the Java wrapper is authoritative).

EyeCandy

mtr::EyeCandy mirrors EyecandyBlockEntityWrapper:

AccessorDescription
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 targeted by the crosshair
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—consistent with the JS event queue behaviour)
game_time_millis() / in_game_time()Clocks

Event details (player, held item and so on) 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 drawing 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 1.5-block height limit, and anything beyond is 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 utilities (zero allocation):

// "A|B|C" rotates one segment every 40 ticks; switching views is just an offset computation
mtr::CycleTracker dest{"KOWLOON|TSUEN WAN"};
StrView current = dest.value(vehicle.in_game_time());
MemberDescription
CycleTracker(packed, interval_ticks=40)Constructs the 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
StrViewA {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 perform 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 */ }
MemberDescription
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 on screen.