plugins { id 'eclipse' id 'idea' id 'maven-publish' id 'net.minecraftforge.gradle' version '[6.0,6.2)' } version = mod_version group = mod_group_id base { archivesName = mod_id } // Mojang ships Java 17 to end users in 1.18+, so your mod should target Java 17. java.toolchain.languageVersion = JavaLanguageVersion.of(17) println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}" minecraft { // The mappings can be changed at any time and must be in the following format. // Channel: Version: // official MCVersion Official field/method names from Mojang mapping files // parchment YYYY.MM.DD-MCVersion Open community-sourced parameter names and javadocs layered on top of official // // You must be aware of the Mojang license when using the 'official' or 'parchment' mappings. // See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md // // Parchment is an unofficial project maintained by ParchmentMC, separate from MinecraftForge // Additional setup is needed to use their mappings: https://parchmentmc.org/docs/getting-started // // Use non-default mappings at your own risk. They may not always work. // Simply re-run your setup task after changing the mappings to update your workspace. mappings channel: mapping_channel, version: mapping_version // When true, this property will have all Eclipse/IntelliJ IDEA run configurations run the "prepareX" task for the given run configuration before launching the game. // In most cases, it is not necessary to enable. // enableEclipsePrepareRuns = true // enableIdeaPrepareRuns = true // This property allows configuring Gradle's ProcessResources task(s) to run on IDE output locations before launching the game. // It is REQUIRED to be set to true for this template to function. // See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html copyIdeResources = true // When true, this property will add the folder name of all declared run configurations to generated IDE run configurations. // The folder name can be set on a run configuration using the "folderName" property. // By default, the folder name of a run configuration is the name of the Gradle project containing it. // generateRunFolders = true // This property enables access transformers for use in development. // They will be applied to the Minecraft artifact. // The access transformer file can be anywhere in the project. // However, it must be at "META-INF/accesstransformer.cfg" in the final mod jar to be loaded by Forge. // This default location is a best practice to automatically put the file in the right place in the final jar. // See https://docs.minecraftforge.net/en/latest/advanced/accesstransformers/ for more information. // accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') // Default run configurations. // These can be tweaked, removed, or duplicated as needed. runs { // applies to all the run configs below configureEach { workingDirectory project.file('run') // Recommended logging data for a userdev environment // The markers can be added/remove as needed separated by commas. // "SCAN": For mods scan. // "REGISTRIES": For firing of registry events. // "REGISTRYDUMP": For getting the contents of all registries. property 'forge.logging.markers', 'REGISTRIES' // Recommended logging level for the console // You can set various levels here. // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels property 'forge.logging.console.level', 'debug' mods { "${mod_id}" { source sourceSets.main } } } client { // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. property 'forge.enabledGameTestNamespaces', mod_id // Inject the built mod jar as a Java agent so PatchAgent.premain() runs before // Minecraft classes are loaded. Use runClient0 (defined below) to ensure the jar is // built first. jvmArgs.add "-javaagent:${tasks.jar.archiveFile.get().asFile.absolutePath}".toString() // Allow the agent to attach to itself if PatchAgent.installPatchesAndRetransform // needs to fall back to the Attach API. jvmArgs.add "-Djdk.attach.allowAttachSelf=true".toString() } server { property 'forge.enabledGameTestNamespaces', mod_id args '--nogui' } // This run config launches GameTestServer and runs all registered gametests, then exits. // By default, the server will crash when no gametests are provided. // The gametest system is also enabled by default for other run configs under the /test command. gameTestServer { property 'forge.enabledGameTestNamespaces', mod_id } data { // example of overriding the workingDirectory set in configureEach above workingDirectory project.file('run-data') // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') } } } // Include resources generated by data generators. sourceSets.main.resources { srcDir 'src/generated/resources' } repositories { // Put repositories for dependencies here // ForgeGradle automatically adds the Forge maven and Maven Central for you // If you have mod jar dependencies in ./libs, you can declare them as a repository like so. // See https://docs.gradle.org/current/userguide/declaring_repositories.html#sub:flat_dir_resolver // flatDir { // dir 'libs' // } maven { url = "https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1" } } dependencies { // Specify the version of Minecraft to use. // Any artifact can be supplied so long as it has a "userdev" classifier artifact and is a compatible patcher artifact. // The "userdev" classifier will be requested and setup by ForgeGradle. // If the group id is "net.minecraft" and the artifact id is one of ["client", "server", "joined"], // then special handling is done to allow a setup of a vanilla dependency without the use of an external repository. minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" compileOnly 'org.projectlombok:lombok:1.18.34' annotationProcessor 'org.projectlombok:lombok:1.18.34' // ASM (already on Forge classpath at runtime via Mojang deps, but declare for IDE/compile) implementation 'org.ow2.asm:asm:9.6' implementation 'org.ow2.asm:asm-tree:9.6' implementation 'org.ow2.asm:asm-commons:9.6' implementation 'org.ow2.asm:asm-util:9.6' // Dev Auth runtimeOnly("me.djtheredstoner:DevAuth-forge-latest:1.2.2") // Example mod dependency with JEI - using fg.deobf() ensures the dependency is remapped to your development mappings // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime // compileOnly fg.deobf("mezz.jei:jei-${mc_version}-common-api:${jei_version}") // compileOnly fg.deobf("mezz.jei:jei-${mc_version}-forge-api:${jei_version}") // runtimeOnly fg.deobf("mezz.jei:jei-${mc_version}-forge:${jei_version}") // Example mod dependency using a mod jar from ./libs with a flat dir repository // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar // The group id is ignored when searching -- in this case, it is "blank" // implementation fg.deobf("blank:coolmod-${mc_version}:${coolmod_version}") // For more info: // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html // http://www.gradle.org/docs/current/userguide/dependency_management.html } // This block of code expands all declared replace properties in the specified resource targets. // A missing property will result in an error. Properties are expanded using ${} Groovy notation. // When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. // See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html tasks.named('processResources', ProcessResources).configure { var replaceProperties = [ minecraft_version: minecraft_version, minecraft_version_range: minecraft_version_range, forge_version: forge_version, forge_version_range: forge_version_range, loader_version_range: loader_version_range, mod_id: mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version, mod_authors: mod_authors, mod_description: mod_description, ] inputs.properties replaceProperties filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) { expand replaceProperties + [project: project] } } // Example for how to get properties into the manifest for reading at runtime. tasks.named('jar', Jar).configure { manifest { attributes([ 'Specification-Title' : mod_id, 'Specification-Vendor' : mod_authors, 'Specification-Version' : '1', // We are version 1 of ourselves 'Implementation-Title' : project.name, 'Implementation-Version' : project.jar.archiveVersion, 'Implementation-Vendor' : mod_authors, 'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), // Java-agent entry points so the jar can be passed via -javaagent: 'Premain-Class' : 'asm.patchify.loader.PatchAgent', 'Agent-Class' : 'asm.patchify.loader.PatchAgent', 'Can-Retransform-Classes' : 'true', 'Can-Redefine-Classes' : 'true' ]) } // This is the preferred method to reobfuscate your jar file finalizedBy 'reobfJar' } // However if you are in a multi-project build, dev time needs unobfed jar files, so you can delay the obfuscation until publishing by doing: // tasks.named('publish').configure { // dependsOn 'reobfJar' // } // IZMK-style entry point: build the jar first (so the -javaagent path exists) and then launch // the client. The client run config above already references tasks.jar via archiveFile. tasks.register('runClient0') { group = 'forgegradle runs' description = 'Build the mod jar (used as -javaagent) and start the client.' dependsOn tasks.jar finalizedBy tasks.runClient } // Example configuration to allow publishing using the maven-publish plugin publishing { publications { register('mavenJava', MavenPublication) { artifact jar } } repositories { maven { url "file://${project.projectDir}/mcmodsrepo" } } } tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation } // ===== Native build (DLL injection path) ===== // // Produces: // build/dist/OpenZen.dll - injectable DLL with embedded zen.jar // build/dist/OpenZenLoader.exe - GUI injector // // Requires: // - JAVA_HOME pointing at a JDK 17 install (for jni.h / jvmti.h) // - CMake either on PATH, bundled with Visual Studio 2019/2022 // ("C++ CMake tools for Windows" workload), or installed standalone. // - MSVC build tools (Visual Studio 2019+). // // Usage: // ./gradlew dll // def nativeDir = file('native') def nativeBuildDir = file("$nativeDir/build") ext.findCmake = { // 1. cmake already on PATH? try { def p = ['cmd', '/c', 'where', 'cmake'].execute() def out = new StringBuilder() p.consumeProcessOutput(out, new StringBuilder()) p.waitForOrKill(2000) if (p.exitValue() == 0) { def first = out.toString().readLines().find { it?.trim() } if (first) return first.trim() } } catch (Exception ignored) {} // 2. Visual Studio bundled cmake via vswhere. def pf86 = System.getenv('ProgramFiles(x86)') ?: 'C:/Program Files (x86)' def vswhere = "${pf86}/Microsoft Visual Studio/Installer/vswhere.exe" if (file(vswhere).exists()) { try { def out = new StringBuilder() def p = [vswhere, '-latest', '-products', '*', '-requires', 'Microsoft.VisualStudio.Component.VC.CMake.Project', '-property', 'installationPath'].execute() p.consumeProcessOutput(out, new StringBuilder()) p.waitForOrKill(5000) def vsPath = out.toString().trim() if (vsPath) { def cmakeExe = "${vsPath}/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe" if (file(cmakeExe).exists()) return cmakeExe } } catch (Exception ignored) {} } // 3. Standalone CMake installations. def pf = System.getenv('ProgramFiles') ?: 'C:/Program Files' def lad = System.getenv('LOCALAPPDATA') ?: '' def candidates = [ "${pf}/CMake/bin/cmake.exe", "${pf86}/CMake/bin/cmake.exe", "${lad}/Programs/CMake/bin/cmake.exe", ] for (path in candidates) { if (path && file(path).exists()) return path } return null } def cmakeMissingMessage = 'CMake not found. Install one of:\n' + ' - Standalone CMake: https://cmake.org/download/ (add to PATH)\n' + ' - Visual Studio 2022 with "C++ CMake tools for Windows" workload\n' + 'After installing, re-run ./gradlew dll' ext.findVcpkg = { def envRoot = System.getenv('VCPKG_ROOT') def candidates = [] if (envRoot) candidates << envRoot candidates += ['C:/vcpkg', 'D:/vcpkg', "${System.getProperty('user.home')}/vcpkg"] // Also accept versioned install dirs like C:/vcpkg-2026.03.18. ['C:/', 'D:/'].each { drive -> def root = file(drive) if (root.isDirectory()) { root.listFiles().each { f -> if (f.isDirectory() && f.name ==~ /^vcpkg(-.*)?$/) { candidates << f.absolutePath.replace('\\', '/') } } } } for (root in candidates) { if (root && file("$root/scripts/buildsystems/vcpkg.cmake").exists()) { return root } } return null } def vcpkgMissingMessage = 'vcpkg not found. The Qt-based loader needs vcpkg for static Qt6.\n' + 'One-time setup:\n' + ' git clone https://github.com/microsoft/vcpkg.git C:/vcpkg\n' + ' C:/vcpkg/bootstrap-vcpkg.bat\n' + 'Optionally set VCPKG_ROOT, or put it at C:/vcpkg or D:/vcpkg.\n' + 'Then re-run ./gradlew dll - the first run will build static Qt\n' + '(can take 30 min to 2 h); subsequent builds are fast.' ext.gitShortRevision = { // CI passes the resolved sha in OPENZEN_BUILD_REVISION so we don't have // to assume git is on PATH inside the runner image. def envRev = System.getenv('OPENZEN_BUILD_REVISION') if (envRev && !envRev.allWhitespace) return envRev.trim().take(7) try { def p = ['git', 'rev-parse', '--short=7', 'HEAD'].execute(null, project.rootDir) def out = new StringBuilder() p.consumeProcessOutput(out, new StringBuilder()) p.waitForOrKill(2000) if (p.exitValue() == 0) { def v = out.toString().trim() if (v) return v } } catch (Exception ignored) {} return null } tasks.register('stageNativeJar', Copy) { group = 'openzen' description = 'Stage the freshly built mod jar at native/zen.jar so the DLL ' + 'resource compiler can embed it.' dependsOn 'jar' from { tasks.jar.archiveFile } into nativeDir rename { 'zen.jar' } } tasks.register('configureNative', Exec) { group = 'openzen' description = 'Run CMake configure on the native sub-projects (DLL + Loader).' dependsOn 'stageNativeJar' workingDir nativeDir commandLine 'cmake', '-S', '.', '-B', 'build', '-A', 'x64' doFirst { def cmake = findCmake() if (!cmake) throw new GradleException(cmakeMissingMessage) def vcpkg = findVcpkg() if (!vcpkg) throw new GradleException(vcpkgMissingMessage) def rev = gitShortRevision() logger.lifecycle("Using CMake : ${cmake}") logger.lifecycle("Using vcpkg : ${vcpkg}") logger.lifecycle("Build rev : ${rev ?: '(unknown)'}") def args = [cmake, '-S', '.', '-B', 'build', '-A', 'x64', "-DCMAKE_TOOLCHAIN_FILE=${vcpkg}/scripts/buildsystems/vcpkg.cmake", '-DVCPKG_TARGET_TRIPLET=x64-windows-static', '-DVCPKG_HOST_TRIPLET=x64-windows-static'] if (rev) args << "-DOPENZEN_BUILD_REVISION=${rev}" commandLine args } } tasks.register('buildNative', Exec) { group = 'openzen' description = 'Compile OpenZen.dll and OpenZenLoader.exe with CMake.' dependsOn 'configureNative' workingDir nativeDir commandLine 'cmake', '--build', 'build', '--config', 'Release' doFirst { def cmake = findCmake() if (!cmake) throw new GradleException(cmakeMissingMessage) commandLine cmake, '--build', 'build', '--config', 'Release' } } tasks.register('packageDist') { group = 'openzen' description = 'Collect the Loader EXE into build/dist for distribution. ' + 'OpenZen.dll is embedded inside the EXE, so we only ship one file.' dependsOn 'buildNative' // Use an inline copy { } block inside doLast rather than the Copy task // type. Copy snapshots its source set at configuration time and reports // NO-SOURCE on a first clean build because the EXE that buildNative is // about to produce does not exist yet. doLast runs after buildNative, // by which point the file is definitely on disk. doLast { def src = file("$nativeBuildDir/loader/Release/OpenZenLoader.exe") if (!src.isFile()) { throw new GradleException( "Expected OpenZenLoader.exe at ${src} after buildNative; missing") } copy { from src into "$buildDir/dist" } logger.lifecycle("Packaged ${src.length()} bytes -> ${buildDir}/dist/${src.name}") } } ext.findUpx = { try { def p = ['cmd', '/c', 'where', 'upx'].execute() def out = new StringBuilder() p.consumeProcessOutput(out, new StringBuilder()) p.waitForOrKill(2000) if (p.exitValue() == 0) { def first = out.toString().readLines().find { it?.trim() } if (first) return first.trim() } } catch (Exception ignored) {} return null } tasks.register('upxCompress') { group = 'openzen' description = 'Run UPX --best --lzma on the packaged OpenZenLoader.exe. ' + 'No-op (warning only) when upx is not on PATH so local builds ' + 'do not need it installed.' dependsOn 'packageDist' doLast { def exe = file("$buildDir/dist/OpenZenLoader.exe") if (!exe.isFile()) { throw new GradleException("packageDist did not produce ${exe}") } def upx = findUpx() if (!upx) { logger.warn("upx not on PATH; skipping compression. Install from " + "https://upx.github.io/ to enable.") return } def before = exe.length() exec { commandLine upx, '--best', '--lzma', exe.absolutePath } def after = exe.length() logger.lifecycle(String.format( "UPX: %,d -> %,d bytes (%.1f%% of original)", before, after, after * 100.0 / before)) } } tasks.register('dll') { group = 'openzen' description = 'Build the injectable DLL and GUI Loader (final dist artifacts).' dependsOn 'packageDist' } // Gradle's stock 'clean' only wipes the project build/ directory, so the // CMake out-of-source build dir and the staged jar would survive a clean // and silently feed stale bits into the next dll build. Wire a dedicated // Delete task into 'clean' so ./gradlew clean really wipes everything. tasks.register('cleanNative', Delete) { group = 'openzen' description = 'Remove native/build/ and the staged native/zen.jar.' delete nativeBuildDir delete file("$nativeDir/zen.jar") } tasks.named('clean').configure { dependsOn 'cleanNative' }