JS 脚本类型
四类脚本各自的 ctx、wrapper 与数据类型。注册方式与生命周期见 JS 脚本入门,公共类见 JS 公共 API。
车辆脚本
一个「车辆」在 MTR 4 里是一节车厢模型条目,玩家在侧线界面自由编组成列。脚本不绑定单节车厢也不绑定整列,而是绑定一个脚本条目:车辆条目以 scriptId 引用它,多个车辆可复用同一条脚本——混编、性能、NTE 兼容三全其美。
三个回调(ctx 是 VehicleScriptContext,vehicle 是 VehicleWrapper):
function create(ctx, state, vehicle) { /* 首次渲染前:建纹理、注册回调 */ }
function render(ctx, state, vehicle) { /* 每帧:读状态、提交绘制与声音 */ }
function dispose(ctx, state, vehicle) { /* 退场:释放纹理 */ }
VehicleScriptContext(ctx)
| 成员 | 说明 |
|---|---|
getCarRenderManager(carIndex): RenderManager? | 取第 carIndex 节车厢的渲染管理器。基准 = 车厢中心、底面上方 1 m。非本条目挂的车返回 null |
getCarBogieRenderManager(carIndex, bogieIndex): RenderManager? | 转向架渲染:基准相对第 bogieIndex 个转向架(0 前 1 后;单转向架车厢为 0) |
getCarSoundManager(carIndex): SoundManager? | 取车厢声源(基准 = 车厢中心) |
playAnnSound(id, volume, pitch) | 播报站广播——只在玩家乘坐本列时可闻,不定位 |
getScriptEntryId(): String | 脚本条目 id |
getMyCars(): int[] | 挂在本条目名下的车厢索引数组 |
setDebugInfo(key, value) / setDebugInfo(value) | 调试覆盖层显示(所有类型 ctx 都有;GraphicsTexture 直接贴图显示) |
已弃用 drawCarModel(...) / playCarSound(...) / setDataFetchMode(...) 仍可用,但新代码请用上面的 RenderManager / SoundManager / 声明字段。drawConnModel(车厢连接件)的两个重载均会抛出 NotImplementedException(未实现异常);drawConnStretchTexture 在此上下文中未提供。两者均不可用。
VehicleWrapper(vehicle)
| 成员 | 说明 |
|---|---|
getStops() / getThisRouteStops() / getNextRouteStops() | 全部 / 本线路 / 下一线路的 Stop 列表 |
getNextStopIndex(stopList, overrunTolerance=0.5): int | 下一停站在列表中的索引;越界容差按米计;全部过完返回 stopList.size() |
isStopsDataFetched(): boolean | 停站数据是否已从服务端取到(SKIP 模式恒 false) |
isRendered() / isCarRendered(cars...) | 整列 / 指定车厢是否在渲染(未被剔除) |
isClientPlayerRiding(): boolean | 本机玩家是否乘坐本列 |
getId(): long · getSiding(): Siding? · getTransportMode(): TransportMode | 唯一 id / 所属侧线 / 交通模式 |
getVehicleId(carIndex): String? · getLength(carIndex) · getWidth(carIndex) · getCarCount() | 资源包车型 id / 车厢长宽 / 车厢数 |
getServiceAcceleration() · getServiceDeceleration() · getMaxManualSpeed() · isManualAllowed() | 侧线配置的加 / 减速度与手动驾驶上限 |
getSpeedMs() / getSpeedKmh() · getNotchLevel() / getNotchPosition() | 速度;档位(-5..5 常规,±7 = 140%,-8 紧急制动)与归一化位置 |
getDoorValue(): double · isDoorOpenable(car, pos[, pos2]) · isDoorOpen(car, pos[, pos2]) | 车门开度 0..1;按车厢内相对坐标查门是否可开 / 已开 |
getPathData() · getRailProgress(carIndex=0) · getRailIndex(progress, roundDown) · getRailSpeed(pathIndex) | 路径与进度(MTR 4 只同步部分路径) |
isCurrentlyManual() · isReversed() · isOnRoute() · getManualToAutomaticTime() | 手动驾驶 / 反向 / 已上线 / 无人驾驶回 ATO 的时限(毫秒) |
getDepartureIndex(): long · getTotalDwellTime() / getElapsedDwellTime() | 发车序号(UTC 零点归零,-1 = 未发 / 人工);停站总时长与已停时长(毫秒,不在站台为 -1;已停时长按服务端同步原样输出) |
getMtrVehicle(): VehicleExtension | MTR 底层载具对象——内部 API 界线以下,慎入 |
晏阳 getDirectionOfDoorOpening(): String | 开门方向记忆:"not" / "left" / "right" / "all",详见 晏阳扩展 |
晏阳 getTransportType(): String | 规范交通类型 id(含 MAGIC 细分),详见 晏阳扩展 |
已弃用:NTE 同名旧接口(getAllPlatforms / trainCars / speed / doorValue / mtrTrain 等)全部保留可用,逐一等价于上表新接口。
Stop(停站)
| 字段 / 方法 | 说明 |
|---|---|
route: SimplifiedRoute? · station: Station? · platform: Platform? | 所属线路 / 车站 / 站台;数据未取到为 null(记得判空) |
destinationName: String · customDestination: String? | 终点显示名与自定义终点原文 |
routeInterchanges: List<RouteInterchange> | 本站换乘线路(不含本线路、不含隐藏线路) |
connectingInterchanges: Map<String, List<RouteInterchange>> | 按「相连车站名」分组的换乘表(同名站合并) |
dwellTimeMillis: long · distance: double | 停站时长(未知为 -1);停点轨道进度(SKIP 模式为 -1) |
isRouteSwitchoverStop: boolean · roundUpRoute: Stop | 是否线路交接站;是则经 roundUpRoute 取「下一线路视角」的同一站副本 |
turnbackPlatform: boolean | 本站是否折返 |
RouteInterchange(换乘线路)
color: int(RGB)· name: String;晏阳 扩展字段 getTransportMode(): TransportMode?、getTransportType(): String(含 MAGIC 细分)、getRouteId(): long(来源线路 id,缺数据为 0)——见 晏阳扩展。
基于原版模型的车辆可用 hideDisplayParts(MTR 4)/ hide_display_cube(MTR 3)隐藏自带的目的地显示方块,改用脚本自绘。
车辆数据获取模式
MTR 4 只同步附近数据,而车辆脚本往往要整趟线路的车站名。JCM 的三级模式(dataFetchMode 字段):
| 模式 | 行为 |
|---|---|
SKIP | MTR 4 默认。不向服务端要数据,本地尽力拼一份停站表——不保证完整。够用于「车一动就响铃」这类只看车速的脚本 |
ALL | 先取整趟停站表,再按表里的 id 逐个取车站 / 线路数据。取到之前表现同 SKIP;即便停站表到手,Stop.station / Stop.route 仍可能为 null——判空 |
MANDATORY | 同 ALL,但数据全部到手前脚本完全不启动。MTR 3 格式默认值(老脚本默认数据即到即用,受不了 null) |
装饰物脚本
挂在 MTR 装饰物(Decoration Object)模型条目上:资源包里选中该模型的装饰物方块被渲染时脚本开始工作。注册用 objects + objectScripts(MTR 3 旧格式写在 eyecandy.json),内容条目上 isScriptControlled: true 可隐藏原模型把画面全交给脚本。
三个回调:ctx 是 EyeCandyScriptContext,eyecandy 是 EyecandyWrapper。
EyeCandyScriptContext(ctx)
| 成员 | 说明 |
|---|---|
getRenderManager(): RenderManager | 基准 = 方块位置 + GUI 位移,含 GUI 旋转 |
getSoundManager(): SoundManager | 基准 = 方块位置;另有便捷的 ctx.playSound(id, volume, pitch) 直接在方块处播放 |
drawModel(model, matrices?) | 画模型(也收 DynamicModelHolder)——等价于 getRenderManager().drawModel(...) |
events(): EyecandyEvents | 取事件:右键 events().onBlockUse.occurred() → 处理 → events().handled() 确认;事件详情 .detail().player() 给出操作者 PlayerEntity(手持刷子时右键不算) |
setOutlineShape(shape) / setCollisionShape(shape) | 自定义轮廓 / 碰撞箱(VoxelShape),玩刷子时无效 |
setDebugInfo(...) | 同其他类型 |
EyecandyWrapper(eyecandy)
getModelId(): String(当前模型条目 id)· getTranslateX/Y/Z(): float(GUI 位移,米)· getRotateX/Y/Z(): float(GUI 旋转,弧度)· pos(): Vector3f(方块 + 位移)· blockPos(): Vector3f · facing(): Direction · facingAngle(): double(朝向角度)· isCrosshairTarget(): boolean(准星指着,可用来弹提示)· getFullBrightness(): boolean · redstoneLevel(): int(0–15)。
已弃用:字段直读形式(eyecandy.translateX、eyecandy.prefabId 等)仍可用。
用 TickableSoundInstance.create(...) + setRepeatable(true) + play() 挂循环音乐,右键 5 秒内 setSoundVolume(0.25)、到时回 1.0,dispose 里 stop——完整代码见上游 sounds 文档。
电梯脚本
挂在电梯轿厢样式(材质)条目上,玩家用电梯刷新器选中该样式后生效。注册用 lifts + liftScripts,样式条目上 isScriptControlled: true 隐藏原模型。
三个回调:ctx 是 LiftScriptContext,lift 是 LiftWrapper。
LiftScriptContext(ctx)
getRenderManager(): RenderManager(基准 = 电梯基础位置 + 偏移,随电梯旋转)· getSoundManager(): SoundManager(同基准)· setDebugInfo(...)。
LiftWrapper(lift)与 Floor
| 成员 | 说明 |
|---|---|
getMtrLift(): Lift · getId(): long | MTR 底层对象与唯一 id |
getWidth() / getHeight() / getDepth() | 轿厢尺寸(米,刷新器配置) |
getOffset(): Vector3f · getPos() / getRawPos() | 偏移;基础位置 + 偏移 / 基础位置 |
getAngleDegrees() / getAngleRadians() · isDoubleSided() | 朝向角与双开门 |
getDoorValue(): float · getDirection(): int | 门开度 0..1;运行方向:-1 下行 / 0 静止 / 1 上行 |
isDoorway1Openable() / isDoorway1Open() / isDoorway2... | 前 / 后门可开与已开 |
isClientPlayerRiding(): boolean · shouldRender(): boolean | 乘坐中;未被遮挡剔除 |
getFloors(): List<Floor> | 停靠楼层,每项 getIndex()(0 为最底)· getNumber() / getDescription()(层轨刷子配置)· getPos() · isCurrentFloor() / isTargetFloor() |
PIDS 脚本
本系统与 PIDS Layout Editor / PIDS Modularization / PIDS JSON(EpicPuppy)及 MTR 4 早期测试版的信息屏自定义互不兼容,格式无法互导——入门门槛更高,但能做更复杂的版面。
在 joban_custom_resources.json 的 pids_images 数组里,预设对象带上 scriptFiles 或 scriptTexts 即为脚本预设(scriptInput / scriptRateLimit 含义同他类;Yanyang 也接受 scriptLocations 混入 C++,见 混合条目):
{
"pids_images": [
{
"id": "pids_demo",
"name": "Demo Scripted PIDS",
"scriptFiles": ["jsblock:scripts/pids_demo.js"],
"scriptInput": { "theme": "dark" },
"scriptRateLimit": 0.75
}
]
}
预设字段 outOfBound: boolean(上游 v2.3.0-prerelease.3):屏幕移出相机视界时仍继续渲染——投影仪预设(pids_projector)须臾不可停者所必需。
PIDS 上画的是世界里的多边形,不是 2D 画布——元素可以溢出屏幕边界,界内裁剪是脚本自己的责任(确需复杂裁剪 / 图像处理时用 GraphicsTexture 画好再贴)。绘制顺序即层次:后画的盖先画的;默认每次绘制沿 z 方向平移一小步(0.0002)防 z-fighting,可用 ctx.setAutoZOrdering(false) 关掉改手动 zOrder()(ctx.setZOrderStep(m) 调步长)。
PIDSScriptContext(ctx)与 PIDSBlockEntity(pids)
ctx.getRenderManager() / ctx.getSoundManager()(基准 = 方块位置)· ctx.setAutoZOrdering(b) · ctx.setZOrderStep(m) · ctx.setDebugInfo(...)。
| pids 成员 | 说明 |
|---|---|
type: String | 屏型:rv_pids / rv_pids_sil_1 / rv_pids_sil_2 / lcd_pids / pids_projector / pids_1a |
width / height / rows | 可用屏幕宽高与到站行数 |
isRowHidden(i) / getCustomMessage(i) | 第 i 行是否被方块配置隐藏;行自定义文案(未设为空串) |
getTargetPlatformIds() · isPlatformAutoDetected() · isPlatformNumberHidden() | 侦测到的站台 id 列表;是否自动侦测;站台号是否隐藏 |
blockPos() · isKeyBlock() · station(): Station? | 方块坐标;是否双面屏的「键」块;所在车站(不在站内或客户端未知为 null) |
arrivals(): ArrivalEntries | 到站数据,见下 |
ArrivalEntries 与 ArrivalEntry
ArrivalEntries:get(i): ArrivalEntry?(每站台最多取 10 条到站)· length(): int(到站总条数,上游 v2.3.0-prerelease.3)· mixedCarLength(): boolean · platforms(): List<Platform> · forEach(consumer) / forEach(platformId, consumer)(逐条遍历,免手动判空)。
ArrivalEntry:destination(): String · arrivalTime() / departureTime()(Unix 毫秒,new Date(v) 转 JS 日期)· arrived() / departed() · deviation(): long(相对图定:正晚点 / 负早点)· realtime(): boolean(图定 or 实时推算)· departureIndex(): long · terminating(): boolean(本站终到)· route(): SimplifiedRoute? / routeId() / routeName() / routeNumber() / routeColor() / circularState() · platform(): Platform? / platformId() / platformName() · carCount() / cars(): List<CarDetails>。
Text / Texture / Rect(绘制三件套)
三者都是「创建 → 链式配置 → draw(ctx)」的流式对象;Rect 等于贴图硬编码为纯白 PNG 的 Texture:
| 通用 | 说明 |
|---|---|
create() / create(comment) | 新建(注释仅便于阅读代码) |
pos(x, y) · size(w, h) | 位置与尺寸(屏幕像素单位) |
color(rgb) | 染色(RGB 整数) |
naturalLight() · renderType(str) · matrices(m) · zOrder(n) · draw(ctx) | 按世界光照取亮度(默认全亮);渲染类型(同 QuadDrawCall);附加变换;手动 z 序(关自动 z 后生效);提交绘制 |
| Text 专属 | 说明 |
|---|---|
text(str) · scale(i) · fontMC() / font(id) | 内容;整体缩放(默认 1,配合 size 要自行换算);原生字体 / 资源字体(默认 mtr:mtr,MTR 设置「自定义字体」关闭时无效) |
leftAlign() / centerAlign() / rightAlign() | 对齐(默认左) |
shadowed() · italic() · bold() · lineHeight(factor) | 阴影 / 斜体 / 粗体;行距因子(换行用,默认 1 = 9 单位) |
stretchXY() / scaleXY() / wrapText() / marquee() / marquee(durationTick) / withMarqueeProgress(p) | 溢出策略四选一:溢出轴拉伸 / 等比缩放 / 换行 / 跑马灯(可指定周期或直接给进度 0..1) |
measureWidth(): int | 按当前配置量宽——返回 int,链式到此为止 |
Texture 专属:texture(id)(PNG 或 mcmeta)、uv(u1, v1, u2, v2)(0–1)。
PIDS 也能贴 GraphicsTexture:create 里 new GraphicsTexture(pids.width, pids.height),render 里画完 upload() 再 Texture.create().texture(state.tex.identifier).size(pids.width, pids.height).draw(ctx),dispose 里 close()——对文字排版无能为力时的兜底方案,代价是性能与内存。
不需要脚本?pids_images 里只写 background / color / fonts / showWeather / showClock / hidePlatform / hideRow / topPadding / textOverflowMode 等 JSON 字段即为「JSON 预设」——简单改脸够用,且与脚本预设二选一不可混写。
PSD 脚本(屏蔽门)
为屏蔽门玻璃与顶部面板提供脚本。在 mtr_custom_resources.json 新增 psdScripts 类别——形式同 vehicleScripts(scriptLocations 支持 JS 与 C++ 混排、逐条目语言判定),新增 type 字段选择绑定表面:
"psdScripts": [
{
"id": "psd_glass_pids",
"type": "glass",
"scriptLocations": [
"mtr:my_pack/js/psd_glass_pids/main.js",
"mtr:my_pack/native/psd_glass_pids"
],
"dataFetchMode": "ALL"
}
]
游戏内用新物品「屏蔽门脚本编辑器」(与 MTR 的「刷」在同一创造分类)对任意屏蔽门玻璃(含端部玻璃、半高闸门 APG 玻璃;MTR、天津地铁等一切基于 MTR PSD 类的模组)或屏蔽门顶部面板右键,在弹出界面中选择该类型的条目或任一脚本化 PIDS 预设(PIDS 脚本原样运行,画布自动适配 PSD 表面);「无脚本」解除绑定。绑定保存在服务端并同步给同维度所有玩家。
- 玻璃:衬上不透明底板——站台一侧黑色(真实 PIDS 的深色显示屏观感),轨道一侧纯白(车内乘客只见白色面板,看不到镜像内容)。脚本画布在站台一侧、夹于玻璃之内,自动裁剪到玻璃的透明窗口(按资源包模型与贴图 alpha 推导;门框、拉筋等不透明部分照常显示在前并遮挡内容);两门之间的整段连续玻璃共用一块画布,整柱两格高的玻璃上下合并为一。画布尺寸随窗口自适应(每方块 96 像素)。
- 顶部面板:同一门区上方的整段 psd_top 共用一块画布,该段不再显示原有箭头 / 线路图——内容完全由脚本控制;其余段保持原样。画布每方块宽 96 像素(左右各留 0.125/16 边距)× 高 42 像素。
连续玻璃段或顶板段不超过 64 格时仍共用一个完整画布,即使跨越坐标网格边界也不拆分。超过 64 格的长段按画布向右方向的有符号世界坐标切为对齐的 64 格区间(首尾区间可更短);编辑器绑定、解除绑定及顶板原有内容的隐藏都只作用于同一个子段。
绘制 API 与 PIDS 完全一致(Text / Texture / Rect + ctx.draw,画布原点为可见面左上角)。数据 API(JS 的 psd 参数 / C++ 的 mtr::Psd,完整访问器见 数据快照 · Psd):
| JS | 说明 |
|---|---|
psd.type() / psd.width() / psd.height() | 绑定表面("glass" / "top")与画布尺寸 |
psd.platform() | 站台:编号 / 名称(number() / name())、车站、停站时间 |
psd.routes() | 经停线路全量信息;dataFetchMode: "ALL" 时每条线路含完整车站列表(换乘 / 出口) |
psd.arrivals() | 到站列车:终点站、线路、车厢数、倒计时(arrivalCountdown())、将开启的门编号 doorNumbers()、实时 / 终到 / 环线状态 |
psd.stop() | 完整车站信息(ABI 13):名称、颜色(ARGB + 十六进制)、票价区 1–3、停靠本站的全部线路、邻站线路(按站名分组)、物理连通的站名、出口及去向、站台编号与总数——无站台 / 车站时返回 null。C++ 侧:has_station() + mtr::PsdStation 访问器(见 数据快照 · Psd) |
psd.doors() / psd.doorValue() / psd.isDoorOpen() | 门单元列表(1 起编号、实时开门 / 上锁)与整墙开门量 |
C++ 侧为第五类基类 mtr::PsdScript<State>(MTR_REGISTER_PSD_SCRIPT),数据快照 JcmPsdSnapshot(ABI 12;车站条目自 ABI 13 起),见 native/examples/psd_glass.cpp。
DisplayHelper(内置多屏助手)
车载多屏的现成方案:include(Resources.id("mtrsteamloco:scripts/display_helper.js")) 后——
let slotCfg = {
"version": 1,
"texSize": [2048, 1024],
"slots": [
{ "name": "lcd_north", "texArea": [0, 0, 2048, 274],
"pos": [[[-0.59, 2.125, -1.75], [-0.755, 2.03, -1.75],
[-0.755, 2.03, -3.25], [-0.59, 2.125, -3.25]]],
"offsets": [[0, 0, -5], [0, 0, 0], [0, 0, 5], [0, 0, 10]] }
]
};
let dhBase = new DisplayHelper(slotCfg);
function create(ctx, state, vehicle) {
state.rl = new RateLimit(0.05);
state.dh = dhBase.create(); // 实例化并绑定纹理
}
function render(ctx, state, vehicle) {
if (state.rl.shouldUpdate()) {
let g = state.dh.graphicsFor("lcd_north"); // 槽位画布(自动平移)
// ... AWT 绘制 ...
state.dh.upload();
}
for (let car of ctx.getMyCars())
ctx.getCarRenderManager(car).drawModel(state.dh.model, null); // 每帧都要画
}
function dispose(ctx, state, vehicle) {
state.dh.close();
}
- 所有槽位共享一张
texSize大纹理;texArea指定槽位用哪块区域(x, y, w, h)。 pos是三维数组:每屏四角坐标(左上 → 左下 → 右下 → 右上),单位米(Blockbench 的 1/16 单位要除 16),原点在列车中心、地板高度,+X 左 / +Y 上 / +Z 后。offsets复制同内容屏幕(门上小屏连排时省纹理);graphicsFor(name)拿自动平移好的画布,graphics()拿整张。- 上传放
RateLimit里、画模型放外面——模型必须每帧提交。
接下来:晏阳扩展 API —— 混合 JS+C++ 条目、开门方向记忆、交通类型细分与直写像素上传。