Skip to content

Commit 1c4cfbc

Browse files
committed
feat(desktop): enable Quilt instances and protect unfinished installer loaders
1 parent f2b1c43 commit 1c4cfbc

10 files changed

Lines changed: 78 additions & 30 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,8 @@ Loader support is intentionally staged:
358358

359359
- Fabric — v0.4
360360
- NeoForge — v0.7
361-
- Forge — v0.8
362-
- Quilt — v0.8
361+
- Forge — v0.8 (official Maven discovery; processor installation not yet implemented)
362+
- Quilt — v0.8 (official Quilt Meta v3 profile and instance selection; actual game acceptance pending)
363363

364364
Local mod management arrives in v0.5, followed by Modrinth integration and dependency resolution in v0.6.
365365

‎docs/ROADMAP.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ Goal: add a second major loader and prove the loader architecture is genuinely r
506506

507507
## NeoForge provider
508508

509-
- [ ] Discover compatible NeoForge versions.
509+
- [ ] 🚧 IN PROGRESS — Discover official NeoForge Maven versions; processor installation and actual launch remain unimplemented.
510510
- [ ] Parse/install NeoForge metadata/processors as required.
511511
- [ ] Resolve required libraries.
512512
- [ ] Integrate generated/processed artifacts safely.
@@ -533,7 +533,7 @@ Goal: broaden loader support while preserving historical-version compatibility d
533533

534534
## Forge
535535

536-
- [ ] Discover compatible Forge versions.
536+
- [ ] 🚧 IN PROGRESS — Discover official Forge Maven versions; processor installation and actual launch remain unimplemented.
537537
- [ ] Support required Forge installation metadata/processors for targeted Minecraft ranges.
538538
- [ ] Document exact supported Minecraft/Forge range if universal support is not practical.
539539
- [ ] Launch Forge through common pipeline.

‎docs/WORKLOG.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,39 @@ Every code/build/documentation/CI task must add an entry according to `AGENTS.md
88

99
---
1010

11+
## 2026-10-20 — Quilt loader integration and Forge-family discovery
12+
13+
**Milestone:** v0.7 NeoForge / v0.8 Forge and Quilt
14+
**Author/Agent:** GPT-6
15+
**Status:** Partial — GitHub CI and game acceptance pending
16+
17+
### Completed
18+
- Added Quilt Meta v3 provider with compatible loader versions and official profile merge, reusing generic loader boundaries.
19+
- Added Quilt to instance creation/editing and instance-scoped local/Modrinth JAR operation.
20+
- Registered NeoForge and Forge Maven version-discovery adapters with safe metadata parsing and fail-closed launch behavior.
21+
- Explicitly withheld the NeoForge/Forge UI choices until processor-based installation is implemented; exposing a download as a playable loader would be misleading.
22+
23+
### Changed files
24+
- `launcher-core/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/core/loader/*`, `launcher-core/src/test/kotlin/pl/syntaxdevteam/syntaxlauncher/core/loader/*`
25+
- `launcher-desktop/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/desktop/Main.kt`
26+
- `launcher-desktop/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/desktop/ui/InstanceWorkspace.kt`, `InstanceSettingsDialog.kt`, `SyntaxLauncherApp.kt`, `LocalModsDialog.kt`
27+
- `launcher-core/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/core/mods/LocalModManager.kt`, `ModrinthInstaller.kt`
28+
- `README.md`, `docs/ROADMAP.md`, `docs/WORKLOG.md`
29+
30+
### Verification
31+
- `./gradlew test`, `./gradlew build` — NOT RUN locally; hosted CI required.
32+
- Real game launch — NOT RUN; Quilt support is unverified, Forge and NeoForge are intentionally not launchable.
33+
34+
### Roadmap updates
35+
- v0.7/v0.8 official version discovery and Quilt adapter marked 🚧.
36+
- Forge/NeoForge processors and generated artifacts remain unchecked.
37+
38+
### Follow-ups / known issues
39+
- Implement verified official installer processors, generated libraries and Minecraft-version-specific support matrix for NeoForge/Forge.
40+
- Confirm Quilt API and actual game boot on real installations before declaring v0.8 complete.
41+
42+
---
43+
1144
## 2026-10-20 — Transactional managed Modrinth updates and rollback
1245

1346
**Milestone:** v0.6 — Modrinth Integration

‎launcher-core/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/core/mods/LocalModManager.kt‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class LocalModManager(private val instances: MinecraftInstanceRepository) {
5959
suspend fun importJar(instanceId: String, source: Path): LocalModInventory =
6060
withContext(Dispatchers.IO) {
6161
instances.withInactiveInstance(instanceId) { instance ->
62-
require(instance.loader?.id == "fabric") {
62+
require(instance.loader?.id in setOf("fabric", "quilt")) {
6363
"Local mod imports currently require a Fabric instance."
6464
}
6565
require(Files.isRegularFile(source, LinkOption.NOFOLLOW_LINKS)) {
@@ -107,7 +107,7 @@ class LocalModManager(private val instances: MinecraftInstanceRepository) {
107107
"Managed identity must match each downloaded JAR."
108108
}
109109
instances.withInactiveInstance(instanceId) { instance ->
110-
require(instance.loader?.id == "fabric") { "Only Fabric installation is available." }
110+
require(instance.loader?.id in setOf("fabric", "quilt")) { "Only Fabric installation is available." }
111111
val mods = modsDirectory(instance, create = true)
112112
val names = sources.map { it.fileName.toString() }
113113
require(names.distinct().size == names.size && names.all(::validJarName)) {
@@ -185,7 +185,7 @@ class LocalModManager(private val instances: MinecraftInstanceRepository) {
185185
withContext(Dispatchers.IO) {
186186
require(replacements.isNotEmpty() && replacements.size <= 32)
187187
instances.withInactiveInstance(instanceId) { instance ->
188-
require(instance.loader?.id == "fabric") { "Managed updates require a Fabric instance." }
188+
require(instance.loader?.id in setOf("fabric", "quilt")) { "Managed updates require a Fabric instance." }
189189
val game = instances.gameDirectory(instance)
190190
val mods = modsDirectory(instance)
191191
val tracked = managedStore.read(game)
@@ -446,7 +446,7 @@ class LocalModManager(private val instances: MinecraftInstanceRepository) {
446446
}
447447
val sorted = found.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.fileName })
448448
val active = sorted.filter { it.enabled }
449-
if (active.isNotEmpty() && instance.loader?.id != "fabric") {
449+
if (active.isNotEmpty() && instance.loader?.id !in setOf("fabric", "quilt")) {
450450
warnings += "Local JARs are present, but this instance has no Fabric loader."
451451
}
452452
val identifiers = active.mapNotNull { mod -> mod.metadata?.id?.let { it to mod.fileName } }

‎launcher-core/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/core/mods/ModrinthInstaller.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class ModrinthInstaller(
2222
suspend fun install(instanceId: String, plan: ModrinthInstallPlan): LocalModInventory =
2323
withContext(Dispatchers.IO) {
2424
val instance = instances.get(instanceId)
25-
require(instance.loader?.id == "fabric" &&
25+
require(instance.loader?.id in setOf("fabric", "quilt") &&
2626
instance.loader.id == plan.loaderId &&
2727
instance.minecraftVersionId == plan.minecraftVersionId) {
2828
"Instance loader or Minecraft version changed. Review the installation again."
@@ -77,7 +77,7 @@ class ModrinthInstaller(
7777
"Empty or conflicting Modrinth update selection."
7878
}
7979
val instance = instances.get(instanceId)
80-
require(instance.loader?.id == "fabric") { "Modrinth updates currently require Fabric." }
80+
require(instance.loader?.id in setOf("fabric", "quilt")) { "Modrinth updates currently require Fabric." }
8181
val game = instances.gameDirectory(instance)
8282
require(Files.isDirectory(game, LinkOption.NOFOLLOW_LINKS))
8383
val current = mods.list(instanceId).mods

‎launcher-desktop/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/desktop/Main.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import pl.syntaxdevteam.syntaxlauncher.core.java.MojangJavaRuntimeManager
1818
import pl.syntaxdevteam.syntaxlauncher.core.instance.MinecraftInstanceRepository
1919
import pl.syntaxdevteam.syntaxlauncher.core.loader.ModLoaderRegistry
2020
import pl.syntaxdevteam.syntaxlauncher.core.loader.FabricLoaderProvider
21+
import pl.syntaxdevteam.syntaxlauncher.core.loader.QuiltLoaderProvider
22+
import pl.syntaxdevteam.syntaxlauncher.core.loader.NeoForgeLoaderProvider
23+
import pl.syntaxdevteam.syntaxlauncher.core.loader.ForgeLoaderProvider
2124
import pl.syntaxdevteam.syntaxlauncher.core.launch.LaunchPlanBuilder
2225
import pl.syntaxdevteam.syntaxlauncher.core.launch.MinecraftLaunchCoordinator
2326
import pl.syntaxdevteam.syntaxlauncher.core.launch.MinecraftProcessManager
@@ -54,7 +57,10 @@ fun main(args: Array<String>) {
5457
LauncherVersion.parse(LauncherBuildInfo.version))
5558
} else null
5659
val processManager = MinecraftProcessManager(paths.logsDir)
57-
val modLoaders = ModLoaderRegistry(listOf(FabricLoaderProvider(httpClient)))
60+
val modLoaders = ModLoaderRegistry(listOf(
61+
FabricLoaderProvider(httpClient), QuiltLoaderProvider(httpClient),
62+
NeoForgeLoaderProvider(httpClient), ForgeLoaderProvider(httpClient),
63+
))
5864
val session = DesktopLauncherSession(
5965
versionCatalog = MinecraftVersionCatalog(httpClient),
6066
versionResolver = MinecraftVersionResolver(httpClient),

‎launcher-desktop/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/desktop/ui/InstanceSettingsDialog.kt‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import pl.syntaxdevteam.syntaxlauncher.core.loader.ModLoaderVersion
3333
fun InstanceSettingsDialog(
3434
instance: MinecraftInstance,
3535
availableVersions: List<String>,
36-
loadFabricVersions: suspend (String) -> List<ModLoaderVersion> = { emptyList() },
36+
loadLoaderVersions: suspend (String, String) -> List<ModLoaderVersion> = { _, _ -> emptyList() },
3737
onSave: (MinecraftInstance) -> Unit,
3838
onCancel: () -> Unit,
3939
) {
@@ -71,10 +71,10 @@ fun InstanceSettingsDialog(
7171
LaunchedEffect(versionId, loaderId) {
7272
compatible = emptyList()
7373
loaderError = null
74-
if (loaderId == "fabric" && versionValid) {
74+
if (loaderId in setOf("fabric", "quilt") && versionValid) {
7575
loaderLoading = true
7676
try {
77-
compatible = loadFabricVersions(versionId)
77+
compatible = loadLoaderVersions(loaderId, versionId)
7878
if (compatible.isEmpty()) {
7979
loaderError = "Fabric is not available for Minecraft $versionId."
8080
} else if (compatible.none { it.version == fabricVersion }) {
@@ -154,12 +154,15 @@ fun InstanceSettingsDialog(
154154
TextButton(onClick = { loaderId = "fabric" }) {
155155
Text(if (loaderId == "fabric") "✓ Fabric" else "Fabric")
156156
}
157+
TextButton(onClick = { loaderId = "quilt" }) {
158+
Text(if (loaderId == "quilt") "✓ Quilt" else "Quilt")
159+
}
157160
}
158-
if (loaderId == "fabric") {
161+
if (loaderId in setOf("fabric", "quilt")) {
159162
OutlinedTextField(
160163
value = fabricVersion,
161164
onValueChange = { fabricVersion = it },
162-
label = { Text("Fabric loader version") },
165+
label = { Text(loaderId.replaceFirstChar { it.uppercase() } + " loader version") },
163166
modifier = Modifier.fillMaxWidth(),
164167
singleLine = true,
165168
isError = !fabricValid,
@@ -231,7 +234,7 @@ fun InstanceSettingsDialog(
231234
minimumMemoryMiB = minRam!!,
232235
maximumMemoryMiB = maxRam!!,
233236
extraJvmArguments = args,
234-
loader = if (loaderId == "fabric") ModLoaderSelection("fabric", fabricVersion) else null,
237+
loader = if (loaderId in setOf("fabric", "quilt")) ModLoaderSelection(loaderId, fabricVersion) else null,
235238
))
236239
},
237240
) { Text("Save settings") }

‎launcher-desktop/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/desktop/ui/InstanceWorkspace.kt‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ fun InstanceWorkspace(
7979
hasLegacyGame: Boolean,
8080
latestVersion: String,
8181
availableVersions: List<String>,
82-
loadFabricVersions: suspend (String) -> List<ModLoaderVersion> = { emptyList() },
82+
loadLoaderVersions: suspend (String, String) -> List<ModLoaderVersion> = { _, _ -> emptyList() },
8383
error: String?,
8484
preview: InstanceCopyPreview?,
8585
copyProgress: InstanceCopyProgress?,
@@ -114,10 +114,10 @@ fun InstanceWorkspace(
114114
LaunchedEffect(newVersion, newLoader) {
115115
compatible = emptyList()
116116
fabricError = null
117-
if (newLoader == "fabric" && newVersion in availableVersions) {
117+
if (newLoader in setOf("fabric", "quilt") && newVersion in availableVersions) {
118118
fabricLoading = true
119119
try {
120-
compatible = loadFabricVersions(newVersion)
120+
compatible = loadLoaderVersions(newLoader, newVersion)
121121
if (compatible.isEmpty()) {
122122
fabricError = "Fabric is not available for Minecraft $newVersion."
123123
} else if (compatible.none { it.version == newFabricVersion }) {
@@ -261,7 +261,7 @@ fun InstanceWorkspace(
261261
Button(onClick = { onEdit(selected) }, enabled = canModify) {
262262
Text("Settings")
263263
}
264-
if (selected.loader?.id == "fabric") {
264+
if (selected.loader?.id in setOf("fabric", "quilt")) {
265265
OutlinedButton(onClick = { onManageMods(selected) },
266266
enabled = canModify) { Text("Mods") }
267267
}
@@ -309,12 +309,15 @@ fun InstanceWorkspace(
309309
TextButton(onClick = { newLoader = "fabric" }, enabled = canModify) {
310310
Text(if (newLoader == "fabric") "✓ Fabric" else "Fabric")
311311
}
312+
TextButton(onClick = { newLoader = "quilt" }, enabled = canModify) {
313+
Text(if (newLoader == "quilt") "✓ Quilt" else "Quilt")
314+
}
312315
}
313-
if (newLoader == "fabric") {
316+
if (newLoader in setOf("fabric", "quilt")) {
314317
OutlinedTextField(
315318
value = newFabricVersion,
316319
onValueChange = { newFabricVersion = it },
317-
label = { Text("Fabric loader version") },
320+
label = { Text(newLoader.replaceFirstChar { it.uppercase() } + " loader version") },
318321
singleLine = true, enabled = canModify,
319322
isError = !fabricValid,
320323
modifier = Modifier.fillMaxWidth(),
@@ -332,9 +335,12 @@ fun InstanceWorkspace(
332335
color = MaterialTheme.colorScheme.error,
333336
style = MaterialTheme.typography.bodySmall)
334337
}
338+
Text("NeoForge and Forge need processor-based installation; not selectable yet.",
339+
style = MaterialTheme.typography.labelSmall,
340+
color = MaterialTheme.colorScheme.onSurfaceVariant)
335341
Button(onClick = {
336342
onCreate(newName.trim(), newVersion.trim(),
337-
if (newLoader == "fabric") ModLoaderSelection("fabric", newFabricVersion) else null)
343+
if (newLoader in setOf("fabric", "quilt")) ModLoaderSelection(newLoader, newFabricVersion) else null)
338344
},
339345
enabled = canModify && newName.trim().length in 1..48 &&
340346
newVersion in availableVersions && fabricValid) { Text("Create instance") }

‎launcher-desktop/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/desktop/ui/LocalModsDialog.kt‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ fun LocalModsDialog(
127127
confirmButton = {
128128
TextButton(enabled = canModify && !busy, onClick = {
129129
confirmingUpdates = null
130-
operate { installer.update(instance.id, updates) }
130+
operate(operation = { installer.update(instance.id, updates) })
131131
}) { Text("Update mods") }
132132
},
133133
dismissButton = { TextButton(onClick = { confirmingUpdates = null }) { Text("Cancel") } },
@@ -170,7 +170,7 @@ fun LocalModsDialog(
170170
style = MaterialTheme.typography.labelMedium)
171171
Text(instance.name, style = MaterialTheme.typography.titleLarge,
172172
fontWeight = FontWeight.SemiBold)
173-
Text("Fabric · Minecraft ${instance.minecraftVersionId} · Files stay in this instance",
173+
Text("${instance.loader?.id ?: "Vanilla"} · Minecraft ${instance.minecraftVersionId} · Files stay in this instance",
174174
style = MaterialTheme.typography.bodySmall,
175175
color = MaterialTheme.colorScheme.onSurfaceVariant)
176176
}
@@ -185,7 +185,7 @@ fun LocalModsDialog(
185185
onClick = { browsing = true }) { Text("Discover on Modrinth") }
186186
Button(enabled = canModify && !busy, onClick = {
187187
val picker = JFileChooser().apply {
188-
dialogTitle = "Import a local Fabric mod"
188+
dialogTitle = "Import a local mod for this instance"
189189
fileFilter = FileNameExtensionFilter("Minecraft mod JAR (*.jar)", "jar")
190190
isAcceptAllFileFilterUsed = false
191191
}
@@ -241,7 +241,7 @@ fun LocalModsDialog(
241241
}
242242
}
243243
OutlinedButton(enabled = canModify && !busy,
244-
onClick = { operate { installer.rollback(instance.id) } }) {
244+
onClick = { operate(operation = { installer.rollback(instance.id) }) }) {
245245
Text("Rollback last Modrinth update")
246246
}
247247
}

‎launcher-desktop/src/main/kotlin/pl/syntaxdevteam/syntaxlauncher/desktop/ui/SyntaxLauncherApp.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ private fun LauncherPanel(
498498
InstanceSettingsDialog(
499499
instance = editing,
500500
availableVersions = manifest.versions.map { it.id },
501-
loadFabricVersions = { game -> session.loaders?.compatibleVersions("fabric", game).orEmpty() },
501+
loadLoaderVersions = { id, game -> session.loaders?.compatibleVersions(id, game).orEmpty() },
502502
onSave = { updated ->
503503
modifyInstance { session.instances.update(updated) }
504504
editingInstance = null
@@ -519,7 +519,7 @@ private fun LauncherPanel(
519519
// not an unrelated newer release that might convert world data.
520520
latestVersion = legacyVersionId.ifBlank { manifest.latestRelease },
521521
availableVersions = manifest.versions.map { it.id },
522-
loadFabricVersions = { game -> session.loaders?.compatibleVersions("fabric", game).orEmpty() },
522+
loadLoaderVersions = { id, game -> session.loaders?.compatibleVersions(id, game).orEmpty() },
523523
error = instanceError,
524524
preview = copyPreview,
525525
copyProgress = copyProgress,

0 commit comments

Comments
 (0)