脚本模型与生命周期

五类脚本

包含 <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 一致。
  • 三个生命周期函数全部可选 override(基类提供空实现),参数签名固定。
  • 一个翻译单元注册一个脚本;多个脚本就分成多个模块库。

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 时能读到什么;绘制与画布 —— 怎么画出来。