Skip to content

About

An old armv7 il2cpp beatsaber hooker adapted to work with Attack On Quest.

Resources

Stars

1 star

Watchers

0 watching

Forks

 
 

Latest commit

 

History

41 Commits

Folders and files

Repository files navigation

Attack on Quest — ARMv7 Modloader + Mod Manager

Forked from jakibaki/beatsaber-hook. Modernized for NDK r26d and Android 12.

Demo

A lightweight C/C++ runtime function hooking framework for injecting custom code into the Unity IL2CPP engine of Attack on Quest (AoQ), with a fully integrated in-game mod manager.

Modern ARM64 modloaders (MelonLoader, QuestHooks) crash on AoQ because it's an older ARMv7 title. This framework uses Android-Inline-Hook to safely patch 32-bit memory offsets.


How It Works

A static constructor (<clinit>) is injected into the game's compiled Smali bytecode. When Unity boots it triggers libmodloader.so, which:

  1. Runs the Mod Manager — installs UI hooks and writes default configs
  2. Scans /sdcard/DCIM/AoQMods/mods/ and dlopens every .so found there, right after the IL2CPP runtime initialises and before the first scene loads, so mods' Awake/Start hooks fire on the very first load

On a fresh install the storage permission isn't granted yet at that point, so that one launch falls back to loading mods on the first frame after the user approves the prompt (the first scene's Awake/Start have already run by then). Every later launch loads early. The EarlyModLoad setting (see Mod Manager Settings) turns early loading off.

The Mod Manager is baked directly into libmodloader.so — it is not a separate file in the mods folder. It injects a Mods button into the game's main menu that opens an in-game panel for enabling/disabling mods and editing mod configs.


Repository Layout

AoQ-ModLoader-For-Quest/
├── signing/
│   └── debug.keystore  # public key every patched APK is signed with (see Step 2.4)
├── modloader/          # Builds libmodloader.so (loader + mod manager)
│   ├── main.c          # Entry point (crash handlers, modmanager_init) + load_mods()
│   ├── Android.mk
│   └── Application.mk
├── modmanager/         # Mod manager source — baked into libmodloader.so
│   ├── main.c          # Hook installation, bundle extraction, config init
│   ├── ui_inject.c     # Injects Mods button into game main menu
│   ├── ui_panel.c      # In-game panel: tab switching, list population
│   ├── il2cpp.c/.h     # IL2CPP runtime bindings (function pointers, type objects)
│   ├── modlist.c/.h    # Mod discovery: scan mods/ and disabledmods/ folders
│   ├── modconfig.c/.h  # JSON config read/write per mod
│   ├── shader_fix.c    # Unity shader compatibility fixes for the UI panel
│   ├── rva.h           # All IL2CPP RVA offsets (AoQ 0.5.0)
│   ├── cJSON.c/.h      # Embedded JSON parser
│   ├── bundle_data.h   # Embedded asset bundle (UI prefab, generated)
│   └── utils.c/.h      # IL2CPP string helpers, Unity object utilities
├── shared/
│   ├── inline-hook/    # Android-Inline-Hook (ARMv7 trampoline engine)
│   ├── utils/          # getRealOffset(), MAKE_HOOK, INSTALL_HOOK macros
│   └── modapi/
│       └── modapi.h    # Header-only mod registration API (include in your mod)
└── mod-sample/         # Minimal example mod showing modapi.h usage

Quick Start — Sideload the Pre-Patched APK

Download the pre-patched APK from the Releases page and sideload it:

adb install -r AoQ_Patched.apk

Then skip to Step 4.


Step 1 — Build the Modloader

Prerequisites: any recent Android NDK (r26+)

git clone https://github.com/Treyo1928/AoQ-Modloader.git
cd AoQ-Modloader/modloader
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk NDK_APPLICATION_MK=./Application.mk APP_ABI=armeabi-v7a APP_PLATFORM=android-21

Output: libs/armeabi-v7a/libmodloader.so

This single binary contains both the mod loader and the mod manager.


Step 2 — Patch the APK

Skip if using the pre-patched release APK.

Prerequisites:

  • apktool
  • zipalign + apksigner (Android SDK build-tools)
  • A valid Android keystore

1. Unpack

apktool d AoQ-0.5.0.apk -o AoQ_Extracted

2. Drop in the payload

Copy libmodloader.so into AoQ_Extracted/lib/armeabi-v7a/.

3. Smali injection

Open AoQ_Extracted/smali/com/unity3d/player/UnityPlayerActivity.smali and paste this block at the very end of the file (after all existing .method blocks):

.method static constructor <clinit>()V
    .locals 1

    const-string v0, "modloader"
    invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

    return-void
.end method

3b. Lower targetSdkVersion to 29 (REQUIRED)

AoQ 0.5.0 ships with targetSdkVersion="30". At target 30, Android 11+ enforces scoped storage and ignores the requestLegacyExternalStorage flag — the game then can't read mods or write configs under /sdcard/DCIM/AoQMods/ even after the user approves the storage permission (you'll see Operation not permitted in logcat and the mod list will show only the mod manager itself). Setting targetSdkVersion="29" re-enables legacy external storage, so the single runtime storage prompt is all a user needs — no adb grants, no "All files access" toggle.

apktool d -r keeps the manifest as binary AXML. Either re-decode without -r so AndroidManifest.xml is editable text (change targetSdkVersion to 29), or binary-patch the targetSdkVersion attribute (Android resource id 0x01010270, INT_DEC value) from 30 to 29 in place. Verify afterwards:

aapt dump badging AoQ_Patched.apk | grep targetSdkVersion   # -> '29'

4. Repack and sign

Sign with the keystore in this repo (signing/debug.keystore). It's the stock debug key bundled with uber-apk-signer, and every released patched APK is signed with it. Android only installs an update over an existing install when the signing key matches, so using it means people can upgrade without uninstalling or losing saves. It's deliberately public; it protects nothing.

apktool b AoQ_Extracted -o AoQ_Modded.apk
zipalign -p -f 4 AoQ_Modded.apk AoQ_Patched.apk
apksigner sign --ks signing/debug.keystore --ks-key-alias androiddebugkey \
    --ks-pass pass:android --key-pass pass:android AoQ_Patched.apk
apksigner verify --print-certs AoQ_Patched.apk | grep SHA-256
# -> 1e08a903aef9c3a721510b64ec764d01d3d094eb954161b62544ea8f187b5953

Updating only the loader in an existing patched APK (e.g. a new modloader release): swap libmodloader.so, drop the old v1 signature files, then re-align and re-sign with the same key:

cp AoQ_Patched_old.apk AoQ_Patched.apk
zip -d AoQ_Patched.apk 'META-INF/*.SF' 'META-INF/*.RSA' 'META-INF/MANIFEST.MF'
mkdir -p stage/lib/armeabi-v7a && cp modloader/libs/armeabi-v7a/libmodloader.so stage/lib/armeabi-v7a/
(cd stage && zip -9 ../AoQ_Patched.apk lib/armeabi-v7a/libmodloader.so)
zipalign -p -f 4 AoQ_Patched.apk AoQ_Patched_aligned.apk
apksigner sign --ks signing/debug.keystore --ks-key-alias androiddebugkey \
    --ks-pass pass:android --key-pass pass:android AoQ_Patched_aligned.apk
adb install -r AoQ_Patched_aligned.apk   # upgrades in place, saves kept

Step 3 — Sideload the Patched APK

The modded APK is signed with a different key than the vanilla game, so Android will reject installing it over the top. You must uninstall the vanilla game first — but you can do this without losing your save data.

Already have a patched APK installed? Newer patched APKs are signed with the same key (signing/debug.keystore), so just install the new one over the top (adb install -r, or drag it onto SideQuest). No uninstall, saves kept.

In SideQuest

  1. Open SideQuest and connect your headset
  2. Click Currently Installed Apps (the grid icon in the top bar)
  3. Find Attack on Quest, click the settings cog, and choose Backup Game Data
  4. Once the backup finishes, uninstall Attack on Quest
  5. Install the modded APK by dragging and dropping AoQ_Patched.apk onto the SideQuest window (or use the Install APK from folder button)
  6. Go back to Currently Installed Apps, find Attack on Quest, and use Restore Game Data to restore your save
  7. Launch the game once and approve the storage permission prompt. This one grant (thanks to the target-SDK-29 patch above) is all that's needed for the game to read your mods — no adb, no extra settings.

If you see INSTALL_FAILED_UPDATE_INCOMPATIBLE you skipped the uninstall step — uninstall the vanilla game in SideQuest first, then install the patched APK.

If the Mods menu opens but lists only the mod manager, the game doesn't have storage access: re-approve the prompt, or in the headset go Settings → Apps → Attack on Quest → Permissions → Storage → Allow. If you built your own APK and skipped step 3b, that's the cause.


Step 4 — Install a Mod

Push your mod:

adb push libmymod.so /sdcard/DCIM/AoQMods/mods/

The modloader creates all required directories automatically on first launch — no manual setup needed. Launch the game and any .so files in the mods folder will be loaded. Use the Mods button in the main menu to manage and configure them in-game.


Step 5 — Write a Mod

Mods are C shared libraries with an auto-executing constructor. See mod-sample/ for a ready-to-build example. See MODDING_GUIDE.md for the full reference.

Minimal mod

#include <android/log.h>
#include "../shared/inline-hook/inlineHook.h"
#include "../shared/utils/utils.h"
#include "../shared/modapi/modapi.h"

#define TAG "MyMod"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)

__attribute__((constructor)) void lib_main()
{
    LOGI("mod loaded!");

    /* Register with the in-game mod manager */
    aoqmm_register("libmymod.so", "My Mod", "1.0.0", "YourName",
                   "Short description shown in the mod manager.");
}

Mod with config

#include "../shared/modapi/modapi.h"

__attribute__((constructor)) void lib_main()
{
    aoqmm_register("libmymod.so", "My Mod", "1.0.0", "YourName",
                   "Does something cool.");

    /* Declare default config — written on first run; entries you add in later
     * versions are merged into existing files, user edits preserved */
    aoqmm_ensure_config("libmymod.so",
        "{\n"
        "  \"entries\": [\n"
        "    {\"key\":\"Enabled\",\"type\":\"bool\",\"value\":true,"
            "\"description\":\"Enables the mod.\"},\n"
        "    {\"key\":\"Strength\",\"type\":\"float\",\"value\":1.0,"
            "\"description\":\"Effect strength (0.0–2.0).\"}\n"
        "  ]\n"
        "}\n"
    );

    /* Read back values at runtime */
    /* (Use modconfig.h load_config / get_entry if you want to read them in C) */
}

Config files are stored at:

/sdcard/DCIM/AoQMods/modconfigs/<modname>.json

They are editable in-game via the Mods → Configure Mods panel.

When a newer version of a mod (or of the loader) adds config entries, they're merged into the existing file on the next launch: new keys appear with their defaults, descriptions are refreshed, and every value the user changed is kept. See MODDING_GUIDE.md §3 for the exact rules.


On-Device File Layout

/sdcard/DCIM/AoQMods/
├── mods/
│   ├── libmymod.so          # enabled mods
│   └── disabledmods/        # disabled mods (folder name configurable)
├── modconfigs/
│   ├── aoqmodmanager.json   # mod manager settings
│   ├── mymod.json           # your mod's config (auto-created by aoqmm_ensure_config)
│   └── mymod.meta.json      # display name / version / author (written by aoqmm_register)

Mod Manager Settings

The mod manager exposes its own settings through the Configure Mods panel under Mod Manager:

Key Type Default Description
ShowModdedText bool true Show "MODDED (N mods)" in the main menu version text
ShowAllMods bool false Show mods with no config in the Configure Mods list
DisabledModsFolder string disabledmods Subfolder inside mods/ for disabled .so files. Renaming this migrates files automatically.
ModRefreshInterval int 0 Auto-refresh interval in seconds (0 = never)
EarlyModLoad bool true Load mods before the first scene starts, so their Awake/Start hooks fire on the first load. Set to false to load on the first frame like v1.3 and earlier (only needed if a mod misbehaves when loaded early). Takes effect next launch.

Settings added by newer loader versions (like EarlyModLoad) are merged into an existing aoqmodmanager.json automatically — no need to delete it.


Verify It's Working

adb logcat -s AoQModManager,QuestHook

You should see:

QuestHook: Welcome!
AoQModManager: aoqmodmanager: loading...
AoQModManager: Hooks installed.
QuestHook: Loading mods!
QuestHook: Done loading mods!
AoQModManager: mods loaded early, before the first scene

On the very first launch after installing (before the storage prompt is approved) the last line reads mods loaded on the first reachable frame instead.

About

An old armv7 il2cpp beatsaber hooker adapted to work with Attack On Quest.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages