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