From 53286871e62fd1b93d2fe2a8b5082d3d73b4ebb3 Mon Sep 17 00:00:00 2001 From: Geferon Date: Tue, 28 Jun 2022 18:52:26 +0200 Subject: [PATCH] Initial MultiMC Forge and MC version support (#31) * Initial MultiMC Forge and MC version support * Added support for MultiMC MC and Forge versioning * Added support for Fabric, added MultiMC confirmation prompt * Refactored prompt method for MultiMC update * Initial PR changes * Refactored most MultiMC code to LauncherUtils --- build.gradle.kts | 2 +- .../infra/packwiz/installer/LauncherUtils.kt | 108 ++++++++++++++++++ .../link/infra/packwiz/installer/Main.kt | 2 + .../infra/packwiz/installer/UpdateManager.kt | 26 ++++- .../packwiz/installer/ui/IUserInterface.kt | 6 + .../packwiz/installer/ui/gui/GUIHandler.kt | 52 +++++++++ 6 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/link/infra/packwiz/installer/LauncherUtils.kt diff --git a/build.gradle.kts b/build.gradle.kts index 0ba42d5..b751379 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,7 +31,7 @@ repositories { dependencies { implementation("commons-cli:commons-cli:1.5.0") implementation("com.moandjiezana.toml:toml4j:0.7.2") - implementation("com.google.code.gson:gson:2.8.9") + implementation("com.google.code.gson:gson:2.9.0") implementation("com.squareup.okio:okio:3.0.0") implementation(kotlin("stdlib-jdk8")) implementation("com.squareup.okhttp3:okhttp:4.9.3") diff --git a/src/main/kotlin/link/infra/packwiz/installer/LauncherUtils.kt b/src/main/kotlin/link/infra/packwiz/installer/LauncherUtils.kt new file mode 100644 index 0000000..1e9eb05 --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/LauncherUtils.kt @@ -0,0 +1,108 @@ +package link.infra.packwiz.installer + +import com.google.gson.Gson +import com.google.gson.JsonIOException +import com.google.gson.JsonParser +import com.google.gson.JsonSyntaxException +import link.infra.packwiz.installer.metadata.PackFile +import link.infra.packwiz.installer.ui.IUserInterface +import link.infra.packwiz.installer.util.Log +import java.io.File +import java.nio.file.Paths + +class LauncherUtils internal constructor(private val opts: UpdateManager.Options, val ui: IUserInterface) { + enum class LauncherStatus { + Succesful, + NoChanges, + Cancelled, + NotFound, // We'll use the NotFound as the neutral return type for now + } + + fun handleMultiMC(pf: PackFile, gson: Gson): LauncherStatus { + // MultiMC MC and loader version checker + val manifestPath = Paths.get(opts.multimcFolder, "mmc-pack.json").toString() + val manifestFile = File(manifestPath) + + if (!manifestFile.exists()) { + return LauncherStatus.NotFound + } + + val multimcManifest = try { + JsonParser.parseReader(manifestFile.reader()) + } catch (e: JsonIOException) { + throw Exception("Cannot read the MultiMC pack file", e) + } catch (e: JsonSyntaxException) { + throw Exception("Invalid MultiMC pack file", e) + }.asJsonObject + + Log.info("Loaded MultiMC config") + + // We only support format 1, if it gets updated in the future we'll have to handle that + // There's only version 1 for now tho, so that's good + if (multimcManifest["formatVersion"].asInt != 1) { + throw Exception("Invalid MultiMC format version") + } + + var manifestModified = false + val modLoaders = hashMapOf("net.minecraft" to "minecraft", "net.minecraftforge" to "forge", "net.fabricmc.fabric-loader" to "fabric", "org.quiltmc.quilt-loader" to "quilt", "com.mumfrey.liteloader" to "liteloader") + val modLoadersClasses = modLoaders.entries.associate{(k,v)-> v to k} + var modLoaderFound = false + val modLoadersFound = HashMap() // Key: modLoader, Value: Version + val components = multimcManifest["components"].asJsonArray + for (componentObj in components) { + val component = componentObj.asJsonObject + + val version = component["version"].asString + // If we find any of the modloaders we support, we save it and check the version + if (modLoaders.containsKey(component["uid"].asString)) { + val modLoader = modLoaders.getValue(component["uid"].asString) + if (modLoader != "minecraft") + modLoaderFound = true // Only set to true if modLoader isn't Minecraft + modLoadersFound[modLoader] = version + if (version != pf.versions?.get(modLoader)) { + manifestModified = true + component.addProperty("version", pf.versions?.get(modLoader)) + } + } + } + + // If we can't find the mod loader in the MultiMC file, we add it + if (!modLoaderFound) { + // Using this filter and loop to handle multiple handlers + for ((_, loader) in modLoaders + .filter { it.value != "minecraft" && !modLoadersFound.containsKey(it.value) && pf.versions?.containsKey(it.value) == true } + ) { + components.add(gson.toJsonTree(hashMapOf("uid" to modLoadersClasses.get(loader), "version" to pf.versions?.get(loader)))) + } + } + + // If mc version change detected, and fabric mappings are found, delete them, MultiMC will add and re-dl the correct one + if (modLoadersFound["minecraft"] != pf.versions?.getValue("minecraft")) + components.find { it.asJsonObject["uid"].asString == "net.fabricmc.intermediary" }?.asJsonObject?.let { components.remove(it) } + + if (manifestModified) { + // The manifest has been modified, so before saving it we'll ask the user + // if they wanna update it, continue without updating it, or exit + val oldVers = modLoadersFound.map { Pair(it.key, it.value) } + val newVers = pf.versions!!.map { Pair(it.key, it.value) } + + + when (ui.showUpdateConfirmationDialog(oldVers, newVers)) { + IUserInterface.UpdateConfirmationResult.CANCELLED -> { + return LauncherStatus.Cancelled + } + IUserInterface.UpdateConfirmationResult.CONTINUE -> { + return LauncherStatus.Succesful // Returning succesful as... Well, the user is telling us to continue + } + else -> {} // Compiler is giving warning about "non-exhaustive when", so i'll just add an empty one + } + + manifestFile.writeText(gson.toJson(multimcManifest)) + Log.info("Updated modpack Minecrafts and/or the modloaders version") + + return LauncherStatus.Succesful + } + + return LauncherStatus.NoChanges + } +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/Main.kt b/src/main/kotlin/link/infra/packwiz/installer/Main.kt index f22632b..2991205 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/Main.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/Main.kt @@ -71,6 +71,7 @@ class Main(args: Array) { downloadURI = SpaceSafeURI(unparsedArgs[0]), side = cmd.getOptionValue("side")?.let((Side)::from), packFolder = cmd.getOptionValue("pack-folder"), + multimcFolder = cmd.getOptionValue("multimc-folder"), manifestFile = cmd.getOptionValue("meta-file") ) } catch (e: URISyntaxException) { @@ -94,6 +95,7 @@ class Main(args: Array) { options.addOption("s", "side", true, "Side to install mods from (client/server, defaults to client)") options.addOption(null, "title", true, "Title of the installer window") options.addOption(null, "pack-folder", true, "Folder to install the pack to (defaults to the JAR directory)") + options.addOption(null, "multimc-folder", true, "The MultiMC pack folder (defaults to the parent of the pack directory)") options.addOption(null, "meta-file", true, "JSON file to store pack metadata, relative to the pack folder (defaults to packwiz.json)") } diff --git a/src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt b/src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt index 3cb2e3d..425e538 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt @@ -45,12 +45,13 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse val downloadURI: SpaceSafeURI, val manifestFile: String, val packFolder: String, + val multimcFolder: String, val side: Side ) { // Horrible workaround for default params not working cleanly with nullable values companion object { - fun construct(downloadURI: SpaceSafeURI, manifestFile: String?, packFolder: String?, side: Side?) = - Options(downloadURI, manifestFile ?: "packwiz.json", packFolder ?: ".", side ?: Side.CLIENT) + fun construct(downloadURI: SpaceSafeURI, manifestFile: String?, packFolder: String?, multimcFolder: String?, side: Side?) = + Options(downloadURI, manifestFile ?: "packwiz.json", packFolder ?: ".", multimcFolder ?: "..", side ?: Side.CLIENT) } } @@ -97,6 +98,26 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse handleCancellation() } + // Launcher checks + val lu = LauncherUtils(opts, ui) + + // MultiMC MC and loader version checker + ui.submitProgress(InstallProgress("Loading MultiMC pack file...")) + try { + when (lu.handleMultiMC(pf, gson)) { + LauncherUtils.LauncherStatus.Cancelled -> cancelled = true + LauncherUtils.LauncherStatus.NotFound -> Log.info("MultiMC not detected") + } + handleCancellation() + } catch (e: Exception) { + ui.showErrorAndExit(e.message!!, e) + } + + if (ui.cancelButtonPressed) { + showCancellationDialog() + handleCancellation() + } + ui.submitProgress(InstallProgress("Checking local files...")) // Invalidation checking must be done here, as it must happen before pack/index hashes are checked @@ -163,7 +184,6 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse handleCancellation() - // TODO: update MMC params, java args etc // If there were errors, don't write the manifest/index hashes, to ensure they are rechecked later if (errorsOccurred) { diff --git a/src/main/kotlin/link/infra/packwiz/installer/ui/IUserInterface.kt b/src/main/kotlin/link/infra/packwiz/installer/ui/IUserInterface.kt index 55c1ead..ecd7bd6 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/ui/IUserInterface.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/ui/IUserInterface.kt @@ -23,6 +23,8 @@ interface IUserInterface { fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT + fun showUpdateConfirmationDialog(oldVersions: List>, newVersions: List>): UpdateConfirmationResult = UpdateConfirmationResult.CANCELLED + fun awaitOptionalButton(showCancel: Boolean) enum class ExceptionListResult { @@ -33,6 +35,10 @@ interface IUserInterface { QUIT, CONTINUE } + enum class UpdateConfirmationResult { + CANCELLED, CONTINUE, UPDATE + } + var optionsButtonPressed: Boolean var cancelButtonPressed: Boolean diff --git a/src/main/kotlin/link/infra/packwiz/installer/ui/gui/GUIHandler.kt b/src/main/kotlin/link/infra/packwiz/installer/ui/gui/GUIHandler.kt index 36a46f6..508e3cd 100644 --- a/src/main/kotlin/link/infra/packwiz/installer/ui/gui/GUIHandler.kt +++ b/src/main/kotlin/link/infra/packwiz/installer/ui/gui/GUIHandler.kt @@ -166,6 +166,58 @@ class GUIHandler : IUserInterface { return future.get() } + override fun showUpdateConfirmationDialog(oldVersions: List>, newVersions: List>): IUserInterface.UpdateConfirmationResult { + assert(newVersions.isNotEmpty()) + val future = CompletableFuture() + EventQueue.invokeLater { + val oldVersIndex = oldVersions.map { it.first to it.second }.toMap() + val newVersIndex = newVersions.map { it.first to it.second }.toMap() + val message = StringBuilder() + message.append("" + + "This modpack uses newer versions of the following:
" + + "
    ") + + for (oldVer in oldVersions) { + val correspondingNewVer = newVersIndex[oldVer.first] + message.append("
  • ") + message.append(oldVer.first.replaceFirstChar { it.uppercase() }) + message.append(": ") + message.append(oldVer.second ?: "Not found") + message.append("
  • ") + } + message.append("
") + + message.append("New versions:" + + "
    ") + for (newVer in newVersions) { + val correspondingOldVer = oldVersIndex[newVer.first] + message.append("
  • ") + message.append(newVer.first.replaceFirstChar { it.uppercase() }) + message.append(": ") + message.append(newVer.second ?: "Not found") + message.append("
  • ") + } + message.append("

" + + "Would you like to update the versions, launch without updating, or cancel the launch?") + + + val options = arrayOf("Cancel", "Continue anyways", "Update") + val result = JOptionPane.showOptionDialog(frmPackwizlauncher, + message, + "Updating MultiMC versions", + JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]) + future.complete( + when (result) { + JOptionPane.CLOSED_OPTION, 0 -> IUserInterface.UpdateConfirmationResult.CANCELLED + 1 -> IUserInterface.UpdateConfirmationResult.CONTINUE + 2 -> IUserInterface.UpdateConfirmationResult.UPDATE + else -> IUserInterface.UpdateConfirmationResult.CANCELLED + } + ) + } + return future.get() + } + override fun awaitOptionalButton(showCancel: Boolean) { EventQueue.invokeAndWait { frmPackwizlauncher.showOk(!showCancel)