Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ android {
minSdk = 28
targetSdk = 35
versionCode = ciBuildNumber
versionName = "1.1.1-ci.$ciBuildNumber"
versionName = "1.1.2-ci.$ciBuildNumber.for-maliang"

ndk {
abiFilters += "arm64-v8a"
Expand Down
8 changes: 5 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
<uses-feature android:glEsVersion="0x00030000" />
<uses-feature android:name="android.hardware.vulkan.version" android:required="false" />

<!-- Android 11+: discover launchable apps for Unity IL2CPP detection -->
<!-- Android 11+: the two supported games, plus launchable apps for detection -->
<queries>
<package android:name="com.LanPiaoPiao.PlantsVsZombiesRH" />
<package android:name="com.LanPiaoPiao.PlantsVsZombiesRHMod" />
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down Expand Up @@ -90,10 +92,10 @@
android:name="com.bepinex.android.StubActivity"
android:taskAffinity="${applicationId}.game"
android:theme="@style/UnityThemeSelector"
android:screenOrientation="landscape"
android:process=":game"
android:launchMode="standard"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:resizeableActivity="false"
android:hardwareAccelerated="true"
android:exported="false" />

Expand Down
12 changes: 9 additions & 3 deletions app/src/main/cpp/fusion/src/fusion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,11 @@ static bool stage_fusion_config(const FusionConfig &config)
patchedIl2CppPath.c_str(),
1024 * 1024); // 1 MB code cave, load AFTER libunity
if (!poolHandle) {
LOGE("allocate_setup_injected_noload failed — falling back to original il2cpp path");
il2cppPath = gameIl2cppPath;
// Loading an unpatched image can leave Dobby branches pointing into a
// non-writable ELF and later abort inside il2cpp. Stop here with a
// diagnostic instead of continuing in an unsafe mixed state.
LOGE("allocate_setup_injected_noload failed — refusing unsafe il2cpp fallback");
return false;
} else {
il2cppPath = patchedIl2CppPath;
LOGI("Patched il2cpp ELF written (load deferred until after libunity)");
Expand Down Expand Up @@ -198,7 +201,10 @@ static int il2cpp_init_hook(char *domain_name)

/* 4. Start CoreCLR + BepInEx */
__android_log_write(ANDROID_LOG_ERROR, "FusionB", "calling dotnet_start_runtime...");
dotnet_start_runtime();
if (!dotnet_start_runtime()) {
__android_log_write(ANDROID_LOG_ERROR, "FusionB", "dotnet_start_runtime FAILED");
return result;
}

__android_log_write(ANDROID_LOG_ERROR, "FusionB", "hook returning");
return result;
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/com/bepinex/android/BepInExPaths.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ object BepInExPaths {
fun isBepInExExtracted(packageName: String): Boolean =
File(getCoreDir(packageName), "BepInEx.Core.dll").exists()

/** Check if dotnet runtime has been extracted for a game */
/**
* Check if the managed .NET runtime has been extracted for a game.
* Native CoreCLR (`libcoreclr.so` and siblings) ships in jniLibs and is
* loaded next to libmain.so, so it is not part of this directory.
*/
fun isDotnetExtracted(filesDir: File, packageName: String): Boolean =
File(getDotnetDir(filesDir, packageName), "System.Private.CoreLib.dll").exists()
File(getDotnetDir(filesDir, packageName), ".launcher-dotnet-ready").isFile &&
File(getDotnetDir(filesDir, packageName), "System.Private.CoreLib.dll").isFile
}
17 changes: 17 additions & 0 deletions app/src/main/java/com/bepinex/android/BootstrapActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ class BootstrapActivity : Activity() {
// 7. Start the registered stub via InstrumentationHooks
updateProgress(getString(R.string.bootstrap_status_launching), "", 95)
try {
ensureLaunchDisplay()
val launcherClass = gameContext.classLoader.loadClass(launcherClassName)
BepInExLog.i("Starting game launcher: ${launcherClass.name}")
initializeFusion(null, null)
Expand All @@ -247,6 +248,22 @@ class BootstrapActivity : Activity() {
}
}

/**
* ActivityThread crashes deep inside ContextImpl when a task is launched
* with an invalid display id (common in clones, secondary users and OEM
* split-screen implementations). Fail before Unity is touched so the
* launcher can report a useful error and the process stays recoverable.
*/
private fun ensureLaunchDisplay() {
val manager = getSystemService(Context.DISPLAY_SERVICE) as? android.hardware.display.DisplayManager
?: throw IllegalStateException("Display service is unavailable")
val display = manager.getDisplay(android.view.Display.DEFAULT_DISPLAY)
?: throw IllegalStateException(
"No default display is available. Exit split-screen/clone mode and retry."
)
BepInExLog.i("Launch display: id=${display.displayId}, state=${display.state}")
}

// Launcher onCreate hook

private fun installLauncherOnCreateHook(
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/com/bepinex/android/DebugCrashCollector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ object DebugCrashCollector {
"com.LanPiaoPiao.PlantsVsZombiesRHMod"
)
private const val MAX_FILE_BYTES = 32L * 1024L * 1024L
private const val MAX_LOG_FILES = 64

fun collect(context: Context) {
try {
Expand Down Expand Up @@ -60,8 +61,9 @@ object DebugCrashCollector {
)
TARGET_PACKAGES.forEach { packageName ->
val gameRoot = BepInExPaths.getGameRootDir(packageName)
gameRoot.walkTopDown()
gameRoot.walkTopDown().maxDepth(8)
.filter { it.isFile && it.length() <= MAX_FILE_BYTES && names.contains(it.name) }
.take(MAX_LOG_FILES)
.forEach { source ->
val relative = source.relativeTo(gameRoot).path.replace(File.separatorChar, '_')
copyIfPresent(source, File(snapshot, "${packageName}_$relative"))
Expand Down
32 changes: 29 additions & 3 deletions app/src/main/java/com/bepinex/android/FileExtractor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import java.io.FileOutputStream
import java.util.zip.ZipInputStream
import java.security.MessageDigest
import java.util.UUID
import java.io.IOException

/**
* Extracts BepInEx and .NET runtime from APK assets to per-game directories.
Expand Down Expand Up @@ -145,9 +146,14 @@ class FileExtractor(private val context: Context) {
*/
fun extractDotnetIfNeeded(packageName: String, onProgress: (String) -> Unit = {}) {
val dotnetDir = BepInExPaths.getDotnetDir(context.filesDir, packageName)
val marker = File(dotnetDir, "System.Private.CoreLib.dll")
val marker = File(dotnetDir, ".launcher-dotnet-ready")
// Native CoreCLR is packaged in jniLibs; dotnet.zip contains the
// managed TPA assemblies only. Do not require native files inside the
// extracted directory or every valid installation would be rejected.
val required = listOf("System.Private.CoreLib.dll")

if (marker.exists()) {
if (marker.isFile && required.all { File(dotnetDir, it).isFile } &&
dotnetDir.walkTopDown().count { it.isFile && it.extension.equals("dll", true) } >= 100) {
BepInExLog.i("dotnet already extracted for $packageName")
return
}
Expand All @@ -159,7 +165,23 @@ class FileExtractor(private val context: Context) {

onProgress("Extracting .NET runtime for $packageName...")
BepInExLog.i("Extracting dotnet.zip → ${dotnetDir.absolutePath}")
extractZip("dotnet.zip", dotnetDir)
val staging = File(dotnetDir.parentFile, ".dotnet-${UUID.randomUUID()}")
try {
extractZip("dotnet.zip", staging)
check(required.all { File(staging, it).isFile } &&
staging.walkTopDown().count { it.isFile && it.extension.equals("dll", true) } >= 100) {
"Incomplete .NET runtime: missing ${required.filterNot { File(staging, it).isFile }}"
}
File(staging, ".launcher-dotnet-ready").writeText(
"version=1\nfiles=${staging.walkTopDown().count { it.isFile }}\n"
)
if (dotnetDir.exists() && !dotnetDir.deleteRecursively()) {
throw IOException("Cannot replace incomplete dotnet directory")
}
check(staging.renameTo(dotnetDir)) { "Cannot install extracted dotnet runtime" }
} finally {
if (staging.exists()) staging.deleteRecursively()
}
BepInExLog.i("dotnet extracted: ${dotnetDir.absolutePath}")
}

Expand Down Expand Up @@ -229,6 +251,10 @@ class FileExtractor(private val context: Context) {
while (entry != null) {
// Normalize path separators: Windows-created zips may use \ instead of /
val normalizedName = entry.name.replace('\\', '/')
require(normalizedName.isNotEmpty() && !normalizedName.startsWith("/") &&
normalizedName.split('/').none { it == ".." }) {
"Unsafe ZIP entry: ${entry.name}"
}
val outFile = File(destDir, normalizedName)
if (entry.isDirectory || normalizedName.endsWith("/")) {
if (!outFile.mkdirs() && !outFile.exists()) {
Expand Down
20 changes: 18 additions & 2 deletions app/src/main/java/com/bepinex/android/GameDetector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,28 @@ object GameDetector {

private const val TAG = "GameDetector"

/** Only packages in this list are shown as supported games. */
private val SUPPORTED_PACKAGES = setOf(
/**
* Only packages in this list are shown as supported games.
* The same names are declared in the manifest `<queries>` block.
*/
internal val SUPPORTED_PACKAGES = setOf(
"com.LanPiaoPiao.PlantsVsZombiesRH",
"com.LanPiaoPiao.PlantsVsZombiesRHMod"
)

/** True when Android 11+ package visibility already reveals a supported game. */
fun supportedPackageVisible(context: Context): Boolean {
val pm = context.packageManager
return SUPPORTED_PACKAGES.any { packageName ->
try {
pm.getApplicationInfo(packageName, 0)
true
} catch (_: PackageManager.NameNotFoundException) {
false
}
}
}

const val PVZ_LAUNCHER_ACTIVITY = "com.unity3d.player.UnityPlayerActivity"

/** ABI directories inside APKs (lib/<abi>/libil2cpp.so). */
Expand Down
Loading
Loading