cz_kelui docs
gorilla tag mod menu — unity quad + kelui software rendering, bnm reflection, xr input.
overview
cz_kelui is a gorilla tag (android/meta quest) mod menu injected as a native shared library. it hooks into the game via bnm (il2cpp reflection), renders a floating menu panel using kelui's widget api rasterized in software, and uploads the result to a unity texture on a world-space quad attached to the left hand.
the menu is toggleable mid-game. right hand acts as a pointer. right trigger clicks. mods are organized into tabs and run every frame or once on enable.
controls
file structure
src/
native-lib.cpp — jni entry, unity panel, bnm hooks, audio
gamebridge.cpp — bnm reflection helpers (player fields)
mods.cpp — mod definitions live here
ui.cpp — menu layout, tabs, widget calls
czkelrender.cpp — software rasterizer for kelui vertices
include/
mods.hpp — mod struct + gmods vector
gamebridge.hpp — player reflection api
ui.hpp — menu control api
czkelrender.hpp — rasterizer api
font8x8.h — embedded 8x8 bitmap font (128 chars)
XRInput.hpp/.cpp — vr controller input wrapper
extern/
BNM-Android/ — il2cpp reflection library
kelUI/ui/ — drawing.cpp, Widgets.cpp
minimp3.h — mp3 decoder for ui sounds
czsounds.h — embedded mp3 audio data
src/mods.cpp.
everything else (rendering, input, ui layout) is handled automatically.
adding a mod
open src/mods.cpp. inside registermods(), call either
addtick() or addoneshot() with your mod definition.
that's it.
addtick("Category", "mod name", "tooltip text", []() {
...
});
addoneshot("Category", "mod name", "tooltip text", []() {
...
});
| param | type | description |
|---|---|---|
category | const char* | tab name in the menu. use an existing one or create a new tab by using a new string. |
name | const char* | label shown next to the checkbox. |
desc | const char* | tooltip text shown on hover. |
fn | void(*)() | lambda or function pointer. no parameters, no return value. |
mod types
tick runs every frame
use addtick for mods that need to apply a continuous effect — overriding a field
every frame, applying a force, moving the player, etc.
the function is called every game frame while the checkbox is checked.
addtick("Movement", "Anti Gravity", "push up every frame", []() {
Rigidbody* rb = gamebridge::rb();
if (!rb) return;
rb->AddForce(Vector3(0, 15.0f, 0), ForceMode::Acceleration);
});
oneshot runs once on enable
use addoneshot for mods that set a value once — changing a field that
persists (like max arm length or jump multiplier), teleporting, one-time game state changes.
the function fires on the rising edge (checkbox off to on).
addoneshot("Movement", "Max Jump", "set jump multiplier to 10", []() {
gamebridge::setpf("jumpMultiplier", 10.0f);
gamebridge::setpf("maxJumpSpeed", 999.0f);
});
categories
the category string you pass to addtick/addoneshot becomes a tab in the menu.
the built-in tabs are:
| tab | what belongs here |
|---|---|
"Movement" | speed, fly, jump, locomotion mods |
"Gravity" | physics gravity changes |
"Body" | player body/collider mods (headless, freeze, slide) |
"Hands" | hand offset / arm length mods |
"settings" | reserved — menu settings tab |
to add a new tab, just use a new category string. the tab appears automatically. tabs are ordered by first registration.
categories[]
inside src/ui.cpp, its tab won't show. add the new name to that array
and increment ncategories.
static const char* categories[] = {
"Movement", "Gravity", "Body", "Hands", "Visual"
};
static constexpr int ncategories = 5;
examples
trigger-gated tick mod
addtick("Movement", "Rocket Jump (R-Trigger)", "hold right trigger to launch upward", []() {
if (XRInput::GetFloatFeature(Trigger, Right) < 0.5f) return;
Rigidbody* rb = gamebridge::rb();
if (rb) rb->AddForce(Vector3(0, 25.0f, 0), ForceMode::VelocityChange);
});
reading and restoring a field
static float origspeed = 0.0f;
static bool saved = false;
addtick("Movement", "Super Speed", "10x jump multiplier", []() {
if (!saved) {
origspeed = gamebridge::getpf("jumpMultiplier");
saved = true;
}
gamebridge::setpf("jumpMultiplier", 10.0f);
});
addoneshot("Movement", "Restore Speed", "reset jump multiplier", []() {
if (saved) { gamebridge::setpf("jumpMultiplier", origspeed); saved = false; }
});
button-gated movement
addtick("Movement", "Dash (R-Primary)", "press right A to dash", []() {
if (!XRInput::GetBoolFeature(PrimaryButton, Right)) return;
Transform* b = gamebridge::body();
Transform* r = gamebridge::roottrans();
if (!b || !r) return;
r->SetPosition(r->GetPosition() + b->GetForward() * Time::GetDeltaTime() * 8.0f);
});
customizing colors
widget colors are defined as inline values in extern/kelUI/ui/Widgets.cpp.
find the button(), checkbox(), and tabbutton() functions
and change the Color{r, g, b, a} literals. values are 0.0 to 1.0 floats.
the menu background, border, and title bar colors are in src/ui.cpp
inside buildframe():
kel::Drawing::rect(0, 0, kmenuwidth, kmenuheight, {0.06f, 0.07f, 0.10f, 0.88f});
kel::Drawing::rect(0, 0, kmenuwidth, 1.5f, {0.28f, 0.48f, 0.90f, 0.55f});
kel::Drawing::rect(0, 0, kmenuwidth, ktitleh, {0.12f, 0.30f, 0.72f, 0.98f});
kel::Drawing::text(kpadding, 8.0f, "gorilla tag mods", {0.95f, 0.96f, 1.0f, 1.0f});
color format is { red, green, blue, alpha } — all values between 0.0 and 1.0.
menu size
the framebuffer and panel dimensions are set at the top of two files. change them together to maintain correct aspect ratio.
static constexpr int kfbw = 540;
static constexpr int kfbh = 360;
static constexpr float kpanelw = 0.40f;
static constexpr float kpanelh = 0.27f;
static constexpr float kmenuwidth = 540.0f;
static constexpr float kmenuheight = 360.0f;
kpanelw / kpanelh == kfbw / kfbh (ratio 1.5) to avoid stretching.
e.g. 480x320, 600x400, 720x480 all work.
menu layout
the menu is built entirely in src/ui.cpp. layout constants at the top:
| constant | default | description |
|---|---|---|
ktitleh | 28 | height of the title bar in pixels |
ktabh | 26 | height of each tab button |
ktabw | 88 | width of each tab button |
kpadding | 14 | left/right content padding |
to change the title text, edit the string in drawtitlebar() inside ui.cpp.
gamebridge api
the gamebridge namespace wraps all bnm player reflection. use these inside mod lambdas.
| function | returns | description |
|---|---|---|
gamebridge::player() | void* | raw pointer to the Player instance |
gamebridge::setpf(name, val) | void | set a float field on the player by name |
gamebridge::getpf(name) | float | get a float field from the player by name |
gamebridge::setpb(name, val) | void | set a bool field on the player |
gamebridge::setpv3(name, val) | void | set a Vector3 field on the player |
gamebridge::head() | Transform* | head collider transform |
gamebridge::body() | Transform* | body collider transform |
gamebridge::rb() | Rigidbody* | player rigidbody |
gamebridge::roottrans() | Transform* | GorillaPlayer root transform |
common player field names
| field name | type | notes |
|---|---|---|
"jumpMultiplier" | float | default ~1.1. higher = bigger swings |
"maxJumpSpeed" | float | default ~6.5. cap on launch velocity |
"maxArmLength" | float | default 1.5m. max grab reach |
"defaultSlideFactor" | float | surface grip. 1.0 = full slide |
"disableMovement" | bool | true = tag-freeze the player |
"rightHandOffset" | Vector3 | hand position offset |
"leftHandOffset" | Vector3 | hand position offset |
xrinput api
use these inside tick mod lambdas to read controller state:
| call | returns | description |
|---|---|---|
XRInput::GetBoolFeature(feat, ctrl) | bool | button is pressed |
XRInput::GetFloatFeature(feat, ctrl) | float | analog value 0.0 to 1.0 |
XRInput::GetVector2Feature(feat, ctrl) | Vector2 | thumbstick x/y |
XRInput::SendHapticImpulse(ctrl, amp, dur) | void | vibrate controller |
controllers
Left
Right
bool features
PrimaryButton
SecondaryButton
GripButton
TriggerButton
float features
Trigger
Grip
vector2 features
Primary2DAxis
Secondary2DAxis
building
open the project in android studio. select the release build variant, then build apk.
Build → Select Build Variant → release
the compiled library is output as cz_kelui.so. inject it into gorilla tag
using your preferred android mod loader.
-O3 -ffast-math -funroll-loops
and strips debug symbols. always test with debug first, release for final build.
extern/BNM-Android is empty after cloning,
run git submodule update --init --recursive.