数据快照

快照机制

每帧开始时,宿主把该实例的数据一次性打包成一块 POD 快照(定长头部 + 变长数组池 + UTF-8 字符串池),连同状态块指针一起传给 mtrRender。SDK 的类型化包装器(mtr::Vehicle、mtr::Pids、mtr::Lift、mtr::EyeCandy)在这块内存上提供与 JS Wrapper 同名的访问器:

  • 字符串以 mtr::StrView(data + len)返回,用 .str() 转 std::string、.empty() 判空;视图只在本次 render 调用内有效。
  • 普通列表的越界索引抛 std::out_of_range(与 Java 端 IndexOutOfBounds 语义一致)。
  • 快照指针与包装器不要跨帧保存。
覆盖范围

快照覆盖高频读取的字段。不在快照里的数据(网络、文件、渲染管理器的深接口等)经 mtr::Api 访问——那是低频通道,别在热路径用。

Vehicle(车辆)

术语:MTR 3 的 train → MTR 4 的 vehicle

MTR 4 统一以 vehicle 称呼载具(资源包声明键 vehicleScripts、Java 侧 VehicleWrapper),本 SDK 的数据包装类因此命名为 mtr::Vehicle。更早版本的 SDK 曾按 MTR 3 习惯命名为 mtr::Train,该名称现保留为源码兼容别名——两种写法均可编译,行为完全一致。

注意:mtr::Vehicle 命名已进入仓库 SDK 源码,将随下一版本的 zip 发布;若你的工程使用 2.3.0-beta-2.yanyang.7 及更早的 SDK zip,请以 mtr::Train 引用该类(新 SDK 中两种写法均可编译)。

由 mtr::Vehicle 提供,镜像 JS 的 VehicleWrapper + NTETrainWrapper:

访问器类型说明
id() / siding_id() / this_route_id() / departure_index()int64_t车辆 / 侧线 / 当前线路 id、发车序号(64 位 id 全程不丢精度)
car_count() / car(i)int32_t / Car车厢数量与逐厢数据(见下)
speed_kmh() / speed_ms()double车速
rail_progress() / rail_progress(car)double轨道进度;带参版本按车厢长度推算第 i 节的位置
door_value()double车门开度 0..1
notch_level() / notch_position()int32_t / double档位 0..5 与归一化位置
reversed() / on_route() / door_opening()bool反向 / 在线路上 / 车门正在开
currently_manual() / manual_allowed()bool手动驾驶状态
client_player_riding() / rendered()bool玩家乘坐 / 任一车厢可见
total_dwell_time_millis() / elapsed_dwell_time_millis()double停站总时长 / 已停留时长
game_time_millis() / in_game_time()int64_t宿主墙钟(已做服务器偏移校正)/ 游戏内 tick 0..24000
stops() / this_route_stops() / next_route_stops()StopList全部 / 当前线路 / 下一线路站点表(见下)
next_stop_index()int32_t当前线路的下一站索引(宿主预算好,别自己重算)
route_name() / route_color() / circular_state() / circular()StrView / uint32_t / uint8_t / bool当前线路名 / 颜色 ARGB / 环线状态(0 无、1 顺时针、2 逆时针)
getTransportMode() / getTransportType()TransportMode / StrView见交通模式与类型
doorDirection() / getDirectionOfDoorOpening()DoorDirection / StrView开门方向:NOT / "not"(两侧都不开)、LEFT / "left"(车头左侧开)、RIGHT / "right"(车头右侧开)、ALL / "all"(两侧开)。停靠站台时不论车门处于未开 / 半开 / 关闭中 / 已关的哪个阶段,回报都与完全打开时相同;正线运行时预测下一站——按停车点摆位、逐车厢做 MTR 同款 canOpenDoors 方块扫描。预测优先采用服务端逐线路持久记忆(服务端装本模组时按存档持久化、跨视距有效,站台方块变化自动增量更新);服务端未装或未应答时,回退为客户端视距内扫描。同侧只要有一节车门可开,整侧即记为可开;左右相对车头(bogie-1 端),行进方向 / 倒行均不改变读数。纯查询——MTR 的实际开门不受影响。与 JS 的 vehicle.getDirectionOfDoorOpening() 完全一致(ABI 11)
siding_name()StrView侧线显示名(用户自定义名称 / 描述)
raw()JcmVehicleSnapshot&原始快照(ABI 细节见 C ABI)

Car(车厢)

访问器说明
length() / width()车厢长度 / 宽度
left_door_open() / right_door_open()左右门是否完全打开
rendered()该厢本帧是否经剔除后仍可见
vehicle_type()车厢类型 id(如 mtr_custom_train_demo_cab_1)

StopList

访问器说明
size() / empty() / at(i) / operator[](i)容量与随机访问(越界抛异常)
next_stop_index(rail_progress, tolerance=0.5)按轨道进度推算下一站;当前线路列表请直接用 Vehicle::next_stop_index()(宿主已预算)

Stop / 换乘 / 出口

mtr::Stop 镜像 JS 的 VehicleWrapper.Stop:

访问器说明
name()站名
destination_name()该站台的终点站显示名
has_custom_destination() / custom_destination()自定义终点站(未设置时 JS 返回 null,C++ 用 has_custom_destination() 判空)
distance()轨道进度(不可用时为 -1)
dwell_time_millis()该站停站时长
route_id() / station_id() / platform_id()线路 / 车站 / 站台 id(64 位)
is_route_switchover()是否线路切换站
route_circular_state()该站所属线路的环线状态(区分跨线后的方向变化)
interchange_count() / interchange(i)换乘信息:每个含 color(ARGB)、name(线路名)、getTransportMode()、getTransportType()
exit_count() / exit(i)车站出口:每个含 name(出口字母,如 "A")与 destination(j)(地标字符串列表)

交通模式与类型

两条互补的口径,与 JS 侧 getTransportMode() / getTransportType() 返回完全相同的值,跨语言脚本可共享布局逻辑:

  • 模式(mtr::TransportMode 枚举):UNKNOWN / TRAIN / BOAT / CABLE_CAR / AIRPLANE,覆盖四种基础载具。
  • 类型(字符串 id,mtr::transport_type 常量):基础模式的小写名("train"、"boat"、"cable_car"、"airplane"),加上 MAGIC MTR Addon 的扩展线路类型 train_normal / train_light_rail / train_high_speed / train_metro / train_bus / train_tram / train_sbahn;无法解析时为 "unknown" 或空视图(旧服务器数据)。
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 类型在客户端装了该 addon 时从本地配置解析;服务器装了而客户端没装时经同步数据集下发;旧服务器不下发线路元数据时返回空 / UNKNOWN——装了本模组的服务器可提供远程车站的完整元数据。

Pids(到站屏)

mtr::Pids 镜像 JS 的 PIDSWrapper:

访问器说明
width() / height()显示屏宽高(1.0 单位 = 16 px,屏幕坐标系)
rows() / is_row_hidden(i)文本行数 / 第 i 行是否被隐藏(最多 32 行)
station_id()所在车站 id
is_key_block() / is_platform_number_hidden()是否按键方块(控制隐藏其它屏)/ 是否隐藏站台号
get_custom_message(i)第 i 行自定义消息;未设置返回空(JS 返回 null)
arrivals()Arrivals:到站列表(见下)
game_time_millis() / in_game_time()时钟(已做服务器偏移校正)

Arrivals / Arrival

访问器说明
arrivals().size() / .at(i)到站条目数与访问
arrivals().next_for_platform(platform_id, out, cursor)按站台顺序遍历(镜像 JS 的 forEach(platformId, cb)),游标复用、零分配
arrivals().mixed_car_length()各条目车厢数是否不一致(镜像 mixedCarLength())
destination() / route_name() / route_number() / platform_name()终点站 / 线路名 / 线路号 / 站台名
arrival_time() / departure_time() / deviation()到站 / 离站时刻与偏差(epoch 毫秒,已校正)
arrived() / departed()是否已到站 / 已离站(按当前时刻比较)
realtime() / terminating() / circular_state()实时数据 / 本班终止 / 环线状态
route_id() / platform_id() / route_color() / car_count() / departure_index()线路 / 站台 id、线路颜色 ARGB、车厢数、发车序号

Psd(屏蔽门)

mtr::Psd 镜像 JS 的 PSDWrapper(psdScripts 条目,见 JS 脚本类型 · PSD 脚本):

访问器说明
type() / is_glass() / is_top()绑定表面:Psd::GLASS(玻璃)或 Psd::TOP(顶部面板)
width() / height()画布尺寸(PIDS 像素):玻璃为整段透明窗口的尺寸(每方块 96 像素,整柱两格上下合并为一);顶部面板每方块宽约 96(扣除边距)× 高 42
platform_id() / has_platform() / station_id()所服务的站台与车站(无站台时 id 为 0)
platform_number() / station_name()站台编号(如 "1")与车站名
door_value() / is_door_open()整墙当前开门量(0..1)与是否有门在开
doors()PsdDoorList:门单元列表(见下)
arrivals()PsdArrivalList:到站列车(见下)
routes()PsdRouteList:经停线路(见下;dataFetchMode: "ALL" 才含车站列表)
stop() / has_station()所服务车站的完整信息(ABI 13):mtr::PsdStation 访问器见下;整墙无站台 / 车站时 has_station() 为假(stop() 为空)
game_time_millis() / in_game_time()时钟(已做服务器偏移校正)

PsdDoor(门单元)

访问器说明
number()门单元编号(1 起,沿墙方向计)——一面连续墙内的滑动门组(2 块 LEFT+RIGHT 或 3 块奇数门为一单元)
is_open() / is_locked()实时开门 / 上锁状态(逐帧直读门方块实体)
x() / y() / z()单元首门方块的坐标

PsdArrival(到站列车)

具备 Pids Arrival 的全部访问器(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,见 Pids),另有:

访问器说明
door_count() / door_number(i)该列车将开启的门单元编号:停站中的列车读实际开门状态(门动画间隙回退预测);未到站按「列车停于站台中点 + 按车辆资源逐卡累加车长」近似预测

PsdRoute / PsdStop(线路与车站)

访问器说明
id() / name() / route_number() / color() / circular_state() / getTransportType()线路 id / 名称 / 编号 / 颜色 ARGB / 环线状态 / 规范交通类型 id
stops()PsdStopList:按序车站,布局同车辆 Stop(name / destination_name / custom_destination / dwell_time_millis / route_id / station_id / platform_id / route_circular_state / interchange(i) / exit(i),见 Stop / 换乘 / 出口)

PsdStation(车站记录,ABI 13)

JcmPsdSnapshot.station_offset(字节偏移 124;-1 = 无车站——该字段在 ABI 12 中是填充位)指向 96 字节的 JcmPsdStation 记录——与 JS psd.stop() 对象同一数据:

PsdStation 及其嵌套车站访问器在索引或记录无效时返回空对象、空字符串或默认值,不抛索引异常。若在宿主之外构造快照,请传入 mtr::Psd(snapshot, byteLength) 的实际缓冲区字节数;仅传指针的兼容构造器要求完整可读的头部与其声明的缓冲区,无法从裸指针推断分配大小。

字段说明
station_id / name车站 id 与名称
color / color_hex车站颜色——ARGB 整数与 #RRGGBB 字符串
zone1 / zone2 / zone3票价区 1–3
interchanges换乘线路:route_id / color / name / transport_mode / type
connecting_groups邻站线路,按站名分组(每组 = 站名 + 该站线路)
connected_station_names物理连通的站名
exits出口:名称 + 去向
platform_numbers本站站台编号(JS 侧另加总数)

ABI 12 → 13 纯增量(填充位原地升格,既有结构零变化),ABI 6–12 模块照常加载。

Lift(电梯)

mtr::Lift(快照 + Java 包装器结合;快照部分):

访问器说明
id()电梯实例 id
should_render() / is_double_sided() / is_client_player_riding()渲染 / 双面 / 玩家乘坐
width() / height() / depth()尺寸
door_value()门开度 0..1
angle_degrees() / angle_radians()朝向角
direction()运行方向
floor_count() / floor(i)楼层表:每层含 index、current_floor / target_floor 标志、position[3]、楼层号与描述字符串(经 string(offset,len) 取)
game_time_millis() / in_game_time()时钟

渲染管理器、声音管理器、可见性等更深的接口经 Api 的 wrapper() 访问(电梯没有独立的 JS 文档页,以 Java 包装器为准)。

EyeCandy(装饰)

mtr::EyeCandy 镜像 EyecandyBlockEntityWrapper:

访问器说明
model_id()当前模型 id
translate_x() / _y() / _z() / rotate_x() / _y() / _z()用户配置的平移 / 旋转
facing()朝向(MC Direction.ordinal(),0..5)
facing_angle()朝向角(Direction.asRotation() 度数:南 0 / 西 90 / 北 180 / 东 270)——与 JS facingAngle() 一致
full_brightness()全亮渲染
redstone_level()红石信号 0..15
is_crosshair_target()是否被准星指向
block_pos(i)方块坐标分量(i = 0..2)
consume_block_use()消费一次本帧累积的 onBlockUse 事件(每帧至多一次;同帧多次点击被合并——与 JS 事件队列行为一致)
game_time_millis() / in_game_time()时钟

事件详情(玩家、手持物品等)经 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");
}

装饰的绘制上下文 EyeCandyContext 提供 draw_model(model, matrices)、play_sound(sound, volume, pitch)、set_outline_shape(boxes, count)、set_collision_shape(boxes, count)(每箱 6 个 float;碰撞箱沿用 Java 端 1.5 格高度上限,超出由宿主校验并记日志)。电梯复用同一上下文类型(LiftContext = EyeCandyContext)。

工具类型

<mtr/util.hpp> 提供热路径友好的轮播与状态追踪工具(零分配):

// "A|B|C" 每 40 tick 轮换一段,视图切换只做偏移计算
mtr::CycleTracker dest{"KOWLOON|TSUEN WAN"};
StrView current = dest.value(vehicle.in_game_time());
成员说明
CycleTracker(packed, interval_ticks=40)构造轮播(镜像 TextUtil.cycleString)
value(game_tick) / value_str(game_tick)当前活跃段(视图 / 字符串)
interval() / part_count()轮换间隔与段数
StrView{data, len} 字符串视图:.str()、.empty(),可 == 比较

状态机追踪 mtr::StateTracker<T>(镜像上游 StateTrackerJS;时钟显式传入——快照的 game_time_millis() 即为天然等价物):把状态字段放进 State,在 render 里只做一次转移判定:

struct State {
    mtr::StateTracker<Phase> phase;   // 跨帧持久
};

// render 里:
if (s.phase.set_state(next, vehicle.game_time_millis())) {
    // 返回 true —— 进入该状态的首帧(上游 prerelease.2 新语义)
}
double staying = s.phase.state_now_duration(vehicle.game_time_millis());
if (s.phase.changed_from_to(PHASE_IDLE, PHASE_ARRIVING)) { /* 播进场动画 */ }
成员说明
set_state(value, now_ms)更新状态;返回是否为进入该状态的首帧(上游 v2.3.0-prerelease.2 起 setState 返回布尔)
state_now() / state_last() / state_now_first()当前 / 上一状态;是否首帧
state_now_duration(now_ms)当前状态已持续时间(毫秒)
changed_to(state) / changed_from_to(old, new)本帧是否进入指定状态 / 从指定状态转入

接下来:绘制与画布 —— 把这些数据画出来。