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);

RenderAPI::RegisterUnderUI(RenderCallback cb)

Registers a render callback that runs beneath the game's UI layer, invoked every frame before the game's UI and overlay elements are drawn.

Useful for background visuals, world-space overlays, or effects that should appear underneath menus and HUD components.

Note: Multiple callbacks may be registered and execute in registration order. The callback pointer must remain valid while registered. Duplicate registrations are ignored or may result in multiple calls depending on implementation.

RenderAPI::UnregisterUnderUI(RenderCallback cb)

Unregisters a previously registered under-UI render callback, removing it from the under-UI render pipeline.

Note: Safe to call even if the callback is not currently registered.

void DrawBackground() {
    // render behind UI elements
}

RenderAPI::RegisterUnderUI(DrawBackground);
// ... later
RenderAPI::UnregisterUnderUI(DrawBackground);

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