Script model and lifecycle
The five script base classes, per-instance state, the lifecycle sequence, and the threading and crash-isolation semantics.
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:
| Resource | Base class | Registration macro | Data wrapper |
|---|---|---|---|
| Vehicle | mtr::VehicleScript<State> | MTR_REGISTER_VEHICLE_SCRIPT | mtr::Vehicle |
| PIDS | mtr::PidsScript<State> | MTR_REGISTER_PIDS_SCRIPT | mtr::Pids |
| PSD | mtr::PsdScript<State> | MTR_REGISTER_PSD_SCRIPT | mtr::Psd |
| Lift | mtr::LiftScript<State> | MTR_REGISTER_LIFT_SCRIPT | mtr::Lift |
| Eyecandy | mtr::EyecandyScript<State> | MTR_REGISTER_EYECANDY_SCRIPT | mtr::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)
IDis the script's namespaced id (e.g.demo:vehicle) and must match theidof its declaration entry in the resource pack'smtr_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 fromsizeof(State)) and constructs it viamtrInit()with placement-new before first use—do not assume zero-filled memory is usable (non-trivial types such asstd::stringover 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
disposereturns; instances retire roughly 5 seconds after disappearing from the client, and everything is destroyed on a resource pack reload (F3+T).
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.
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 / disposefor 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.loggains a singlenative script faulted … module disabledline. - 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 returns | Meaning |
|---|---|
0 | Success |
-1 | Null argument |
-2 | ABI version mismatch (host–module negotiation failed) |
-3 | Resource 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.