mtr::Api and Java interop
Hot-path ban

Every Api call = one JSON encode + reflection + JSON decode round trip. Use it only in create(), background tasks, or low-frequency calls you can count on one hand per second; never inside per-draw calls โ€” historically, "one Api call per frame per vehicle" was precisely the source of the periodic GC stutter. Read high-frequency data from snapshots; send high-frequency drawing through the record stream.

When to use Api

NeedChannel
Per-frame vehicle / arrival / lift / eyecandy stateSnapshot wrappers (zero cost)
Text / texture / model / sound drawingThe draw record stream (zero JVM crossings)
Script input configuration, one-shot resource creation, object handles cached across framesApi (call it in create; store handles in State)
Networking requests, files, BackgroundWorker, sound managers, and other deep interfacesApi (call it from background tasks)

Objects and calls

Construct an 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 memberPurpose
api.context() / api.wrapper()The current script's context / wrapper (the same pair of objects JS sees)
api.input()The input JSON from the resource pack declaration (= JS's SCRIPT_INPUT)
api.create(class, args)Instantiates a Java object (new)
api.call(class, member, args) / api.get(class, member)Static methods / static fields
api.root(name)Root objects by name: api.root("console"), api.root("Timing"), โ€ฆ
Object memberPurpose
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 nullables
obj.enum_name()The Java enum's name (e.g. "BOAT")
obj.close()Explicitly calls close() (session cleanup also closes leftover resources as a backstop)
Handle semantics

Java objects are managed by the session with reference counting; handles are decimal strings. Destroying an Object releases its reference, so store objects you want to keep across frames in State. Numeric ids (including 64-bit) keep full precision end to end. Fields and methods resolve against the real Java types, including inherited public members and standard Java collections.

JS โ†” C++ cheat sheet

JavaScriptC++
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 = vobj.get("field") / obj.set("field", v)
TransportMode.BOATapi.get("TransportMode", "BOAT")
SCRIPT_INPUTapi.input() (a 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 GraphicsTexture fields graphics / bufferedImage 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 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 the upstream documentation marks as unimplemented (the vehicle coupler's drawConnModel / drawConnStretchTexture) are equally not invented here.

Background callbacks

Api::Callback wraps a C++ function into a Java functional interface (Runnable / Consumer / Function, โ€ฆ). Callback objects must stay alive until Java can no longer possibly call them โ€” put them in State:

struct State {
    std::unique_ptr<mtr::Api::Callback> task;   // keep it 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 synchronized state; read it in render
        return mtr::Json();
    });
    api.root("BackgroundWorker").call("submit", {*s.task});
}
  • render and callbacks can run concurrently โ€” lock shared mutable C++ state yourself.
  • A reload first stops callbacks and waits for in-flight calls to return, then destroys the state and unloads the code.
  • GPU models prepared in the background upload via DynamicModelHolder.uploadLater; a direct ModelManager.upload still requires the Java render thread (call it from create or a main-thread callback).

Coverage

The commonly used API surface: console, Resources, ModelManager / Model / RawModel / RawMeshBuilder / DynamicModelHolder, Vector3f, Matrices, GraphicsTexture plus 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 callable member lists for PIDS / eyecandy / vehicle are 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 the real MTR jar:

python native/bench/api_coverage.py \
    --classes fabric/build/classes/java/main \
    --jar path/to/mtr.jar
Version stability

The internal fields of MTRClientData are subject to version stability limits, same as on the JS side; methods marked deprecated remain callable. Prefer the equivalent data in snapshots and wrappers โ€” it is faster and more stable.


Up next: C ABI and host services โ€” the complete contract at the module boundary.