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
88 changes: 25 additions & 63 deletions app/src/main/java/app/gamenative/ui/PluviaMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,31 @@ fun preLaunchApp(

// set up Ubuntu file system — download required files and install
SplitCompat.install(context)

try {
LaunchDependencies().ensureLaunchDependencies(
context = context,
container = container,
gameSource = gameSource,
gameId = gameId,
setLoadingMessage = setLoadingMessage,
setLoadingProgress = setLoadingProgress,
)
} catch (e: Exception) {
Timber.tag("preLaunchApp").e(e, "ensureLaunchDependencies failed")
setLoadingDialogVisible(false)
setMessageDialogState(
MessageDialogState(
visible = true,
type = DialogType.SYNC_FAIL,
title = context.getString(R.string.launch_dependency_failed_title),
message = e.message ?: context.getString(R.string.launch_dependency_failed_message),
dismissBtnText = context.getString(R.string.ok),
),
)
return@launch
}

try {
if (!SteamService.isImageFsInstallable(context, container.containerVariant)) {
setLoadingMessage("Downloading first-time files")
Expand All @@ -1621,45 +1646,6 @@ fun preLaunchApp(
this,
context = context,
).await()
} else {
if (container.wineVersion.contains("proton-9.0-arm64ec") &&
!SteamService.isFileInstallable(context, "proton-9.0-arm64ec.txz")
) {
setLoadingMessage("Downloading arm64ec Proton")
SteamService.downloadFile(
onDownloadProgress = { setLoadingProgress(it / 1.0f) },
this,
context = context,
"proton-9.0-arm64ec.txz",
).await()
} else if (container.wineVersion.contains("proton-9.0-x86_64") &&
!SteamService.isFileInstallable(context, "proton-9.0-x86_64.txz")
) {
setLoadingMessage("Downloading x86_64 Proton")
SteamService.downloadFile(
onDownloadProgress = { setLoadingProgress(it / 1.0f) },
this,
context = context,
"proton-9.0-x86_64.txz",
).await()
}
if (container.wineVersion.contains("proton-9.0-x86_64") || container.wineVersion.contains("proton-9.0-arm64ec")) {
val protonVersion = container.wineVersion
val imageFs = ImageFs.find(context)
val outFile = File(imageFs.rootDir, "/opt/$protonVersion")
val binDir = File(outFile, "bin")
if (!binDir.exists() || !binDir.isDirectory) {
Timber.i("Extracting $protonVersion to /opt/")
setLoadingMessage("Extracting $protonVersion")
setLoadingProgress(-1f)
val downloaded = File(imageFs.getFilesDir(), "$protonVersion.txz")
TarCompressorUtils.extract(
TarCompressorUtils.Type.XZ,
downloaded,
outFile,
)
}
}
}

if (!container.isUseLegacyDRM && !container.isLaunchRealSteam &&
Expand Down Expand Up @@ -1705,30 +1691,6 @@ fun preLaunchApp(
return@launch
}

try {
LaunchDependencies().ensureLaunchDependencies(
context = context,
container = container,
gameSource = gameSource,
gameId = gameId,
setLoadingMessage = setLoadingMessage,
setLoadingProgress = setLoadingProgress,
)
} catch (e: Exception) {
Timber.tag("preLaunchApp").e(e, "ensureLaunchDependencies failed")
setLoadingDialogVisible(false)
setMessageDialogState(
MessageDialogState(
visible = true,
type = DialogType.SYNC_FAIL,
title = context.getString(R.string.launch_dependency_failed_title),
message = e.message ?: context.getString(R.string.launch_dependency_failed_message),
dismissBtnText = context.getString(R.string.ok),
),
)
return@launch
}

val loadingMessage = if (container.containerVariant.equals(Container.GLIBC)) {
context.getString(R.string.main_installing_glibc)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.gamenative.utils
import android.content.Context
import app.gamenative.R
import app.gamenative.data.GameSource
import app.gamenative.utils.launchdependencies.BionicDefaultProtonDependency
import app.gamenative.utils.launchdependencies.GogScriptInterpreterDependency
import app.gamenative.utils.launchdependencies.LaunchDependencyCallbacks
import app.gamenative.utils.launchdependencies.LaunchDependency
Expand All @@ -18,6 +19,7 @@ const val LOADING_PROGRESS_UNKNOWN: Float = -1f
class LaunchDependencies {
companion object {
private val launchDependencies: List<LaunchDependency> = listOf(
BionicDefaultProtonDependency,
GogScriptInterpreterDependency,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package app.gamenative.utils.launchdependencies

import android.content.Context
import app.gamenative.data.GameSource
import app.gamenative.service.SteamService
import app.gamenative.utils.LOADING_PROGRESS_UNKNOWN
import com.winlator.container.Container
import com.winlator.core.TarCompressorUtils
import com.winlator.xenvironment.ImageFs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File

/**
* Ensures Proton (arm64ec or x86_64) is downloaded and extracted into opt/<wineVersion> for Bionic.
* Only runs when container variant is BIONIC and wine version is proton-9.0-arm64ec or proton-9.0-x86_64.
*/
Comment thread
unbelievableflavour marked this conversation as resolved.
object BionicDefaultProtonDependency : LaunchDependency {
override fun appliesTo(container: Container, gameSource: GameSource, gameId: Int): Boolean {
if (container.containerVariant != Container.BIONIC) return false
val v = container.wineVersion
return v.contains("proton-9.0-arm64ec") || v.contains("proton-9.0-x86_64")
}

override fun isSatisfied(context: Context, container: Container, gameSource: GameSource, gameId: Int): Boolean {
val protonVersion = container.wineVersion
val outFile = File(ImageFs.find(context).getRootDir(), "opt/$protonVersion")
val binDir = File(outFile, "bin")
return binDir.exists() && binDir.isDirectory
}

override fun getLoadingMessage(context: Context, container: Container, gameSource: GameSource, gameId: Int): String {
return when {
container.wineVersion.contains("proton-9.0-arm64ec") -> "Downloading arm64ec Proton"
container.wineVersion.contains("proton-9.0-x86_64") -> "Downloading x86_64 Proton"
else -> "Extracting Proton"
}
}

override suspend fun install(
context: Context,
container: Container,
callbacks: LaunchDependencyCallbacks,
gameSource: GameSource,
gameId: Int,
) = coroutineScope {
val protonVersion = container.wineVersion
val imageFs = withContext(Dispatchers.IO) { ImageFs.find(context) }
val archiveName = when {
protonVersion.contains("proton-9.0-arm64ec") -> "proton-9.0-arm64ec.txz"
protonVersion.contains("proton-9.0-x86_64") -> "proton-9.0-x86_64.txz"
else -> return@coroutineScope
}

if (!withContext(Dispatchers.IO) { SteamService.isFileInstallable(context, archiveName) }) {
callbacks.setLoadingMessage("Downloading $protonVersion")
withContext(Dispatchers.IO) {
SteamService.downloadFile(
onDownloadProgress = { callbacks.setLoadingProgress(it) },
parentScope = this@coroutineScope,
context = context,
fileName = archiveName,
).await()
Comment thread
unbelievableflavour marked this conversation as resolved.
}
}

val outFile = File(ImageFs.find(context).getRootDir(), "opt/$protonVersion")
val binDir = File(outFile, "bin")
val needsExtract = withContext(Dispatchers.IO) { !binDir.exists() || !binDir.isDirectory }
if (needsExtract) {
Timber.i("Extracting $protonVersion to ${outFile.absolutePath}")
callbacks.setLoadingMessage("Extracting $protonVersion")
callbacks.setLoadingProgress(LOADING_PROGRESS_UNKNOWN)
val downloaded = File(imageFs.getFilesDir(), archiveName)
withContext(Dispatchers.IO) {
val success = TarCompressorUtils.extract(
TarCompressorUtils.Type.XZ,
downloaded,
outFile,
)
if (!success) {
throw IllegalStateException("Failed to extract $protonVersion from ${downloaded.absolutePath}")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package app.gamenative.utils.launchdependencies

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import app.gamenative.data.GameSource
import app.gamenative.service.SteamService
import com.winlator.container.Container
import com.winlator.core.TarCompressorUtils
import com.winlator.xenvironment.ImageFs
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.File

@RunWith(RobolectricTestRunner::class)
class BionicDefaultProtonDependencyTest {
private lateinit var context: Context
private lateinit var container: Container

@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
container = mockk(relaxed = true)
}

@After
fun tearDown() {
unmockkAll()
}

@Test
fun appliesTo_returnsFalse_whenContainerIsNotBionic() {
every { container.containerVariant } returns "glibc"
every { container.wineVersion } returns "proton-9.0-arm64ec"

val result = BionicDefaultProtonDependency.appliesTo(container, GameSource.STEAM, 1)

assertFalse(result)
}

@Test
fun appliesTo_returnsTrue_whenBionicAndSupportedProtonVersion() {
every { container.containerVariant } returns Container.BIONIC
every { container.wineVersion } returns "proton-9.0-arm64ec"

val armResult = BionicDefaultProtonDependency.appliesTo(container, GameSource.STEAM, 2)

every { container.wineVersion } returns "proton-9.0-x86_64"
val x64Result = BionicDefaultProtonDependency.appliesTo(container, GameSource.STEAM, 3)

assertTrue(armResult)
assertTrue(x64Result)
}

@Test
fun isSatisfied_returnsTrue_whenProtonBinDirectoryExists() {
every { container.wineVersion } returns "proton-9.0-arm64ec"

val imageFsRoot = ImageFs.find(context).rootDir
val binDir = File(imageFsRoot, "opt/proton-9.0-arm64ec/bin")
binDir.mkdirs()

val result = BionicDefaultProtonDependency.isSatisfied(context, container, GameSource.STEAM, 4)

assertTrue(result)
}

@Test
fun getLoadingMessage_returnsExpectedMessagePerVersion() {
every { container.wineVersion } returns "proton-9.0-arm64ec"
val armMessage = BionicDefaultProtonDependency.getLoadingMessage(context, container, GameSource.STEAM, 5)

every { container.wineVersion } returns "proton-9.0-x86_64"
val x64Message = BionicDefaultProtonDependency.getLoadingMessage(context, container, GameSource.STEAM, 6)

every { container.wineVersion } returns "wine-ge-custom"
val fallbackMessage = BionicDefaultProtonDependency.getLoadingMessage(context, container, GameSource.STEAM, 7)

assertEquals("Downloading arm64ec Proton", armMessage)
assertEquals("Downloading x86_64 Proton", x64Message)
assertEquals("Extracting Proton", fallbackMessage)
}

@Test
fun install_returnsEarly_whenProtonVersionIsUnsupported() = runBlocking {
every { container.wineVersion } returns "wine-ge-custom"
mockkObject(SteamService.Companion)
mockkStatic(TarCompressorUtils::class)

BionicDefaultProtonDependency.install(
context = context,
container = container,
callbacks = LaunchDependencyCallbacks({}, {}),
gameSource = GameSource.STEAM,
gameId = 8,
)

verify(exactly = 0) {
SteamService.downloadFile(
onDownloadProgress = any(),
parentScope = any(),
context = any(),
fileName = any(),
)
}
}

@Test
fun install_downloadsArchive_andForwardsProgress() = runBlocking {
every { container.wineVersion } returns "proton-9.0-arm64ec"
mockkObject(SteamService.Companion)

val imageFsRoot = ImageFs.find(context).rootDir
val binDir = File(imageFsRoot, "opt/proton-9.0-arm64ec/bin")
binDir.mkdirs()

every { SteamService.isFileInstallable(context, "proton-9.0-arm64ec.txz") } returns false

val onProgressSlot = slot<(Float) -> Unit>()
val deferred = mockk<Deferred<Unit>>()
every {
SteamService.downloadFile(
onDownloadProgress = capture(onProgressSlot),
parentScope = any(),
context = context,
fileName = "proton-9.0-arm64ec.txz",
)
} returns deferred
coEvery { deferred.await() } returns Unit

val progressValues = mutableListOf<Float>()
BionicDefaultProtonDependency.install(
context = context,
container = container,
callbacks = LaunchDependencyCallbacks(
setLoadingMessage = {},
setLoadingProgress = { progressValues += it },
),
gameSource = GameSource.STEAM,
gameId = 9,
)

onProgressSlot.captured(0.42f)

verify(exactly = 1) {
SteamService.downloadFile(
onDownloadProgress = any(),
parentScope = any(),
context = context,
fileName = "proton-9.0-arm64ec.txz",
)
}
assertEquals(listOf(0.42f), progressValues)
}
}
Loading