Script model and lifecycle

The five script kinds

Include <mtr/script.hpp>, derive from the matching base class, and export it with a registration macro. The base class's template argument is the per-instance state type:

ResourceBase classRegistration macroData wrapper
Vehiclemtr::VehicleScript<State>MTR_REGISTER_VEHICLE_SCRIPTmtr::Vehicle
PIDSmtr::PidsScript<State>MTR_REGISTER_PIDS_SCRIPTmtr::Pids
PSDmtr::PsdScript<State>MTR_REGISTER_PSD_SCRIPTmtr::Psd
Liftmtr::LiftScript<State>MTR_REGISTER_LIFT_SCRIPTmtr::Lift
Eyecandymtr::EyecandyScript<State>MTR_REGISTER_EYECANDY_SCRIPTmtr::EyeCandy

A vehicle script skeleton (the other three kinds just swap the base class and parameter types):

#include <mtr/script.hpp>

struct MyVehicleState {
    mtr::CycleTracker dest{"KOWLOON|TSUEN WAN"};   // Any C++ type of your own
    mtr::Api::Object tracker;                        // Java object handle
    std::unique_ptr<mtr::GraphicsTexture> lcd;    // An owned resource
};

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

    void create(mtr::VehicleContext& ctx, MyVehicleState& state,
                const mtr::Vehicle& vehicle) override {
        // Called once before the first frame: create resources, register callbacks
    }

    void render(mtr::VehicleContext& ctx, MyVehicleState& state,
                const mtr::Vehicle& vehicle) override {
        // Called every frame: read snapshots, write draw records
    }

    void dispose(mtr::VehicleContext& ctx, MyVehicleState& state,
                 const mtr::Vehicle& vehicle) override {
        // Called before the instance is destroyed: release resources
    }
};

MTR_REGISTER_VEHICLE_SCRIPT(MyVehicleScript)
  • ID is the script's namespaced id (e.g. demo:vehicle) and must match the id of its declaration entry in the resource pack's mtr_custom_resources.json.
  • All three lifecycle functions are optional overrides (the base class provides empty implementations); the parameter signatures are fixed.
  • One translation unit registers one script; split multiple scripts across multiple module libraries.

State: per-instance state

Every script instance (each vehicle, each PIDS block entity, โ€ฆ) gets its own independent State block, allocated and constructed by the host:

  • The host allocates the state block per mtrStateSize() (generated by the registration macro from sizeof(State)) and constructs it through mtrInit() with placement-new before first use โ€” do not assume zero-filled memory is usable (non-trivial types such as std::string over zeroed memory are undefined behavior).
  • State block contents persist across frames: keep textures, model handles, callbacks, and caches here.
  • Members of the script class (the fields you write in MyVehicleScript) are shared by all instances of that module โ€” put only read-only constants and global configuration there.
  • The state is destructed as soon as dispose returns; instances retire about 5 seconds after disappearing from the client, and all of them are destroyed on resource pack reload (F3+T).
Tip

Background callback objects (mtr::Api::Callback) must live in State until destruction โ€” the Java side may invoke them at any moment.

Lifecycle ordering

Allocation + construction

The host allocates the instance's state block and calls mtrInit() โ†’ the State() constructor runs.

create

Called once before the instance first renders. Draw records submitted during create are kept and reused by the first render frame โ€” put one-shot static content (shapes, persistent textures) here and let render draw only the dynamic parts. Resource acquisition, Api calls, and callback registration belong here as well.

render (every frame)

Read the snapshot โ†’ compute โ†’ write draw records into the frame arenas. Snapshots and wrappers are valid only within the current call; the host reads the output and reclaims the frame as soon as render returns. While an instance is invisible, render is not called, but its state stays hot.

dispose

Called when the instance retires or on reload. Close owned resources such as textures and models here; State destruction follows immediately. Even if dispose throws, the state is destructed safely.

The JS counterpart

This create / render / dispose + per-instance state model is the native mapping of exactly what JCM offers JS scripts (one state object per ScriptInstance), so JS scripts can be migrated section by section.

Threading model

  • The host invokes all lifecycle functions of each module serially on a single background thread (mirroring JCM's per-script single-threaded executor for JS scripts) โ€” create / render / dispose for the same instance never run concurrently.
  • However, render and asynchronous callbacks can run concurrently (for instance, a task submitted to BackgroundWorker may be executing on another thread at this very moment): the script is responsible for synchronizing shared mutable state.
  • A resource pack reload first stops callbacks and waits for in-flight calls to return, then destroys the state and unloads the code.

Crash isolation

The registration macros wrap every exported function in a crash guard (crash_guard.hpp):

  • Hardware faults such as segfaults โ†’ only that module is disabled; the game process keeps running, and latest.log is left with one line: native script faulted โ€ฆ module disabled.
  • Uncaught C++ exceptions are intercepted by the guard as well โ€” but please try/catch them yourself to keep the semantics clear.
  • POSIX signals are not "recovered" longjmp-style: jumping out of a signal handler is more dangerous than the crash itself, so taking the module offline outright is a deliberate design.

Return codes

mtrCreate / mtrRender / mtrDispose returnMeaning
0Success
-1Null input argument
-2ABI version mismatch (host and module failed to negotiate)
-3Resource type mismatch (e.g. a vehicle module invoked as a PIDS)

The SDK generates these return codes, so ordinary script code never needs to care about them; if you are writing a custom host / test driver, interpret them per the table above (see C ABI and host services for details).


Up next: Data snapshots โ€” what you can read during render; Drawing and canvases โ€” how to get it drawn.