diff --git a/build.gradle.kts b/build.gradle.kts
index 3acfeb8..8991aab 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,9 +4,6 @@ import java.text.SimpleDateFormat
 import java.util.*
 
 plugins {
-    // Since this mod/bot is written in Kotlin and expected to run on Minecraft and as such
-    // the JVM, the Kotlin plugin is needed
-    alias(libs.plugins.kotlin)
     // For generating documentation based on comments in the code
     alias(libs.plugins.dokka)
     java
@@ -39,16 +36,6 @@ subprojects {
     val modAuthor: String by project
     val isCommon = modLoader == rootProject.projects.common.name
 
-    base {
-        // This will be the final name of the exported JAR file
-        archivesName.set("$modId-$modLoader-$minecraftVersion")
-    }
-
-    extensions.configure<JavaPluginExtension> {
-        toolchain.languageVersion.set(JavaLanguageVersion.of(21))
-        withSourcesJar()
-    }
-
     repositories {
         mavenCentral()
         maven(url = "https://maven.neoforged.net/releases/")
@@ -76,11 +63,8 @@ subprojects {
     val runtimeLib by configurations.registering {
         isTransitive = false
     }
-    // Configuration for depending on the common project
-    val commonDep by configurations.creating
     configurations.implementation.extendsFrom(configurations.named(includeBotDep.name))
     configurations.implementation.extendsFrom(configurations.named(runtimeLib.name))
-    configurations.implementation.extendsFrom(configurations.named(commonDep.name))
 
     dependencies {
         arrayOf(
@@ -91,16 +75,16 @@ subprojects {
 
             // Library used to communicate with Discord, see https://jda.wiki
             rootProject.jda.jda,
-                // JDA's dependencies
-                rootProject.jda.commons.collections,
-                rootProject.jda.trove4j,
-                rootProject.jda.jackson.annotations,
-                rootProject.jda.jackson.core,
-                rootProject.jda.jackson.databind,
-                rootProject.jda.websocket,
-                rootProject.jda.okhttp,
-                rootProject.jda.okio,
-                rootProject.jda.tink,
+            // JDA's dependencies
+            rootProject.jda.commons.collections,
+            rootProject.jda.trove4j,
+            rootProject.jda.jackson.annotations,
+            rootProject.jda.jackson.core,
+            rootProject.jda.jackson.databind,
+            rootProject.jda.websocket,
+            rootProject.jda.okhttp,
+            rootProject.jda.okio,
+            rootProject.jda.tink,
             // Library used for sending messages via Discord Webhooks
             rootProject.dcwebhooks.webhooks,
             rootProject.dcwebhooks.json,
@@ -196,20 +180,6 @@ subprojects {
         inputs.properties(expandProps)
     }
 
-    if (!isCommon) {
-        dependencies {
-            commonDep(project(":common")) {
-                isTransitive = false
-            }
-        }
-
-        tasks.jar {
-            dependsOn(project(":common").tasks.jar)
-            from(project(":common").sourceSets.main.get().output)
-            duplicatesStrategy = DuplicatesStrategy.EXCLUDE
-        }
-    }
-
     // Disables Gradle's custom module metadata from being published to maven. The
     // metadata includes mapped dependencies which are not reasonably consumable by
     // other mod developers.
@@ -268,10 +238,6 @@ subprojects {
     }
 }
 
-kotlin {
-    jvmToolchain(21)
-}
-
 
 // IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior.
 idea {
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..dcfa1e7
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+    id("groovy-gradle-plugin")
+}
+
+dependencies {
+    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20")
+}
+
+repositories {
+    mavenCentral()
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/multiloader-common.gradle b/buildSrc/src/main/groovy/multiloader-common.gradle
new file mode 100644
index 0000000..9260810
--- /dev/null
+++ b/buildSrc/src/main/groovy/multiloader-common.gradle
@@ -0,0 +1,101 @@
+plugins {
+    id 'java-library'
+    id 'maven-publish'
+    id "org.jetbrains.kotlin.jvm"
+}
+
+base {
+    archivesName = "${modId}-${project.name}-${minecraftVersion}"
+}
+
+java {
+    toolchain.languageVersion = JavaLanguageVersion.of(javaVersion)
+    withSourcesJar()
+    withJavadocJar()
+}
+
+kotlin {
+    jvmToolchain(Integer.parseInt(javaVersion))
+}
+
+repositories {
+    mavenCentral()
+    // https://docs.gradle.org/current/userguide/declaring_repositories.html#declaring_content_exclusively_found_in_one_repository
+    exclusiveContent {
+        forRepository {
+            maven {
+                name = 'Sponge'
+                url = 'https://repo.spongepowered.org/repository/maven-public'
+            }
+        }
+        filter { includeGroupAndSubgroups('org.spongepowered') }
+    }
+    exclusiveContent {
+        forRepositories(
+                maven {
+                    name = 'ParchmentMC'
+                    url = 'https://maven.parchmentmc.org/'
+                },
+                maven {
+                    name = "NeoForge"
+                    url = 'https://maven.neoforged.net/releases'
+                }
+        )
+        filter { includeGroup('org.parchmentmc.data') }
+    }
+    maven {
+        name = 'BlameJared'
+        url = 'https://maven.blamejared.com'
+    }
+}
+
+// Declare capabilities on the outgoing configurations.
+// Read more about capabilities here: https://docs.gradle.org/current/userguide/component_capabilities.html#sec:declaring-additional-capabilities-for-a-local-component
+['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements'].each { variant ->
+    configurations."$variant".outgoing {
+        capability("$group:${base.archivesName.get()}:$version")
+        capability("$group:$modId-${project.name}-${minecraftVersion}:$version")
+        capability("$group:$modId:$version")
+    }
+    publishing.publications.configureEach {
+        suppressPomMetadataWarningsFor(variant)
+    }
+}
+
+sourcesJar {
+    from(rootProject.file('LICENSE')) {
+        rename { "${it}_${title}" }
+    }
+}
+
+jar {
+    from(rootProject.file('LICENSE')) {
+        rename { "${it}_${title}" }
+    }
+
+    manifest {
+        attributes([
+                'Specification-Title'   : title,
+                'Specification-Vendor'  : modAuthor,
+                'Specification-Version' : project.jar.archiveVersion,
+                'Implementation-Title'  : project.name,
+                'Implementation-Version': project.jar.archiveVersion,
+                'Implementation-Vendor' : modAuthor,
+                'Built-On-Minecraft'    : minecraftVersion
+        ])
+    }
+}
+
+publishing {
+    publications {
+        register('mavenJava', MavenPublication) {
+            artifactId base.archivesName.get()
+            from components.java
+        }
+    }
+    repositories {
+        maven {
+            url System.getenv('local_maven_url')
+        }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/multiloader-loader.gradle b/buildSrc/src/main/groovy/multiloader-loader.gradle
new file mode 100644
index 0000000..2fe8969
--- /dev/null
+++ b/buildSrc/src/main/groovy/multiloader-loader.gradle
@@ -0,0 +1,57 @@
+plugins {
+    id 'multiloader-common'
+    id "org.jetbrains.kotlin.jvm"
+}
+
+configurations {
+    commonJava {
+        canBeResolved = true
+    }
+    commonKotlin {
+        canBeResolved = true
+    }
+    commonResources {
+        canBeResolved = true
+    }
+}
+
+dependencies {
+    compileOnly(project(':common')) {
+        capabilities {
+            requireCapability "$group:$modId"
+        }
+    }
+    commonJava project(path: ':common', configuration: 'commonJava')
+    commonKotlin project(path: ":common", configuration: "commonKotlin")
+    commonResources project(path: ':common', configuration: 'commonResources')
+}
+
+tasks.named('compileJava', JavaCompile) {
+    dependsOn(configurations.commonJava)
+    source(configurations.commonJava)
+}
+
+tasks.named("compileKotlin") {
+    dependsOn(configurations.commonKotlin)
+    source(configurations.commonJava)
+    source(configurations.commonKotlin)
+}
+
+processResources {
+    dependsOn(configurations.commonResources)
+    from(configurations.commonResources)
+}
+
+tasks.named('javadoc', Javadoc).configure {
+    dependsOn(configurations.commonJava)
+    source(configurations.commonJava)
+}
+
+tasks.named('sourcesJar', Jar) {
+    dependsOn(configurations.commonJava)
+    from(configurations.commonJava)
+    dependsOn(configurations.commonKotlin)
+    from(configurations.commonKotlin)
+    dependsOn(configurations.commonResources)
+    from(configurations.commonResources)
+}
\ No newline at end of file
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
index e1d4819..c7cc52e 100644
--- a/common/build.gradle.kts
+++ b/common/build.gradle.kts
@@ -1,7 +1,7 @@
 plugins {
     idea
     java
-    alias(libs.plugins.kotlin)
+    id("multiloader-common")
     alias(libs.plugins.moddev)
 }
 
@@ -15,6 +15,13 @@ val modId: String by project
 neoForge {
     neoFormVersion = neoformVersion
 
+    // Automatically enable neoforge AccessTransformers if the file exists
+    // This location is hardcoded in FML and can not be changed.
+    // https://github.com/neoforged/FancyModLoader/blob/a952595eaaddd571fbc53f43847680b00894e0c1/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java#L118
+    val transformerFile = project(":common").file("src/main/resources/META-INF/accesstransformer.cfg")
+    if (transformerFile.exists())
+        accessTransformers.from(transformerFile)
+
     parchment {
         minecraftVersion = parchmentMinecraft
         mappingsVersion = parchmentVersion
@@ -27,4 +34,25 @@ dependencies {
     api("fuzs.forgeconfigapiport:forgeconfigapiport-common-neoforgeapi:$forgeConfigAPIVersion")
     compileOnly("org.spongepowered:mixin:0.8.7")
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
+}
+
+configurations {
+    create("commonJava") {
+        isCanBeResolved = false
+        isCanBeConsumed = true
+    }
+    create("commonKotlin") {
+        isCanBeResolved = false
+        isCanBeConsumed = true
+    }
+    create("commonResources") {
+        isCanBeResolved = false
+        isCanBeConsumed = true
+    }
+}
+
+artifacts {
+    add("commonJava", sourceSets.main.get().java.sourceDirectories.singleFile)
+    add("commonKotlin", sourceSets.main.get().kotlin.sourceDirectories.filter { !it.name.endsWith("java") }.singleFile)
+    add("commonResources", sourceSets.main.get().resources.sourceDirectories.singleFile)
 }
\ No newline at end of file
diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts
index 6fde5f2..0cb9a92 100644
--- a/fabric/build.gradle.kts
+++ b/fabric/build.gradle.kts
@@ -4,8 +4,8 @@ import me.modmuss50.mpp.platforms.modrinth.ModrinthOptions
 
 plugins {
     java
-    alias(libs.plugins.kotlin)
     idea
+    id("multiloader-loader")
     alias(libs.plugins.loom)
     alias(libs.plugins.publish)
 }
@@ -41,8 +41,6 @@ loom {
 
     @Suppress("UnstableApiUsage")
     mixin {
-        // TODO: Somehow figure out a way to get loom to create a refmap for common mixins
-        // add(project(":common").sourceSets.main.get())
         defaultRefmapName.set("${modId}.refmap.json")
     }
 
diff --git a/gradle.properties b/gradle.properties
index a5ee74b..3a76bd8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,8 @@
 kotlin.code.style=official
 org.gradle.jvmargs=-Xmx3G -XX:ThreadStackSize=4096 -XX:CompilerThreadStackSize=4096
 
+javaVersion=21
+
 # Minecraft things
 enabledPlatforms=fabric,neoforge
 
diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts
index 468063d..09c6f80 100644
--- a/neoforge/build.gradle.kts
+++ b/neoforge/build.gradle.kts
@@ -1,13 +1,15 @@
 import me.modmuss50.mpp.ReleaseType
 import me.modmuss50.mpp.platforms.curseforge.CurseforgeOptions
 import me.modmuss50.mpp.platforms.modrinth.ModrinthOptions
+import org.gradle.internal.extensions.stdlib.capitalized
 import org.jetbrains.kotlin.gradle.utils.extendsFrom
 
 plugins {
     idea
     java
-    alias(libs.plugins.kotlin)
+    id("multiloader-loader")
     alias(libs.plugins.moddev)
+    alias(libs.plugins.publish)
 }
 
 val modId: String by project
@@ -34,7 +36,7 @@ neoForge {
     // Automatically enable neoforge AccessTransformers if the file exists
     // This location is hardcoded in FML and can not be changed.
     // https://github.com/neoforged/FancyModLoader/blob/a952595eaaddd571fbc53f43847680b00894e0c1/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java#L118
-    val transformerFile = project.file("src/main/resources/META-INF/accesstransformer.cfg")
+    val transformerFile = project(":common").file("src/main/resources/META-INF/accesstransformer.cfg")
     if (transformerFile.exists())
         accessTransformers.from(transformerFile)
 
@@ -48,13 +50,16 @@ neoForge {
     runs {
         create("server") {
             server()
-            systemProperty("neoforge.enabledGameTestNamespaces", modId)
             programArgument("--nogui")
         }
 
         create("gameTestServer") {
             type = "gameTestServer"
+        }
+
+        configureEach {
             systemProperty("neoforge.enabledGameTestNamespaces", modId)
+            ideName = "NeoForge ${name.capitalized()} (${project.path})"
         }
     }
 }
@@ -69,13 +74,6 @@ dependencies {
     configurations.jarJar.extendsFrom(configurations.includeBotDep)
 }
 
-tasks {
-    // Fixes IDE runs not processing common resources
-    processResources {
-        from(project(":common").sourceSets.main.get().resources)
-    }
-}
-
 publishMods {
     val minecraftVersion: String by project
     val title: String by project