mtr::Api and Java interop
Every public Java API accessible to JavaScript scripts is available to C++ without exception. Api calls the same public objects JS does—it does not make C++ execute JavaScript.
Every Api call = one JSON encode + reflection + JSON decode round trip. Use it only in create(), background tasks, or a handful of low-frequency calls per second; never inside per-draw calls—historically, 'one Api call per vehicle per frame' was precisely the source of the periodic GC stutters. Read high-frequency data from the snapshot; push high-frequency drawing through the record stream.
When to use Api
| Need | Channel |
|---|---|
| Per-frame state of vehicles / arrivals / lifts / eyecandy | Snapshot wrappers (zero cost) |
| Text / texture / model / sound drawing | The draw record stream (zero JVM crossings) |
Script input configuration, one-off resource creation, object handles cached across frames | Api (call it in create; store handles in State) |
| Deep interfaces such as network requests, files, BackgroundWorker, the sound manager | Api (call it from background tasks) |
Objects and calls
Construct Api from the current frame's input; navigate from the root objects (context() / wrapper()), static classes or new:
struct State { mtr::Api::Object tracker; };
struct Example : mtr::PidsScript<State> {
static constexpr auto ID = "example:arrivals";
void create(mtr::PidsContext& ctx, State& state, const mtr::Pids&) override {
mtr::Api api(ctx.input());
state.tracker = api.create("StateTracker"); // new StateTracker()
api.context().call("setZOrderStep", {0.0002}); // ctx.setZOrderStep(0.0002)
}
void render(mtr::PidsContext& ctx, State& state, const mtr::Pids&) override {
mtr::Api api(ctx.input());
auto arrivals = api.wrapper().call("arrivals"); // wrapper.arrivals()
if (arrivals.call("size").as<int>() == 0) return;
auto arrival = arrivals.call("get", {0});
if (arrival.is_null()) return;
auto text = api.call("Text", "create"); // new Text()
text.call("text", {arrival.call("destination")});
text.call("pos", {2.0, 3.0});
text.call("draw", {api.context()});
}
};
MTR_REGISTER_PIDS_SCRIPT(Example)
| Api member | Purpose |
|---|---|
api.context() / api.wrapper() | The current script's context / wrapper (the same pair of objects as JS) |
api.input() | The input JSON from the resource pack declaration (= JS's SCRIPT_INPUT) |
api.create(class, args) | news a Java object |
api.call(class, member, args) / api.get(class, member) | Static methods / static fields |
api.root(name) | Fetches a root object by name: api.root("console"), api.root("Timing")… |
| Object member | Purpose |
|---|---|
obj.call(member, args) | Instance method call; returns a new Object |
obj.get(member) / obj.set(member, value) | Field read / write (including inherited public members and standard collections) |
obj.as<T>() | Extracts the raw value (int, double, bool, std::string, …) |
obj.is_null() | Null test for nullable values |
obj.enum_name() | The name of a Java enum (e.g. "BOAT") |
obj.close() | Explicitly calls close() (session cleanup also closes leftover resources as a backstop) |
Java objects are managed by the session with reference counting, and handles are represented as decimal strings; destroying an Object releases its reference, so store objects you want to keep across frames in State. Numeric ids (64-bit included) keep full precision throughout. Fields and methods are resolved against the real Java types, including inherited public members and standard Java collections.
JS ↔ C++ mapping table
| JavaScript | C++ |
|---|---|
Resources.id("example:file") | api.call("Resources", "id", {"example:file"}) |
new Matrices() | api.create("Matrices") |
ctx.getRenderManager() | api.context().call("getRenderManager") |
obj.method(a, b) | obj.call("method", {a, b}) |
obj.field / obj.field = v | obj.get("field") / obj.set("field", v) |
TransportMode.BOAT | api.get("TransportMode", "BOAT") |
SCRIPT_INPUT | api.input() (JSON value) |
console.log(...) / print(...) | api.root("console").call("log", {...}) |
Timing.delta() | api.root("Timing").call("delta").as<double>() |
array.length / array[i] | array.call("length") / array.call("get", {i}) |
include(...) | C++ compile-time #include |
AWT classes use fully qualified names: java.awt.Color, java.awt.Font, java.awt.BasicStroke, java.awt.geom.AffineTransform. The public fields graphics / bufferedImage of GraphicsTexture expose the real Java2D objects. Resource path resolution: Resources.idr resolves against this module's declaration path (with multiple library declarations, each uses its own); Resources.manager() is null on MTR 4, same as in JS. Members new in upstream v2.3.0-prerelease.2 — the Resources.id(id, suffix) concatenation form, the Resources.mgr() alias (ANTE resource-pack compatibility), string-argument variants such as VanillaText.withFont(String) — work out of the box over the same JSON-RPC channel. Capabilities that upstream documentation marks as unimplemented (the vehicle connector's drawConnModel / drawConnStretchTexture) are equally not invented by this mod.
Background callbacks
Api::Callback wraps a C++ function as a Java functional interface (Runnable / Consumer / Function, …). The callback object must stay alive for as long as Java could possibly call it—keep it in State:
struct State {
std::unique_ptr<mtr::Api::Callback> task; // Keep until destruction
};
void create(mtr::PidsContext& ctx, State& s, const mtr::Pids&) {
mtr::Api api(ctx.input());
s.task = std::make_unique<mtr::Api::Callback>([api](mtr::Json) {
auto response = api.call("Networking", "fetch",
{"https://example.org/data"});
// Publish the result into your own synchronised state; read it in render
return mtr::Json();
});
api.root("BackgroundWorker").call("submit", {*s.task});
}
- render and callbacks may run concurrently—lock shared mutable C++ state yourself.
- A reload first stops callbacks and waits for in-flight calls to return before destroying state and unloading the code.
- Upload GPU models prepared in the background with
DynamicModelHolder.uploadLater; a directModelManager.uploadstill requires the Java render thread (call it fromcreateor a main-thread callback).
Coverage
The commonly used API surface: console, Resources, ModelManager / Model / RawModel / RawMeshBuilder / DynamicModelHolder, Vector3f, Matrices, GraphicsTexture and Java2D, RenderManager / QuadDrawCall, SoundManager / TickableSoundInstance, MinecraftClient, PlayerEntity / ItemStack / VanillaText / VoxelShape, Files, Networking, BackgroundWorker, TextUtil / Timing / StateTracker / CycleTracker / RateLimit, DataReader, MTRClientData, the TSC data classes and DisplayHelper. The full list of callable members for PIDS / eyecandy / vehicles is recorded in the repository's bench/api-coverage.json; for lifts, the Java wrapper is authoritative.
After building, you can audit your call surface against a real MTR jar:
python native/bench/api_coverage.py \
--classes fabric/build/classes/java/main \
--jar path/to/mtr.jar
The internal fields of MTRClientData are subject to version stability limits exactly as on the JS side; methods marked deprecated can still be called. Prefer the equivalent data in the snapshots and wrappers—faster and more stable.
Up next: C ABI and host services — the complete contract at the module boundary.