Skip to content

Commit 928d911

Browse files
committed
feat: add ModLoader.launch() static entry and setup guide
Add one-shot ModLoader.launch() that runs CoreCLR on a background thread, simplifying smali injection to a single invoke-static call. New: - ModLoader.launch(): static method, new thread, configurable paths - ModLoader$1: lambda Runnable for async launch with error handling - GET_STARTED.md: full smali injection guide with manual smali code
1 parent fdcaaee commit 928d911

2 files changed

Lines changed: 261 additions & 0 deletions

File tree

‎Android/library/src/main/java/starray/android/modloader/ModLoader.java‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,32 @@ public int start(String dll, String type, String method) {
2727
}
2828

2929
public void stop() { MonoRunner.stop(); }
30+
31+
public static void launch() {
32+
final String runtimeRoot = "/sdcard/ModLoader/runtime";
33+
final String[] assemblyDirs = {
34+
runtimeRoot,
35+
"/sdcard/ModLoader/plugins",
36+
};
37+
final String[] nativeDirs = {
38+
runtimeRoot,
39+
};
40+
41+
new Thread(new Runnable() {
42+
@Override
43+
public void run() {
44+
try {
45+
var loader = new ModLoader()
46+
.dotnetRoot(runtimeRoot);
47+
for (String dir : assemblyDirs)
48+
loader.addAssemblyDir(dir);
49+
for (String dir : nativeDirs)
50+
loader.addNativeDir(dir);
51+
loader.start("ModLoader.dll", "StArray.ModLoader.Mono", "Entry");
52+
} catch (Exception e) {
53+
Log.e(TAG, "launch failed", e);
54+
}
55+
}
56+
}, "ModLoader-Main").start();
57+
}
3058
}

‎GET_STARTED.md‎

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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

Comments
 (0)