Quick start

Prerequisites

ToolVersionNotes
C++ compilerC++17Any of MSVC (Windows) / GCC / Clang
CMakeโ‰ฅ 3.16Recommended, but not required โ€” the SDK is header-only
JDKOptionalOnly needed to run the JVM integration tests bundled with the SDK
Tip

The SDK ships entirely as headers (native/include/mtr/) with no precompiled library dependencies; simply add include/ to the include path of whatever build system you already use.

Get the SDK

The SDK ships with every release as jcm-native-sdk-<version>.zip (see the GitHub Releases assets); its contents are the source of the repository's native/ directory:

jcm-native-sdk-2.3.0-1.0.2/
โ”œโ”€โ”€ include/mtr/          # All SDK headers (script.hpp is the single entry point)
โ”‚   โ”œโ”€โ”€ mtr_native.h      # C ABI: exported functions, snapshots, draw records, host services
โ”‚   โ”œโ”€โ”€ script.hpp        # Script base classes + registration macros (one #include is all you need)
โ”‚   โ”œโ”€โ”€ vehicle.hpp  pids.hpp  lift.hpp  eyecandy.hpp
โ”‚   โ”œโ”€โ”€ gfx.hpp  gfx2d.hpp  font5x7.hpp  text.hpp  matrices.hpp
โ”‚   โ”œโ”€โ”€ api.hpp           # mtr::Api (full Java API access)
โ”‚   โ””โ”€โ”€ crash_guard.hpp ...
โ”œโ”€โ”€ examples/             # Three minimal example modules
โ”œโ”€โ”€ bench/                # Tests and tooling
โ””โ”€โ”€ CMakeLists.txt

Set up a module project

The easiest path: unzip the SDK into your resource pack project and append your own targets to the end of its CMakeLists.txt โ€” the SDK already wraps every platform's linking details (prefix-less .dll on Windows, .dylib plus -undefined dynamic_lookup on macOS, 16 KB page alignment on Android) in the mtr_add_script() function:

# Append to the end of the SDK's native/CMakeLists.txt
mtr_add_script(my_pids src/my_pids.cpp)

If you prefer a standalone project, here is a complete template equivalent to that function:

cmake_minimum_required(VERSION 3.16)
project(my_native_scripts LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# SDK header path (point this at the extracted SDK include directory)
add_library(mtr_script_sdk INTERFACE)
target_include_directories(mtr_script_sdk INTERFACE path/to/sdk/include)

function(mtr_add_script name)          # Module target function, same as the SDK's
  add_library(${name} MODULE ${ARGN})
  target_link_libraries(${name} PRIVATE mtr_script_sdk)
  set_target_properties(${name} PROPERTIES CXX_VISIBILITY_PRESET hidden)
  if(WIN32)
    set_target_properties(${name} PROPERTIES PREFIX "" SUFFIX ".dll")
  elseif(APPLE)
    set_target_properties(${name} PROPERTIES PREFIX "lib" SUFFIX ".dylib")
    target_link_options(${name} PRIVATE "-Wl,-undefined,dynamic_lookup")
  elseif(ANDROID)
    target_link_options(${name} PRIVATE "-Wl,-z,max-page-size=16384")
  endif()
endfunction()

mtr_add_script(my_pids src/my_pids.cpp)
Note

Scripts are MODULE libraries (no SDK binaries are linked); symbols are hidden by default and only the 7 mtr* functions the ABI requires are exported (done by the registration macros in script.hpp). C++17 is the minimum standard.

Your first module

A minimal PIDS arrival display module โ€” complete, compilable, and ready to drop straight into the game:

#include <mtr/script.hpp>

struct ArrivalState {};                    // Per-instance persistent state (empty in this example)

struct ArrivalDisplay : mtr::PidsScript<ArrivalState> {
    static constexpr auto ID = "example:arrivals";   // Must match the resource pack declaration

    void render(mtr::PidsContext& ctx, ArrivalState&,
                const mtr::Pids& pids) override {
        for (int row = 0; row < pids.rows(); row++) {
            if (pids.is_row_hidden(row)) continue;

            std::string text = pids.get_custom_message(row).str();
            if (text.empty() && row < pids.arrivals().size())
                text = pids.arrivals().at(row).destination().str();

            if (!text.empty())
                mtr::Text::create()
                    .text(text.c_str())
                    .pos(2, row * 10.0 + 2)
                    .size(pids.width() - 4, 9)
                    .draw(ctx);
        }
    }
};

MTR_REGISTER_PIDS_SCRIPT(ArrivalDisplay)

Key points:

  • The base class determines the script type โ€” PidsScript / VehicleScript / LiftScript / EyecandyScript; the template argument is the per-instance state type.
  • The lifecycle is the same as JS: override the three virtual functions create / render / dispose as needed (all optional).
  • The registration macro expands to all ABI exports; one translation unit registers exactly one script.

More examples live in the SDK's examples/: minimal samples for each of the vehicle / lift / eyecandy kinds.

Build

cmake -S native -B native/build -DCMAKE_BUILD_TYPE=Release
cmake --build native/build --config Release --parallel 4
ctest --test-dir native/build -C Release --output-on-failure   # Optional: run the SDK's bundled tests
Target platformArtifactNotes
Windows x64my_pids.dllMSVC; /utf-8 is preset by the SDK project
Linux x64 / ARM64libmy_pids.soGCC / Clang
macOS x64 / arm64libmy_pids.dylibSelect the architecture with -DCMAKE_OSX_ARCHITECTURES=arm64 (or x86_64); -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
Android arm64libmy_pids.soCross-compiled with the NDK toolchain, API 21+; 16 KB page alignment is already handled by the project template
iOS / iPadOS arm64libmy_pids.dylibMach-O cross-compile, no signing required (re-sign when sideloading)
Tip

Inside a resource pack, libraries bind to platforms by file extension (.dll โ†’ Windows, .so โ†’ Linux/Android, .dylib โ†’ macOS/iOS); the exact directory rules are in the next section.

Package into a resource pack

Directory layout

Drop each platform's build output into the resource pack's natives/<platform>/ directory:

my_pack/
โ”œโ”€โ”€ natives/
โ”‚   โ”œโ”€โ”€ windows-x64/my_pids.dll
โ”‚   โ”œโ”€โ”€ windows-arm64/my_pids.dll
โ”‚   โ”œโ”€โ”€ linux-x64/libmy_pids.so
โ”‚   โ”œโ”€โ”€ linux-arm64/libmy_pids.so
โ”‚   โ”œโ”€โ”€ macos-x64/libmy_pids.dylib
โ”‚   โ”œโ”€โ”€ macos-arm64/libmy_pids.dylib
โ”‚   โ”œโ”€โ”€ android-arm64/libmy_pids.so
โ”‚   โ””โ”€โ”€ ios-arm64/libmy_pids.dylib
โ””โ”€โ”€ ... (the rest of the pack: textures, models, and so on)

Declare it in joban_custom_resources.json (PIDS preset)

No language field is needed โ€” the mod detects the language automatically from the characteristics of each scriptLocations entry, and any number of JS and C++ files can be mixed within one entry:

{
    "pids_images": [{
        "id": "example:arrivals",
        "scriptLocations": [
            "mtr:my_pack/natives/my_pids"
        ]
    }]
}

Entry resolution rules:

scriptLocations entryResolution
โ€ฆ/main.js (case-insensitive)JavaScript
mtr:my_pack/natives/my_pids (no extension)platform-agnostic C++ stem โ†’ resolves per platform to natives/linux-x64/libmy_pids.so, natives/windows-x64/my_pids.dll, natives/macos-arm64/libmy_pids.dylib, and so on; on ARM64 Linux the ios-arm64 / android-arm64 candidates are appended automatically
โ€ฆ/libfoo.so / โ€ฆ.dll / โ€ฆ.dylibFull C++ path (bound to the platform that extension maps to; other platforms skip it)
{"windows": โ€ฆ, "linux": โ€ฆ, "macos": โ€ฆ}C++ per-platform mapping table; precise keys such as macos-arm64 are supported (precise keys take precedence over generic ones)
  • Same rules for all four script kinds: vehicleScripts / objectScripts / liftScripts in mtr_custom_resources.json and pids_images presets (via scriptLocations) in joban_custom_resources.json all support the mixed declarations above.
  • Legacy format still works: old declarations using "language": "cpp" plus nativeLibrary / nativeLibraries keep working and can be mixed with .js; "language": "javascript" forces the whole entry to be parsed as JS.
  • JS fields behave as usual: fields such as prependExpressions / input / dataFetchMode keep their meaning, and the C++ side reads input through api.input().
  • Keep rendering off-screen: the PIDS preset field outOfBound: true (upstream v2.3.0-prerelease.3) keeps the preset rendering when the screen is out of the camera's bounds โ€” needed for projector presets that must run constantly.
Note

Script modules (the .dll/.so/.dylib files you compiled) belong to the resource pack and go in natives/<platform>/; the assets/jcm/natives/ inside the mod jar contains only the native bridge โ€” don't confuse the two. The game only loads the library for the current operating system's platform.

Verify in game

  1. Install JCM Yanyang and MTR, drop the resource pack into resourcepacks/ and enable it (or ship it as built-in assets with a world).
  2. Place the corresponding block (e.g. a PIDS) and select your script in its configuration / make the declaration entry take effect.
  3. Open logs/latest.log and search for native script โ€” at load time the mod prints a module census summary (module count, script types, transport channel, and so on). Seeing your example:arrivals means it loaded successfully.
  4. After changing library files, press F3+T to reload resource packs โ€” that also reloads C++ scripts (or use the mod's "Reload all MTR scripts" keybind); no game restart needed.
Important

You must use a Release jar or a Complete Build artifact. Fast CI artifacts from pushes/PRs do not include the native bridge โ€” the log will say native bridge unavailable and every C++ script will be skipped. That is not a bug in your module.


Next up: Script model and lifecycle โ€” the full semantics of the four script kinds; or jump straight to Data snapshots for a look at the readable data.