Ambient API Documentation
This page covers how to use the JS API and Native API, plus how to set up a native development environment on phone, Linux, and Windows using Clang + Android NDK.
Introduction
Primary native template repo: Lodingglue/nise-api
Primary API header reference: include/nise/stub.h
JS: Logging
Routes messages through the client logger (tag: "JS").
log(message)
Logs an informational message to the client logger.
log("Script initialised successfully.");
logError(message)
Logs an error message to the client logger.
logError("Hook failed at address 0x" + addr.toString(16));
logWarn(message)
Logs a warning message to the client logger.
logWarn("Symbol not found, using fallback.");
JS: Utility Functions
File I/O and timing helpers operating on the real device filesystem.
sleep(milliseconds)
Pauses execution of the current script for the specified duration.
log("Waiting 500ms...");
sleep(500);
log("Done.");
readFile(path)
Reads the entire contents of a file from the real filesystem.
Returns: {ArrayBuffer} Raw file bytes.
const data = readFile("/sdcard/MyMod/patch.bin");
Hook.writeMemory(targetAddr, data);
writeFile(path, data)
Writes data to a file on the real filesystem.
Returns: {boolean} true if the write succeeded without stream errors.
writeFile("/sdcard/MyMod/log.txt", "hello world");
const buf = new Uint8Array([0x90, 0x90, 0x90, 0x90]).buffer;
writeFile("/sdcard/MyMod/patch.bin", buf);
fileExists(path)
Checks whether a file exists on the real filesystem.
Returns: {boolean} true if the path exists.
if (fileExists("/sdcard/MyMod/config.json")) {
const cfg = readFile("/sdcard/MyMod/config.json");
} else {
logWarn("Config not found, using defaults.");
}
JS: Path Functions
Helpers for resolving paths relative to the currently running script file.
getScriptPath()
Returns the absolute file path of the currently executing script.
Returns: {string} Absolute path (e.g. "/sdcard/MyMod/main.js").
log("Running from: " + getScriptPath());
getScriptDir()
Returns the directory that contains the currently executing script.
Returns: {string} Absolute directory path (e.g. "/sdcard/MyMod").
const dir = getScriptDir();
const assetPath = joinPath(dir, "assets", "texture.webp");
joinPath(base, ...parts)
Joins two or more path segments into a single path string.
Returns: {string} The combined path.
joinPath("/sdcard/MyMod", "assets", "texture.webp");
// => "/sdcard/MyMod/assets/texture.webp"
joinPath(getScriptDir(), "config.json");
// => "/sdcard/MyMod/config.json" (if script is in /sdcard/MyMod)
JS: Virtual Assets
Hooks AAssetManager to layer a virtual filesystem transparently on top of
VirtualAssets.blockFile(path)
Blocks an asset path so the game cannot open it.
// Prevent the game from loading vanilla music entirely
VirtualAssets.blockFile("assets/music/game/creative.ogg");
VirtualAssets.unblockFile(path)
Lifts a block previously applied by blockFile().
VirtualAssets.unblockFile("textures/terrain/grass.webp");
VirtualAssets.addFile(path, data)
Injects a file into the virtual asset registry.
// Inject a binary file read from device storage
const buf = readFile("/sdcard/MyMod/custom_grass.webp");
VirtualAssets.addFile("textures/terrain/grass.webp", buf);
// Inject raw text
VirtualAssets.addFile("config/override.json", '{"key":"value"}');
VirtualAssets.addTextFile(path, content)
Convenience wrapper for injecting a plain-text asset.
VirtualAssets.addTextFile("config/mod_settings.json", JSON.stringify({
enabled: true,
multiplier: 2.5
}));
VirtualAssets.removeFile(path)
Removes a virtual entry from the registry.
VirtualAssets.removeFile("textures/terrain/grass.webp");
VirtualAssets.hasFile(path)
Returns whether a virtual entry is registered for path.
Returns: {boolean}
if (!VirtualAssets.hasFile("shaders/glsl/terrain.vertex")) {
VirtualAssets.addTextFile("shaders/glsl/terrain.vertex", shaderSrc);
}
VirtualAssets.loadDir(storageDir, virtualBaseDir, recursive = false)
Bulk-registers all files in an on-device directory as virtual assets.
Returns: {number} Number of files registered, or -1 if storageDir is invalid.
// Device: /sdcard/MyMod/textures/grass.webp
// Served as: textures/grass.webp in the virtual asset tree
const count = VirtualAssets.loadDir("/sdcard/MyMod/textures", "textures", true);
log("Loaded " + count + " assets.");
VirtualAssets.replaceFile(virtualPath, storagePath)
Points a virtual asset entry at an on-device file.
Returns: {boolean} true on success; false if storagePath cannot be accessed.
VirtualAssets.replaceFile(
"textures/terrain/grass.webp",
"/sdcard/MyMod/grass.webp"
);
VirtualAssets.readFile(path)
Reads a virtual asset into an ArrayBuffer.
Returns: {ArrayBuffer|null} Raw file bytes, or null if not found/blocked.
const data = VirtualAssets.readFile("config/mod_settings.json");
if (data) {
const text = new TextDecoder().decode(data);
const cfg = JSON.parse(text);
}
JS: Hook
Inline function hooking, NOP patching, raw memory access,
Hook.hookAddr(name, targetAddr, hookFunc, originalFunc, hookType, priority)
Installs an inline hook at targetAddr, redirecting execution to hookFunc.
Returns: {number} Trampoline address on success; 0 on failure.
const base = Hook.getBaseAddr();
const target = base + 0x1A2B3C;
// originalFnPtr is a number holding the address where the trampoline
// pointer will be stored so we can call the original later.
const trampoline = Hook.hookAddr(
"myHook", target, myReplacementFn, originalFnPtr,
Hook.Type.INLINE, Hook.Priority.HIGH
);
if (trampoline === 0) logError("Hook failed.");
Hook.unhookAddr(name)
Removes a hook installed by hookAddr() and restores the original bytes
Returns: {boolean} true if the hook was found and removed.
Hook.unhookAddr("myHook");
Hook.patchNop(name, addr, size = 4)
Overwrites size bytes at addr with NOP instructions, saving the originals
Returns: {boolean}
// Disable a bounds-check (8 bytes)
Hook.patchNop("disableBoundsCheck", Hook.getBaseAddr() + 0xDEAD, 8);
Hook.restoreNopPatch(name)
Restores the original bytes that were overwritten by patchNop().
Returns: {boolean}
Hook.restoreNopPatch("disableBoundsCheck");
Hook.writeMemory(addr, data)
Writes raw bytes from an ArrayBuffer to an arbitrary memory address.
Returns: {boolean}
// Write a 4-byte ARM64 NOP (0xD503201F)
const nop = new Uint8Array([0x1F, 0x20, 0x03, 0xD5]).buffer;
Hook.writeMemory(Hook.getBaseAddr() + 0xCAFE, nop);
Hook.readMemory(addr, size)
Reads size bytes from an arbitrary memory address into an ArrayBuffer.
Returns: {ArrayBuffer|null} Raw bytes, or null on failure.
const bytes = Hook.readMemory(Hook.getBaseAddr() + 0xCAFE, 16);
if (bytes) {
const view = new Uint8Array(bytes);
log("First byte: 0x" + view[0].toString(16));
}
Hook.getAddress(baseAddr, offsets)
Resolves a multi-level pointer chain.
Returns: {number} Final resolved address; 0 if any dereference hits null.
// base → +0x10 → +0x28 → +0x08 = final address
const healthAddr = Hook.getAddress(Hook.getBaseAddr(), [0x10, 0x28, 0x08]);
Hook.getBaseAddr()
Returns the cached base load address of libminecraft.so.
Returns: {number}
const base = Hook.getBaseAddr();
const target = base + 0x1A2B3C;
Hook.getModuleAddr(moduleName, permissions = "")
Finds the base address of any named module from /proc/self/maps.
Returns: {number} Base address of the first matching mapping; 0 if not found.
const fmodBase = Hook.getModuleAddr("libfmod.so");
const fmodExec = Hook.getModuleAddr("libfmod.so", "r-xp");
Hook.getProtection(addr)
Returns the current mmap protection flags for the page at addr.
Returns: {number} PROT_* bitmask (e.g. PROT_READ | PROT_EXEC); -1 if not mapped.
const prot = Hook.getProtection(Hook.getBaseAddr());
log("Protection flags: " + prot.toString(16));
Hook.sigScanSetup(signature, libName, flags = 0)
Prepares a signature scan using an IDA-style pattern string.
Returns: {number|null} Opaque scan handle (pointer as number); null on failure.
const handle = Hook.sigScanSetup(
"48 8B 05 ?? ?? ?? ?? 48 85 C0",
"libminecraft.so"
);
Hook.getSigScanResult(handle)
Executes the prepared scan and returns the address of the first match.
Returns: {number|null} Address of first matching bytes; null if not found.
const result = Hook.getSigScanResult(handle);
if (result) {
log("Pattern found at: 0x" + result.toString(16));
} else {
logWarn("Pattern not found.");
}
Hook.sigScanCleanup(handle)
Frees all resources associated with a scan handle.
const handle = Hook.sigScanSetup("AA BB CC ?? DD", "libminecraft.so");
const result = Hook.getSigScanResult(handle);
Hook.sigScanCleanup(handle); // always call this
Hook.debugStatus()
Emits a debug dump of the current hook table to the log.
Hook.debugStatus();
JS: System
Two layers:
System_loadLibrary(libraryId, path, flags)
Loads a shared library (.so) and registers it under a string ID.
Returns: {boolean} true on success.
if (!System_isLibraryLoaded("mylib")) {
const ok = System_loadLibrary("mylib", "/data/local/tmp/mylib.so");
if (!ok) logError(System_getLastError());
}
System_unloadLibrary(libraryId)
Unloads a managed library and removes it from the registry.
Returns: {boolean} true if the library was found and dlclose succeeded.
System_unloadLibrary("mylib");
System_isLibraryLoaded(libraryId)
Checks whether a library is currently loaded in the registry.
Returns: {boolean}
if (System_isLibraryLoaded("mylib")) {
log("mylib is ready.");
}
System_getSymbol(libraryId, symbolName)
Looks up an exported symbol in a specific registered library.
Returns: {number|null} int64 symbol address on success; null if not found.
const addr = System_getSymbol("mylib", "myExportedFunction");
if (addr) Hook.hookAddr("myHook", addr, replacement, orig);
System_getSymbolAddress(symbolName)
Searches all registered libraries for a symbol and returns the first match.
Returns: {number|null} int64 symbol address; null if not found anywhere.
const fmodInit = System_getSymbolAddress("FMOD_System_Create");
System_getLibraryInfo(libraryId)
Returns metadata for a registered library.
Returns: {{ path: string, flags: number, loaded: boolean, handle: number }|null}
const info = System_getLibraryInfo("mylib");
if (info) log("Loaded from: " + info.path);
System_getLoadedLibraries()
Returns the IDs of all currently registered libraries.
Returns: {string[]}
System_getLoadedLibraries().forEach(id => log("Registered: " + id));
System_getLastError()
Returns the last error string recorded by any System_* operation.
Returns: {string|null} Human-readable error, or null if no error pending.
if (!System_loadLibrary("x", "/bad/path.so")) {
logError(System_getLastError());
}
System_dlOpen(path, flags)
Raw dlopen wrapper. The returned handle is NOT tracked by the registry.
Returns: {number|null} int64 handle on success; null on failure.
const h = System_dlOpen("/system/lib64/liblog.so");
const fn = System_dlSym(h, "__android_log_print");
// ... use fn ...
System_dlClose(h);
System_dlSym(handle, symbolName)
Raw dlsym wrapper.
Returns: {number|null} int64 symbol address; null if not found.
const logPrint = System_dlSym(liblogHandle, "__android_log_print");
System_dlClose(handle)
Raw dlclose wrapper.
Returns: {boolean} true on success.
System_dlClose(handle);
System_dlError()
Returns and clears the last error from the dl* subsystem.
Returns: {string|null} Error string; null if no error is pending.
System_dlOpen("/bad/path.so");
const err = System_dlError();
if (err) logError("dlopen failed: " + err);
JS: FMOD
Hooks Minecraft's FMOD Core library to intercept audio stream creation,
FMOD.addPathOverride(originalPath, customPath)
Redirects an FMOD audio file open from originalPath to customPath.
FMOD.addPathOverride(
"assets/music/game/creative.ogg",
"/sdcard/MyMod/music/creative.ogg"
);
FMOD.removePathOverride(originalPath)
Removes a single path override.
FMOD.removePathOverride("assets/music/game/creative.ogg");
FMOD.clearPathOverrides()
Removes ALL registered path overrides.
FMOD.clearPathOverrides();
FMOD.pauseCurrentTrack()
Pauses the channel of the currently tracked active track.
Returns: {boolean} true if a playing track was found and paused.
FMOD.pauseCurrentTrack();
FMOD.resumeCurrentTrack()
Resumes the currently paused track.
Returns: {boolean} true if a paused track was found and resumed.
FMOD.resumeCurrentTrack();
FMOD.stopCurrentTrack()
Stops only the currently tracked active track.
Returns: {boolean} true if an active track was found and stopped.
FMOD.stopCurrentTrack();
FMOD.stopAll()
Stops EVERY FMOD channel that is currently playing.
Returns: {boolean} true if the stop command was dispatched successfully.
FMOD.stopAll();
FMOD.getCurrentTrackPath()
Returns the original (pre-override) path of the currently active track.
Returns: {string} Path string; empty string if nothing is playing.
const path = FMOD.getCurrentTrackPath();
if (path) log("Now playing: " + path);
FMOD.isTrackPlaying()
Returns whether a track is currently in the playing (non-paused, non-stopped) state.
Returns: {boolean}
if (FMOD.isTrackPlaying()) {
log("Audio is currently active.");
}
FMOD.setVolume(volume)
Sets the volume on the currently active FMOD channel.
Returns: {boolean} true if a channel was found and volume was applied.
FMOD.setVolume(0.25); // set to 25%
FMOD.setVolume(0.0); // mute
FMOD.setVolume(1.0); // full volume
FMOD.getVolume()
Returns the last-known volume of the current track.
Returns: {number} Volume in [0.0, 1.0]; -1.0 if no track is active.
const vol = FMOD.getVolume();
log("Current volume: " + (vol * 100).toFixed(1) + "%");
C++ API Overview (nise-api)
The native API is exposed in include/nise/stub.h and is designed for Android native mods loaded from JavaScript. Core surfaces include SystemUtils, HookManager, VirtualAssets, FMODHook, plus runtime integration APIs like RenderAPI, TouchAPI, KeyAPI, and ClientLog.
Repository: https://github.com/Lodingglue/nise-api
Header: include/nise/stub.h
C++: Template Workflow
Use the official template project to start quickly, then build your native module and load it through the JS System API.
1) Download the template
https://github.com/Lodingglue/nise-api/archive/refs/heads/master.zip
Download nise-api template (.zip)
2) Minimal exported native function
// example_mod.h
extern "C" void StopAllMusic();
// example_mod.cpp
#include "example_mod.h"
#include "nise/stub.h"
extern "C" void StopAllMusic() {
FMODHook::getInstance().stopAll();
}
3) Load and call from JavaScript
System.loadLibrary("my_mod", "/sdcard/MyMod/libmymod.so");
const fn = System.getSymbol("my_mod", "StopAllMusic");
if (fn) {
fn();
}
C++: VirtualAssets
C-style API for virtual asset injection and override of APK assets via hooked AAssetManager.
VirtualAssets_BlockFile(path)
Blocks an asset path so open calls fail for that path.
VirtualAssets_AddFile(path, data, size)
Registers binary data as a virtual asset entry.
VirtualAssets_AddTextFile(path, content)
Registers UTF-8 text data as a virtual asset entry.
VirtualAssets_LoadDir(storageDir, virtualBaseDir, recursive)
Bulk-registers directory files from storage into virtual assets tree.
Returns: int (number of files registered, -1 on error)
int count = VirtualAssets_LoadDir(
"/sdcard/MyMod/textures",
"textures",
true
);
VirtualAssets_ReplaceFile(virtualPath, storagePath)
Maps one virtual path to a real on-disk replacement file.
Returns: bool
C++: FMODHook
Audio path override and playback control over Minecraft FMOD streams.
addPathOverride(original_path, custom_path)
Redirects one in-game FMOD asset path to your custom file path.
FMODHook::getInstance().addPathOverride(
"assets/music/game/creative.ogg",
"/sdcard/MyMod/music/creative.ogg"
);
pauseCurrentTrack() / resumeCurrentTrack()
Pauses or resumes current track channel.
Returns: bool
stopCurrentTrack() / stopAll()
Stops current track only, or all active FMOD channels.
Returns: bool
getCurrentTrackPath()
Gets current original (pre-override) track path.
Returns: std::string
setVolume(volume) / getVolume()
Sets or reads volume of the current track channel.
Returns: bool for set, float for get
C++: RenderAPI
Frame render callbacks executed from the engine render thread inside the active OpenGL ES context (during eglSwapBuffers).
RenderCallback
Type: typedef void (*RenderCallback)();
Called once per frame. This is the correct place for ImGui draw calls or custom OpenGL rendering because the GL context is current.
Note: Keep callback work lightweight and avoid blocking operations.
RenderAPI::Register(RenderCallback cb)
Registers a render callback to run every frame after game rendering and before buffer swap.
Note: The callback pointer must remain valid while registered.
RenderAPI::Unregister(RenderCallback cb)
Unregisters a previously registered render callback.
Note: Safe to call even if the callback is not currently registered.
void DrawOverlay() {
// issue ImGui or OpenGL draw commands here
}
RenderAPI::Register(DrawOverlay);
// ... later
RenderAPI::Unregister(DrawOverlay);
C++: ClientLog
Emits structured messages through the Apps client logger.
ClientLog(threadName, tag, message)
Signature: extern "C" void ClientLog(const char* threadName, const char* tag, const char* message);
threadName: Human-readable source name (for example: "RenderThread", "ModInit").
tag: Log category for filtering (for example: "VirtualAssets", "HookManager").
message: Null-terminated UTF-8 text to emit.
ClientLog("ModInit", "MyMod", "Native module initialised.");
ClientLog("RenderThread", "Overlay", "Frame callback active.");
C++: TouchAPI
Touch event interception API for Android input events with optional event consumption.
TouchEvent
Fields: action, pointerId, x, y
Coordinates are screen-space pixels in the current surface resolution.
Common action values: 0 = ACTION_DOWN, 1 = ACTION_UP, 2 = ACTION_MOVE, 5 = ACTION_POINTER_DOWN, 6 = ACTION_POINTER_UP.
TouchCallback
Type: typedef bool (*TouchCallback)(const TouchEvent* ev);
Return true to consume/block the event from the game, false to let it continue.
TouchAPI::RegisterCallback(TouchCallback cb)
Registers a touch callback. Callbacks execute in registration order.
TouchAPI::UnregisterCallback(TouchCallback cb)
Unregisters a callback from the touch pipeline.
Note: Safe to call even if the callback is not currently registered.
bool OnTouch(const TouchEvent* ev) {
if (ev->action == 0) {
ClientLog("Input", "Touch", "ACTION_DOWN received");
}
return false; // let the game also receive the touch
}
TouchAPI::RegisterCallback(OnTouch);
// ... later
TouchAPI::UnregisterCallback(OnTouch);
C++: KeyAPI
Keyboard/button event interception from the engine input pipeline.
KeyHandler
Type: typedef bool (*KeyHandler)(int keyCode, int action, int unicodeChar);
action values: 0 = ACTION_DOWN, 1 = ACTION_UP, 2 = ACTION_MULTIPLE.
Return true to consume/block the key event, false to pass it to the game.
Note: Callback runs on the input (JNI) thread. Keep handlers lightweight to avoid lag.
KeyAPI::RegisterHandler(KeyHandler handler)
Registers a key handler. Handlers run in registration order and dispatch stops when one returns true.
KeyAPI::UnregisterHandler(KeyHandler handler)
Unregisters a previously registered handler.
Note: Safe to call even when not registered.
bool OnKey(int keyCode, int action, int unicodeChar) {
if (action == 0) {
ClientLog("Input", "Key", "Key down event");
}
return false;
}
KeyAPI::RegisterHandler(OnKey);
// ... later
KeyAPI::UnregisterHandler(OnKey);
Native Dev Env (Linux)
Build With CMake + Android NDK
# install prerequisites
sudo pacman -S --needed git cmake ninja clang lld jdk17-openjdk unzip
# install Android NDK (example path)
mkdir -p $HOME/Android/Sdk/ndk
# place ndk at: $HOME/Android/Sdk/ndk/27.1.12297006
export ANDROID_NDK_HOME=$HOME/Android/Sdk/ndk/27.1.12297006
export CC=clang
export CXX=clang++
git clone https://github.com/Lodingglue/nise-api.git
cd nise-api
cmake -S . -B build-arm64 \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-26 \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake
cmake --build build-arm64 -j
Native Dev Env (Windows)
Build With CMake + Ninja (Windows)
# install:
# - Git
# - CMake
# - Ninja
# - LLVM/Clang
# - Android NDK (via Android Studio SDK Manager)
set ANDROID_NDK_HOME=C:\Android\Sdk\ndk\27.1.12297006
set CC=clang
set CXX=clang++
git clone https://github.com/Lodingglue/nise-api.git
cd nise-api
cmake -S . -B build-arm64 -G Ninja ^
-DCMAKE_BUILD_TYPE=Release ^
-DANDROID_ABI=arm64-v8a ^
-DANDROID_PLATFORM=android-26 ^
-DCMAKE_TOOLCHAIN_FILE=%ANDROID_NDK_HOME%\build\cmake\android.toolchain.cmake
cmake --build build-arm64 -j
Native Dev Env (Phone / Termux)
Build In Termux + Android Toolchain
pkg update
pkg install -y git cmake ninja clang lld
# If building directly with Android toolchain files,
# keep an NDK copy accessible in storage and point to it:
export ANDROID_NDK_HOME=/sdcard/Android/Sdk/ndk/27.1.12297006
git clone https://github.com/Lodingglue/nise-api.git
cd nise-api
cmake -S . -B build-arm64 \
-G Ninja \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-26 \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake
cmake --build build-arm64 -j