Script model and lifecycle

The five script kinds

Include <mtr/script.hpp>, inherit from the matching base class and export it with a registration macro. The base class template parameter 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 (for the other three kinds, only the base class and parameter types change):

#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;    // Held resources
};

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: build resources, register callbacks
    }

    void render(mtr::VehicleContext& ctx, MyVehicleState& state,
                const mtr::Vehicle& vehicle) override {
        // Called every frame: read the snapshot, 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 via 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 behaviour).
  • The state block's contents persist across frames: put textures, model handles, callbacks and caches here.
  • Script class members (the fields you write in MyVehicleScript) are shared by every instance of the module—store only read-only constants and global configuration there.
  • The state is destroyed as soon as dispose returns; instances retire roughly 5 seconds after disappearing from the client, and everything is destroyed on a resource pack reload (F3+T).
Tip

Background callback objects (mtr::Api::Callback) must be kept in State until destruction—Java may invoke them at any moment.

Lifecycle sequence

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 retained and reused by the first render frame—put one-off static content (shapes, resident textures) here and let render draw only the dynamic part. Resource acquisition, Api calls and callback registration also belong here.

render (every frame)

Read the snapshot → compute → write draw records into the frame arenas. The snapshot and wrappers are valid only for the duration of the call; the host reads the output and reclaims the frame immediately after render returns. While an instance is invisible render is not called, but the state stays warm.

dispose

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

Correspondence with JS

This create / render / dispose + per-instance state model is precisely the native mapping of the model JCM provides to JS scripts (one state object per ScriptInstance); when migrating a JS script the logic can be translated section by section.

Threading model

  • The host invokes every lifecycle function 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 may run concurrently (a task submitted to BackgroundWorker, for instance, may be running on another thread at this very moment): the script itself is responsible for synchronising shared mutable state.
  • A resource pack reload first stops callbacks, 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 segmentation violations → only that module is disabled; the game process keeps running, and latest.log gains a single native script faulted … module disabled line.
  • 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 immediately is a deliberate design choice.

Return codes

mtrCreate / mtrRender / mtrDispose returnsMeaning
0Success
-1Null argument
-2ABI version mismatch (host–module negotiation failed)
-3Resource kind mismatch (e.g. a vehicle module invoked as a PIDS)

These return codes are generated by the SDK; ordinary script code never needs to care about them. If you are writing a custom host or test driver, interpret them using 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 draw it.