diff --git a/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt b/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt
new file mode 100644
index 000000000..0dfb861b6
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.client
+
+import com.lambda.Lambda.mc
+import com.lambda.event.events.GuiEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.gui.LambdaScreen
+import com.lambda.gui.components.ClickGuiLayout
+import com.lambda.gui.dsl.ImGuiBuilder.buildLayout
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import imgui.ImGui
+import imgui.flag.ImGuiWindowFlags
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import net.fabricmc.loader.api.FabricLoader
+import net.minecraft.SharedConstants
+import org.slf4j.LoggerFactory
+import java.net.URI
+import java.nio.file.Path
+import javax.xml.parsers.DocumentBuilderFactory
+
+object AutoUpdater : Module(
+ name = "AutoUpdater",
+ description = "Installs / uninstalls Lambda loader",
+ tag = ModuleTag.CLIENT,
+) {
+ private val logger = LoggerFactory.getLogger("AutoUpdater")
+
+ private val debug by setting("Debug", false, "Enable debug logging")
+ private val loaderBranch by setting("Loader Branch", Branch.Stable, "Select loader update branch")
+ private val clientBranch by setting("Client Branch", Branch.Snapshot, "Select client update branch")
+
+ var showInstallModal = false
+ private set
+ var showUninstallModal = false
+ private set
+
+ private const val MAVEN_URL = "https://maven.lambda-client.org"
+ private const val LOADER_RELEASES_META = "$MAVEN_URL/releases/com/lambda/loader/maven-metadata.xml"
+ private const val LOADER_SNAPSHOTS_META = "$MAVEN_URL/snapshots/com/lambda/loader/maven-metadata.xml"
+ private const val CLIENT_RELEASES_META = "$MAVEN_URL/releases/com/lambda/lambda/maven-metadata.xml"
+ private const val CLIENT_SNAPSHOTS_META = "$MAVEN_URL/snapshots/com/lambda/lambda/maven-metadata.xml"
+
+ private enum class Branch {
+ Stable,
+ Snapshot
+ }
+
+ init {
+ onEnable {
+ if (mc.currentScreen is LambdaScreen && ClickGuiLayout.open)
+ showInstallModal = true
+ }
+
+ onDisable {
+ if (mc.currentScreen is LambdaScreen && ClickGuiLayout.open)
+ showUninstallModal = true
+ }
+
+ listen(alwaysListen = true) {
+ if (showInstallModal) {
+ buildLayout {
+ openPopup("install-lambda-loader")
+ popupModal("install-lambda-loader", ImGuiWindowFlags.AlwaysAutoResize) {
+ text("Do you want to install Lambda Client?")
+ ImGui.spacing()
+ text("This will close the client automatically once the install is finished.")
+ ImGui.spacing()
+
+ button("Install", 120f, 0f) {
+ showInstallModal = false
+ installLoader()
+ }
+
+ sameLine()
+
+ button("Cancel", 120f, 0f) {
+ showInstallModal = false
+ }
+ }
+ }
+ return@listen
+ }
+
+ if (showUninstallModal) {
+ buildLayout {
+ openPopup("uninstall-lambda-loader")
+ popupModal("uninstall-lambda-loader", ImGuiWindowFlags.AlwaysAutoResize) {
+ text("Do you want to uninstall Lambda Loader?")
+ ImGui.spacing()
+ text("This will close the client automatically once the uninstall is finished.")
+ ImGui.spacing()
+
+ button("Uninstall", 120f, 0f) {
+ showUninstallModal = false
+ installClient()
+ }
+
+ sameLine()
+
+ button("Cancel", 120f, 0f) {
+ showUninstallModal = false
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun installLoader() {
+ runBlocking(Dispatchers.IO) {
+ try {
+ logger.info("Starting Lambda loader install...")
+
+ val loaderJar = downloadLatestLoader()
+ if (loaderJar == null) {
+ logger.error("Failed to download latest Lambda loader")
+ return@runBlocking
+ }
+
+ val clientJarPath = getModJarPath("lambda")
+ if (debug) logger.info("Lambda client JAR path: $clientJarPath")
+
+ val jarFile = clientJarPath.toFile()
+ jarFile.writeBytes(loaderJar)
+
+ logger.info("Successfully installed Lambda loader! Restarting...")
+
+ mc.stop()
+ } catch (e: Exception) {
+ logger.error("Error installing Lambda loader", e)
+ }
+ }
+ }
+
+ private fun installClient() {
+ runBlocking(Dispatchers.IO) {
+ try {
+ logger.info("Starting Lambda client install...")
+
+ val clientJar = downloadLatestClient()
+ if (clientJar == null) {
+ logger.error("Failed to download latest Lambda client")
+ return@runBlocking
+ }
+
+ val loaderJarPath = getModJarPath("lambda-loader")
+ if (debug) logger.info("Lambda loader JAR path: $loaderJarPath")
+
+ val jarFile = loaderJarPath.toFile()
+ jarFile.writeBytes(clientJar)
+
+ logger.info("Successfully installed Lambda client! Restarting...")
+
+ mc.stop()
+ } catch (e: Exception) {
+ logger.error("Error installing Lambda client", e)
+ }
+ }
+ }
+
+ fun downloadLatestLoader(): ByteArray? {
+ return try {
+ val branch = loaderBranch
+ val mcVersion = getMinecraftVersion()
+
+ if (debug) logger.info("Downloading loader for MC $mcVersion from ${branch.name} branch")
+
+ var version: String?
+ var baseUrl: String?
+
+ when (branch) {
+ Branch.Stable -> {
+ val xml = URI(LOADER_RELEASES_META).toURL().readText()
+ version = parseLatestVersion(xml, null)
+ baseUrl = "$MAVEN_URL/releases"
+ }
+ Branch.Snapshot -> {
+ val xml = URI(LOADER_SNAPSHOTS_META).toURL().readText()
+ version = parseLatestVersion(xml, null)
+ baseUrl = "$MAVEN_URL/snapshots"
+ }
+ }
+
+ if (version == null && branch == Branch.Stable) {
+ logger.warn("No stable loader found, falling back to snapshot")
+ val xml = URI(LOADER_SNAPSHOTS_META).toURL().readText()
+ version = parseLatestVersion(xml, null)
+ baseUrl = "$MAVEN_URL/snapshots"
+ }
+
+ if (version == null) {
+ logger.error("No loader version found")
+ return null
+ }
+
+ val jarUrl = if (version.endsWith("-SNAPSHOT")) {
+ val snapshotInfo = getSnapshotInfo(baseUrl, "com/lambda/loader", version) ?: return null
+ val baseVersion = version.replace("-SNAPSHOT", "")
+ "$baseUrl/com/lambda/loader/$version/loader-$baseVersion-${snapshotInfo.timestamp}-${snapshotInfo.buildNumber}.jar"
+ } else {
+ "$baseUrl/com/lambda/loader/$version/loader-$version.jar"
+ }
+
+ if (debug) logger.info("Downloading from: $jarUrl")
+
+ URI(jarUrl).toURL().readBytes()
+ } catch (e: Exception) {
+ logger.error("Failed to download loader", e)
+ null
+ }
+ }
+
+ fun downloadLatestClient(): ByteArray? {
+ return try {
+ val branch = clientBranch
+ val mcVersion = getMinecraftVersion()
+
+ if (debug) logger.info("Downloading client for MC $mcVersion from ${branch.name} branch")
+
+ var version: String?
+ var baseUrl: String?
+
+ when (branch) {
+ Branch.Stable -> {
+ val xml = URI(CLIENT_RELEASES_META).toURL().readText()
+ version = parseLatestVersion(xml, mcVersion)
+ baseUrl = "$MAVEN_URL/releases"
+ }
+ Branch.Snapshot -> {
+ val xml = URI(CLIENT_SNAPSHOTS_META).toURL().readText()
+ version = parseLatestVersion(xml, mcVersion)
+ baseUrl = "$MAVEN_URL/snapshots"
+ }
+ }
+
+ if (version == null && branch == Branch.Stable) {
+ logger.warn("No stable client found for MC $mcVersion, falling back to snapshot")
+ val xml = URI(CLIENT_SNAPSHOTS_META).toURL().readText()
+ version = parseLatestVersion(xml, mcVersion)
+ baseUrl = "$MAVEN_URL/snapshots"
+ }
+
+ if (version == null) {
+ logger.error("No client version found for MC $mcVersion")
+ return null
+ }
+
+ val jarUrl = if (version.endsWith("-SNAPSHOT")) {
+ val snapshotInfo = getSnapshotInfo(baseUrl, "com/lambda/lambda", version) ?: return null
+ val baseVersion = version.replace("-SNAPSHOT", "")
+ "$baseUrl/com/lambda/lambda/$version/lambda-$baseVersion-${snapshotInfo.timestamp}-${snapshotInfo.buildNumber}.jar"
+ } else "$baseUrl/com/lambda/lambda/$version/lambda-$version.jar"
+
+ if (debug) logger.info("Downloading from: $jarUrl")
+
+ URI(jarUrl).toURL().readBytes()
+ } catch (e: Exception) {
+ logger.error("Failed to download client", e)
+ null
+ }
+ }
+
+ private fun parseLatestVersion(xml: String, mcVersion: String? = null): String? {
+ return try {
+ val factory = DocumentBuilderFactory.newInstance()
+ val builder = factory.newDocumentBuilder()
+ val document = builder.parse(xml.byteInputStream())
+
+ val versionNodes = document.getElementsByTagName("version")
+ val versions = mutableListOf()
+
+ (0 until versionNodes.length).forEach { i ->
+ versions.add(versionNodes.item(i).textContent)
+ }
+
+ if (debug) {
+ if (mcVersion != null) {
+ logger.info("Target MC version: $mcVersion")
+ }
+ logger.info("Available versions: ${versions.joinToString(", ")}")
+ }
+
+ val matchingVersions = if (mcVersion != null) {
+ versions.filter { version ->
+ val mcVersionInArtifact = version.substringAfter("+").substringBefore("-")
+ val normalizedArtifact = mcVersionInArtifact.replace(".", "")
+ val normalizedTarget = mcVersion.replace(".", "")
+ normalizedArtifact == normalizedTarget
+ }
+ } else {
+ versions
+ }
+
+ if (matchingVersions.isEmpty()) {
+ if (debug) {
+ val versionMsg = mcVersion?.let { "for MC $it" } ?: ""
+ logger.warn("No versions found $versionMsg")
+ }
+ null
+ } else matchingVersions.last()
+ } catch (e: Exception) {
+ logger.error("Error parsing version", e)
+ null
+ }
+ }
+
+ private fun getSnapshotInfo(baseUrl: String, artifactPath: String, version: String): SnapshotInfo? {
+ return try {
+ val snapshotMetaUrl = URI("$baseUrl/$artifactPath/$version/maven-metadata.xml").toURL()
+ val xml = snapshotMetaUrl.readText()
+
+ val factory = DocumentBuilderFactory.newInstance()
+ val builder = factory.newDocumentBuilder()
+ val document = builder.parse(xml.byteInputStream())
+
+ val timestamp = document.getElementsByTagName("timestamp").item(0).textContent
+ val buildNumber = document.getElementsByTagName("buildNumber").item(0).textContent
+
+ SnapshotInfo(version, timestamp, buildNumber)
+ } catch (e: Exception) {
+ logger.error("Error getting snapshot info", e)
+ null
+ }
+ }
+
+ private fun getMinecraftVersion() = SharedConstants.getGameVersion().name()
+
+ private fun getModJarPath(modId: String): Path {
+ val fabricLoader = FabricLoader.getInstance()
+ val modContainer = fabricLoader.getModContainer(modId)
+ return modContainer.get().origin.paths[0].toAbsolutePath()
+ }
+
+ private data class SnapshotInfo(
+ val version: String,
+ val timestamp: String,
+ val buildNumber: String
+ )
+}
\ No newline at end of file