Quick start
From zero to running in game: get the SDK โ write your first module โ build โ package it into a resource pack.
Prerequisites
| Tool | Version | Notes |
|---|---|---|
| C++ compiler | C++17 | Any of MSVC (Windows) / GCC / Clang |
| CMake | โฅ 3.16 | Recommended, but not required โ the SDK is header-only |
| JDK | Optional | Only needed to run the JVM integration tests bundled with the SDK |
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)
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/disposeas 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 platform | Artifact | Notes |
|---|---|---|
| Windows x64 | my_pids.dll | MSVC; /utf-8 is preset by the SDK project |
| Linux x64 / ARM64 | libmy_pids.so | GCC / Clang |
| macOS x64 / arm64 | libmy_pids.dylib | Select the architecture with -DCMAKE_OSX_ARCHITECTURES=arm64 (or x86_64); -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 |
| Android arm64 | libmy_pids.so | Cross-compiled with the NDK toolchain, API 21+; 16 KB page alignment is already handled by the project template |
| iOS / iPadOS arm64 | libmy_pids.dylib | Mach-O cross-compile, no signing required (re-sign when sideloading) |
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 entry | Resolution |
|---|---|
โฆ/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 / โฆ.dylib | Full 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/liftScriptsinmtr_custom_resources.jsonandpids_imagespresets (viascriptLocations) injoban_custom_resources.jsonall support the mixed declarations above. - Legacy format still works: old declarations using
"language": "cpp"plusnativeLibrary/nativeLibrarieskeep 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/dataFetchModekeep their meaning, and the C++ side readsinputthroughapi.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.
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
- Install JCM Yanyang and MTR, drop the resource pack into
resourcepacks/and enable it (or ship it as built-in assets with a world). - Place the corresponding block (e.g. a PIDS) and select your script in its configuration / make the declaration entry take effect.
- Open
logs/latest.logand search fornative scriptโ at load time the mod prints a module census summary (module count, script types, transport channel, and so on). Seeing yourexample:arrivalsmeans it loaded successfully. - 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.
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.