Script model and lifecycle
The five script base classes, per-instance state, lifecycle ordering, and the threading and crash isolation semantics.
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:
| 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 (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)
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 throughmtrInit()with placement-new before first use โ do not assume zero-filled memory is usable (non-trivial types such asstd::stringover 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
disposereturns; instances retire about 5 seconds after disappearing from the client, and all of them are destroyed on resource pack reload (F3+T).
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.
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 / disposefor the same instance never run concurrently. - However, render and asynchronous callbacks can run concurrently (for instance, a task submitted to
BackgroundWorkermay 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.logis 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 return | Meaning |
|---|---|
0 | Success |
-1 | Null input argument |
-2 | ABI version mismatch (host and module failed to negotiate) |
-3 | Resource 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.