Skip to content

Commit

Permalink
Feat: Check file checksums if provided
Browse files Browse the repository at this point in the history
Feat: Download mods to separate file and symlink them, allowing a separate user mods directory
Fix: Handle mod updates correctly
Fix: Replace provider for avatars
  • Loading branch information
0ffz committed Mar 9, 2024
1 parent 2f733b7 commit b973bbd
Show file tree
Hide file tree
Showing 39 changed files with 546 additions and 324 deletions.
2 changes: 0 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import de.undercouch.gradle.tasks.download.Download
import org.apache.tools.ant.taskdefs.condition.Os
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
Expand All @@ -9,7 +8,6 @@ plugins {
alias(idofrontLibs.plugins.kotlinx.serialization)
alias(idofrontLibs.plugins.compose)
id("de.undercouch.download") version "5.3.1"
id("com.github.johnrengelman.shadow") version "8.1.1"
}

repositories {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
group=com.mineinabyss
version=2.0.0-alpha.8
version=2.0.0-alpha.9
idofrontVersion=0.22.3
3 changes: 3 additions & 0 deletions src/main/kotlin/com/mineinabyss/launchy/data/Typealiases.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ typealias ModName = String
typealias GroupName = String
typealias DownloadURL = String
typealias ConfigURL = String


typealias ModID = String
33 changes: 19 additions & 14 deletions src/main/kotlin/com/mineinabyss/launchy/data/config/GameInstance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.compose.runtime.setValue
import com.charleskorn.kaml.encodeToStream
import com.mineinabyss.launchy.data.Dirs
import com.mineinabyss.launchy.data.Formats
import com.mineinabyss.launchy.logic.AppDispatchers
import com.mineinabyss.launchy.logic.Downloader
import com.mineinabyss.launchy.logic.UpdateResult
import com.mineinabyss.launchy.logic.showDialogOnError
Expand All @@ -26,13 +27,12 @@ class GameInstance(

val overridesDir = configDir / "overrides"

init {
require(configDir.isDirectory()) { "Game instance at $configDir must be a directory" }
}


val minecraftDir = config.overrideMinecraftDir?.let { Path(it) } ?: Dirs.modpackDir(configDir.name)

val modsDir = (minecraftDir / "mods").createDirectories()
val userMods = (minecraftDir / "modsFromUser").createDirectories()

val downloadsDir: Path = minecraftDir / "launchyDownloads"
val userConfigFile = (configDir / "config.yml")

val updateCheckerScope = CoroutineScope(Dispatchers.IO)
Expand All @@ -43,17 +43,17 @@ class GameInstance(
suspend fun createModpackState(state: LaunchyState): ModpackState? {
val userConfig = ModpackUserConfig.load(userConfigFile).getOrNull() ?: ModpackUserConfig()

state.inProgressTasks["loadingModpack"] = InProgressTask("Loading modpack ${config.name}")
val modpack = config.source.loadInstance(this)
.showDialogOnError("Failed to read instance")
.getOrElse {
it.printStackTrace()
return null
}
state.inProgressTasks.remove("loadingModpack")
val modpack = state.runTask("loadingModpack ${config.name}", InProgressTask("Loading modpack ${config.name}")) {
config.source.loadInstance(this)
.showDialogOnError("Failed to read instance")
.getOrElse {
it.printStackTrace()
return null
}
}

val cloudUrl = config.cloudInstanceURL
if (cloudUrl != null) state.ioScope.launch {
if (cloudUrl != null) AppDispatchers.IO.launch {
val updates = Downloader.checkUpdates(cloudUrl)
if (updates.result != UpdateResult.UpToDate) {
updatesAvailable = true
Expand All @@ -62,6 +62,11 @@ class GameInstance(
return ModpackState(this, modpack, userConfig)
}

init {
require(configDir.isDirectory()) { "Game instance at $configDir must be a directory" }
userMods
}

companion object {
fun create(state: LaunchyState, config: GameInstanceConfig) {
val instanceDir = Dirs.modpackConfigDir(config.name)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,57 @@
package com.mineinabyss.launchy.data.config

import com.charleskorn.kaml.decodeFromStream
import com.mineinabyss.launchy.data.*
import com.mineinabyss.launchy.data.modpacks.PackDependencies
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
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import java.nio.file.Path
import java.security.MessageDigest
import kotlin.io.path.*

enum class HashCheck {
UNKNOWN, VERIFIED, FAILED
}

@Serializable
data class DownloadInfo(
val url: String,
val path: String,
val desiredHash: String?,
val hashCheck: HashCheck,
val result: ModDownloader.DownloadResult,
) {
@Transient
val systemPath = Path(path)

fun failed(): Boolean {
return result == ModDownloader.DownloadResult.Failed
|| systemPath.isRegularFile()
|| (desiredHash != null && hashCheck == HashCheck.FAILED)
}

fun calculateSha1Hash(minecraftDir: Path): String {
val md = MessageDigest.getInstance("SHA-1")
return (minecraftDir / systemPath).checksum(md)
}
}

@Serializable
data class ModpackUserConfig(
val userAgreedDeps: PackDependencies? = null,
val userAgreedDeps: InstanceModLoaders? = null,
val fullEnabledGroups: Set<GroupName> = setOf(),
val fullDisabledGroups: Set<GroupName> = setOf(),
val toggledMods: Set<ModName> = setOf(),
val toggledConfigs: Set<ModName> = setOf(),
val seenGroups: Set<GroupName> = setOf(),
val modDownloads: Map<ModName, DownloadURL> = mapOf(),
val modConfigs: Map<ModName, ConfigURL> = mapOf(),
val modDownloadInfo: Map<ModID, DownloadInfo> = mapOf(),
// val configDownloadInfo: Map<ModID, DownloadInfo> = mapOf(),
val downloadUpdates: Boolean = true,
) {
fun save(file: Path) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable
class ModReference(
val urlContains: String,
val info: ModInfo? = null,
val info: ModConfig? = null,
)
@Serializable
class ExtraPackInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

@Serializable
data class PackDependencies(
data class InstanceModLoaders(
val minecraft: String,
@SerialName("fabric-loader")
val fabricLoader: String? = null,
Expand Down
19 changes: 11 additions & 8 deletions src/main/kotlin/com/mineinabyss/launchy/data/modpacks/Mod.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
package com.mineinabyss.launchy.data.modpacks

import com.mineinabyss.launchy.data.Dirs
import com.mineinabyss.launchy.data.modpacks.formats.ModrinthPackFormat
import io.ktor.http.*
import java.nio.file.Path
import kotlin.io.path.div
import kotlin.io.path.exists

data class Mod(
val packDir: Path,
val info: ModInfo
private val downloadDir: Path,
val info: ModConfig,
val modId: String,
val desiredHashes: ModrinthPackFormat.Hashes?,
) {
val file =
if (info.downloadPath != null) packDir / info.downloadPath
else packDir / "mods" / "${info.name}.jar"
val absoluteDownloadDest =
if (info.downloadPath != null) downloadDir / info.downloadPath.validated
else downloadDir / "mods" / "${info.id ?: info.name}.jar"

val config = Dirs.tmp / "${info.name}-config.zip"
val downloadUrl: Url = Url(info.url)

val isDownloaded get() = file.exists()
val config = Dirs.tmp / "${info.name}-config.zip"


fun compatibleWith(other: Mod) =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.mineinabyss.launchy.data.modpacks

import com.mineinabyss.launchy.data.modpacks.formats.ModDownloadPath
import kotlinx.serialization.Serializable

@Serializable
data class ModInfo(
data class ModConfig(
val id: String? = null,
val name: String,
val license: String = "",
val homepage: String = "",
Expand All @@ -14,6 +16,6 @@ data class ModInfo(
val forceConfigDownload: Boolean = false,
val dependency: Boolean = false,
val incompatibleWith: List<String> = emptyList(),
val downloadPath: String? = null,
val downloadPath: ModDownloadPath? = null,
val requires: List<String> = emptyList(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.mineinabyss.launchy.data.modpacks
import java.nio.file.Path

class Modpack(
val dependencies: PackDependencies,
val modLoaders: InstanceModLoaders,
val mods: Mods,
val overridesPaths: List<Path> = listOf(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ data class ExtraInfoFormat(
val format: PackFormat,
val extraInfoPack: ExtraPackInfo,
) : PackFormat by format {
override fun toGenericMods(minecraftDir: Path): Mods {
val originalMods = format.toGenericMods(minecraftDir)
override fun toGenericMods(downloadsDir: Path): Mods {
val originalMods = format.toGenericMods(downloadsDir)
val foundMods = mutableSetOf<Mod>()
val mods: Map<Group, Set<Mod>> = extraInfoPack.modGroups
.mapKeys { (name, _) -> extraInfoPack.groups.single { it.name == name } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,25 @@ data class LaunchyPackFormat(
val fabricVersion: String? = null,
val minecraftVersion: String,
val groups: Set<Group>,
private val modGroups: Map<GroupName, Set<ModInfo>>,
private val modGroups: Map<GroupName, Set<ModConfig>>,
) : PackFormat {
override fun toGenericMods(minecraftDir: Path): Mods {
override fun toGenericMods(downloadsDir: Path): Mods {
return Mods(modGroups
.mapKeys { (name, _) -> groups.single { it.name == name } }
.mapValues { (_, mods) -> mods.map { Mod(minecraftDir, it) }.toSet() })
.mapValues { (_, mods) ->
mods.map {
Mod(
downloadDir = downloadsDir,
info = it,
modId = it.id ?: it.name,
desiredHashes = null,
)
}.toSet()
})
}

override fun getDependencies(minecraftDir: Path): PackDependencies {
return PackDependencies(minecraft = minecraftVersion, fabricLoader = fabricVersion)
override fun getModLoaders(): InstanceModLoaders {
return InstanceModLoaders(minecraft = minecraftVersion, fabricLoader = fabricVersion)
}

override fun getOverridesPaths(configDir: Path): List<Path> = emptyList()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.mineinabyss.launchy.data.modpacks.formats

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.nio.file.Path
import kotlin.io.path.Path

@Serializable(with = ValidModPathSerializer::class)
class ModDownloadPath(
val validated: Path
)

object ValidModPathSerializer : KSerializer<ModDownloadPath> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("modPath", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): ModDownloadPath {
val pathString = decoder.decodeString()
if (pathString.contains("..")) error("Mod path cannot contain ..")
val path = Path(pathString)
if (path.isAbsolute) error("Path cannot be absolute")
return ModDownloadPath(path)
}

override fun serialize(encoder: Encoder, value: ModDownloadPath) {
encoder.encodeString(value.validated.toString())
}

}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package com.mineinabyss.launchy.data.modpacks.formats

import com.mineinabyss.launchy.data.modpacks.InstanceModLoaders
import com.mineinabyss.launchy.data.modpacks.Mod
import com.mineinabyss.launchy.data.modpacks.ModInfo
import com.mineinabyss.launchy.data.modpacks.ModConfig
import com.mineinabyss.launchy.data.modpacks.Mods
import com.mineinabyss.launchy.data.modpacks.PackDependencies
import kotlinx.serialization.Serializable
import java.nio.file.Path
import kotlin.io.path.div

@Serializable
data class ModrinthPackFormat(
val dependencies: PackDependencies,
val dependencies: InstanceModLoaders,
val files: List<PackFile>,
val formatVersion: Int,
val name: String,
Expand All @@ -20,25 +20,34 @@ data class ModrinthPackFormat(
data class PackFile(
val downloads: List<String>,
val fileSize: Long,
val path: String,
val path: ModDownloadPath,
val hashes: Hashes,
) {
fun toMod(packDir: Path) = Mod(
packDir,
ModInfo(
name = path.removePrefix("mods/").removeSuffix(".jar"),
ModConfig(
name = path.validated.toString().removePrefix("mods/").removeSuffix(".jar"),
desc = "",
url = downloads.single(),
downloadPath = path,
)
),
modId = downloads.single().removePrefix("https://cdn.modrinth.com/data/").substringBefore("/versions"),
desiredHashes = hashes,
)
}

override fun getDependencies(minecraftDir: Path): PackDependencies {
@Serializable
data class Hashes(
val sha1: String,
val sha512: String,
)

override fun getModLoaders(): InstanceModLoaders {
return dependencies
}

override fun toGenericMods(minecraftDir: Path) =
Mods.withSingleGroup(files.map { it.toMod(minecraftDir) })
override fun toGenericMods(downloadsDir: Path) =
Mods.withSingleGroup(files.map { it.toMod(downloadsDir) })

override fun getOverridesPaths(configDir: Path): List<Path> = listOf(configDir / "mrpack" / "overrides")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.mineinabyss.launchy.data.modpacks.formats

import com.mineinabyss.launchy.data.modpacks.InstanceModLoaders
import com.mineinabyss.launchy.data.modpacks.Mods
import com.mineinabyss.launchy.data.modpacks.PackDependencies
import java.nio.file.Path

sealed interface PackFormat {
fun toGenericMods(minecraftDir: Path): Mods
fun toGenericMods(downloadsDir: Path): Mods

fun getDependencies(minecraftDir: Path): PackDependencies
fun getModLoaders(): InstanceModLoaders

fun getOverridesPaths(configDir: Path): List<Path>
}
Loading

0 comments on commit b973bbd

Please sign in to comment.