diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/config/InstanceUserConfig.kt b/src/main/kotlin/com/mineinabyss/launchy/data/config/InstanceUserConfig.kt index 63bc7f5..4e3dc58 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/config/InstanceUserConfig.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/config/InstanceUserConfig.kt @@ -4,7 +4,6 @@ import com.charleskorn.kaml.decodeFromStream import com.mineinabyss.launchy.data.Formats import com.mineinabyss.launchy.data.GroupName import com.mineinabyss.launchy.data.ModID -import com.mineinabyss.launchy.data.ModName import com.mineinabyss.launchy.data.modpacks.InstanceModLoaders import com.mineinabyss.launchy.logic.ModDownloader import com.mineinabyss.launchy.logic.hashing.Hashing.checksum @@ -47,8 +46,8 @@ data class InstanceUserConfig( val userAgreedDeps: InstanceModLoaders? = null, val fullEnabledGroups: Set = setOf(), val fullDisabledGroups: Set = setOf(), - val toggledMods: Set = setOf(), - val toggledConfigs: Set = setOf(), + val toggledMods: Set = setOf(), + val toggledConfigs: Set = setOf(), val seenGroups: Set = setOf(), val modDownloadInfo: Map = mapOf(), // val configDownloadInfo: Map = mapOf(), diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt index d79426c..731033a 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mods.kt @@ -1,7 +1,7 @@ package com.mineinabyss.launchy.data.modpacks import com.mineinabyss.launchy.data.GroupName -import com.mineinabyss.launchy.data.ModName +import com.mineinabyss.launchy.data.ModID data class Mods( val modGroups: Map>, @@ -10,12 +10,12 @@ data class Mods( val mods = modGroups.values.flatten().toSet() private val nameToGroup: Map = groups.associateBy { it.name } - private val nameToMod: Map = modGroups.values + private val idToMod: Map = modGroups.values .flatten() - .associateBy { it.info.name } + .associateBy { it.modId } // - fun getMod(name: ModName): Mod? = nameToMod[name] + fun getModById(id: ModID): Mod? = idToMod[id] fun getGroup(name: GroupName): Group? = nameToGroup[name] companion object { diff --git a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt index 1d43b77..1019200 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/data/modpacks/source/PackSource.kt @@ -48,9 +48,8 @@ sealed class PackSource { override suspend fun updateInstance(instance: GameInstance): Result { return runCatching { val downloadTo = type.getFilePath(instance.configDir) - Downloader.download(url, downloadTo, whenChanged = { - type.afterDownload(instance.configDir) - }) + Downloader.download(url, downloadTo) + type.afterDownload(instance.configDir) GameInstance(instance.configDir) } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt index 3e3038e..cad9641 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/Launcher.kt @@ -77,6 +77,10 @@ object Launcher { override fun onExit(p0: Int) { println("Exited with state $p0") + + when (p0) { + 255 -> dialog = Dialog.Error("Minecraft crashed!", "See logs for more info.") + } state.setProcessFor(pack.instance, null) } diff --git a/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt b/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt index 7e8fbad..03e65f8 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/logic/ModDownloader.kt @@ -1,5 +1,6 @@ package com.mineinabyss.launchy.logic +import com.mineinabyss.launchy.data.ModID import com.mineinabyss.launchy.data.config.DownloadInfo import com.mineinabyss.launchy.data.config.HashCheck import com.mineinabyss.launchy.data.modpacks.InstanceModLoaders @@ -92,9 +93,9 @@ object ModDownloader { * Primarily the mod loader/minecraft version. */ suspend fun GameInstanceState.ensureDependenciesReady(state: LaunchyState) = coroutineScope { - val currentDeps = userAgreedModLoaders + val currentDeps = queued.userAgreedModLoaders if (currentDeps == null) { - userAgreedModLoaders = modpack.modLoaders + queued.userAgreedModLoaders = modpack.modLoaders } installMCAndModLoaders(state, currentDeps ?: modpack.modLoaders) } @@ -140,12 +141,30 @@ object ModDownloader { } } + suspend fun GameInstanceState.checkHashes( + state: Map + ): List> = coroutineScope { + state.map { (modId, info) -> + async(AppDispatchers.IOContext) { + val check = runCatching { info.calculateSha1Hash(instance.minecraftDir) }.getOrNull() + modId to info.copy( + hashCheck = when { + check == info.desiredHash -> HashCheck.VERIFIED + else -> HashCheck.FAILED + } + ) + } + }.awaitAll() + } + /** * Updates mod loader versions and mods to latest modpack definition. */ - suspend fun GameInstanceState.startInstall(state: LaunchyState, ignoreCachedCheck: Boolean = false): Result<*> = - coroutineScope { - userAgreedModLoaders = modpack.modLoaders + suspend fun GameInstanceState.startInstall( + state: LaunchyState, + ignoreCachedCheck: Boolean = false + ): Result<*> = coroutineScope { + queued.userAgreedModLoaders = modpack.modLoaders ensureDependenciesReady(state) copyOverrides(state) @@ -170,19 +189,9 @@ object ModDownloader { } // Check hashes - val updatedHashes = queued.modDownloadInfo - .filterValues { it.hashCheck == HashCheck.UNKNOWN || it.hashCheck == HashCheck.FAILED } - .map { (modId, info) -> - async(AppDispatchers.IOContext) { - val check = runCatching { info.calculateSha1Hash(instance.minecraftDir) }.getOrNull() - modId to info.copy( - hashCheck = when { - check == info.desiredHash -> HashCheck.VERIFIED - else -> HashCheck.FAILED - } - ) - } - }.awaitAll() + val updatedHashes = checkHashes(queued.modDownloadInfo + .filterValues { it.hashCheck == HashCheck.UNKNOWN || it.hashCheck == HashCheck.FAILED }) + updatedHashes.forEach { (modId, newInfo) -> queued.modDownloadInfo[modId] = newInfo @@ -198,6 +207,6 @@ object ModDownloader { saveToConfig() - return@coroutineScope Result.success(Unit) + return@coroutineScope Result.success(Unit) } } diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/DownloadQueueState.kt b/src/main/kotlin/com/mineinabyss/launchy/state/modpack/DownloadQueueState.kt index f1674c3..10089f0 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/DownloadQueueState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/state/modpack/DownloadQueueState.kt @@ -1,8 +1,6 @@ package com.mineinabyss.launchy.state.modpack -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.* import com.mineinabyss.launchy.data.ModID import com.mineinabyss.launchy.data.config.DownloadInfo import com.mineinabyss.launchy.data.config.InstanceUserConfig @@ -15,7 +13,8 @@ class DownloadQueueState( ) { /** Live mod download info, including mods that have been removed from the latest modpack version. */ val modDownloadInfo = mutableStateMapOf().apply { - putAll(userConfig.modDownloadInfo) + val availableIds = toggles.availableMods.map { it.modId } + putAll(userConfig.modDownloadInfo.filter { it.key in availableIds }) } /** Mods whose download url matches a previously downloaded url and exist on the filesystem */ @@ -44,9 +43,11 @@ class DownloadQueueState( } val areModLoaderUpdatesAvailable by derivedStateOf { - modpack.modLoaders != userConfig.userAgreedDeps + modpack.modLoaders != userAgreedModLoaders } + var userAgreedModLoaders by mutableStateOf(userConfig.userAgreedDeps) + val needsInstall by derivedStateOf { updates + newDownloads + failures } val areUpdatesQueued by derivedStateOf { updates.isNotEmpty() } diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/GameInstanceState.kt b/src/main/kotlin/com/mineinabyss/launchy/state/modpack/GameInstanceState.kt index 18f0e5b..80112d7 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/GameInstanceState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/state/modpack/GameInstanceState.kt @@ -1,8 +1,5 @@ package com.mineinabyss.launchy.state.modpack -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import com.mineinabyss.launchy.data.config.GameInstance import com.mineinabyss.launchy.data.config.InstanceUserConfig import com.mineinabyss.launchy.data.modpacks.Modpack @@ -15,16 +12,15 @@ class GameInstanceState( val toggles: ModTogglesState = ModTogglesState(modpack, userConfig) val queued = DownloadQueueState(userConfig, modpack, toggles) val downloads = DownloadState() - var userAgreedModLoaders by mutableStateOf(userConfig.userAgreedDeps) fun saveToConfig() { userConfig.copy( fullEnabledGroups = modpack.mods.modGroups .filter { toggles.enabledMods.containsAll(it.value) }.keys .map { it.name }.toSet(), - userAgreedDeps = userAgreedModLoaders, - toggledMods = toggles.enabledMods.mapTo(mutableSetOf()) { it.info.name }, - toggledConfigs = toggles.enabledConfigs.mapTo(mutableSetOf()) { it.info.name } + toggles.enabledMods.filter { it.info.forceConfigDownload } + userAgreedDeps = queued.userAgreedModLoaders, + toggledMods = toggles.enabledMods.mapTo(mutableSetOf()) { it.modId }, + toggledConfigs = toggles.enabledConfigs.mapTo(mutableSetOf()) { it.modId } + toggles.enabledMods.filter { it.info.forceConfigDownload } .mapTo(mutableSetOf()) { it.info.name }, seenGroups = modpack.mods.groups.map { it.name }.toSet(), modDownloadInfo = queued.modDownloadInfo, diff --git a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/ModTogglesState.kt b/src/main/kotlin/com/mineinabyss/launchy/state/modpack/ModTogglesState.kt index ba8411e..2b3ec8a 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/state/modpack/ModTogglesState.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/state/modpack/ModTogglesState.kt @@ -12,8 +12,11 @@ class ModTogglesState( val modpack: Modpack, val modpackConfig: InstanceUserConfig ) { + val availableMods = mutableStateSetOf().apply { + addAll(modpack.mods.mods) + } val enabledMods = mutableStateSetOf().apply { - addAll(modpackConfig.toggledMods.mapNotNull { modpack.mods.getMod(it) }) + addAll(modpackConfig.toggledMods.mapNotNull { modpack.mods.getModById(it) }) val defaultEnabled = modpack.mods.groups .filter { it.enabledByDefault } .map { it.name } - modpackConfig.seenGroups @@ -35,7 +38,7 @@ class ModTogglesState( } val enabledConfigs: MutableSet = mutableStateSetOf().apply { - addAll(modpackConfig.toggledConfigs.mapNotNull { modpack.mods.getMod(it) }) + addAll(modpackConfig.toggledConfigs.mapNotNull { modpack.mods.getModById(it) }) } init { diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt index 2b2878a..f21d44e 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/main/buttons/PlayButton.kt @@ -68,7 +68,7 @@ fun PlayButton( if (process == null) { when { // Assume this means not launched before - packState.userAgreedModLoaders == null -> { + packState.queued.userAgreedModLoaders == null -> { AppDispatchers.profileLaunch.launchOrShowDialog { packState.startInstall(state) Launcher.launch(state, packState, state.profile) diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InfoBar.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InfoBar.kt index 03ac8e2..75e9d54 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InfoBar.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InfoBar.kt @@ -49,7 +49,7 @@ fun InfoBar(modifier: Modifier = Modifier) { InstallButton( state.processFor(packState.instance) == null && !packState.downloads.isDownloading - && (packState.queued.areOperationsQueued || packState.userAgreedModLoaders == null) + && (packState.queued.areOperationsQueued || packState.queued.userAgreedModLoaders == null) && state.inProgressTasks.isEmpty(), Modifier.width(Constants.SETTINGS_PRIMARY_BUTTON_WIDTH) ) @@ -60,7 +60,7 @@ fun InfoBar(modifier: Modifier = Modifier) { ActionButton( shown = packState.queued.areModLoaderUpdatesAvailable, icon = Icons.Rounded.HistoryEdu, - desc = "Mod loader updates:\n${packState.userAgreedModLoaders?.fullVersionName ?: "Not installed"} -> ${packState.modpack.modLoaders.fullVersionName}", + desc = "Mod loader updates:\n${packState.queued.userAgreedModLoaders?.fullVersionName ?: "Not installed"} -> ${packState.modpack.modLoaders.fullVersionName}", count = 1 ) ActionButton( diff --git a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt index 6e1d7d9..598a8b5 100644 --- a/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt +++ b/src/main/kotlin/com/mineinabyss/launchy/ui/screens/modpack/settings/InstanceSettingsScreen.kt @@ -21,13 +21,17 @@ import com.mineinabyss.launchy.data.Constants.SETTINGS_HORIZONTAL_PADDING import com.mineinabyss.launchy.data.modpacks.Group import com.mineinabyss.launchy.data.modpacks.Mod import com.mineinabyss.launchy.data.modpacks.ModConfig +import com.mineinabyss.launchy.logic.AppDispatchers import com.mineinabyss.launchy.logic.DesktopHelpers import com.mineinabyss.launchy.logic.Instances.delete import com.mineinabyss.launchy.logic.Instances.updateInstance +import com.mineinabyss.launchy.logic.ModDownloader.checkHashes +import com.mineinabyss.launchy.state.InProgressTask import com.mineinabyss.launchy.ui.elements.* import com.mineinabyss.launchy.ui.screens.LocalGameInstanceState import com.mineinabyss.launchy.ui.screens.Screen import com.mineinabyss.launchy.ui.screens.screen +import kotlinx.coroutines.launch import kotlin.io.path.listDirectoryEntries @Composable @@ -110,6 +114,17 @@ fun OptionsTab() { OutlinedButton(onClick = { DesktopHelpers.openDirectory(pack.instance.minecraftDir) }) { Text("Open .minecraft folder") } + OutlinedButton(onClick = { + AppDispatchers.IO.launch { + state.runTask("checkHashes", InProgressTask("Checking hashes")) { + pack.checkHashes(pack.queued.modDownloadInfo).forEach { (modId, newInfo) -> + pack.queued.modDownloadInfo[modId] = newInfo + } + } + } + }) { + Text("Re-check hashes") + } } TitleSmall("Danger zone")