This project provides tools for setting up a Quest-like emulator and patching APK files to run within it.
Based upon the guide written here: https://github.com/kodenamekrak/beat-saber-emulator-guide
This CLI intends to streamline many of the nuisance steps in the guide.
This deliberately does not include any APKs related to Horizon services. See https://github.com/kodenamekrak/beat-saber-emulator-guide#installing-beat-saber
we first need to install Horizon, otherwise the game will immediately crash. You will need to install an older version of Horizon as recent ones will also crash. You can download it here, otherwise you can extract the (unsigned) APK yourself from the system partition of old OS versions via https://cocaine.trade/Quest_2_firmware. The version I used is 51062580065800150
Usage: quest_emu_setup [OPTIONS] <COMMAND>
Commands:
setup
apk
help Print this message or the help of the given subcommand(s)
Options:
--yes Skip all prompts and use default values
-h, --help Print helpEach device backend is a feature. Both are on by default, and at least one is required:
| Feature | Default | Enables |
|---|---|---|
avd |
yes | The Android SDK emulator backend (--backend emulator): SDK/emulator/system image setup avd, create, and running AVDs with start avd/stop |
waydroid |
yes | The Waydroid backend (--backend waydroid). Waydroid itself only runs on Linux |
The adb-based commands (apk install, debug) work with either backend, as
does APK download/patch.
AVD is Google's emulatorm provided with their Android SDK. It has at times worked while others not. On Linux, I encountered various bugs and was rather frustrated with it. However, this is the most "official" emulator for Android you can use.
quest_emu setup avd # SDK cmdline-tools, then the emulator, platform-tools and system image
quest_emu create # create an AVD (default: android13desktop, 1920x1080, 60 fps)
quest_emu start avd # run it in the foreground until it exitssetup avd installs into $ANDROID_SDK_ROOT / $ANDROID_HOME (default
~/Android/Sdk) and asks before each download (--yes skips the prompts). The
default system image matches the host CPU: x86_64, or arm64-v8a on arm64 hosts.
create --overwrite replaces an existing AVD of the same name. start avd --fresh
boots without the saved snapshot, and anything after -- is passed to the emulator.
A Waydroid container, reached over TCP adb, built with the
waydroid feature. Waydroid has no AVDs, so there is no create step.
Waydroid is interesting as it runs natively on Linux with little overhead (GPU accelerated on non-NVIDIA GPUs). ARM64 instruction translation is configured additionally.
quest_emu setup waydroid # checks install, `waydroid init` and ARM translation
quest_emu start waydroid # container + session + UI, then adb connectsetup waydroid checks that Waydroid is installed and initialized
(sudo waydroid init -s VANILLA), and that ARM translation (libndk_translation or
libhoudini) is configured. Quest apps are arm64-only and won't install without it.
quest_emu never runs anything as root. When a step is missing, it stops and
prints the command to run, for ARM translation typically via
waydroid_script. Likewise,
start waydroid asks you to sudo systemctl start waydroid-container if the
container service isn't running.
start waydroid opens the Waydroid UI on purpose. While no UI is
open, Waydroid freezes the container, and adb then reports the device as offline.
stop, apk install and debug take --backend emulator|waydroid. If it's
omitted, they use whatever is running (Waydroid first, then an emulator), then
the backend last used by setup or start, then whatever is installed.
quest_emu stop --backend waydroid # waydroid session stop
quest_emu stop --backend emulator # adb emu killapk and debug talk to exactly one adb device:
-s/--serial <serial>(TCP serials like192.168.240.112:5555areadb connected automatically)$ANDROID_SERIAL--backend waydroid/--backend emulator- otherwise the only online device, or a running Waydroid session
If several devices are connected and none was picked, the command errors instead of guessing.
quest_emu apk install ./apk/1.44.1_20239 # folder with one .apk + its .obb files
quest_emu apk install --backend waydroid ./beatsaber.apkapk install reads the package name from the APK's manifest, so the file can
have any name. It never deletes the input folder. apk download --install --cleanup
deletes the downloaded version folder after a successful install.
The app must be debuggable (android:debuggable="true"), since lldb-server
is copied in with run-as. The lldb-server arch is taken from the installed
app's ABI, not the device's. On an x86_64 device with ARM translation
(Waydroid, x86_64 emulator images), an arm64 app resolves to arm64.
quest_emu debug install <package> [arch] # push + copy lldb-server into the app's private storage
quest_emu debug attach <package> # install + start lldb-server + attach info, in one goattach installs lldb-server into the app's private storage, starts it as a
platform server, forwards the debug port, and by default attaches to the app
as it's already running — pass --relaunch to force-stop and relaunch it
fresh first (useful right after installing/reinstalling the app, or when you
want a clean process). It prints the 4 lines needed to actually connect a
debugger once the target process is found:
Port: 5039
Attach URI: vscode://llvm-vs-code-extensions.lldb-dap/start?config=...
PID: 12345
Config: {"request":"attach","pid":12345,"attachCommands":[...]}
If you're iterating on a QPM
mod, build it first so quest_emu has a .so with debug symbols to load, then
attach with that build folder as --symbols (any .so files under it are
picked up automatically):
qpm build # or ./build.sh / build.ps1, produces ./build/libmymod.so
quest_emu debug attach com.beatgames.beatsaber --relaunch --symbols ./build --open--open opens the printed Attach URI in VS Code directly (requires the
llvm-vs-code-extensions.lldb-dap
extension) and starts debugging immediately, no launch.json needed.
For a reusable debug entry instead of --open, use quest_emu as a
preLaunchTask that writes the fresh pid to a file, and read it back into
launch.json with the Command Variable
extension:
.vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "quest-emu: attach",
"type": "shell",
"command": "quest_emu",
"args": [
"debug", "attach", "com.beatgames.beatsaber",
"--relaunch",
"--symbols", "./build",
"--pid-file", "${workspaceFolder}/.vscode/.quest-pid"
],
"problemMatcher": [],
"presentation": { "reveal": "always", "panel": "dedicated" }
}
]
}.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Quest mod (lldb-dap)",
"type": "lldb-dap",
"request": "attach",
"preLaunchTask": "quest-emu: attach",
"pid": "${input:questPid}",
"attachCommands": [
"platform select remote-android",
"settings set target.inherit-env false",
"platform connect connect://localhost:5039",
"attach -p ${input:questPid}",
"pro hand -p true -s false SIGPWR",
"pro hand -p true -s false SIGXCPU",
"pro hand -p true -s false SIG33",
"target symbols add ./build/libmymod.so"
]
}
],
"inputs": [
{
"id": "questPid",
"type": "command",
"command": "extension.commandvariable.file.content",
"args": { "fileName": "${workspaceFolder}/.vscode/.quest-pid" }
}
]
}Hitting F5 runs the task (installs/starts lldb-server, force-stops and
relaunches the app, writes its pid to .vscode/.quest-pid), then the
questPid input re-reads that file — after the task finishes, so it's always
current — and feeds it into both the pid field and the attach -p command
before lldb-dap connects. No copy-pasting between runs.
platform connect connect://localhost:<port>: the port isadb forwarded to the device, so it's alwayslocalhost; update it if you pass--port.target symbols add <path>: one per.sounder the path(s) passed to--symbols— here, QPM's./buildoutput.