Architecture
How the Launcher Works
The PVZRH Launcher injects BepInEx into the game process at runtime using Pine (a Java method hooking framework). No native bootstrap library is required — all hooks are installed from Kotlin/Java.
Injection Flow
- Package Context —
createPackageContext()for PVZRH obtains the game's class loader and DEX access. - Pine Hooks — Installs hooks for ClassLoader (bidirectional), Instrumentation, PackageManager, native library loading, and UnityPlayer.
- Activity Redirection —
Instrumentation.execStartActivityhook redirects the game activity to a manifest-registeredStubActivity. - Activity Restoration —
Instrumentation.newActivityrestores the real game activity class and original Intent. - Context Wrapping —
Activity.attachBaseContexthook wraps the Context with a three-wayCustomContextWrapper:- Game resources (Assets, Resources, Theme) → PVZRH package context
- File/storage (getFilesDir, SharedPreferences) → launcher Application
- Window services (getDisplay, getSystemService) → original Activity base Context
- Native Library Redirection —
ClassLoader.findLibrary()hook routes .so loading: game libs from game APK, Fusion libs from launcher, .NET/il2cpp/unity libs from data directory. - UnityPlayer Setup — Constructor hook sets the activity field and shows injection overlay.
- Integrity Check Bypass —
UnityPlayer.kill()hook blocks the first call for 5 seconds to survive Unity's integrity checks.
Context Wrapper
The CustomContextWrapper is the core mechanism that allows the game and launcher to coexist in the same process:
| Method | Routes To | Purpose |
|---|---|---|
getAssets(), getResources(), getTheme() | Game package context | Game loads its own resources |
getFilesDir(), getCacheDir(), getSharedPreferences() | Launcher Application | Launcher storage isolated from game |
getSystemService(), getDisplay() | Original Activity context | Window services work normally |
getApplicationContext() | Launcher Application | App-level singletons stay in launcher |
Native Library Loading
The ClassLoader.findLibrary() hook uses a three-way redirection table:
| Category | Source | Example |
|---|---|---|
| Fusion libraries | Launcher APK | libmain.so, libfusion.so |
| Data libraries | Data directory | libil2cpp.so, libunity.so |
| Game libraries | Game APK | All other .so files |
ClassLoader Hook
A bidirectional ClassLoader hook enables cross-class-loader references:
- Game classes can reference BepInEx mod types
- BepInEx mods can reference game types
- Falls back to the other loader on
ClassNotFoundException
Save Data Access
The launcher uses different methods to access game save data depending on the Android version:
| Android Version | API Level | Method | Notes |
|---|---|---|---|
| Android 10 and below | ≤29 | Direct file access | No special permissions needed |
| Android 11 | 30 | SAF (Storage Access Framework) | Requires user to grant directory access via system picker |
| Android 12+ | ≥31 | Shizuku | Requires Shizuku app installed and authorized |
Why Shizuku? Android 12+ restricted SAF's ability to persistently access the Android/data directory. Shizuku provides shell-level (uid 2000) access that works across all Android versions without the restrictions of SAF.
Backup location:
/storage/emulated/0/PVZRH_Launcher/<package>/saves_backup/
├── G2L/ # Game → Launcher backup
└── L2G/ # Launcher → Game restore