diff --git a/Fabric/build.gradle.kts b/Fabric/build.gradle.kts
index 06253a0..eed0938 100644
--- a/Fabric/build.gradle.kts
+++ b/Fabric/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.gradle.jvm.tasks.Jar
+
 val modId: String by project
 val minecraftVersion: String by project
 val fabricVersion: String by project
@@ -45,6 +47,11 @@ tasks.jar {
 	exclude("com/terraformersmc/modmenu/")
 }
 
-tasks.remapJar {
-	archiveVersion.set(tasks.jar.get().archiveVersion)
+tasks.register<Jar>("uncompressedRemapJar") {
+	group = "fabric"
+	
+	from(tasks.remapJar.map { it.outputs.files.map(::zipTree) })
+
+	archiveClassifier.set("uncompressed")
+	entryCompression = ZipEntryCompression.STORED // Reduces size of multiloader jar.
 }
diff --git a/Fabric/src/main/resources/assets/bettercontrols/icon.png b/Fabric/src/main/resources/assets/bettercontrols/icon.png
deleted file mode 100644
index a31f768..0000000
Binary files a/Fabric/src/main/resources/assets/bettercontrols/icon.png and /dev/null differ
diff --git a/Fabric/src/main/resources/fabric.mod.json b/Fabric/src/main/resources/fabric.mod.json
index 8931b7c..981905c 100644
--- a/Fabric/src/main/resources/fabric.mod.json
+++ b/Fabric/src/main/resources/fabric.mod.json
@@ -6,7 +6,7 @@
   "version": "${version}",
   "license": "${license}",
   
-  "icon": "assets/bettercontrols/icon.png",
+  "icon": "assets/${id}/logo.png",
   
   "authors": [
     "${author}"
@@ -18,7 +18,7 @@
     "sources": "${sourcesURL}"
   },
   
-  "environment": "client",
+  "environment": "${sidesForFabric}",
   "entrypoints": {
     "client": [ "chylex.bettercontrols.BetterControlsMod" ],
     "modmenu": [ "chylex.bettercontrols.compatibility.ModMenuSupport" ]
@@ -26,7 +26,7 @@
   
   "mixins": [{
     "config": "${id}.mixins.json",
-    "environment": "client"
+    "environment": "${sidesForFabric}"
   }],
   
   "depends": {
diff --git a/NeoForge/src/main/resources/META-INF/neoforge.mods.toml b/NeoForge/src/main/resources/META-INF/neoforge.mods.toml
index b29d45d..36d973f 100644
--- a/NeoForge/src/main/resources/META-INF/neoforge.mods.toml
+++ b/NeoForge/src/main/resources/META-INF/neoforge.mods.toml
@@ -11,7 +11,7 @@ displayURL = "${sourcesURL}"
 description = "${description}"
 authors = "${author}"
 version = "${version}"
-logoFile = "icon.png"
+logoFile = "assets/${id}/logo.png"
 
 [[mixins]]
 config = "${id}.mixins.json"
@@ -21,11 +21,11 @@ modId = "minecraft"
 type = "required"
 versionRange = "[${minimumMinecraftVersion},)"
 ordering = "NONE"
-side = "CLIENT"
+side = "${sidesForNeoForge}"
 
 [[dependencies.${id}]]
 modId = "neoforge"
 type = "required"
 versionRange = "[${minimumNeoForgeVersion},)"
 ordering = "NONE"
-side = "CLIENT"
+side = "${sidesForNeoForge}"
diff --git a/NeoForge/src/main/resources/icon.png b/NeoForge/src/main/resources/icon.png
deleted file mode 100644
index a31f768..0000000
Binary files a/NeoForge/src/main/resources/icon.png and /dev/null differ
diff --git a/build.gradle.kts b/build.gradle.kts
index 68ca0e0..c1ae8dd 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,6 +1,6 @@
 @file:Suppress("ConvertLambdaToReference")
 
-import org.gradle.api.file.DuplicatesStrategy.EXCLUDE
+import org.gradle.jvm.tasks.Jar
 
 val modId: String by project
 val modName: String by project
@@ -10,6 +10,7 @@ val modVersion: String by project
 val modLicense: String by project
 val modSourcesURL: String by project
 val modIssuesURL: String by project
+val modSides: String by project
 
 val minecraftVersion: String by project
 val mixinVersion: String by project
@@ -19,7 +20,6 @@ val minimumNeoForgeVersion: String by project
 val minimumFabricVersion: String by project
 
 val modNameStripped = modName.replace(" ", "")
-val jarVersion = "$minecraftVersion+v$modVersion"
 
 plugins {
 	idea
@@ -95,6 +95,33 @@ allprojects {
 		implementation("org.jetbrains:annotations:24.1.0")
 	}
 	
+	tasks.withType<ProcessResources> {
+		val (sidesForNeoForge, sidesForFabric) = when (modSides) {
+			"both"   -> Pair("BOTH", "*")
+			"client" -> Pair("CLIENT", "client")
+			"server" -> Pair("SERVER", "server")
+			else     -> error("Invalid modSides value: $modSides")
+		}
+		
+		inputs.property("id", modId)
+		inputs.property("name", modName)
+		inputs.property("description", modDescription)
+		inputs.property("version", modVersion)
+		inputs.property("author", modAuthor)
+		inputs.property("license", modLicense)
+		inputs.property("sourcesURL", modSourcesURL)
+		inputs.property("issuesURL", modIssuesURL)
+		inputs.property("sidesForNeoForge", sidesForNeoForge)
+		inputs.property("sidesForFabric", sidesForFabric)
+		inputs.property("minimumMinecraftVersion", minimumMinecraftVersion)
+		inputs.property("minimumNeoForgeVersion", minimumNeoForgeVersion)
+		inputs.property("minimumFabricVersion", minimumFabricVersion)
+		
+		from(rootProject.file("logo.png")) {
+			into("assets/$modId")
+		}
+	}
+	
 	tasks.withType<AbstractArchiveTask>().configureEach {
 		isPreserveFileTimestamps = false
 		isReproducibleFileOrder = true
@@ -117,37 +144,16 @@ subprojects {
 	}
 	
 	tasks.processResources {
-		inputs.property("id", modId)
-		inputs.property("name", modName)
-		inputs.property("description", modDescription)
-		inputs.property("version", modVersion)
-		inputs.property("author", modAuthor)
-		inputs.property("license", modLicense)
-		inputs.property("sourcesURL", modSourcesURL)
-		inputs.property("issuesURL", modIssuesURL)
-		inputs.property("minimumMinecraftVersion", minimumMinecraftVersion)
-		inputs.property("minimumNeoForgeVersion", minimumNeoForgeVersion)
-		inputs.property("minimumFabricVersion", minimumFabricVersion)
-		
 		from(rootProject.sourceSets.main.get().resources) {
 			expand(inputs.properties)
 		}
 	}
 	
 	tasks.jar {
-		archiveVersion.set(jarVersion)
-		
-		from(rootProject.file("LICENSE"))
+		entryCompression = ZipEntryCompression.STORED // Reduces size of multiloader jar.
 		
 		manifest {
-			attributes(
-				"Specification-Title" to modId,
-				"Specification-Vendor" to modAuthor,
-				"Specification-Version" to "1",
-				"Implementation-Title" to "$modNameStripped-${project.name}",
-				"Implementation-Vendor" to modAuthor,
-				"Implementation-Version" to modVersion,
-			)
+			packageInformation(modId, "$modNameStripped-${project.name}")
 		}
 	}
 	
@@ -156,18 +162,55 @@ subprojects {
 	}
 }
 
-val copyJars = tasks.register<Copy>("copyJars") {
+fun Manifest.packageInformation(specificationTitle: String, implementationTitle: String) {
+	attributes(
+		"Specification-Title" to specificationTitle,
+		"Specification-Vendor" to modAuthor,
+		"Specification-Version" to "1",
+		"Implementation-Title" to implementationTitle,
+		"Implementation-Vendor" to modAuthor,
+		"Implementation-Version" to modVersion,
+	)
+}
+
+val multiloaderSources = sourceSets.register("multiloader")
+
+val multiloaderJar = tasks.register<Jar>("multiloaderJar") {
 	group = "build"
-	duplicatesStrategy = EXCLUDE
 	
-	for (subproject in subprojects) {
-		dependsOn(subproject.tasks.assemble)
-		from(subproject.base.libsDirectory.file("${subproject.base.archivesName.get()}-$jarVersion.jar"))
+	archiveBaseName.set(modNameStripped)
+	archiveVersion.set("$minecraftVersion+v$modVersion")
+	
+	destinationDirectory = layout.buildDirectory.dir("dist")
+	
+	fun includeJar(project: Project, jarTaskName: String) {
+		from(project.tasks.named(jarTaskName).map { it.outputs }) {
+			into("jars")
+			rename { "$modNameStripped-${project.name}.jar" }
+		}
 	}
 	
-	into(project.layout.buildDirectory.dir("dist"))
+	findProject(":NeoForge")?.let { includeJar(it, "jar") }
+	findProject(":Fabric")?.let { includeJar(it, "uncompressedRemapJar") }
+	
+	from(rootProject.file("LICENSE"))
+	from(multiloaderSources.map { it.output })
+	
+	manifest {
+		packageInformation("$modId-multiloader", modNameStripped)
+		attributes("FMLModType" to "GAMELIBRARY")
+	}
+}
+
+tasks.named<ProcessResources>("processMultiloaderResources").configure {
+	inputs.property("group", project.group)
+	inputs.property("jarPrefix", modNameStripped)
+	
+	filesMatching(listOf("fabric.mod.json", "META-INF/jarjar/metadata.json")) {
+		expand(inputs.properties)
+	}
 }
 
 tasks.assemble {
-	finalizedBy(copyJars)
+	finalizedBy(multiloaderJar)
 }
diff --git a/gradle.properties b/gradle.properties
index 7601921..d494e95 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,6 +7,7 @@ modVersion=1.3.1
 modLicense=MPL-2.0
 modSourcesURL=https://github.com/chylex/Better-Controls
 modIssuesURL=https://github.com/chylex/Better-Controls/issues
+modSides=client
 
 # Dependencies
 minecraftVersion=1.21
diff --git a/src/multiloader/resources/META-INF/jarjar/metadata.json b/src/multiloader/resources/META-INF/jarjar/metadata.json
new file mode 100644
index 0000000..6891596
--- /dev/null
+++ b/src/multiloader/resources/META-INF/jarjar/metadata.json
@@ -0,0 +1,13 @@
+{
+  "jars": [{
+    "identifier": {
+      "group": "${group}",
+      "artifact": "${id}"
+    },
+    "version": {
+      "artifactVersion": "${version}",
+      "range": "[${version}]"
+    },
+    "path": "jars/${jarPrefix}-NeoForge.jar"
+  }]
+}
diff --git a/src/multiloader/resources/fabric.mod.json b/src/multiloader/resources/fabric.mod.json
new file mode 100644
index 0000000..9b9adbf
--- /dev/null
+++ b/src/multiloader/resources/fabric.mod.json
@@ -0,0 +1,34 @@
+{
+  "schemaVersion": 1,
+  "id": "${id}_multiloader",
+  "name": "${name} (Multiloader)",
+  "description": "${description}",
+  "version": "${version}",
+  "license": "${license}",
+  
+  "icon": "assets/${id}/logo.png",
+  
+  "authors": [
+    "${author}"
+  ],
+  
+  "contact": {
+    "homepage": "https://chylex.com",
+    "issues": "${issuesURL}",
+    "sources": "${sourcesURL}"
+  },
+  
+  "environment": "${sidesForFabric}",
+  
+  "jars": [{
+      "file": "jars/${jarPrefix}-Fabric.jar"
+  }],
+  
+  "custom": {
+    "modmenu": {
+      "parent": "${id}",
+      "badges": [ "library" ],
+      "update_checker": false
+    }
+  }
+}