Skip to content
Open
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
5 changes: 5 additions & 0 deletions app/src/main/java/app/gamenative/PrefManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,11 @@ object PrefManager {
setPref(CUSTOM_GAME_MANUAL_FOLDERS, Json.encodeToString(value))
}

// Detailed per-step reporting on the boot splash; off means the plain indeterminate bar
private val VERBOSE_BOOT_PROGRESS = booleanPreferencesKey("verbose_boot_progress")
var verboseBootProgress: Boolean
get() = getPref(VERBOSE_BOOT_PROGRESS, false)
set(value) = setPref(VERBOSE_BOOT_PROGRESS, value)
private val FAVORITE_APP_IDS = stringPreferencesKey("favorite_app_ids")
var favoriteAppIds: Set<String>
get() {
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/app/gamenative/events/AndroidEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface AndroidEvent<T> : Event<T> {
data class ShowGameFeedback(val appId: String) : AndroidEvent<Unit>
data class ShowLaunchingOverlay(val appName: String) : AndroidEvent<Unit>
data object HideLaunchingOverlay : AndroidEvent<Unit>
data class SetBootingSplashText(val text: String) : AndroidEvent<Unit>
data class SetBootingSplashText(val text: String, val progress: Float = -1f) : AndroidEvent<Unit>
data object ClearBootingSplash : AndroidEvent<Unit>
data class DownloadPausedDueToConnectivity(val appId: Int) : AndroidEvent<Unit>
data class DownloadStatusChanged(val appId: Int, val isDownloading: Boolean) : AndroidEvent<Unit>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/app/gamenative/ui/PluviaMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,7 @@ fun PluviaMain(
BootingSplash(
visible = state.showBootingSplash,
text = state.bootingSplashText,
progress = state.bootingSplashProgress,
heroImageUrl = state.bootingSplashHeroImageUrl,
)
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/app/gamenative/ui/data/MainState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ data class MainState(
val diagnostics: Boolean = false,
val showBootingSplash: Boolean = false,
val bootingSplashText: String = "Booting...",
val bootingSplashProgress: Float = -1f,
val bootingSplashHeroImageUrl: String = "",

// Connection state for background reconnection
Expand Down
17 changes: 16 additions & 1 deletion app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import app.gamenative.utils.CustomGameScanner
import app.gamenative.ui.data.MainState
import app.gamenative.ui.enums.ConnectionState
import app.gamenative.ui.screen.PluviaScreen
import app.gamenative.utils.BootProgress
import app.gamenative.utils.ContainerUtils
import app.gamenative.utils.IntentLaunchManager
import app.gamenative.utils.SteamUtils
Expand Down Expand Up @@ -247,6 +248,7 @@ class MainViewModel @Inject constructor(

private val onSetBootingSplashText: (AndroidEvent.SetBootingSplashText) -> Unit = {
setBootingSplashText(it.text)
_state.update { state -> state.copy(bootingSplashProgress = it.progress) }
setShowBootingSplash(true)
}

Expand Down Expand Up @@ -326,6 +328,7 @@ class MainViewModel @Inject constructor(
PluviaApp.events.off<SteamEvent.LoggedOut, Unit>(onLoggedOut)
PluviaApp.events.off<AndroidEvent.ServiceReady, Unit>(onServiceReady)
connectionTimeoutJob?.cancel()
BootProgress.stop()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the other activity's MainViewModel is cleared while an immersive launch is still booting, this global cleanup stops progress for the surviving activity. Tie BootProgress.stop() to the activity that started the boot, or add ownership/reference tracking instead of stopping the singleton from every ViewModel.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/model/MainViewModel.kt, line 331:

<comment>When the other activity's `MainViewModel` is cleared while an immersive launch is still booting, this global cleanup stops progress for the surviving activity. Tie `BootProgress.stop()` to the activity that started the boot, or add ownership/reference tracking instead of stopping the singleton from every ViewModel.</comment>

<file context>
@@ -328,6 +328,7 @@ class MainViewModel @Inject constructor(
         PluviaApp.events.off<SteamEvent.LoggedOut, Unit>(onLoggedOut)
         PluviaApp.events.off<AndroidEvent.ServiceReady, Unit>(onServiceReady)
         connectionTimeoutJob?.cancel()
+        BootProgress.stop()
     }
 
</file context>

}

fun setTheme(value: AppTheme) {
Expand Down Expand Up @@ -357,7 +360,19 @@ class MainViewModel @Inject constructor(
}

fun setShowBootingSplash(value: Boolean) {
_state.update { it.copy(showBootingSplash = value) }
// Single choke point for every dismissal path, so boot reporting can never outlive the splash.
if (!value) BootProgress.stop()
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
_state.update {
if (value) {
it.copy(showBootingSplash = true)
} else {
it.copy(
showBootingSplash = false,
bootingSplashText = MainState().bootingSplashText,
bootingSplashProgress = MainState().bootingSplashProgress,
)
}
}
}

fun setBootingSplashText(value: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ fun SettingsGroupDebug() {
var enableBox86Logs by rememberSaveable { mutableStateOf(
if (isPreview) false else WinlatorPrefManager.getBoolean("enable_box86_64_logs", false)
) }
var verboseBootProgress by rememberSaveable {
mutableStateOf(if (isPreview) false else PrefManager.verboseBootProgress)
}
var latestCrashFile: File? by rememberSaveable { mutableStateOf(null) }
LaunchedEffect(Unit) {
val crashDir = File(context.getExternalFilesDir(null), "crash_logs")
Expand Down Expand Up @@ -222,6 +225,18 @@ fun SettingsGroupDebug() {
}
},
)
SettingsSwitch(
colors = settingsTileColorsAlt(),
state = verboseBootProgress,
title = { Text(text = stringResource(R.string.settings_debug_verbose_boot_title)) },
subtitle = { Text(text = stringResource(R.string.settings_debug_verbose_boot_subtitle)) },
onCheckedChange = {
verboseBootProgress = it
if (!isPreview) {
PrefManager.verboseBootProgress = it
}
},
)
SettingsMenuLink(
colors = settingsTileColors(),
title = { Text(text = stringResource(R.string.settings_debug_view_crash_title)) },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package app.gamenative.ui.screen.xserver

import android.content.Context
import app.gamenative.PluviaApp
import app.gamenative.data.GameSource
import app.gamenative.events.AndroidEvent
import app.gamenative.service.SteamService
import app.gamenative.service.amazon.AmazonService
import app.gamenative.service.epic.EpicService
import app.gamenative.service.gog.GOGService
import app.gamenative.utils.BootProgress
import app.gamenative.utils.ContainerUtils
import app.gamenative.utils.CustomGameScanner
import app.gamenative.utils.FileUtils
Expand Down Expand Up @@ -134,7 +133,7 @@ object XAudioUtils {
return
}

PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Extracting XAudio DLLs..."))
BootProgress.detail("extracting XAudio DLLs", legacy = "Extracting XAudio DLLs...")

val batFile = File(tempDir, "extract_dx_audio_dlls.bat")
val batContent = buildCabarcBatchScript(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import app.gamenative.ui.component.QuickMenu
import app.gamenative.ui.component.QuickMenuAction
import app.gamenative.ui.component.SteamInviteState
import app.gamenative.ui.component.parseBooleanExtra
import app.gamenative.utils.BootProgress
import app.gamenative.ui.component.parsePositiveFpsLimit
import app.gamenative.ui.data.PerformanceHudConfig
import app.gamenative.ui.data.PerformanceHudSize
Expand Down Expand Up @@ -2150,6 +2151,7 @@ fun XServerScreen(

setupExecutor.submit {
try {
BootProgress.start()
val containerManager = ContainerManager(context)
// Configure WinHandler with container's input API settings
val handler = getxServer().winHandler
Expand Down Expand Up @@ -2231,6 +2233,7 @@ fun XServerScreen(
Timber.i("Doing things once")
val envVars = EnvVars()

BootProgress.phase(BootProgress.Phase.WINE_FILES)
runBlocking {
setupWineSystemFiles(
context,
Expand All @@ -2247,6 +2250,7 @@ fun XServerScreen(
extractArm64ecInputDLLs(context, container) // REQUIRED: Uses updated xinput1_3 main.c from x86_64 build, prevents crashes with 3+ players, avoids need for input shim dlls.
extractx86_64InputDlls(context, container)

BootProgress.phase(BootProgress.Phase.GRAPHICS)
runBlocking {
extractGraphicsDriverFiles(
context,
Expand Down Expand Up @@ -3684,6 +3688,7 @@ private fun setupXEnvironment(
onGameLaunchError: ((String) -> Unit)? = null,
offline: Boolean = false
): XEnvironment {
BootProgress.phase(BootProgress.Phase.ENVIRONMENT)
ProcessHelper.hardKillStaleWineProcesses()

val gameSource = ContainerUtils.extractGameSourceFromContainerId(appId)
Expand Down Expand Up @@ -3877,9 +3882,9 @@ private fun setupXEnvironment(
onError = onGameLaunchError
)
if (preInstallCommands.isNotEmpty()) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing prerequisites..."))
BootProgress.phase(BootProgress.Phase.PREREQS, "1/${preInstallCommands.size}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When an intermediate Wine window maps during setup, BootProgress.stop() disables these later prerequisite and launch updates. This changes the default-mode behavior because the old direct splash event would still show the next label; keep legacy splash emission independent of the detailed-progress lifecycle, or stop progress only when boot actually terminates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt, line 3885:

<comment>When an intermediate Wine window maps during setup, `BootProgress.stop()` disables these later prerequisite and launch updates. This changes the default-mode behavior because the old direct splash event would still show the next label; keep legacy splash emission independent of the detailed-progress lifecycle, or stop progress only when boot actually terminates.</comment>

<file context>
@@ -3877,9 +3882,9 @@ private fun setupXEnvironment(
             )
             if (preInstallCommands.isNotEmpty()) {
-                PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing prerequisites..."))
+                BootProgress.phase(BootProgress.Phase.PREREQS, "1/${preInstallCommands.size}")
             } else {
-                PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Launching game..."))
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure that problem is exactly like you describe it, but i found one related and i'll fix it

} else {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Launching game..."))
BootProgress.phase(BootProgress.Phase.LAUNCH)
}
}

Expand Down Expand Up @@ -3976,9 +3981,10 @@ private fun setupXEnvironment(
}
val nextRemaining = remaining.drop(1)
if (nextRemaining.isEmpty()) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Launching game..."))
BootProgress.phase(BootProgress.Phase.LAUNCH)
} else {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing prerequisites..."))
val step = preInstallCommands.size - nextRemaining.size + 1
BootProgress.phase(BootProgress.Phase.PREREQS, "$step/${preInstallCommands.size}")
}
chainPreInstallSteps(nextRemaining)
guestProgramLauncherComponent.start()
Expand Down Expand Up @@ -4757,7 +4763,9 @@ private fun unpackExecutableFile(
var output = StringBuilder()
if (needsUnpacking || containerVariantChanged){
try {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Installing Mono..."))
BootProgress.phase(BootProgress.Phase.MONO)
// msiexec reports nothing back, so track the prefix directory it writes into instead.
BootProgress.watchOutput(File(imageFs.wineprefix, "drive_c/windows/mono"))
val monoCmd = "wine msiexec /i Z:\\opt\\mono-gecko-offline\\wine-mono-11.0.0-x86.msi && wineserver -k"
Timber.i("Install mono command $monoCmd")
val monoOutput = guestProgramLauncherComponent.execShellCommand(monoCmd)
Expand Down Expand Up @@ -4787,7 +4795,7 @@ private fun unpackExecutableFile(
val rootDir: File = imageFs.getRootDir()

try {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Handling DRM..."))
BootProgress.phase(BootProgress.Phase.DRM, "reading interfaces")
// a:/.../GameDir/orig_dll_path.txt (same dir as the EXE inside A:)
val origTxtFile = File("${imageFs.wineprefix}/dosdevices/a:/orig_dll_path.txt")

Expand Down Expand Up @@ -4851,11 +4859,13 @@ private fun unpackExecutableFile(
if (exePaths.isEmpty()) {
Timber.w("No executable path set, skipping Steamless")
} else {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Handling DRM..."))
BootProgress.phase(BootProgress.Phase.DRM)
for ((index, executablePath) in exePaths.withIndex()) {
if (exePaths.size > 1) {
PluviaApp.events.emit(AndroidEvent.SetBootingSplashText("Handling DRM (${index + 1}/${exePaths.size})"))
}
BootProgress.update(
index.toFloat() / exePaths.size,
"${index + 1}/${exePaths.size}: ${extractExecutableBasename(executablePath)}",
legacy = "Handling DRM (${index + 1}/${exePaths.size})".takeIf { exePaths.size > 1 },
)
var batchFile: File? = null
try {
// Normalize path: use forward slashes for Unix format, backslashes for Windows
Expand Down Expand Up @@ -5061,7 +5071,7 @@ private suspend fun setupWineSystemFiles(

// Download or use cached/bundled openal component
val openalFile = WinComponentDownloader.ensureWinComponentAvailable(context, "openal") { progress ->
Timber.d("Downloading openal component: ${(progress * 100).toInt()}%")
BootProgress.download("OpenAL", progress)
}

if (openalFile == null) {
Expand Down Expand Up @@ -5195,7 +5205,7 @@ private suspend fun extractGraphicsDriverComponent(
onExtractFileListener: OnExtractFileListener? = null
) {
val componentFile = GraphicsDriverDownloader.ensureGraphicsDriverAvailable(context, componentId) { progress ->
Timber.d("Downloading graphics driver $componentId: ${(progress * 100).toInt()}%")
BootProgress.download("graphics driver $componentId", progress)
}

if (componentFile == null) {
Expand Down Expand Up @@ -5231,7 +5241,7 @@ private suspend fun extractDXWrapperComponent(
onExtractFileListener: OnExtractFileListener?
) {
val componentFile = DXWrapperDownloader.ensureDXWrapperAvailable(context, componentId) { progress ->
Timber.d("Downloading dxwrapper $componentId: ${(progress * 100).toInt()}%")
BootProgress.download("dxwrapper $componentId", progress)
}

if (componentFile == null) {
Expand Down Expand Up @@ -5468,7 +5478,7 @@ private suspend fun extractWinComponentFiles(
val componentFile = WinComponentDownloader.ensureWinComponentAvailable(
context, identifier
) { progress ->
Timber.d("Downloading wincomponent $identifier: ${(progress * 100).toInt()}%")
BootProgress.download("component $identifier", progress)
}

if (componentFile == null) {
Expand Down Expand Up @@ -5626,7 +5636,7 @@ private suspend fun extractGraphicsDriverFiles(

// Download or get cached core driver
val driverFile = CoreDriverDownloader.ensureCoreDriverAvailable(context, assetZip) { progress ->
Timber.d("Downloading core driver $assetZip: ${(progress * 100).toInt()}%")
BootProgress.download("core driver $assetZip", progress)
}

// Read manifest name from zip to determine folder name
Expand Down
Loading
Loading