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。