腳本模型與生命週期

五類腳本

包含 <mtr/script.hpp>,繼承對應基類並用註冊巨集匯出。基類樣板參數是逐實例狀態型別:

資源基類註冊巨集資料包裝器
車輛mtr::VehicleScript<State>MTR_REGISTER_VEHICLE_SCRIPTmtr::Vehicle
PIDSmtr::PidsScript<State>MTR_REGISTER_PIDS_SCRIPTmtr::Pids
屏蔽門mtr::PsdScript<State>MTR_REGISTER_PSD_SCRIPTmtr::Psd
電梯mtr::LiftScript<State>MTR_REGISTER_LIFT_SCRIPTmtr::Lift
裝飾mtr::EyecandyScript<State>MTR_REGISTER_EYECANDY_SCRIPTmtr::EyeCandy

一個車輛腳本骨架(其餘三類只換基類與參數型別):

#include <mtr/script.hpp>

struct MyVehicleState {
    mtr::CycleTracker dest{"KOWLOON|TSUEN WAN"};   // 你自己的任意 C++ 型別
    mtr::Api::Object tracker;                        // Java 物件控制代碼
    std::unique_ptr<mtr::GraphicsTexture> lcd;    // 持有的資源
};

struct MyVehicleScript : mtr::VehicleScript<MyVehicleState> {
    static constexpr auto ID = "demo:vehicle";

    void create(mtr::VehicleContext& ctx, MyVehicleState& state,
                const mtr::Vehicle& vehicle) override {
        // 首幀前呼叫一次:建立資源、註冊回呼
    }

    void render(mtr::VehicleContext& ctx, MyVehicleState& state,
                const mtr::Vehicle& vehicle) override {
        // 每幀呼叫:讀取快照、寫入繪製記錄
    }

    void dispose(mtr::VehicleContext& ctx, MyVehicleState& state,
                 const mtr::Vehicle& vehicle) override {
        // 實例銷毀前呼叫:釋放資源
    }
};

MTR_REGISTER_VEHICLE_SCRIPT(MyVehicleScript)
  • ID 是腳本的命名空間 id(如 demo:vehicle),必須與資源包 mtr_custom_resources.json 裡宣告條目的 id 一致。
  • 三個生命週期函式全部可選擇性覆寫(基類提供空實作),參數簽章固定。
  • 一個轉譯單元註冊一個腳本;多個腳本就分成多個模組程式庫。

State:逐實例狀態

每個腳本實例(每輛車、每個 PIDS 方塊實體……)各有一塊獨立的 State,由宿主配置並建構:

  • 宿主按 mtrStateSize()(由註冊巨集從 sizeof(State) 生成)配置狀態區塊,在首次使用前經 mtrInit() 用 placement-new 建構——不要假設零填充記憶體可用(std::string 等非平凡型別在零記憶體上是未定義行為)。
  • 狀態區塊內容跨幀持久:把紋理、模型控制代碼、回呼、快取放在這裡。
  • 腳本類別的成員(你在 MyVehicleScript 裡寫的欄位)則由該模組的全部實例共用——只放唯讀常數與全域設定。
  • dispose 傳回後狀態即被解構;實例從用戶端消失約 5 秒後回收、資源包重新載入(F3+T)時全部銷毀。
提示

背景回呼物件(mtr::Api::Callback)必須保存在 State 裡直到銷毀——Java 側可能在任意時刻呼叫它。

生命週期時序

配置 + 建構

宿主為實例配置狀態區塊並呼叫 mtrInit() → State() 建構。

create

實例首次渲染前呼叫一次。create 期間提交的繪製記錄會被保留到第一幀 render 複用——一次性靜態內容(形狀、常駐貼圖)放在這裡,render 只畫動態部分。資源取得、Api 呼叫、回呼註冊也適合放在這裡。

render(每幀)

讀取快照 → 計算 → 把繪製記錄寫進幀記憶體區。快照與包裝器只在本次呼叫內有效;宿主在 render 傳回後立即讀取輸出並回收幀。實例不可見期間 render 不會被呼叫,但狀態原樣保留。

dispose

實例回收或重新載入時呼叫。在這裡關閉紋理 / 模型等持有的資源;State 解構緊隨其後。即使 dispose 擲出例外,狀態也會被安全解構。

與 JS 的對應

這套 create / render / dispose + 逐實例 state 正是 JCM 為 JS 腳本提供的模型(每個 ScriptInstance 一個 state 物件)的原生映射,遷移 JS 腳本時邏輯可以逐段對譯。

執行緒模型

  • 宿主為每個模組在單一背景執行緒上序列呼叫其全部生命週期函式(對應 JCM 對 JS 腳本的逐腳本單執行緒執行器)——同一實例的 create / render / dispose 永遠不會並行。
  • 但 render 與非同步回呼可以並行(比如 BackgroundWorker 裡提交的任務此刻在別的執行緒跑):腳本自己負責同步共用可變狀態。
  • 資源包重新載入會先停止回呼、等待進行中的呼叫傳回,再銷毀狀態並卸載程式碼。

崩潰隔離

註冊巨集把每個匯出函式包進崩潰防護(crash_guard.hpp):

  • 分段錯誤等硬體故障 → 只停用該模組,遊戲行程繼續執行;latest.log 留下一行 native script faulted … module disabled。
  • 未被捕捉的 C++ 例外同樣被防護攔截——但請自行 try/catch 以保證語意清晰。
  • POSIX 訊號不會被 longjmp 式的「復原」:越過訊號處理常式的跳轉比崩潰本身更危險,模組直接下線是刻意設計。

傳回碼

mtrCreate / mtrRender / mtrDispose 傳回含義
0成功
-1空入參
-2ABI 版本不符(宿主與模組協商失敗)
-3資源類型不符(例如車輛模組被當作 PIDS 呼叫)

這些傳回碼由 SDK 生成,一般腳本程式碼無需關心;若你在寫自訂宿主 / 測試驅動程式,按上表解釋即可(詳見 C ABI 與宿主服務)。


接下來:資料快照 —— render 時能讀到什麼;繪製與畫布 —— 怎麼畫出來。