Skip to content

Commit 3553c65

Browse files
committed
Companion Mod updating and Mod Source
1 parent 175e700 commit 3553c65

6 files changed

Lines changed: 175 additions & 27 deletions

File tree

src/main/kotlin/io/github/tritium_launcher/launcher/companion/CompanionModProvider.kt

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.github.tritium_launcher.launcher.companion
33
import io.github.tritium_launcher.launcher.core.mod.InstalledMod
44
import io.github.tritium_launcher.launcher.core.mod.ModDatabase
55
import io.github.tritium_launcher.launcher.core.mod.ModSide
6+
import io.github.tritium_launcher.launcher.core.source.ModVersionOption
67
import io.github.tritium_launcher.launcher.fromTR
78
import io.github.tritium_launcher.launcher.io.VPath
89
import io.github.tritium_launcher.launcher.logger
@@ -20,14 +21,29 @@ import kotlin.time.Clock
2021
@Serializable
2122
data class CompanionManifest(
2223
val schema: Int,
24+
val project: CompanionProjectMeta? = null,
2325
val entries: List<CompanionManifestEntry>
2426
)
2527

28+
@Serializable
29+
data class CompanionProjectMeta(
30+
val title: String = "Tritium Companion Mod",
31+
val summary: String = "Bridges Tritium launcher features into Minecraft.",
32+
val description: String = "No description available.",
33+
val descriptionUrl: String? = null,
34+
val iconUrl: String? = null,
35+
val author: String? = null,
36+
val website: String? = null,
37+
)
38+
2639
@Serializable
2740
data class CompanionManifestEntry(
2841
val mcVersion: String,
2942
val loaders: List<String>,
3043
val modVersion: String,
44+
val displayName: String? = null,
45+
val releaseType: String? = null,
46+
val changelog: String? = null,
3147
val jars: Map<String, CompanionManifestJar>
3248
)
3349

@@ -45,14 +61,61 @@ object CompanionModProvider {
4561
private val sharedClient = HttpClient(CIO)
4662
private val CACHE_DIR: VPath = fromTR("cache", "companion-mods")
4763

48-
private const val COMPANION_MOD_ID = "tritium-companion"
49-
private const val COMPANION_SOURCE = "tritium-companion"
64+
const val COMPANION_MOD_ID = "tritium-companion"
65+
const val COMPANION_SOURCE = "tritium-companion"
5066
private const val COMPANION_DISPLAY_NAME = "Tritium Companion Mod"
51-
private const val COMPANION_FILE_NAME = "$COMPANION_MOD_ID.jar"
67+
const val COMPANION_FILE_NAME = "$COMPANION_MOD_ID.jar"
5268

5369
fun jarExists(projectRoot: VPath): Boolean =
5470
projectRoot.resolve("mods").resolve(COMPANION_FILE_NAME).exists()
5571

72+
suspend fun checkUpdate(companionMod: InstalledMod): ModVersionOption? {
73+
if (companionMod.source != COMPANION_SOURCE) return null
74+
val prefix = "$COMPANION_MOD_ID-"
75+
val suffix = companionMod.projectId.removePrefix(prefix)
76+
val parts = suffix.split("-", limit = 2)
77+
if (parts.size != 2) return null
78+
val (mcVersion, loaderId) = parts
79+
val entries = allVersions(mcVersion, loaderId)
80+
val latest = entries.firstOrNull() ?: return null
81+
if (latest.modVersion == companionMod.versionId) return null
82+
return ModVersionOption(
83+
id = latest.modVersion,
84+
label = latest.displayName ?: latest.modVersion,
85+
)
86+
}
87+
88+
suspend fun fetchProjectMeta(): CompanionProjectMeta? {
89+
return try {
90+
val body = sharedClient.get(DEFAULT_MANIFEST_URL).bodyAsText()
91+
val manifest = json.decodeFromString<CompanionManifest>(body)
92+
manifest.project
93+
} catch (t: Throwable) {
94+
logger.warn("Failed to fetch companion project meta", t)
95+
null
96+
}
97+
}
98+
99+
suspend fun allVersions(mcVersion: String, loaderId: String): List<CompanionManifestEntry> {
100+
return try {
101+
val body = sharedClient.get(DEFAULT_MANIFEST_URL).bodyAsText()
102+
val manifest = json.decodeFromString<CompanionManifest>(body)
103+
manifest.entries
104+
.filter { it.mcVersion == mcVersion && loaderId in it.loaders }
105+
.sortedByDescending { it.modVersion }
106+
} catch (t: Throwable) {
107+
logger.warn("Failed to fetch companion mod versions", t)
108+
emptyList()
109+
}
110+
}
111+
112+
suspend fun resolveEntry(mcVersion: String, loaderId: String, versionId: String): CompanionManifestEntry? {
113+
return allVersions(mcVersion, loaderId).find { it.modVersion == versionId }
114+
}
115+
116+
private suspend fun resolveEntry(mcVersion: String, loaderId: String): CompanionManifestEntry? =
117+
allVersions(mcVersion, loaderId).firstOrNull()
118+
56119
suspend fun installIfNeeded(
57120
projectRoot: VPath,
58121
mcVersion: String,
@@ -135,18 +198,6 @@ object CompanionModProvider {
135198
return response.body()
136199
}
137200

138-
private suspend fun resolveEntry(mcVersion: String, loaderId: String): CompanionManifestEntry? {
139-
return try {
140-
val response = sharedClient.get(DEFAULT_MANIFEST_URL)
141-
val body = response.bodyAsText()
142-
val manifest = json.decodeFromString<CompanionManifest>(body)
143-
manifest.entries.find { it.mcVersion == mcVersion && loaderId in it.loaders }
144-
} catch (t: Throwable) {
145-
logger.warn("Failed to fetch companion mod manifest from {}", DEFAULT_MANIFEST_URL, t)
146-
null
147-
}
148-
}
149-
150201
private fun sha256(bytes: ByteArray): String {
151202
val digest = MessageDigest.getInstance("SHA-256")
152203
return digest.digest(bytes).joinToString("") { "%02x".format(it) }
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.github.tritium_launcher.launcher.companion
2+
3+
import io.github.tritium_launcher.launcher.core.source.*
4+
import io.github.tritium_launcher.launcher.ui.theme.TIcons
5+
import io.ktor.client.*
6+
import io.ktor.client.engine.cio.*
7+
import io.ktor.client.request.*
8+
import io.ktor.client.statement.*
9+
import io.qt.gui.QPixmap
10+
11+
class CompanionModSource : ModSource() {
12+
override val id = CompanionModProvider.COMPANION_SOURCE
13+
override val displayName = "Tritium Companion"
14+
override val icon: QPixmap = TIcons.Tritium
15+
override val webpage = "https://github.com/Tritium-Launcher/Tritium-Companion"
16+
override val order = 100
17+
override val descriptionFormat = DescriptionFormat.MARKDOWN
18+
19+
private val httpClient = HttpClient(CIO)
20+
21+
override suspend fun search(context: ModBrowserContext, query: ModSearchQuery): ModSearchPage =
22+
ModSearchPage(emptyList(), 0)
23+
24+
override suspend fun details(context: ModBrowserContext, projectId: String): ModDetails {
25+
val meta = CompanionModProvider.fetchProjectMeta()
26+
val description = if (meta?.descriptionUrl != null) {
27+
try {
28+
httpClient.get(meta.descriptionUrl).bodyAsText()
29+
} catch (_: Exception) {
30+
meta.description
31+
}
32+
} else {
33+
meta?.description ?: "No description available."
34+
}
35+
return ModDetails(
36+
id = projectId,
37+
title = meta?.title ?: "Tritium Companion Mod",
38+
summary = meta?.summary ?: "Provides helpful utilities for ModPack Developers",
39+
description = description,
40+
author = meta?.author,
41+
iconUrl = meta?.iconUrl ?: "https://raw.githubusercontent.com/Tritium-Launcher/Tritium-Companion/gh-pages/icon.png",
42+
website = meta?.website ?: webpage,
43+
)
44+
}
45+
46+
override suspend fun versions(context: ModBrowserContext, projectId: String): List<ModVersionOption> {
47+
val mcVersion = context.minecraftVersion ?: return emptyList()
48+
val loaderId = context.modLoaderId ?: return emptyList()
49+
return CompanionModProvider.allVersions(mcVersion, loaderId).map { entry ->
50+
ModVersionOption(
51+
id = entry.modVersion,
52+
label = entry.displayName ?: entry.modVersion,
53+
fileHash = entry.jars[loaderId]?.sha256,
54+
releaseType = try {
55+
entry.releaseType?.let { ReleaseType.valueOf(it.uppercase()) }
56+
} catch (_: IllegalArgumentException) { null },
57+
)
58+
}
59+
}
60+
61+
override suspend fun resolveInstall(
62+
context: ModBrowserContext,
63+
projectId: String,
64+
versionId: String
65+
): ModInstallPlan {
66+
val mcVersion = context.minecraftVersion ?: error("No MC version in context")
67+
val loaderId = context.modLoaderId ?: error("No loader in context")
68+
val entry = CompanionModProvider.resolveEntry(mcVersion, loaderId, versionId)
69+
?: error("Companion mod version $versionId not found for $mcVersion/$loaderId")
70+
val jar = entry.jars[loaderId] ?: error("No JAR for loader $loaderId")
71+
return ModInstallPlan(
72+
projectId = projectId,
73+
versionId = versionId,
74+
versionLabel = entry.displayName ?: versionId,
75+
fileName = jar.fileName,
76+
downloadUrl = jar.url,
77+
fileHash = jar.sha256,
78+
)
79+
}
80+
}

src/main/kotlin/io/github/tritium_launcher/launcher/core/mod/ModUpdateChecker.kt

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.tritium_launcher.launcher.core.mod
22

3+
import io.github.tritium_launcher.launcher.companion.CompanionModProvider
34
import io.github.tritium_launcher.launcher.core.project.ModpackMeta
45
import io.github.tritium_launcher.launcher.core.project.Project
56
import io.github.tritium_launcher.launcher.core.project.ProjectBase
@@ -56,24 +57,29 @@ object ModUpdateChecker {
5657
project: ProjectBase,
5758
mods: List<InstalledMod>? = null
5859
): Map<String, ModVersionOption> {
59-
val context = resolveContext(project) ?: return emptyMap()
60-
val source = resolveSource(context) ?: return emptyMap()
60+
val context = resolveContext(project)
61+
val source = context?.let { resolveSource(it) }
6162

6263
val installedMods = mods ?: withContext(Dispatchers.IO) {
63-
ModDatabase(project.projectDir).use { db -> db.getBySource(source.id) }
64+
if (source != null) {
65+
ModDatabase(project.projectDir).use { db -> db.getBySource(source.id) }
66+
} else {
67+
emptyList()
68+
}
6469
}
6570

6671
val results = coroutineScope {
67-
installedMods
68-
.map { mod ->
69-
async {
70-
val update = checkMod(project, mod)
71-
if (update != null) mod.projectId to update else null
72+
installedMods.map { mod ->
73+
async {
74+
when {
75+
mod.source == CompanionModProvider.COMPANION_SOURCE ->
76+
CompanionModProvider.checkUpdate(mod)?.let { mod.projectId to it }
77+
source != null && mod.source == source.id ->
78+
checkMod(project, mod)?.let { mod.projectId to it }
79+
else -> null
7280
}
7381
}
74-
.awaitAll()
75-
.filterNotNull()
76-
.toMap()
82+
}.awaitAll().filterNotNull().toMap()
7783
}
7884

7985
return results

src/main/kotlin/io/github/tritium_launcher/launcher/extension/core/CoreExtension.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import io.github.tritium_launcher.launcher.accounts.CurseForgeAccount
44
import io.github.tritium_launcher.launcher.accounts.MicrosoftAccountProvider
55
import io.github.tritium_launcher.launcher.accounts.ModrinthAccount
66
import io.github.tritium_launcher.launcher.applyRainbowOverlay
7+
import io.github.tritium_launcher.launcher.companion.CompanionModSource
78
import io.github.tritium_launcher.launcher.core.mod_config.ConfigFormat
89
import io.github.tritium_launcher.launcher.core.modloader.Fabric
910
import io.github.tritium_launcher.launcher.core.modloader.NeoForge
@@ -90,6 +91,7 @@ internal object CoreExtension : Extension {
9091

9192
modSources.register(Modrinth())
9293
modSources.register(CurseForge())
94+
modSources.register(CompanionModSource())
9395

9496
projectTypes.register(ModpackProjectType())
9597

src/main/kotlin/io/github/tritium_launcher/launcher/ui/project/editor/panes/ModDetailPane.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.tritium_launcher.launcher.ui.project.editor.panes
22

3+
import io.github.tritium_launcher.launcher.companion.CompanionModProvider
34
import io.github.tritium_launcher.launcher.connect
45
import io.github.tritium_launcher.launcher.core.TritiumEvent
56
import io.github.tritium_launcher.launcher.core.TritiumEventBus
@@ -552,6 +553,9 @@ class ModDetailPane(
552553
shared.versionsCache[details.id]?.firstOrNull()?.label ?: details.latestVersion
553554

554555
private fun resolveSource(): ModSource? {
556+
if (modId == CompanionModProvider.COMPANION_MOD_ID) {
557+
return BuiltinRegistries.ModSource.get(CompanionModProvider.COMPANION_SOURCE)
558+
}
555559
val sourceId = ((project as? Project<*>)?.typedMeta as? ModpackMeta)?.source
556560
return sourceId?.let { id -> BuiltinRegistries.ModSource.all().find { s -> s.id == id } }
557561
}

src/main/kotlin/io/github/tritium_launcher/launcher/ui/project/sidebar/ProjectInstalledModsSidePanelProvider.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.tritium_launcher.launcher.ui.project.sidebar
22

3+
import io.github.tritium_launcher.launcher.companion.CompanionModProvider
34
import io.github.tritium_launcher.launcher.connect
45
import io.github.tritium_launcher.launcher.core.TritiumEvent
56
import io.github.tritium_launcher.launcher.core.TritiumEventBus
@@ -170,8 +171,10 @@ class InstalledModsPanel(
170171
val mod = withContext(Dispatchers.IO) {
171172
ModDatabase(project.projectDir).use { db -> db.getByProjectId(projectId) }
172173
}
174+
val modId = if (mod?.source == CompanionModProvider.COMPANION_SOURCE)
175+
CompanionModProvider.COMPANION_MOD_ID else projectId
173176
val title = mod?.displayName ?: projectId
174-
onOpenDetailRequested?.invoke(projectId, title)
177+
onOpenDetailRequested?.invoke(modId, title)
175178
}
176179
}
177180

@@ -254,6 +257,8 @@ class InstalledModsPanel(
254257

255258
val jarFiles = modsDirFile.listFiles { f -> f.fileName().endsWith(".jar", ignoreCase = true) }
256259
for (jarFile in jarFiles) {
260+
val jarName = jarFile.fileName()
261+
if (jarName == CompanionModProvider.COMPANION_FILE_NAME) continue
257262
val bytes = try { jarFile.bytesOrNothing() } catch (_: Exception) { continue }
258263
val hash = ModDatabase.sha1(bytes)
259264
if (hash in existingHashes) continue

0 commit comments

Comments
 (0)