JavaScript 脚本入门
JCM 自 v2 起允许资源包用 JavaScript 控制车辆、装饰物、电梯的渲染与声音,以及 PIDS 屏幕的显示内容。本页覆盖注册、生命周期、调试与守则;API 细节见后续三页。
什么是 JS 脚本
脚本是一段随资源包分发的 JavaScript 代码,由游戏在加载资源时解析,并在运行期间按帧调用,用来在原版机制之外渲染模型、文字、贴图,或播放声音。
- 引擎是 Rhino——纯 Java 实现的 JavaScript 引擎。你只需要掌握语言本身(变量、函数、条件逻辑)与
Date、Map等内建对象;浏览器环境(HTML/CSS/DOM)完全用不上,IDE 的网页类补全不要照单全收。 - Rhino 提供 LiveConnect 互操作:脚本可以直接调用 Java 类与方法。JCM 的许多内建 API 本身就是暴露给脚本的 Java 类。
- JavaScript 与 Java 是两门不同的语言;会 Java 不是写脚本的前提,但理解 Java 的类型系统有助于避免互操作陷阱(见撰写守则)。
Yanyang 分支在 JS 之外增加了 C++ 原生脚本,两者共用同一套生命周期与渲染管线,甚至可以在同一条声明里混用(见 晏阳扩展 API)。简单的一次性逻辑用 JS 迭代最快;高分辨率逐像素重绘、复杂计算交给 C++。
四类脚本
每类脚本面向一个专门场景,可用的全局对象与回调略有不同:
| 类型 | 控制对象 | 典型用途 |
|---|---|---|
| 车辆脚本 | MTR 载具(整列) | 车内屏、目的地显示、报站声音、电机音 |
| 装饰物脚本 | MTR 装饰物方块 | 动态标识、闸机灯、互动装置(右键事件) |
| 电梯脚本 | MTR 电梯(轿厢样式) | 楼层显示器、到站钟 |
| PIDS 脚本 | JCM 到站屏(预设) | 自定义到站显示版式、动态纹理 |
同一份脚本代码只解析一次;游戏里每个具体实例(每列车、每块屏)各自持有独立的 state 并分别回调。
注册脚本
脚本随资源包加载。先保证资源包能被游戏识别——在包根目录放一个 pack.mcmeta:
{
"pack_format": 13,
"description": "你的资源包描述"
}
pack_format 对 JCM 脚本不起约束作用(不必随 MC 版本逐个改),但必须是合法 JSON 数字。
声明条目
车辆 / 装饰物 / 电梯脚本声明在 mtr_custom_resources.json(放在 assets/mtr/ 下);PIDS 脚本声明在 joban_custom_resources.json(放在 assets/jsblock/ 下)的 pids_images 数组里。MTR 4 格式把「内容」与「脚本」分成两个数组,内容条目通过 scriptId 引用脚本条目——同一份脚本可被多个车辆复用:
{
"vehicles": [
{ "id": "demo_sp1900", "name": "Demo SP1900", "scriptId": "demo_lcd" }
],
"vehicleScripts": [
{
"id": "demo_lcd",
"scriptLocations": ["demo:js/sp1900_lcd.js"],
"prependExpressions": ["print('Hello world')"],
"input": { "era": 2004 },
"rateLimit": 0.75,
"dataFetchMode": "ALL"
}
]
}
| 字段 | 含义 |
|---|---|
scriptLocations | JS 文件位置数组(可多个)。自 JCM v2.3 起多数 API 也可直接收字符串 id。无扩展名的条目按 Yanyang 规则可能被识别为 C++ 模块——见 混合条目 |
prependExpressions | 直接内嵌的 JS 代码,在 scriptLocations 之前执行 |
input | 任意 JSON 对象,脚本内以 SCRIPT_INPUT 只读访问 |
rateLimit | 脚本调用频率下限(秒)。低频逻辑调高它可省性能 |
dataFetchMode | 仅车辆脚本:SKIP(默认,不取数据)/ ALL / MANDATORY,详见 数据获取模式 |
forceLoad | 同包内没有内容引用该脚本时 JCM 默认拒绝加载;置 true 强制加载(跨包共享脚本条目时用) |
isScriptControlled | 装饰物 / 电梯的内容条目字段:隐藏原模型,把渲染完全交给脚本 |
三类脚本的数组名:vehicleScripts(车辆,内容在 vehicles)、objectScripts(装饰物,内容在 objects)、liftScripts(电梯,内容在 lifts)。PIDS 没有 scriptId 一环——直接在 pids_images 的预设对象上写 scriptFiles / scriptTexts 即被视作脚本预设(键名沿用 JCM 惯例,见 PIDS 脚本)。
所有字段都可省略,但脚本条目至少要有 scriptLocations 或 prependExpressions 之一才会被解析。Yanyang 还接受 NTE 文档示例里的 script_input 写法(与 scriptInput 等价),照老文档抄的包不会失效。
MTR 3 / NTE 旧格式
MTR 3 时代的声明继续有效:车辆写在 custom_trains(字段名 script_files / script_texts / script_input / script_rate_limit),装饰物写在 eyecandy.json。旧格式车辆默认按 MANDATORY 模式取数。
生命周期
解析阶段
游戏重载资源(启动或按 F3+T)时,每个脚本在主线程执行一遍顶层代码,随后 JCM 记录其中的 create / render / dispose 函数供运行期调用。模型加载、纹理读取等一次性工作放这里最合适——但重操作会直接拖慢整个资源重载,能省则省。
运行阶段
let displaySpeed = 1; // 解析阶段执行
function create(ctx, state, vehicle) {
state.speed = 0.75; // 每个实例第一次渲染前执行一次
}
function render(ctx, state, vehicle) {
state.speed += Timing.delta(); // 每帧(尽力而为)执行
}
function dispose(ctx, state, vehicle) { } // 实例退场时执行
print(displaySpeed); // 解析阶段执行,输出 1
| 参数 | 含义 |
|---|---|
ctx | 上下文:提交绘制、播放声音的「动作」入口,各类型不同(车辆是 VehicleScriptContext……) |
state | 普通 JS 对象,逐实例持久——两列车各有一份,互不串扰;初始为 {} |
wrapper | 只读信息源:车速、门值、屏尺寸、到站列表……各类型不同 |
- 异步执行:运行期回调发生在专用的 4 条后台线程上(解析期仍在主线程)。别用
Thread.sleep或死循环占住线程——同一脚本条目固定绑在同一条线程上,别的脚本可能也在排队。 - 逐帧但不保证逐帧:
render以每帧一次为目标,但上一帧没跑完就不会重入;帧率波动时别假设「每秒固定 N 次」。需要按真实时间推进的量用Timing.delta()缩放。 - 运行时错误:日志以
[Scripting] Error executing script!开头报错并指明文件行号;随后整份脚本暂停 4 秒再试。游戏内可开调试覆盖层直接在屏幕上看错。 - 无 JS 线程保证:所有回调函数之外看不到彼此的局部变量——跨调用共享的数据放
state。
create / render / dispose + 逐实例状态正是 C++ 原生脚本沿用的模型(见 脚本模型与生命周期),两种语言可以逐段对译迁移。
开发辅助
- 脚本调试覆盖层:Mod Menu(Fabric)或模组配置界面(Forge)→ JCM → Configure → 勾选 Script debug mode。屏幕左上角列出每个脚本条目与其实例,实例按耗时标蓝(正常)/ 黄(偏慢)/ 红(慢)。单次执行超过 5 秒会标记为无响应,再等 5 秒直接终止。按
[/]在 [MTR] 与 [JCM] 两个来源之间切换。 - 快速重载:调试模式开启后,Ctrl+R 重载 MTR 侧脚本(车辆 / 装饰物 / 电梯),Shift+R 重载 JCM 侧脚本(PIDS 预设)——不需要完整的 F3+T。三个键实际绑定的是「冲刺」「潜行」「JCM 重载」键,可在按键设置改。
- 日志:
print(...)与console.log(...)输出到游戏控制台;解析期错误只会在聊天栏提示一句,细节看启动器的logs/latest.log。 - IDE 补全:VSCode 有第三方扩展 MTR Scripting Tools(Visual Studio Marketplace / Open VSX 可搜到)为 JCM 脚本类型提供智能提示;问题请反馈给扩展作者。
撰写守则
语言层面
- 严格模式:不声明就赋值(
x = 1)会直接报错。全局用var或let,函数内一律let。 - const 是函数作用域:Rhino 里的
const不像现代 JS 那样按块作用域——在for循环体里声明会在第二次迭代报「重复声明」。需要块作用域语义时用let。 - JS 字符串 vs Java 字符串:MTR/JCM 返回的多是 Java
String。取长度用str.length();想用 JS 的.length属性,先拼一个空串转换:(""+str).length。JavaList可用list[0]与for (x of list),但 JS 数组方法(map等)不可用。 - 可空值:返回类型带
?的方法可能返回null。用if (x != null)、三元、可选链x?.getName()与空值合并x?.length() ?? 0处理;对null取属性或调方法会立刻抛错。 - 重载解析:Java 方法按参数类型挑重载,数字默认偏好
double;拿不准时可用签名后缀直呼:graphics["drawString(java.lang.String,int,int)"]("文字", 15, 60)、new Color["(int,int,int)"](40, 68, 130)。报「method not found」时先怀疑参数类型。 - 特性范围:Rhino 不支持所有最新 JS 特性,详见 Mozilla 的兼容表。
行为层面
- 别阻塞、别死循环:没有「等一会儿再继续」这种写法——把动作计时后放到后续回调里做;耗时操作(网络、大文件)交给
BackgroundWorker。死循环约 10 秒后被强制终止并按运行时错误处理。 - 内部 API 界线:本文档没写的方法 / 字段、
MTRClientData的内部结构、VehicleWrapper.getMtrVehicle()以下的深层对象都属内部实现,版本间可能直接变;公开分发的资源包不要依赖它们。 - 类访问白名单:默认只放行这些包——
com.lx862.jcm.mod.scripting、com.lx862.mtrscripting.util、com.lx862.mtrscripting.wrapper、java.lang、java.awt、java.text、java.math、java.util、java.time、javax.imageio、sun.java2d、sun.font、sun.awt、org.mtr。玩家可在设置里解除,但分发的包若要求解除限制,务必在说明里醒目告知。 - state 变量加前缀:多个脚本可挂在同一实例上,
state是共享的——用state.myfx_ratelimit这类前缀避免撞名。 - 性能有预算:单帧 16 ms 看似够用,但玩家可能同时跑几十个脚本实例。逐帧重绘大纹理前先算总账:一列 8 节、每节 10 块屏,每帧全量
upload()就是 80 次纹理搬运——降分辨率、跨门复用、降低更新频率(RateLimit)都是正道。客户端脚本能做得「够好」即可,别追求 1:1 复刻。 - 脚本不是安全边界:脚本纯客户端运行,任何人都能本地改写——任何权限判断(如
hasPermissionLevel)都挡不住有心人,不要依赖它做访问控制。
与 NTE / MTR 3 的关系
JCM 脚本体系受 Nemo Transit Expansion(NTE)启发,是 NTE 脚本功能在 MTR 4 上的非官方延续,保留了大量同名或等价的兼容 API(文档中标「已弃用」的部分)。差异要点:
- 旧格式注册照常工作;MTR 3 与 MTR 4 双栖的脚本可用
Resources.getAddonVersion("mtr")(旧脚本里的Resources.getMTRVersion())判断版本后分支。 - NTE 里返回
void的Vector3f运算现在返回自身,支持链式调用;Resources.manager()在 MTR 4 上返回null,相关函数也接受null。 - ANTE 风格的兼容类(
SoundHelper/TickableSound/GlobalRegister/ComponentUtil/ 全局asJavaArray())自 v2.3 起可用,方法签名与 ANTE 一致。 - MTR 4 只把附近的车站 / 线路数据同步给客户端——脚本查不到远处数据不是 bug,是刻意省流量;车辆脚本的数据获取三模式见 数据获取模式。
接下来:JS 公共 API —— 所有脚本共用的类与函数;JS 脚本类型 —— 四类脚本各自的 ctx 与 wrapper。