Quick start
From zero to running in game: get the SDK β write your first module β compile β 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 mandatoryβ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 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)
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/disposeas 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 platform | Artefact | 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) |
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 entry | Resolution |
|---|---|
β¦/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 / β¦.dylib | Full 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/liftScriptsinmtr_custom_resources.json, andpids_imagespresets (viascriptLocations) injoban_custom_resources.json, all support the mixed declarations above. - Legacy formats remain supported: old-style declarations with
"language": "cpp"+nativeLibrary/nativeLibrarieskeep 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/dataFetchModekeep their meaning; the C++ side readsinputviaapi.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 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
- 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). - 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 counts, script kinds, transport channel and so on). Seeing yourexample:arrivalsthere means the load succeeded. - 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.
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.