JS 脚本类型

车辆脚本

一个「车辆」在 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(): VehicleExtensionMTR 底层载具对象——内部 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 字段):

模式行为
SKIPMTR 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(): longMTR 底层对象与唯一 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

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++ 条目、开门方向记忆、交通类型细分与直写像素上传。