plugins { id 'java' id 'fabric-loom' apply false } /** * Compatibility buckets, not Minecraft versions. * * Every project under :platform is compiled against one representative version * and declares -- in its own gradle.properties -- the full range of Minecraft * versions that the resulting jar actually runs on. The grouping comes from * measured API breakpoints, documented in docs/PORTING.md. */ ext.platformProjects = { subprojects.findAll { it.path.startsWith(':platform:') } } ext.sharedProjects = { subprojects.findAll { it.path.startsWith(':shared:') } } subprojects { apply plugin: 'java' group = rootProject.mod_group version = rootProject.mod_version repositories { maven { name = 'Fabric'; url = 'https://maven.fabricmc.net/' } mavenCentral() } dependencies { compileOnly "org.projectlombok:lombok:${rootProject.lombok_version}" annotationProcessor "org.projectlombok:lombok:${rootProject.lombok_version}" testCompileOnly "org.projectlombok:lombok:${rootProject.lombok_version}" testAnnotationProcessor "org.projectlombok:lombok:${rootProject.lombok_version}" testImplementation platform("org.junit:junit-bom:${rootProject.junit_version}") testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } tasks.withType(Test).configureEach { useJUnitPlatform() } } // --------------------------------------------------------------------------- // Shared modules: zero Minecraft on the classpath. // // These compile to Java 17 bytecode because 1.20-1.20.4 run on a Java 17 JVM. // That is a real constraint on the code, not just a flag: no virtual threads // and no Java 21 pattern matching in shared code. // --------------------------------------------------------------------------- configure(sharedProjects()) { // java-library so a module can re-export what its consumers legitimately // need: :shared:ui hands RenderBridge types straight to platform code. apply plugin: 'java-library' java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } tasks.withType(JavaCompile).configureEach { options.release = rootProject.shared_java as int } dependencies { // Gson ships inside Minecraft's own dependency set on every supported // version, so we compile against it but never bundle it. See // docs/PORTING.md for the version range this has to stay within. compileOnly "com.google.code.gson:gson:${rootProject.gson_api_version}" compileOnly "org.slf4j:slf4j-api:${rootProject.slf4j_api_version}" testImplementation "com.google.code.gson:gson:${rootProject.gson_api_version}" testImplementation "org.slf4j:slf4j-api:${rootProject.slf4j_api_version}" testRuntimeOnly "org.slf4j:slf4j-simple:${rootProject.slf4j_api_version}" } } // --------------------------------------------------------------------------- // Platform modules: the only code allowed to touch Minecraft types. // --------------------------------------------------------------------------- configure(platformProjects()) { apply plugin: 'fabric-loom' def mcVersion = project.property('minecraft_version') def mcJava = project.property('mc_java') as int def deobfuscated = project.property('deobfuscated').toBoolean() // Minecraft 26.x ships already deobfuscated, so Loom has nothing to remap. // It still insists on a mappings artifact carrying a 'named' namespace, so // we hand it a header-only tiny-v2 file: three namespaces, zero rows, which // Loom reads as "every name maps to itself". // // This is written during configuration rather than by a task because Loom // resolves the mappings configuration while it configures the project -- // a task output would not exist yet. It is 47 bytes and deterministic. def identityMappings = null if (deobfuscated) { identityMappings = layout.buildDirectory .file("identity-mappings/identity-mappings-${mcVersion}.jar").get().asFile if (!identityMappings.exists()) { identityMappings.parentFile.mkdirs() new java.util.zip.ZipOutputStream(identityMappings.newOutputStream()).withCloseable { zip -> zip.putNextEntry(new java.util.zip.ZipEntry('mappings/mappings.tiny')) zip.write("tiny\t2\t0\tofficial\tintermediary\tnamed\n".getBytes('UTF-8')) zip.closeEntry() } } } java { // javac can always read older bytecode, so one toolchain covers every // bucket except 26.x, whose Minecraft classes are Java 25. toolchain { languageVersion = JavaLanguageVersion.of(Math.max(mcJava, 21)) } withSourcesJar() } // The compatibility layer. platform/common holds the adapter code that is // textually identical on all nine buckets -- the Fabric entrypoint, the // ClientBridge assembly, and the two seams (I18n, KeyboardHandler) that have // not moved since 1.20. It is a source root rather than a project because it // imports net.minecraft, so it has to be compiled once per bucket against // that bucket's Minecraft; there is no single jar it could produce. // // Everything it calls that *does* differ lives in dev.photosync.platform.impl, // which each bucket supplies from its own src/main/java under the same // package. Common code names those classes directly and the bucket's source // root satisfies them -- so the invariant half is written once, and the half // that churns is a per-bucket file the compiler checks. sourceSets.main.java.srcDir rootProject.file('platform/common/src/main/java') sourceSets.main.resources.srcDir rootProject.file('platform/common/src/main/resources') tasks.withType(JavaCompile).configureEach { options.release = mcJava } dependencies { minecraft "com.mojang:minecraft:${mcVersion}" if (deobfuscated) { mappings project.files(identityMappings) } else { mappings loom.officialMojangMappings() } modImplementation "net.fabricmc:fabric-loader:${project.property('loader_version')}" modImplementation "net.fabricmc.fabric-api:fabric-api:${project.property('fabric_api_version')}" // Shared code is compiled once per bucket's bytecode target and folded // into the platform jar below. No shading, no jar-in-jar: these are our // own classes and they carry no third-party dependencies. rootProject.sharedProjects().each { implementation it } } loom { runs { client { client() ideConfigGenerated = true } } } processResources { def tokens = [ mod_id : rootProject.mod_id, mod_name : rootProject.mod_name, mod_version : rootProject.mod_version, mod_description : rootProject.mod_description, mod_license : rootProject.mod_license, mod_sources : rootProject.mod_sources, minecraft_range : project.property('minecraft_range'), loader_version : project.property('loader_version'), java_version : String.valueOf(mcJava), // Mixin refuses to apply a class whose bytecode is newer than the // declared level, and platform classes are compiled at the // bucket's own level -- so this has to follow mc_java. mixin_compat : "JAVA_${mcJava}", ] inputs.properties(tokens) filesMatching(['fabric.mod.json', '*.mixins.json']) { expand(tokens) } } jar { rootProject.sharedProjects().each { from it.sourceSets.main.output } manifest { attributes( 'Implementation-Title': rootProject.mod_name, 'Implementation-Version': rootProject.mod_version, 'Minecraft-Version': mcVersion, 'Minecraft-Range': project.property('minecraft_range') ) } } // Give every bucket's artifact a distinguishable name. base.archivesName = "${rootProject.mod_id}-${mcVersion}" } // Convenience: build every bucket's shippable jar in one go. tasks.register('buildAllPlatforms') { group = 'build' description = 'Builds the remapped mod jar for every compatibility bucket.' dependsOn platformProjects().collect { "${it.path}:build" } }