Quick start

Prerequisites

ToolVersionNotes
C++ compilerC++17Any of MSVC (Windows) / GCC / Clang
CMakeβ‰₯ 3.16Recommended, but not mandatoryβ€”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 whichever build system you are comfortable with.

Get the SDK

Every release publishes the SDK as jcm-native-sdk-<version>.zip (see the GitHub Releases assets); its contents are exactly 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 (this one #include is enough)
β”‚   β”œβ”€β”€ 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 utilities
└── CMakeLists.txt

Set up a module project

The least-effort route: unzip the SDK into your resource pack project and append your own targets to the end of its CMakeLists.txtβ€”the SDK has already wrapped every platform's linking details (Windows prefix-less .dll, macOS .dylib with -undefined dynamic_lookup, Android 16 KB page alignment) into 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 include directory of the unpacked SDK)
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 required by the ABI 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 go 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 parameter is the per-instance state type.
  • The lifecycle matches JS: override the three virtual functions create / render / dispose as needed (all optional).
  • The registration macro expands to all the ABI exports; one translation unit registers exactly one script.

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

Compile

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 platformArtefactNotes
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

Libraries inside a resource pack bind to a platform 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

Put 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
└── ... (any remaining resources such as textures and models)

Declare it in joban_custom_resources.json (PIDS preset)

No language field is neededβ€”the mod determines the language automatically from the characteristics of each entry in scriptLocations, and any number of JS and C++ scripts can be mixed within the same 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)C++ platform-independent stem β†’ resolved automatically per platform to natives/linux-x64/libmy_pids.so, natives/windows-x64/my_pids.dll, natives/macos-arm64/libmy_pids.dylib and so on; in an ARM64 Linux environment the ios-arm64 / android-arm64 candidates are appended automatically
…/libfoo.so / ….dll / ….dylibFull C++ path (binds 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 keys)
  • One rule 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 formats remain supported: old-style declarations with "language": "cpp" + nativeLibrary / nativeLibraries keep working and can still 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; the C++ side reads input via 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 compile) belong to the resource pack and go into natives/<platform>/; the assets/jcm/natives/ inside the mod's own jar contains only the native bridgeβ€”do not confuse the two. The game loads only the library matching 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 distribute it as built-in resources bundled 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 counts, script kinds, transport channel and so on). Seeing your example:arrivals there means the load succeeded.
  4. After changing a library file, press F3+T to reload resource packs and with them the C++ scripts (or use the mod's 'Reload all MTR scripts' keybind); no game restart is needed.
Important

You must use a Release jar or a Complete Build artefact. The fast CI artefacts produced by pushes/PRs do not contain the native bridge; the log will say native bridge unavailable and every C++ script will be skippedβ€”that is not a fault in your module.


Next: Script model and lifecycle β€” the full semantics of the four script kinds; or jump straight to the Data snapshots reference to see what you can read.