|
| 1 | +# Getting Started |
| 2 | + |
| 3 | +Inject StArray.ModLoader into an existing Unity IL2CPP Android APK. |
| 4 | + |
| 5 | +## Prerequisites |
| 6 | + |
| 7 | +- JDK 17+ |
| 8 | +- Android SDK / NDK 27+ |
| 9 | +- apktool (https://apktool.org) |
| 10 | +- uber-apk-signer (https://github.com/patrickfav/uber-apk-signer) |
| 11 | +- A Unity IL2CPP arm64-v8a APK |
| 12 | + |
| 13 | +## Build the Loader |
| 14 | + |
| 15 | +```bash |
| 16 | +# 1. Build native libraries |
| 17 | +cd Android |
| 18 | +./gradlew :library:assembleRelease |
| 19 | +# output: library/build/outputs/aar/library-release.aar |
| 20 | + |
| 21 | +# 2. Build managed mod |
| 22 | +cd StArray.ModLoader |
| 23 | +dotnet build -c Release |
| 24 | +# output: bin/Release/net10.0/ModLoader.dll |
| 25 | +``` |
| 26 | + |
| 27 | +## Inject via Smali |
| 28 | + |
| 29 | +### Step 1: Decompile the target APK |
| 30 | + |
| 31 | +```bash |
| 32 | +apktool d target.apk -o target_src |
| 33 | +``` |
| 34 | + |
| 35 | +### Step 2: Add native libraries |
| 36 | + |
| 37 | +Copy these `.so` files to `target_src/lib/arm64-v8a/`: |
| 38 | + |
| 39 | +From `library-release.aar` (extract with zip): |
| 40 | +- `jni/arm64-v8a/libmodloader.so` |
| 41 | +- `jni/arm64-v8a/libmonodroid.so` |
| 42 | +- `jni/arm64-v8a/libcimgui.so` |
| 43 | + |
| 44 | +From CoreCLR runtime (`Runtime/runtime/native/`): |
| 45 | +- `libcoreclr.so` |
| 46 | +- `libclrjit.so` |
| 47 | +- `libSystem.Native.so` |
| 48 | +- `libSystem.Globalization.Native.so` |
| 49 | +- `libSystem.IO.Compression.Native.so` |
| 50 | +- `libSystem.Security.Cryptography.Native.Android.so` |
| 51 | + |
| 52 | +### Step 3: Add .NET runtime and mod files |
| 53 | + |
| 54 | +Push to device or embed in APK assets: |
| 55 | + |
| 56 | +``` |
| 57 | +/sdcard/ModLoader/runtime/ |
| 58 | + *.dll (.NET runtime assemblies from Runtime/runtime/) |
| 59 | + ModLoader.dll (built managed mod) |
| 60 | +``` |
| 61 | + |
| 62 | +Alternatively, place them in `target_src/assets/ModLoader/` and adjust the |
| 63 | +startup path in Step 4 accordingly. |
| 64 | + |
| 65 | +### Step 4: Inject smali startup code |
| 66 | + |
| 67 | +Find the Unity player activity smali file (usually `com/unity3d/player/UnityPlayerActivity.smali` |
| 68 | +or a subclass). In its `onCreate` method, insert after `invoke-super`: |
| 69 | + |
| 70 | +```smali |
| 71 | +invoke-static {}, Lstarray/android/modloader/ModLoader;->launch()V |
| 72 | +``` |
| 73 | + |
| 74 | +Also add these smali classes to `target_src/smali/`. If you compiled from |
| 75 | +source, extract them from the AAR. Otherwise create them manually: |
| 76 | + |
| 77 | +`smali/starray/android/modloader/ModLoader.smali`: |
| 78 | + |
| 79 | +```smali |
| 80 | +.class public Lstarray/android/modloader/ModLoader; |
| 81 | +.super Ljava/lang/Object; |
| 82 | +.source "ModLoader.java" |
| 83 | +
|
| 84 | +.field private static final TAG:Ljava/lang/String; = "StArray.ModLoader" |
| 85 | +
|
| 86 | +.method public constructor <init>()V |
| 87 | + .registers 1 |
| 88 | + invoke-direct {p0}, Ljava/lang/Object;-><init>()V |
| 89 | + return-void |
| 90 | +.end method |
| 91 | +
|
| 92 | +.method public static launch()V |
| 93 | + .registers 7 |
| 94 | +
|
| 95 | + const-string v0, "/sdcard/ModLoader/runtime" |
| 96 | + filled-new-array {v0, v0}, [Ljava/lang/String; |
| 97 | + move-result-object v1 |
| 98 | + filled-new-array {v0}, [Ljava/lang/String; |
| 99 | + move-result-object v2 |
| 100 | +
|
| 101 | + new-instance v3, Lstarray/android/modloader/ModLoader$1; |
| 102 | + invoke-direct {v3, v0, v1, v2}, Lstarray/android/modloader/ModLoader$1;-><init>(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V |
| 103 | +
|
| 104 | + new-instance v4, Ljava/lang/Thread; |
| 105 | + const-string v5, "ModLoader-Main" |
| 106 | + invoke-direct {v4, v3, v5}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;Ljava/lang/String;)V |
| 107 | + invoke-virtual {v4}, Ljava/lang/Thread;->start()V |
| 108 | + return-void |
| 109 | +.end method |
| 110 | +
|
| 111 | +.method public dotnetRoot(Ljava/lang/String;)Lstarray/android/modloader/ModLoader; |
| 112 | + .registers 2 |
| 113 | + invoke-static {p1}, Lnet/dot/MonoRunner;->dotnetRoot(Ljava/lang/String;)Lnet/dot/MonoRunner; |
| 114 | + return-object p0 |
| 115 | +.end method |
| 116 | +
|
| 117 | +.method public addAssemblyDir(Ljava/lang/String;)Lstarray/android/modloader/ModLoader; |
| 118 | + .registers 2 |
| 119 | + invoke-static {p1}, Lnet/dot/MonoRunner;->addAssemblyDir(Ljava/lang/String;)Lnet/dot/MonoRunner; |
| 120 | + return-object p0 |
| 121 | +.end method |
| 122 | +
|
| 123 | +.method public addNativeDir(Ljava/lang/String;)Lstarray/android/modloader/ModLoader; |
| 124 | + .registers 2 |
| 125 | + invoke-static {p1}, Lnet/dot/MonoRunner;->addNativeDir(Ljava/lang/String;)Lnet/dot/MonoRunner; |
| 126 | + return-object p0 |
| 127 | +.end method |
| 128 | +
|
| 129 | +.method public start(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I |
| 130 | + .registers 5 |
| 131 | + invoke-static {p1, p2, p3}, Lnet/dot/MonoRunner;->run(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I |
| 132 | + move-result v0 |
| 133 | + return v0 |
| 134 | +.end method |
| 135 | +``` |
| 136 | + |
| 137 | +`smali/starray/android/modloader/ModLoader$1.smali`: |
| 138 | + |
| 139 | +```smali |
| 140 | +.class Lstarray/android/modloader/ModLoader$1; |
| 141 | +.super Ljava/lang/Object; |
| 142 | +.implements Ljava/lang/Runnable; |
| 143 | +.source "ModLoader.java" |
| 144 | +
|
| 145 | +.field final synthetic val$assemblyDirs:[Ljava/lang/String; |
| 146 | +.field final synthetic val$nativeDirs:[Ljava/lang/String; |
| 147 | +.field final synthetic val$runtimeRoot:Ljava/lang/String; |
| 148 | +
|
| 149 | +.method constructor <init>(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)V |
| 150 | + .registers 4 |
| 151 | + invoke-direct {p0}, Ljava/lang/Object;-><init>()V |
| 152 | + iput-object p1, p0, Lstarray/android/modloader/ModLoader$1;->val$runtimeRoot:Ljava/lang/String; |
| 153 | + iput-object p2, p0, Lstarray/android/modloader/ModLoader$1;->val$assemblyDirs:[Ljava/lang/String; |
| 154 | + iput-object p3, p0, Lstarray/android/modloader/ModLoader$1;->val$nativeDirs:[Ljava/lang/String; |
| 155 | + return-void |
| 156 | +.end method |
| 157 | +
|
| 158 | +.method public run()V |
| 159 | + .registers 7 |
| 160 | + :try_start |
| 161 | + new-instance v0, Lstarray/android/modloader/ModLoader; |
| 162 | + invoke-direct {v0}, Lstarray/android/modloader/ModLoader;-><init>()V |
| 163 | + iget-object v1, p0, Lstarray/android/modloader/ModLoader$1;->val$runtimeRoot:Ljava/lang/String; |
| 164 | + invoke-virtual {v0, v1}, Lstarray/android/modloader/ModLoader;->dotnetRoot(Ljava/lang/String;)Lstarray/android/modloader/ModLoader; |
| 165 | +
|
| 166 | + iget-object v1, p0, Lstarray/android/modloader/ModLoader$1;->val$assemblyDirs:[Ljava/lang/String; |
| 167 | + array-length v2, v1 |
| 168 | + const/4 v3, 0x0 |
| 169 | + :loop_asm |
| 170 | + if-lt v3, v2, :loop_nat |
| 171 | + aget-object v4, v1, v3 |
| 172 | + invoke-virtual {v0, v4}, Lstarray/android/modloader/ModLoader;->addAssemblyDir(Ljava/lang/String;)Lstarray/android/modloader/ModLoader; |
| 173 | + add-int/lit8 v3, v3, 0x1 |
| 174 | + goto :loop_asm |
| 175 | +
|
| 176 | + :loop_nat |
| 177 | + iget-object v1, p0, Lstarray/android/modloader/ModLoader$1;->val$nativeDirs:[Ljava/lang/String; |
| 178 | + array-length v2, v1 |
| 179 | + const/4 v3, 0x0 |
| 180 | + :loop_n |
| 181 | + if-lt v3, v2, :call |
| 182 | + aget-object v4, v1, v3 |
| 183 | + invoke-virtual {v0, v4}, Lstarray/android/modloader/ModLoader;->addNativeDir(Ljava/lang/String;)Lstarray/android/modloader/ModLoader; |
| 184 | + add-int/lit8 v3, v3, 0x1 |
| 185 | + goto :loop_n |
| 186 | +
|
| 187 | + :call |
| 188 | + const-string v1, "ModLoader.dll" |
| 189 | + const-string v2, "StArray.ModLoader.Mono" |
| 190 | + const-string v3, "Entry" |
| 191 | + invoke-virtual {v0, v1, v2, v3}, Lstarray/android/modloader/ModLoader;->start(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I |
| 192 | + :try_end |
| 193 | + .catch Ljava/lang/Exception; {:try_start .. :try_end} :catch_ex |
| 194 | + return-void |
| 195 | +
|
| 196 | + :catch_ex |
| 197 | + move-exception v0 |
| 198 | + const-string v1, "StArray.ModLoader" |
| 199 | + const-string v2, "launch failed" |
| 200 | + invoke-static {v1, v2, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I |
| 201 | + return-void |
| 202 | +.end method |
| 203 | +``` |
| 204 | + |
| 205 | +Also add `MonoRunner` and `ModManagerUtils` (with their inner classes) |
| 206 | +from the AAR. |
| 207 | + |
| 208 | +### Step 5: Repackage and sign |
| 209 | + |
| 210 | +```bash |
| 211 | +apktool b target_src -o target_patched.apk |
| 212 | +uber-apk-signer --apks target_patched.apk |
| 213 | +``` |
| 214 | + |
| 215 | +### Step 6: Install and run |
| 216 | + |
| 217 | +```bash |
| 218 | +adb install target_patched.apk |
| 219 | +adb shell mkdir -p /sdcard/ModLoader/runtime |
| 220 | +adb push Runtime/runtime/*.dll /sdcard/ModLoader/runtime/ |
| 221 | +adb push StArray.ModLoader/bin/Release/net10.0/ModLoader.dll /sdcard/ModLoader/runtime/ |
| 222 | +adb logcat -s StArray ImGuiRender JNIHelper |
| 223 | +``` |
| 224 | + |
| 225 | +## Verify |
| 226 | + |
| 227 | +The ImGui overlay window should appear after the Unity splash screen. Logcat |
| 228 | +should show: |
| 229 | + |
| 230 | +``` |
| 231 | +StArray.MonoRunner: CoreCLR initialized |
| 232 | +ImGuiRender: ImGui initialized with official OpenGL3 + Android input backends |
| 233 | +``` |
0 commit comments