JavaScript 之腳入門

JS 之腳,何物也

腳本者,隨資源包而散之 JavaScript 碼也。遊戲載資源之際為之解析,運行之期按幀而呼之,所以於原版機制之外,渲染模型、文字、貼圖,或播聲音也。

  • 其引擎曰 Rhino——純以 Java 所成之 JavaScript 引擎也。君惟須通語言之本(變數、函數、條件之邏輯),與 Date、Map 諸內建之物;瀏覽器之境(HTML/CSS/DOM)一無所用,IDE 網頁類之補全,勿照單全收也。
  • Rhino 具 LiveConnect 互通之道:腳本得徑呼 Java 之類與方法。JCM 諸內建 API,多即曝予腳本之 Java 類也。
  • JavaScript 與 Java,二語也,非一物;諳 Java 非撰腳本之先決,然解 Java 類型之制,有助於避互通之阱(見撰寫守則)。
與 C++ 原生腳本之關係

Yanyang 之支,於 JS 之外增C++ 原生腳本;二者共用同一生命週期與渲染之管線,甚且得雜處於同一宣告之條(見 晏陽增補)。簡單一次性之邏輯,以 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 四代之式,「內容」與「腳本」分為二陣列;內容之條,經 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"
        }
    ]
}
欄位釋義
scriptLocationsJS 文件所在之陣列(可多條)。自 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 三代之宣告,依然有效:車輛書於 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唯讀之訊源:車速也,門值也,屏之尺寸也,到站之列也……各類各異
  • 非同步之行:運行期之回呼,行於專用後臺線程凡四之上(解析之期,仍在主線程)。勿以 Thread.sleep 或死循環佔其線程——同一腳本之條,恆繫於同一線程,他腳本或亦列於佇列之中。
  • 逐幀而不保其逐幀:render 以每幀一遭為則;前幀未竟,則不入焉。幀率起伏之際,勿設「每秒恆 N 遭」。須按真時而進之量,以 Timing.delta() 縮放之。
  • 運行之誤:日誌以 [Scripting] Error executing script! 起首而誌其誤,並指文件行號;既而全份腳本停四秒復試。遊戲之內,可啟除錯覆層,逕於屏上觀其誤。
  • JS 線程無所保:諸回呼函數之外,彼此局部之變數不相見——跨呼共享之數據,納諸 state。
與 C++ 生命週期之應

create / render / dispose 加逐實例之狀態,即 C++ 原生腳本所沿之模型也(見 腳本體例與生命週期);二語得逐段對譯而遷焉。

開發之輔

  • 腳本除錯之覆層:Mod Menu(Fabric)或模組配置之界面(Forge)→ JCM → Configure → 勾 Script debug mode。屏之左上,列每腳本之條與其實例;實例按其所費,藍者(如常)也,黃者(偏遲)也,紅者(遲甚)也。一遭逾五秒,則標為無應;更候五秒,則逕止之。按 [ / ],於 [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 字串與 Java 字串:MTR/JCM 所返者,多 Java String 也。取其長,用 str.length();欲用 JS .length 之欄,先拼空串以化之:(""+str).length。Java List 可用 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。死循環約十秒,則遭強止,依運行之誤而處之。
  • 內部 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,視若足用;然玩家或同時行數十腳本之實例。逐幀重繪巨紋理之前,先算其總帳:一列八節,每節十屏,每幀全量 upload(),即紋理之搬凡八十遭——降解析度、跨門復用、降低更新之頻(RateLimit),皆正道也。客戶端之腳,得其「足矣」即止,勿求 1:1 之複刻。
  • 腳本非安全之界:腳本純行於客戶端,人人得於本地改寫之——凡權限之判(如 hasPermissionLevel),皆莫能禦有心之人;勿恃之以為訪問之制。

與 NTE / MTR 3 之淵源

JCM 腳本之體,受 Nemo Transit Expansion(NTE)之啟,乃 NTE 腳本之能於 MTR 四代之上非官方之續也;同名等價之相容 API,存者甚夥(檔中標「已廢」者是也)。其異之要曰:

  • 舊格之註冊,行如常;MTR 三代與四代雙棲之腳本,得以 Resources.getAddonVersion("mtr")(舊腳本之 Resources.getMTRVersion())辨其版本而後分支。
  • NTE 之 Vector3f 運算,昔返 void 者,今返其自身,得鏈式而呼;Resources.manager() 於 MTR 四代返 null,相關之函亦受 null 焉。
  • ANTE 風格相容之類(SoundHelper / TickableSound / GlobalRegister / ComponentUtil / 全域 asJavaArray())自 v2.3 而可用,法之簽名與 ANTE 一致。
  • MTR 四代惟以鄰近之車站 / 線路數據同步於客戶端——腳本查不得遠處之數據,非 bug 也,乃刻意省其流也;車輛腳本數據獲取之三式,見 數據獲取之式。

繼之:JS 公器 —— 諸腳本共用之類與函;JS 之腳類型 —— 四類腳本各自之 ctx 與 wrapper。