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
14 changes: 13 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ android {
jvmTarget = "17"
}

lint {
// lifecycle-runtime-lint's NullSafeMutableLiveData detector was built when
// KaCallableMemberCall was an interface. Kotlin 2.2 makes it a class, so the
// detector crashes lintVitalAnalyzeRelease. Newer lifecycle builds that fix
// this require compileSdk 37 and AGP 9.
disable += "NullSafeMutableLiveData"
}

packaging {
jniLibs {
useLegacyPackaging = true
Expand All @@ -95,7 +103,7 @@ android {

dependencies {
// Compose BOM - manages all Compose library versions
val composeBom = platform("androidx.compose:compose-bom:2024.09.00")
val composeBom = platform("androidx.compose:compose-bom:2025.11.00")
implementation(composeBom)

// Compose UI
Expand All @@ -122,6 +130,10 @@ dependencies {
// AppCompat (DayNight theme support)
implementation("androidx.appcompat:appcompat:1.7.0")

// Liquid glass surfaces. 1.0.0 is the newest Android artifact this toolchain can compile.
// https://github.com/Kyant0/AndroidLiquidGlass
implementation("io.github.kyant0:backdrop:1.0.0")

// Kotlin Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")

Expand Down
1 change: 1 addition & 0 deletions app/src/main/cpp/fusion/include/fusion.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct FusionConfig {
std::string unityDataDir;
std::string unityVersion;
bool useOriginalLibUnity = false;
bool disableTieredCompilation = false;
};

extern FusionConfig g_config;
Expand Down
19 changes: 11 additions & 8 deletions app/src/main/cpp/fusion/src/dotnet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,17 @@ bool dotnet_start_runtime()

setenv("DOTNET_ReadyToRun", "0", 1);

/* CoreCLR JIT aborts (SIGABRT inside libclrjit, abort msg "terminating")
when compiling the huge methods generated by large modpacks on Android 16.
Tier 0 quick-JIT runs on the caller thread (UnityMain) where the crash
happens; disabling tiering switches every method to the full compiler
path as a workaround. */
setenv("DOTNET_TieredCompilation", "0", 1);

/* Initialize CoreCLR 鈥?with W^X retry (FusionCore pattern) */
/* Optional workaround for a libclrjit abort while compiling huge methods.
Off unless the launcher setting says so: the full-JIT path can load
plugins without applying their in-game content. */
if (g_config.disableTieredCompilation) {
setenv("DOTNET_TieredCompilation", "0", 1);
LOGI("Tiered compilation disabled by launcher setting");
} else {
setenv("DOTNET_TieredCompilation", "1", 1);
}

/* Initialize CoreCLR — with W^X retry (FusionCore pattern) */
setenv("DOTNET_EnableWriteXorExecute", "0", 1);

void *host = nullptr;
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/cpp/fusion/src/fusion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ static bool parse_fusion_config(const char *text, FusionConfig *out)
else if (key == "unityDataDirectory") out->unityDataDir = value;
else if (key == "unityVersion") out->unityVersion = value;
else if (key == "useOriginalLibUnity") out->useOriginalLibUnity = (value == "true");
else if (key == "disableTieredCompilation") out->disableTieredCompilation = (value == "true");
}

return !out->appLibraryDir.empty();
Expand All @@ -114,6 +115,7 @@ static bool stage_fusion_config(const FusionConfig &config)
LOGI(" unityDataDir: %s", config.unityDataDir.c_str());
LOGI(" unityVersion: %s", config.unityVersion.c_str());
LOGI(" useOrigUnity: %s", config.useOriginalLibUnity ? "true" : "false");
LOGI(" noTieredJit: %s", config.disableTieredCompilation ? "true" : "false");

/* Set library paths */
std::string unityPath;
Expand Down
10 changes: 7 additions & 3 deletions app/src/main/java/com/bepinex/android/BootstrapActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,10 @@ class BootstrapActivity : Activity() {

// 3. Prepare Fusion state (paths, extract zips, copy data, detect version)
val useUnstripped = AppSettings.isUseUnstrippedLibUnity(this)
val disableTieredCompilation = AppSettings.isTieredCompilationDisabled(this)
BepInExLog.i("Use unstripped libunity: $useUnstripped")
preparedConfig = prepareFusionState(targetPackage, gameContext, useUnstripped)
BepInExLog.i("Disable tiered compilation: $disableTieredCompilation")
preparedConfig = prepareFusionState(targetPackage, gameContext, useUnstripped, disableTieredCompilation)

// 4. Register game native libraries (match FusionCore: no exclusions)
updateProgress(getString(R.string.bootstrap_status_registering_libraries), "", 60)
Expand Down Expand Up @@ -368,7 +370,8 @@ class BootstrapActivity : Activity() {
private fun prepareFusionState(
targetPackage: String,
gameContext: Context,
useUnstripped: Boolean = false
useUnstripped: Boolean = false,
disableTieredCompilation: Boolean = false
): FusionConfig {
var gameLibDir = gameContext.applicationInfo.nativeLibraryDir
if (gameLibDir.isNullOrEmpty()) {
Expand Down Expand Up @@ -496,7 +499,8 @@ class BootstrapActivity : Activity() {
dotnetDirectory = dotnetDir.absolutePath,
unityDataDirectory = copiedData.absolutePath,
unityVersion = unityVersion,
useOriginalLibUnity = useOriginalLibUnity
useOriginalLibUnity = useOriginalLibUnity,
disableTieredCompilation = disableTieredCompilation
)
}

Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/bepinex/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.bepinex.android.settings.AppSettings
import com.bepinex.android.ui.navigation.BepInExNavHost
import com.bepinex.android.ui.onboarding.OnboardingHost
import com.bepinex.android.ui.theme.BepInExTheme
import com.bepinex.android.ui.theme.LiquidGlassHost
import com.bepinex.android.update.UpdateChecker
import com.bepinex.android.update.AnnouncementDialog
import com.bepinex.android.update.IncompleteTranslationDialog
Expand Down Expand Up @@ -91,6 +92,7 @@ class MainActivity : ComponentActivity() {
private var themeMode by mutableStateOf(AppSettings.ThemeMode.SYSTEM)
private var language by mutableStateOf(AppSettings.Language.SYSTEM)
private var dynamicColor by mutableStateOf(false)
private var liquidGlass by mutableStateOf(false)
private var animationDisabled by mutableStateOf(false)

// Update check state
Expand Down Expand Up @@ -171,6 +173,7 @@ class MainActivity : ComponentActivity() {
themeMode = AppSettings.getThemeMode(this)
language = AppSettings.getLanguage(this)
dynamicColor = AppSettings.isDynamicColorEnabled(this)
liquidGlass = AppSettings.isLiquidGlassEnabled(this)
animationDisabled = AppSettings.isAnimationDisabled(this)

showOnboarding = !AppSettings.isOnboardingCompleted(this)
Expand Down Expand Up @@ -1182,6 +1185,7 @@ class MainActivity : ComponentActivity() {
private fun setupContent() {
setContent {
BepInExTheme(themeMode = themeMode, dynamicColor = dynamicColor) {
LiquidGlassHost(enabled = liquidGlass) {
if (showOnboarding) {
OnboardingHost(
detectedGames = detectedGames,
Expand Down Expand Up @@ -1209,6 +1213,7 @@ class MainActivity : ComponentActivity() {
themeMode = themeMode,
language = language,
dynamicColor = dynamicColor,
liquidGlass = liquidGlass,
animationDisabled = animationDisabled,
onSelectGame = { selectGame(it) },
onRescan = {
Expand All @@ -1221,6 +1226,10 @@ class MainActivity : ComponentActivity() {
onDynamicColorChanged = { enabled ->
dynamicColor = enabled
},
onLiquidGlassChanged = { enabled ->
liquidGlass = enabled
AppSettings.setLiquidGlassEnabled(this@MainActivity, enabled)
},
onAnimationDisabledChanged = { disabled ->
animationDisabled = disabled
},
Expand Down Expand Up @@ -1310,6 +1319,7 @@ class MainActivity : ComponentActivity() {
}
)
}
}
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/com/bepinex/android/fusion/FusionConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ data class FusionConfig(
val unityVersion: String,

/** If true, use the game's original libunity.so instead of downloading a patched one */
val useOriginalLibUnity: Boolean = false
val useOriginalLibUnity: Boolean = false,

/** If true, start CoreCLR with DOTNET_TieredCompilation=0. */
val disableTieredCompilation: Boolean = false
) {
/**
* Serialize to the config file format expected by native fusion.cpp parser.
Expand All @@ -40,5 +43,6 @@ data class FusionConfig(
appendLine("unityDataDirectory=${unityDataDirectory}")
appendLine("unityVersion=${unityVersion}")
appendLine("useOriginalLibUnity=${useOriginalLibUnity}")
appendLine("disableTieredCompilation=${disableTieredCompilation}")
}
}
17 changes: 17 additions & 0 deletions app/src/main/java/com/bepinex/android/settings/AppSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ object AppSettings {
private const val KEY_FLOATING_MOD_MENU = "floating_mod_menu"
private const val KEY_ACTIVE_MODPACK_PREFIX = "active_modpack_"
private const val KEY_USE_UNSTRIPPED_LIBUNITY = "use_unstripped_libunity"
private const val KEY_DISABLE_TIERED_COMPILATION = "disable_tiered_compilation"
private const val KEY_USE_DYNAMIC_COLOR = "use_dynamic_color"
private const val KEY_LIQUID_GLASS = "liquid_glass"
private const val KEY_LAST_SEEN_ANNOUNCEMENT = "last_seen_announcement_date"
private const val KEY_ANIMATION_DISABLED = "animation_disabled"
private const val KEY_LANGUAGE_INCOMPLETE_SHOWN = "language_incomplete_shown"
Expand Down Expand Up @@ -176,6 +178,14 @@ object AppSettings {
prefs(context).edit().putBoolean(KEY_USE_UNSTRIPPED_LIBUNITY, enabled).apply()
}

/** When true, CoreCLR is started with DOTNET_TieredCompilation=0. Default off. */
fun isTieredCompilationDisabled(context: Context): Boolean =
prefs(context).getBoolean(KEY_DISABLE_TIERED_COMPILATION, false)

fun setTieredCompilationDisabled(context: Context, disabled: Boolean) {
prefs(context).edit().putBoolean(KEY_DISABLE_TIERED_COMPILATION, disabled).apply()
}

// Dynamic Color (Material You / Monet)

fun isDynamicColorEnabled(context: Context): Boolean =
Expand All @@ -185,6 +195,13 @@ object AppSettings {
prefs(context).edit().putBoolean(KEY_USE_DYNAMIC_COLOR, enabled).apply()
}

fun isLiquidGlassEnabled(context: Context): Boolean =
prefs(context).getBoolean(KEY_LIQUID_GLASS, false)

fun setLiquidGlassEnabled(context: Context, enabled: Boolean) {
prefs(context).edit().putBoolean(KEY_LIQUID_GLASS, enabled).apply()
}

// Animation

fun isAnimationDisabled(context: Context): Boolean =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.bepinex.android.ui.components

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
Expand All @@ -19,6 +20,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import com.bepinex.android.R
import com.bepinex.android.ui.theme.GlassAlertDialog
import com.bepinex.android.ui.theme.glassContainerColor
import com.bepinex.android.ui.theme.glassSurface
import com.bepinex.android.ui.theme.glassTopBarColors
import java.io.File

/**
Expand Down Expand Up @@ -55,8 +60,10 @@ fun ConfigEditorDialog(
}

Scaffold(
containerColor = glassContainerColor(MaterialTheme.colorScheme.background),
topBar = {
TopAppBar(
modifier = Modifier.glassSurface(RoundedCornerShape(bottomStart = 22.dp, bottomEnd = 22.dp)),
title = {
Column {
Text(
Expand Down Expand Up @@ -100,9 +107,7 @@ fun ConfigEditorDialog(
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
colors = glassTopBarColors()
)
}
) { padding ->
Expand Down Expand Up @@ -157,7 +162,7 @@ fun ConfigEditorDialog(
}

if (showDiscardDialog) {
AlertDialog(
GlassAlertDialog(
onDismissRequest = { showDiscardDialog = false },
title = { Text(stringResource(R.string.config_editor_discard_title)) },
text = { Text(stringResource(R.string.config_editor_discard_msg)) },
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/bepinex/android/ui/liquid/Capsule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.bepinex.android.ui.liquid

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.Shape

internal fun Capsule(): Shape = RoundedCornerShape(50)
Loading
Loading