diff --git a/.github/workflows/check-local-changes.yml b/.github/workflows/check-local-changes.yml
index ef101ceacd..0609d67e89 100644
--- a/.github/workflows/check-local-changes.yml
+++ b/.github/workflows/check-local-changes.yml
@@ -41,7 +41,7 @@ jobs:
run: ./gradlew generatePackageInfos
- name: Gen patches
- run: ./gradlew :neoforge:unpackSourcePatches
+ run: ./gradlew :neoforge:genPatches
- name: Run datagen with Gradle
run: ./gradlew :neoforge:runData :tests:runData
diff --git a/build.gradle b/build.gradle
index 6c46fb5ba6..fbc90b7d61 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,7 +4,7 @@ import java.util.regex.Pattern
plugins {
id 'net.neoforged.gradleutils' version '3.0.0'
id 'com.diffplug.spotless' version '6.22.0' apply false
- id 'net.neoforged.licenser' version '0.7.2'
+ id 'net.neoforged.licenser' version '0.7.5'
id 'neoforge.formatting-conventions'
id 'neoforge.versioning'
}
@@ -23,19 +23,22 @@ System.out.println("NeoForge version ${project.version}")
allprojects {
version rootProject.version
group 'net.neoforged'
- repositories {
- mavenLocal()
- }
-}
-subprojects {
apply plugin: 'java'
java.toolchain.languageVersion.set(JavaLanguageVersion.of(project.java_version))
}
-repositories {
- mavenCentral()
+// Remove src/ sources from the root project. They are used in the neoforge subproject.
+sourceSets {
+ main {
+ java {
+ srcDirs = []
+ }
+ resources {
+ srcDirs = []
+ }
+ }
}
// Put licenser here otherwise it tries to license all source sets including decompiled MC sources
diff --git a/buildSrc/README.md b/buildSrc/README.md
new file mode 100644
index 0000000000..2e63a427c0
--- /dev/null
+++ b/buildSrc/README.md
@@ -0,0 +1,81 @@
+# NeoForge Development Gradle Plugin
+
+## NeoForge Project Structure
+
+Before understanding the `buildSrc` plugin, one should understand the structure of the NeoForge Gradle project it is
+applied to.
+
+The project consists of a project tree with the following structure:
+
+| Folder Path | Gradle Project Path | Applied Plugins | Description |
+|------------------------------------------------------------------------|----------------------|:------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [`/build.gradle`](../build.gradle) | `:` | — | The root project. Since this project is reused for Kits, the root project name is based on the checkout folder, which actually can lead to issues if it is called `NeoForge`. |
+| [`/projects/neoforge/build.gradle`](../projects/neoforge/build.gradle) | `:neoforge` | [NeoDevPlugin](#neodevplugin) | The core NeoForge project, which produces the artifacts that will be published. |
+| [`/projects/base/build.gradle`](../projects/base/build.gradle) | `:base` | [NeoDevBasePlugin](#neodevbaseplugin) | A utility project that contains the Minecraft sources without any NeoForge additions. Can be used to quickly compare what NeoForge has changed. |
+| [`/tests/build.gradle`](../tests/build.gradle) | `:tests` | [NeoDevExtraPlugin](#neodevextraplugin) | Contains the game and unit tests for NeoForge. |
+| [`/testframework/build.gradle`](../testframework/build.gradle) | `:testframework` | [MinecraftDependenciesPlugin](#minecraftdependenciesplugin) | A library providing support classes around writing game tests. |
+| [`/coremods/build.gradle`](../coremods/build.gradle) | `:neoforge-coremods` | — | Java Bytecode transformers that are embedded into NeoForge as a nested Jar file. |
+|
+
+## Plugins
+
+### NeoDevBasePlugin
+
+Sources: [NeoDevBasePlugin.java](src/main/java/net/neoforged/neodev/NeoDevBasePlugin.java)
+
+Implicitly applies: [MinecraftDependenciesPlugin](#minecraftdependenciesplugin).
+
+Sets up a `setup` task that reuses code from [NeoDevPlugin](#neodevplugin) to decompile Minecraft and place the
+decompiled sources in `projects/base/src/main/java`.
+
+### NeoDevPlugin
+
+Sources: [NeoDevPlugin.java](src/main/java/net/neoforged/neodev/NeoDevPlugin.java)
+
+Implicitly applies: [MinecraftDependenciesPlugin](#minecraftdependenciesplugin).
+
+This is the primary of this repository and is used to configure the `neoforge` subproject.
+
+#### Setup
+
+It creates a `setup` task that performs the following actions via various subtasks:
+
+- Decompile Minecraft using the [NeoForm Runtime](https://github.com/neoforged/neoformruntime) and Minecraft version specific [NeoForm data](https://github.com/neoforged/NeoForm).
+- Applies [Access Transformers](../src/main/resources/META-INF/accesstransformer.cfg) to Minecraft sources.
+- Applies [NeoForge patches](../patches) to Minecraft sources. Any rejects are saved to the `/rejects` folder in the repository for manual inspection. During updates to new versions, the task can be run with `-Pupdating=true` to apply patches more leniently.
+- Unpacks the patched sources to `projects/neoforge/src/main/java`.
+
+#### Config Generation
+
+The plugin creates and configures the tasks to create various configuration files used downstream to develop
+mods with this version of NeoForge ([CreateUserDevConfig](src/main/java/net/neoforged/neodev/CreateUserDevConfig.java)), or install it ([CreateInstallerProfile](src/main/java/net/neoforged/neodev/installer/CreateInstallerProfile.java) and [CreateLauncherProfile](src/main/java/net/neoforged/neodev/installer/CreateLauncherProfile.java)).
+
+A separate userdev profile is created for use by other subprojects in this repository.
+The only difference is that it uses the FML launch types ending in `dev` rather than `userdev`.
+
+#### Patch Generation
+
+NeoForge injects its hooks into Minecraft by patching the decompiled source code.
+Changes are made locally to the decompiled and patched source.
+Since that source cannot be published, patches need to be generated before checking in.
+The plugin configures the necessary task to do this
+([GenerateSourcePatches](src/main/java/net/neoforged/neodev/GenerateSourcePatches.java)).
+
+The source patches are only used during development of NeoForge itself and development of mods that use Gradle plugins implementing the decompile/patch/recompile pipeline.
+For use by the installer intended for players as well as Gradle plugins wanting to replicate the production artifacts more closely, binary patches are generated using the ([GenerateBinaryPatches](src/main/java/net/neoforged/neodev/GenerateBinaryPatches.java)) task.
+
+### NeoDevExtraPlugin
+
+Sources: [NeoDevExtraPlugin.java](src/main/java/net/neoforged/neodev/NeoDevExtraPlugin.java)
+
+This plugin can be applied to obtain a dependency on the `neoforge` project to depend on NeoForge including Minecraft
+itself. Besides wiring up the dependency, it also creates run configurations based on the run-types defined in the
+`neoforge` project.
+
+### MinecraftDependenciesPlugin
+
+This plugin is reused from [ModDevGradle](https://github.com/neoforged/ModDevGradle/).
+
+It sets up repositories and attributes such that
+the [libraries that Minecraft itself depends upon](https://github.com/neoforged/GradleMinecraftDependencies) can be
+used.
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 6784052459..6d0ce4c5af 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -1,3 +1,26 @@
plugins {
+ id 'java-gradle-plugin'
id 'groovy-gradle-plugin'
}
+
+repositories {
+ gradlePluginPortal()
+ mavenCentral()
+ maven {
+ name = "NeoForged"
+ url = "https://maven.neoforged.net/releases"
+ content {
+ includeGroup "codechicken"
+ includeGroup "net.neoforged"
+ }
+ }
+}
+
+dependencies {
+ // buildSrc is an includedbuild of the parent directory (gradle.parent)
+ // ../settings.gradle sets these version properties accordingly
+ implementation "net.neoforged:moddev-gradle:${gradle.parent.ext.moddevgradle_plugin_version}"
+
+ implementation "com.google.code.gson:gson:${gradle.parent.ext.gson_version}"
+ implementation "io.codechicken:DiffPatch:${gradle.parent.ext.diffpatch_version}"
+}
diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/buildSrc/src/main/groovy/neoforge.formatting-conventions.gradle b/buildSrc/src/main/groovy/neoforge.formatting-conventions.gradle
index 0ba7b48852..e0f554b820 100644
--- a/buildSrc/src/main/groovy/neoforge.formatting-conventions.gradle
+++ b/buildSrc/src/main/groovy/neoforge.formatting-conventions.gradle
@@ -2,9 +2,14 @@ import java.util.regex.Matcher
project.plugins.apply('com.diffplug.spotless')
-final generatePackageInfos = tasks.register('generatePackageInfos', Task) {
- doLast {
- fileTree('src/main/java').each { javaFile ->
+abstract class GeneratePackageInfos extends DefaultTask {
+ @InputFiles
+ @PathSensitive(PathSensitivity.RELATIVE)
+ abstract ConfigurableFileCollection getFiles();
+
+ @TaskAction
+ void generatePackageInfos() {
+ getFiles().each { javaFile ->
def packageInfoFile = new File(javaFile.parent, 'package-info.java')
if (!packageInfoFile.exists()) {
def pkgName = javaFile.toString().replaceAll(Matcher.quoteReplacement(File.separator), '/')
@@ -27,6 +32,9 @@ final generatePackageInfos = tasks.register('generatePackageInfos', Task) {
}
}
}
+final generatePackageInfos = tasks.register('generatePackageInfos', GeneratePackageInfos) {
+ it.files.from fileTree("src/main/java")
+}
spotless {
java {
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/ApplyAccessTransformer.java b/buildSrc/src/main/java/net/neoforged/neodev/ApplyAccessTransformer.java
new file mode 100644
index 0000000000..7131cc8fdd
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/ApplyAccessTransformer.java
@@ -0,0 +1,72 @@
+package net.neoforged.neodev;
+
+import net.neoforged.neodev.utils.FileUtils;
+import org.gradle.api.file.ConfigurableFileCollection;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.provider.Property;
+import org.gradle.api.tasks.Classpath;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.Internal;
+import org.gradle.api.tasks.JavaExec;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+
+import javax.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Runs JavaSourceTransformer to apply
+ * access transformers to the Minecraft source code for extending the access level of existing classes/methods/etc.
+ *
+ * Note that at runtime, FML also applies access transformers.
+ */
+abstract class ApplyAccessTransformer extends JavaExec {
+ @InputFile
+ public abstract RegularFileProperty getInputJar();
+
+ @InputFile
+ public abstract RegularFileProperty getAccessTransformer();
+
+ @Input
+ public abstract Property getValidate();
+
+ @OutputFile
+ public abstract RegularFileProperty getOutputJar();
+
+ // Used to give JST more information about the classes.
+ @Classpath
+ public abstract ConfigurableFileCollection getLibraries();
+
+ @Internal
+ public abstract RegularFileProperty getLibrariesFile();
+
+ @Inject
+ public ApplyAccessTransformer() {}
+
+ @Override
+ @TaskAction
+ public void exec() {
+ try {
+ FileUtils.writeLinesSafe(
+ getLibrariesFile().getAsFile().get().toPath(),
+ getLibraries().getFiles().stream().map(File::getAbsolutePath).toList(),
+ StandardCharsets.UTF_8);
+ } catch (IOException exception) {
+ throw new UncheckedIOException("Failed to write libraries for JST.", exception);
+ }
+
+ args(
+ "--enable-accesstransformers",
+ "--access-transformer", getAccessTransformer().getAsFile().get().getAbsolutePath(),
+ "--access-transformer-validation", getValidate().get() ? "error" : "log",
+ "--libraries-list", getLibrariesFile().getAsFile().get().getAbsolutePath(),
+ getInputJar().getAsFile().get().getAbsolutePath(),
+ getOutputJar().getAsFile().get().getAbsolutePath());
+
+ super.exec();
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/ApplyPatches.java b/buildSrc/src/main/java/net/neoforged/neodev/ApplyPatches.java
new file mode 100644
index 0000000000..037e5f9bf6
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/ApplyPatches.java
@@ -0,0 +1,79 @@
+package net.neoforged.neodev;
+
+import io.codechicken.diffpatch.cli.PatchOperation;
+import io.codechicken.diffpatch.util.Input.MultiInput;
+import io.codechicken.diffpatch.util.Output.MultiOutput;
+import io.codechicken.diffpatch.util.PatchMode;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.Project;
+import org.gradle.api.file.DirectoryProperty;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.provider.Property;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.InputDirectory;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.PathSensitive;
+import org.gradle.api.tasks.PathSensitivity;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.work.DisableCachingByDefault;
+
+import javax.inject.Inject;
+import java.io.IOException;
+
+/**
+ * Applies Java source patches to a source jar and produces a patched source jar as an output.
+ * It can optionally store rejected hunks into a given folder, which is primarily used for updating
+ * when the original sources changed and some hunks are expected to fail.
+ */
+@DisableCachingByDefault(because = "Not worth caching")
+abstract class ApplyPatches extends DefaultTask {
+ @InputFile
+ public abstract RegularFileProperty getOriginalJar();
+
+ @InputDirectory
+ @PathSensitive(PathSensitivity.NONE)
+ public abstract DirectoryProperty getPatchesFolder();
+
+ @OutputFile
+ public abstract RegularFileProperty getPatchedJar();
+
+ @OutputDirectory
+ public abstract DirectoryProperty getRejectsFolder();
+
+ @Input
+ protected abstract Property getIsUpdating();
+
+ @Inject
+ public ApplyPatches(Project project) {
+ getIsUpdating().set(project.getProviders().gradleProperty("updating").map(Boolean::parseBoolean).orElse(false));
+ }
+
+ @TaskAction
+ public void applyPatches() throws IOException {
+ var isUpdating = getIsUpdating().get();
+
+ var builder = PatchOperation.builder()
+ .logTo(getLogger()::lifecycle)
+ .baseInput(MultiInput.detectedArchive(getOriginalJar().get().getAsFile().toPath()))
+ .patchesInput(MultiInput.folder(getPatchesFolder().get().getAsFile().toPath()))
+ .patchedOutput(MultiOutput.detectedArchive(getPatchedJar().get().getAsFile().toPath()))
+ .rejectsOutput(MultiOutput.folder(getRejectsFolder().get().getAsFile().toPath()))
+ .mode(isUpdating ? PatchMode.FUZZY : PatchMode.ACCESS)
+ .aPrefix("a/")
+ .bPrefix("b/")
+ .level(isUpdating ? io.codechicken.diffpatch.util.LogLevel.ALL : io.codechicken.diffpatch.util.LogLevel.WARN)
+ .minFuzz(0.9f); // The 0.5 default in DiffPatch is too low.
+
+ var result = builder.build().operate();
+
+ int exit = result.exit;
+ if (exit != 0 && exit != 1) {
+ throw new RuntimeException("DiffPatch failed with exit code: " + exit);
+ }
+ if (exit != 0 && !isUpdating) {
+ throw new RuntimeException("Patches failed to apply.");
+ }
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/CreateCleanArtifacts.java b/buildSrc/src/main/java/net/neoforged/neodev/CreateCleanArtifacts.java
new file mode 100644
index 0000000000..2cb2e89ade
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/CreateCleanArtifacts.java
@@ -0,0 +1,36 @@
+package net.neoforged.neodev;
+
+import net.neoforged.nfrtgradle.CreateMinecraftArtifacts;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.tasks.OutputFile;
+
+import javax.inject.Inject;
+
+abstract class CreateCleanArtifacts extends CreateMinecraftArtifacts {
+ @OutputFile
+ abstract RegularFileProperty getCleanClientJar();
+
+ @OutputFile
+ abstract RegularFileProperty getRawServerJar();
+
+ @OutputFile
+ abstract RegularFileProperty getCleanServerJar();
+
+ @OutputFile
+ abstract RegularFileProperty getCleanJoinedJar();
+
+ @OutputFile
+ abstract RegularFileProperty getMergedMappings();
+
+ @Inject
+ public CreateCleanArtifacts() {
+ getAdditionalResults().put("node.stripClient.output.output", getCleanClientJar().getAsFile());
+ getAdditionalResults().put("node.downloadServer.output.output", getRawServerJar().getAsFile());
+ getAdditionalResults().put("node.stripServer.output.output", getCleanServerJar().getAsFile());
+ getAdditionalResults().put("node.rename.output.output", getCleanJoinedJar().getAsFile());
+ getAdditionalResults().put("node.mergeMappings.output.output", getMergedMappings().getAsFile());
+
+ // TODO: does anyone care about this? they should be contained in the client mappings
+ //"--write-result", "node.downloadServerMappings.output.output:" + getServerMappings().get().getAsFile().getAbsolutePath()
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/CreateUserDevConfig.java b/buildSrc/src/main/java/net/neoforged/neodev/CreateUserDevConfig.java
new file mode 100644
index 0000000000..ab24c91d4d
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/CreateUserDevConfig.java
@@ -0,0 +1,203 @@
+package net.neoforged.neodev;
+
+import com.google.gson.GsonBuilder;
+import net.neoforged.neodev.utils.FileUtils;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.provider.ListProperty;
+import org.gradle.api.provider.Property;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Creates the userdev configuration file used by the various Gradle plugins used to develop
+ * mods for NeoForge, such as Architectury Loom,
+ * ModDevGradle
+ * or NeoGradle.
+ */
+abstract class CreateUserDevConfig extends DefaultTask {
+ @Inject
+ public CreateUserDevConfig() {}
+
+ /**
+ * Toggles the launch type written to the userdev configuration between *dev and *userdev.
+ */
+ @Input
+ abstract Property getForNeoDev();
+
+ @Input
+ abstract Property getFmlVersion();
+
+ @Input
+ abstract Property getMinecraftVersion();
+
+ @Input
+ abstract Property getNeoForgeVersion();
+
+ @Input
+ abstract Property getRawNeoFormVersion();
+
+ @Input
+ abstract ListProperty getLibraries();
+
+ @Input
+ abstract ListProperty getModules();
+
+ @Input
+ abstract ListProperty getTestLibraries();
+
+ @Input
+ abstract ListProperty getIgnoreList();
+
+ @Input
+ abstract Property getBinpatcherGav();
+
+ @OutputFile
+ abstract RegularFileProperty getUserDevConfig();
+
+ @TaskAction
+ public void writeUserDevConfig() throws IOException {
+ var config = new UserDevConfig(
+ 2,
+ "net.neoforged:neoform:%s-%s@zip".formatted(getMinecraftVersion().get(), getRawNeoFormVersion().get()),
+ "ats/",
+ "joined.lzma",
+ new BinpatcherConfig(
+ getBinpatcherGav().get(),
+ List.of("--clean", "{clean}", "--output", "{output}", "--apply", "{patch}")),
+ "patches/",
+ "net.neoforged:neoforge:%s:sources".formatted(getNeoForgeVersion().get()),
+ "net.neoforged:neoforge:%s:universal".formatted(getNeoForgeVersion().get()),
+ getLibraries().get(),
+ getTestLibraries().get(),
+ new LinkedHashMap<>(),
+ getModules().get());
+
+ for (var runType : RunType.values()) {
+ var launchTarget = switch (runType) {
+ case CLIENT -> "forgeclient";
+ case DATA -> "forgedata";
+ case GAME_TEST_SERVER, SERVER -> "forgeserver";
+ case JUNIT -> "forgejunit";
+ } + (getForNeoDev().get() ? "dev" : "userdev");
+
+ List args = new ArrayList<>();
+ Collections.addAll(args,
+ "--launchTarget", launchTarget);
+
+ if (runType == RunType.CLIENT || runType == RunType.JUNIT) {
+ // TODO: this is copied from NG but shouldn't it be the MC version?
+ Collections.addAll(args,
+ "--version", getNeoForgeVersion().get());
+ }
+
+ if (runType == RunType.CLIENT || runType == RunType.DATA || runType == RunType.JUNIT) {
+ Collections.addAll(args,
+ "--assetIndex", "{asset_index}",
+ "--assetsDir", "{assets_root}");
+ }
+
+ Collections.addAll(args,
+ "--gameDir", ".",
+ "--fml.fmlVersion", getFmlVersion().get(),
+ "--fml.mcVersion", getMinecraftVersion().get(),
+ "--fml.neoForgeVersion", getNeoForgeVersion().get(),
+ "--fml.neoFormVersion", getRawNeoFormVersion().get());
+
+ Map systemProperties = new LinkedHashMap<>();
+ systemProperties.put("java.net.preferIPv6Addresses", "system");
+ systemProperties.put("ignoreList", String.join(",", getIgnoreList().get()));
+ systemProperties.put("legacyClassPath.file", "{minecraft_classpath_file}");
+
+ if (runType == RunType.CLIENT || runType == RunType.GAME_TEST_SERVER) {
+ systemProperties.put("neoforge.enableGameTest", "true");
+
+ if (runType == RunType.GAME_TEST_SERVER) {
+ systemProperties.put("neoforge.gameTestServer", "true");
+ }
+ }
+
+ config.runs().put(runType.jsonName, new UserDevRunType(
+ runType != RunType.JUNIT,
+ "cpw.mods.bootstraplauncher.BootstrapLauncher",
+ args,
+ List.of(
+ "-p", "{modules}",
+ "--add-modules", "ALL-MODULE-PATH",
+ "--add-opens", "java.base/java.util.jar=cpw.mods.securejarhandler",
+ "--add-opens", "java.base/java.lang.invoke=cpw.mods.securejarhandler",
+ "--add-exports", "java.base/sun.security.util=cpw.mods.securejarhandler",
+ "--add-exports", "jdk.naming.dns/com.sun.jndi.dns=java.naming"),
+ runType == RunType.CLIENT || runType == RunType.JUNIT,
+ runType == RunType.GAME_TEST_SERVER || runType == RunType.SERVER,
+ runType == RunType.DATA,
+ runType == RunType.CLIENT || runType == RunType.GAME_TEST_SERVER,
+ runType == RunType.JUNIT,
+ Map.of(
+ "MOD_CLASSES", "{source_roots}"),
+ systemProperties
+ ));
+ }
+
+ FileUtils.writeStringSafe(
+ getUserDevConfig().getAsFile().get().toPath(),
+ new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(config),
+ // TODO: Not sure what this should be? Most likely the file is ASCII.
+ StandardCharsets.UTF_8);
+ }
+
+ private enum RunType {
+ CLIENT("client"),
+ DATA("data"),
+ GAME_TEST_SERVER("gameTestServer"),
+ SERVER("server"),
+ JUNIT("junit");
+
+ private final String jsonName;
+
+ RunType(String jsonName) {
+ this.jsonName = jsonName;
+ }
+ }
+}
+
+record UserDevConfig(
+ int spec,
+ String mcp,
+ String ats,
+ String binpatches,
+ BinpatcherConfig binpatcher,
+ String patches,
+ String sources,
+ String universal,
+ List libraries,
+ List testLibraries,
+ Map runs,
+ List modules) {}
+
+record BinpatcherConfig(
+ String version,
+ List args) {}
+
+record UserDevRunType(
+ boolean singleInstance,
+ String main,
+ List args,
+ List jvmArgs,
+ boolean client,
+ boolean server,
+ boolean dataGenerator,
+ boolean gameTest,
+ boolean unitTest,
+ Map env,
+ Map props) {}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/GenerateBinaryPatches.java b/buildSrc/src/main/java/net/neoforged/neodev/GenerateBinaryPatches.java
new file mode 100644
index 0000000000..35a06a8c33
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/GenerateBinaryPatches.java
@@ -0,0 +1,70 @@
+package net.neoforged.neodev;
+
+import org.gradle.api.GradleException;
+import org.gradle.api.file.DirectoryProperty;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.tasks.InputDirectory;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.JavaExec;
+import org.gradle.api.tasks.Optional;
+import org.gradle.api.tasks.OutputFile;
+
+import javax.inject.Inject;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+abstract class GenerateBinaryPatches extends JavaExec {
+ @Inject
+ public GenerateBinaryPatches() {}
+
+ /**
+ * The jar file containing classes in the base state.
+ */
+ @InputFile
+ abstract RegularFileProperty getCleanJar();
+
+ /**
+ * The jar file containing classes in the desired target state.
+ */
+ @InputFile
+ abstract RegularFileProperty getPatchedJar();
+
+ @InputFile
+ abstract RegularFileProperty getMappings();
+
+ /**
+ * This directory of patch files for the Java sources is used as a hint to only diff class files that
+ * supposedly have changed. If it is not set, the tool will diff every .class file instead.
+ */
+ @InputDirectory
+ @Optional
+ abstract DirectoryProperty getSourcePatchesFolder();
+
+ /**
+ * The location where the LZMA compressed binary patches are written to.
+ */
+ @OutputFile
+ abstract RegularFileProperty getOutputFile();
+
+ @Override
+ public void exec() {
+ args("--clean", getCleanJar().get().getAsFile().getAbsolutePath());
+ args("--dirty", getPatchedJar().get().getAsFile().getAbsolutePath());
+ args("--srg", getMappings().get().getAsFile().getAbsolutePath());
+ if (getSourcePatchesFolder().isPresent()) {
+ args("--patches", getSourcePatchesFolder().get().getAsFile().getAbsolutePath());
+ }
+ args("--output", getOutputFile().get().getAsFile().getAbsolutePath());
+
+ var logFile = new File(getTemporaryDir(), "console.log");
+ try (var out = new BufferedOutputStream(new FileOutputStream(logFile))) {
+ getLogger().info("Logging binpatcher console output to {}", logFile.getAbsolutePath());
+ setStandardOutput(out);
+ super.exec();
+ } catch (IOException e) {
+ throw new GradleException("Failed to create binary patches.", e);
+ }
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/GenerateSourcePatches.java b/buildSrc/src/main/java/net/neoforged/neodev/GenerateSourcePatches.java
new file mode 100644
index 0000000000..c6e1c7e730
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/GenerateSourcePatches.java
@@ -0,0 +1,55 @@
+package net.neoforged.neodev;
+
+import io.codechicken.diffpatch.cli.CliOperation;
+import io.codechicken.diffpatch.cli.DiffOperation;
+import io.codechicken.diffpatch.util.Input.MultiInput;
+import io.codechicken.diffpatch.util.Output.MultiOutput;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.file.DirectoryProperty;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.tasks.InputDirectory;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.PathSensitive;
+import org.gradle.api.tasks.PathSensitivity;
+import org.gradle.api.tasks.TaskAction;
+
+import javax.inject.Inject;
+import java.io.IOException;
+
+abstract class GenerateSourcePatches extends DefaultTask {
+ @InputFile
+ public abstract RegularFileProperty getOriginalJar();
+
+ @InputDirectory
+ @PathSensitive(PathSensitivity.RELATIVE)
+ public abstract DirectoryProperty getModifiedSources();
+
+ @OutputFile
+ public abstract RegularFileProperty getPatchesJar();
+
+ @Inject
+ public GenerateSourcePatches() {}
+
+ @TaskAction
+ public void generateSourcePatches() throws IOException {
+ var builder = DiffOperation.builder()
+ .logTo(getLogger()::lifecycle)
+ .baseInput(MultiInput.detectedArchive(getOriginalJar().get().getAsFile().toPath()))
+ .changedInput(MultiInput.folder(getModifiedSources().get().getAsFile().toPath()))
+ .patchesOutput(MultiOutput.detectedArchive(getPatchesJar().get().getAsFile().toPath()))
+ .autoHeader(true)
+ .level(io.codechicken.diffpatch.util.LogLevel.WARN)
+ .summary(false)
+ .aPrefix("a/")
+ .bPrefix("b/")
+ .lineEnding("\n");
+
+ CliOperation.Result result = builder.build().operate();
+
+ int exit = result.exit;
+ if (exit != 0 && exit != 1) {
+ throw new RuntimeException("DiffPatch failed with exit code: " + exit);
+ }
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/NeoDevBasePlugin.java b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevBasePlugin.java
new file mode 100644
index 0000000000..c6e3e8ea6d
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevBasePlugin.java
@@ -0,0 +1,60 @@
+package net.neoforged.neodev;
+
+import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin;
+import net.neoforged.moddevgradle.internal.NeoDevFacade;
+import net.neoforged.nfrtgradle.CreateMinecraftArtifacts;
+import net.neoforged.nfrtgradle.DownloadAssets;
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.tasks.Sync;
+
+public class NeoDevBasePlugin implements Plugin {
+ @Override
+ public void apply(Project project) {
+ // These plugins allow us to declare dependencies on Minecraft libraries needed to compile the official sources
+ project.getPlugins().apply(MinecraftDependenciesPlugin.class);
+
+ var dependencyFactory = project.getDependencyFactory();
+ var tasks = project.getTasks();
+ var neoDevBuildDir = project.getLayout().getBuildDirectory().dir("neodev");
+
+ var extension = project.getExtensions().create(NeoDevExtension.NAME, NeoDevExtension.class);
+
+ var createSources = NeoDevPlugin.configureMinecraftDecompilation(project);
+ // Task must run on sync to have MC resources available for IDEA nondelegated builds.
+ NeoDevFacade.runTaskOnProjectSync(project, createSources);
+
+ tasks.register("setup", Sync.class, task -> {
+ task.setGroup(NeoDevPlugin.GROUP);
+ task.setDescription("Replaces the contents of the base project sources with the unpatched, decompiled Minecraft source code.");
+ task.from(project.zipTree(createSources.flatMap(CreateMinecraftArtifacts::getSourcesArtifact)));
+ task.into(project.file("src/main/java/"));
+ });
+
+ var downloadAssets = tasks.register("downloadAssets", DownloadAssets.class, task -> {
+ task.setGroup(NeoDevPlugin.INTERNAL_GROUP);
+ task.getNeoFormArtifact().set(createSources.flatMap(CreateMinecraftArtifacts::getNeoFormArtifact));
+ task.getAssetPropertiesFile().set(neoDevBuildDir.map(dir -> dir.file("minecraft_assets.properties")));
+ });
+
+ // MC looks for its resources on the classpath.
+ var runtimeClasspath = project.getConfigurations().getByName(JavaPlugin.RUNTIME_ONLY_CONFIGURATION_NAME);
+ runtimeClasspath.getDependencies().add(
+ dependencyFactory.create(
+ project.files(createSources.flatMap(CreateMinecraftArtifacts::getResourcesArtifact))
+ )
+ );
+ NeoDevFacade.setupRuns(
+ project,
+ neoDevBuildDir,
+ extension.getRuns(),
+ // Pass an empty file collection for the userdev config.
+ // This will cause MDG to generate a dummy config suitable for vanilla.
+ project.files(),
+ modulePath -> {},
+ legacyClasspath -> {},
+ downloadAssets.flatMap(DownloadAssets::getAssetPropertiesFile)
+ );
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/NeoDevConfigurations.java b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevConfigurations.java
new file mode 100644
index 0000000000..35eb5b570d
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevConfigurations.java
@@ -0,0 +1,204 @@
+package net.neoforged.neodev;
+
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.ConfigurationContainer;
+import org.gradle.api.attributes.Bundling;
+import org.gradle.api.plugins.JavaPlugin;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Helper class to keep track of the many {@link Configuration}s used for the {@code neoforge} project.
+ */
+class NeoDevConfigurations {
+ static NeoDevConfigurations createAndSetup(Project project) {
+ return new NeoDevConfigurations(project);
+ }
+
+ //
+ // Configurations against which dependencies should be declared ("dependency scopes").
+ //
+
+ /**
+ * Only the NeoForm data zip and the dependencies to run NeoForm.
+ * Does not contain the dependencies to run vanilla Minecraft.
+ */
+ final Configuration neoFormData;
+ /**
+ * Only the NeoForm dependencies.
+ * These are the dependencies required to run NeoForm-decompiled Minecraft.
+ * Does not contain the dependencies to run the NeoForm process itself.
+ */
+ final Configuration neoFormDependencies;
+ /**
+ * Libraries used by NeoForge at compilation and runtime.
+ * These will end up on the MC-BOOTSTRAP module layer.
+ */
+ final Configuration libraries;
+ /**
+ * Libraries used by NeoForge at compilation and runtime that need to be placed on the jvm's module path to end up in the boot layer.
+ * Currently, this only contains the few dependencies that are needed to create the MC-BOOTSTRAP module layer.
+ * (i.e. BootstrapLauncher and its dependencies).
+ */
+ final Configuration moduleLibraries;
+ /**
+ * Libraries that should be accessible in mod development environments at compilation time only.
+ * Currently, this is only used for MixinExtras, which is already available at runtime via JiJ in the NeoForge universal jar.
+ */
+ final Configuration userdevCompileOnly;
+ /**
+ * Libraries that should be accessible at runtime in unit tests.
+ * Currently, this only contains the fml-junit test fixtures.
+ */
+ final Configuration userdevTestFixtures;
+
+ //
+ // Resolvable configurations.
+ //
+
+ /**
+ * Resolved {@link #neoFormData}.
+ * This is used to add NeoForm to the installer libraries.
+ * Only the zip is used (for the mappings), not the NeoForm tools, so it's not transitive.
+ */
+ final Configuration neoFormDataOnly;
+ /**
+ * Resolvable {@link #neoFormDependencies}.
+ */
+ final Configuration neoFormClasspath;
+ /**
+ * Resolvable {@link #moduleLibraries}.
+ */
+ final Configuration modulePath;
+ /**
+ * Userdev dependencies (written to a json file in the userdev jar).
+ * This should contain all of NeoForge's additional dependencies for userdev,
+ * but does not need to include Minecraft or NeoForm's libraries.
+ */
+ final Configuration userdevClasspath;
+ /**
+ * Resolvable {@link #userdevCompileOnly}, to add these entries to the ignore list of BootstrapLauncher.
+ */
+ final Configuration userdevCompileOnlyClasspath;
+ /**
+ * Resolvable {@link #userdevTestFixtures}, to write it in the userdev jar.
+ */
+ final Configuration userdevTestClasspath;
+ /**
+ * Libraries that need to be added to the classpath when launching NeoForge through the launcher.
+ * This contains all dependencies added by NeoForge, but does not include all of Minecraft's libraries.
+ * This is also used to produce the legacy classpath file for server installs.
+ */
+ final Configuration launcherProfileClasspath;
+
+ //
+ // The configurations for resolution only are declared in the build.gradle file.
+ //
+
+ /**
+ * To download each executable tool, we use a resolvable configuration.
+ * These configurations support both declaration and resolution.
+ */
+ final Map toolClasspaths;
+
+ private static Configuration dependencyScope(ConfigurationContainer configurations, String name) {
+ return configurations.create(name, configuration -> {
+ configuration.setCanBeConsumed(false);
+ configuration.setCanBeResolved(false);
+ });
+ }
+
+ private static Configuration resolvable(ConfigurationContainer configurations, String name) {
+ return configurations.create(name, configuration -> {
+ configuration.setCanBeConsumed(false);
+ configuration.setCanBeDeclared(false);
+ });
+ }
+
+ private NeoDevConfigurations(Project project) {
+ var configurations = project.getConfigurations();
+
+ neoFormData = dependencyScope(configurations, "neoFormData");
+ neoFormDependencies = dependencyScope(configurations, "neoFormDependencies");
+ libraries = dependencyScope(configurations, "libraries");
+ moduleLibraries = dependencyScope(configurations, "moduleLibraries");
+ userdevCompileOnly = dependencyScope(configurations, "userdevCompileOnly");
+ userdevTestFixtures = dependencyScope(configurations, "userdevTestFixtures");
+
+ neoFormDataOnly = resolvable(configurations, "neoFormDataOnly");
+ neoFormClasspath = resolvable(configurations, "neoFormClasspath");
+ modulePath = resolvable(configurations, "modulePath");
+ userdevClasspath = resolvable(configurations, "userdevClasspath");
+ userdevCompileOnlyClasspath = resolvable(configurations, "userdevCompileOnlyClasspath");
+ userdevTestClasspath = resolvable(configurations, "userdevTestClasspath");
+ launcherProfileClasspath = resolvable(configurations, "launcherProfileClasspath");
+
+ // Libraries & module libraries & MC dependencies need to be available when compiling in NeoDev,
+ // and on the runtime classpath too for IDE debugging support.
+ configurations.getByName("implementation").extendsFrom(libraries, moduleLibraries, neoFormDependencies);
+
+ // runtimeClasspath is our reference for all MC dependency versions.
+ // Make sure that any classpath we resolve is consistent with it.
+ var runtimeClasspath = configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
+
+ neoFormDataOnly.setTransitive(false);
+ neoFormDataOnly.extendsFrom(neoFormData);
+
+ neoFormClasspath.extendsFrom(neoFormDependencies);
+
+ modulePath.extendsFrom(moduleLibraries);
+ modulePath.shouldResolveConsistentlyWith(runtimeClasspath);
+
+ userdevClasspath.extendsFrom(libraries, moduleLibraries, userdevCompileOnly);
+ userdevClasspath.shouldResolveConsistentlyWith(runtimeClasspath);
+
+ userdevCompileOnlyClasspath.extendsFrom(userdevCompileOnly);
+ userdevCompileOnlyClasspath.shouldResolveConsistentlyWith(runtimeClasspath);
+
+ userdevTestClasspath.extendsFrom(userdevTestFixtures);
+ userdevTestClasspath.shouldResolveConsistentlyWith(runtimeClasspath);
+
+ launcherProfileClasspath.extendsFrom(libraries, moduleLibraries);
+ launcherProfileClasspath.shouldResolveConsistentlyWith(runtimeClasspath);
+
+ toolClasspaths = createToolClasspaths(project);
+ }
+
+ private static Map createToolClasspaths(Project project) {
+ var configurations = project.getConfigurations();
+ var dependencyFactory = project.getDependencyFactory();
+
+ var result = new HashMap();
+
+ for (var tool : Tools.values()) {
+ var configuration = configurations.create(tool.getGradleConfigurationName(), spec -> {
+ spec.setDescription("Resolves the executable for tool " + tool.name());
+ spec.setCanBeConsumed(false);
+ // Tools are considered to be executable jars.
+ // Gradle requires the classpath for JavaExec to only contain a single file for these.
+ if (tool.isRequestFatJar()) {
+ spec.attributes(attr -> {
+ attr.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.SHADOWED));
+ });
+ }
+
+ var gav = tool.asGav(project);
+ spec.getDependencies().add(dependencyFactory.create(gav));
+ });
+ result.put(tool, configuration);
+ }
+
+ return Map.copyOf(result);
+ }
+
+ /**
+ * Gets a configuration representing the classpath for an executable tool.
+ * Some tools are assumed to be executable jars, and their configurations only contain a single file.
+ */
+ public Configuration getExecutableTool(Tools tool) {
+ return Objects.requireNonNull(toolClasspaths.get(tool));
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/NeoDevExtension.java b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevExtension.java
new file mode 100644
index 0000000000..224c7ab7e9
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevExtension.java
@@ -0,0 +1,35 @@
+package net.neoforged.neodev;
+
+import net.neoforged.moddevgradle.dsl.ModModel;
+import net.neoforged.moddevgradle.dsl.RunModel;
+import org.gradle.api.Action;
+import org.gradle.api.NamedDomainObjectContainer;
+import org.gradle.api.Project;
+
+public class NeoDevExtension {
+ public static final String NAME = "neoDev";
+
+ private final NamedDomainObjectContainer mods;
+ private final NamedDomainObjectContainer runs;
+
+ public NeoDevExtension(Project project) {
+ mods = project.container(ModModel.class);
+ runs = project.container(RunModel.class, name -> project.getObjects().newInstance(RunModel.class, name, project, mods));
+ }
+
+ public NamedDomainObjectContainer getMods() {
+ return mods;
+ }
+
+ public void mods(Action> action) {
+ action.execute(mods);
+ }
+
+ public NamedDomainObjectContainer getRuns() {
+ return runs;
+ }
+
+ public void runs(Action> action) {
+ action.execute(runs);
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/NeoDevExtraPlugin.java b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevExtraPlugin.java
new file mode 100644
index 0000000000..912fd4bfe5
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevExtraPlugin.java
@@ -0,0 +1,79 @@
+package net.neoforged.neodev;
+
+import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin;
+import net.neoforged.moddevgradle.internal.NeoDevFacade;
+import net.neoforged.nfrtgradle.CreateMinecraftArtifacts;
+import net.neoforged.nfrtgradle.DownloadAssets;
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.ProjectDependency;
+import org.gradle.api.artifacts.dsl.DependencyFactory;
+import org.gradle.api.tasks.testing.Test;
+
+import java.util.function.Consumer;
+
+// TODO: the only point of this is to configure runs that depend on neoforge. Maybe this could be done with less code duplication...
+// TODO: Gradle says "thou shalt not referenceth otherth projects" yet here we are
+// TODO: depend on neoforge configurations that the moddev plugin also uses
+public class NeoDevExtraPlugin implements Plugin {
+ @Override
+ public void apply(Project project) {
+ project.getPlugins().apply(MinecraftDependenciesPlugin.class);
+
+ var neoForgeProject = project.getRootProject().getChildProjects().get("neoforge");
+
+ var dependencyFactory = project.getDependencyFactory();
+ var tasks = project.getTasks();
+ var neoDevBuildDir = project.getLayout().getBuildDirectory().dir("neodev");
+
+ var extension = project.getExtensions().create(NeoDevExtension.NAME, NeoDevExtension.class);
+
+ var modulePathDependency = projectDep(dependencyFactory, neoForgeProject, "net.neoforged:neoforge-moddev-module-path");
+
+ // TODO: this is temporary
+ var downloadAssets = neoForgeProject.getTasks().named("downloadAssets", DownloadAssets.class);
+ var writeNeoDevConfig = neoForgeProject.getTasks().named("writeNeoDevConfig", CreateUserDevConfig.class);
+
+ Consumer configureLegacyClasspath = spec -> {
+ spec.getDependencies().add(projectDep(dependencyFactory, neoForgeProject, "net.neoforged:neoforge-dependencies"));
+ };
+
+ extension.getRuns().configureEach(run -> {
+ configureLegacyClasspath.accept(run.getAdditionalRuntimeClasspathConfiguration());
+ });
+ NeoDevFacade.setupRuns(
+ project,
+ neoDevBuildDir,
+ extension.getRuns(),
+ writeNeoDevConfig,
+ modulePath -> modulePath.getDependencies().add(modulePathDependency),
+ configureLegacyClasspath,
+ downloadAssets.flatMap(DownloadAssets::getAssetPropertiesFile)
+ );
+
+ var testExtension = project.getExtensions().create(NeoDevTestExtension.NAME, NeoDevTestExtension.class);
+ var testTask = tasks.register("junitTest", Test.class, test -> test.setGroup("verification"));
+ tasks.named("check").configure(task -> task.dependsOn(testTask));
+
+ NeoDevFacade.setupTestTask(
+ project,
+ neoDevBuildDir,
+ testTask,
+ writeNeoDevConfig,
+ testExtension.getLoadedMods(),
+ testExtension.getTestedMod(),
+ modulePath -> modulePath.getDependencies().add(modulePathDependency),
+ configureLegacyClasspath,
+ downloadAssets.flatMap(DownloadAssets::getAssetPropertiesFile)
+ );
+ }
+
+ private static ProjectDependency projectDep(DependencyFactory dependencyFactory, Project project, String capabilityNotation) {
+ var dep = dependencyFactory.create(project);
+ dep.capabilities(caps -> {
+ caps.requireCapability(capabilityNotation);
+ });
+ return dep;
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/NeoDevPlugin.java b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevPlugin.java
new file mode 100644
index 0000000000..e7f9f77934
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevPlugin.java
@@ -0,0 +1,540 @@
+package net.neoforged.neodev;
+
+import net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin;
+import net.neoforged.moddevgradle.internal.NeoDevFacade;
+import net.neoforged.moddevgradle.tasks.JarJar;
+import net.neoforged.neodev.installer.CreateArgsFile;
+import net.neoforged.neodev.installer.CreateInstallerProfile;
+import net.neoforged.neodev.installer.CreateLauncherProfile;
+import net.neoforged.neodev.installer.InstallerProcessor;
+import net.neoforged.neodev.utils.DependencyUtils;
+import net.neoforged.nfrtgradle.CreateMinecraftArtifacts;
+import net.neoforged.nfrtgradle.DownloadAssets;
+import net.neoforged.nfrtgradle.NeoFormRuntimePlugin;
+import net.neoforged.nfrtgradle.NeoFormRuntimeTask;
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.repositories.MavenArtifactRepository;
+import org.gradle.api.file.Directory;
+import org.gradle.api.file.RegularFile;
+import org.gradle.api.plugins.BasePluginExtension;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.plugins.JavaPluginExtension;
+import org.gradle.api.provider.Provider;
+import org.gradle.api.tasks.Sync;
+import org.gradle.api.tasks.TaskProvider;
+import org.gradle.api.tasks.bundling.AbstractArchiveTask;
+import org.gradle.api.tasks.bundling.Jar;
+import org.gradle.api.tasks.bundling.Zip;
+
+import java.io.File;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class NeoDevPlugin implements Plugin {
+ static final String GROUP = "neoforge development";
+ static final String INTERNAL_GROUP = "neoforge development/internal";
+
+ @Override
+ public void apply(Project project) {
+ project.getPlugins().apply(MinecraftDependenciesPlugin.class);
+
+ var dependencyFactory = project.getDependencyFactory();
+ var tasks = project.getTasks();
+ var neoDevBuildDir = project.getLayout().getBuildDirectory().dir("neodev");
+
+ var rawNeoFormVersion = project.getProviders().gradleProperty("neoform_version");
+ var fmlVersion = project.getProviders().gradleProperty("fancy_mod_loader_version");
+ var minecraftVersion = project.getProviders().gradleProperty("minecraft_version");
+ var neoForgeVersion = project.provider(() -> project.getVersion().toString());
+ var mcAndNeoFormVersion = minecraftVersion.zip(rawNeoFormVersion, (mc, nf) -> mc + "-" + nf);
+
+ var extension = project.getExtensions().create(NeoDevExtension.NAME, NeoDevExtension.class);
+ var configurations = NeoDevConfigurations.createAndSetup(project);
+
+ /*
+ * MINECRAFT SOURCES SETUP
+ */
+ // 1. Obtain decompiled Minecraft sources jar using NeoForm.
+ var createSourceArtifacts = configureMinecraftDecompilation(project);
+ // Task must run on sync to have MC resources available for IDEA nondelegated builds.
+ NeoDevFacade.runTaskOnProjectSync(project, createSourceArtifacts);
+
+ // 2. Apply AT to the source jar from 1.
+ var atFile = project.getRootProject().file("src/main/resources/META-INF/accesstransformer.cfg");
+ var applyAt = configureAccessTransformer(
+ project,
+ configurations,
+ createSourceArtifacts,
+ neoDevBuildDir,
+ atFile);
+
+ // 3. Apply patches to the source jar from 2.
+ var patchesFolder = project.getRootProject().file("patches");
+ var applyPatches = tasks.register("applyPatches", ApplyPatches.class, task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.getOriginalJar().set(applyAt.flatMap(ApplyAccessTransformer::getOutputJar));
+ task.getPatchesFolder().set(patchesFolder);
+ task.getPatchedJar().set(neoDevBuildDir.map(dir -> dir.file("artifacts/patched-sources.jar")));
+ task.getRejectsFolder().set(project.getRootProject().file("rejects"));
+ });
+
+ // 4. Unpack jar from 3.
+ var mcSourcesPath = project.file("src/main/java");
+ tasks.register("setup", Sync.class, task -> {
+ task.setGroup(GROUP);
+ task.from(project.zipTree(applyPatches.flatMap(ApplyPatches::getPatchedJar)));
+ task.into(mcSourcesPath);
+ });
+
+ /*
+ * RUNS SETUP
+ */
+
+ // 1. Write configs that contain the runs in a format understood by MDG/NG/etc. Currently one for neodev and one for userdev.
+ var writeNeoDevConfig = tasks.register("writeNeoDevConfig", CreateUserDevConfig.class, task -> {
+ task.getForNeoDev().set(true);
+ task.getUserDevConfig().set(neoDevBuildDir.map(dir -> dir.file("neodev-config.json")));
+ });
+ var writeUserDevConfig = tasks.register("writeUserDevConfig", CreateUserDevConfig.class, task -> {
+ task.getForNeoDev().set(false);
+ task.getUserDevConfig().set(neoDevBuildDir.map(dir -> dir.file("userdev-config.json")));
+ });
+ for (var taskProvider : List.of(writeNeoDevConfig, writeUserDevConfig)) {
+ taskProvider.configure(task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.getFmlVersion().set(fmlVersion);
+ task.getMinecraftVersion().set(minecraftVersion);
+ task.getNeoForgeVersion().set(neoForgeVersion);
+ task.getRawNeoFormVersion().set(rawNeoFormVersion);
+ task.getLibraries().addAll(DependencyUtils.configurationToGavList(configurations.userdevClasspath));
+ task.getModules().addAll(DependencyUtils.configurationToGavList(configurations.modulePath));
+ task.getTestLibraries().addAll(DependencyUtils.configurationToGavList(configurations.userdevTestClasspath));
+ task.getTestLibraries().add(neoForgeVersion.map(v -> "net.neoforged:testframework:" + v));
+ task.getIgnoreList().addAll(configurations.userdevCompileOnlyClasspath.getIncoming().getArtifacts().getResolvedArtifacts().map(results -> {
+ return results.stream().map(r -> r.getFile().getName()).toList();
+ }));
+ task.getIgnoreList().addAll("client-extra", "neoforge-");
+ task.getBinpatcherGav().set(Tools.BINPATCHER.asGav(project));
+ });
+ }
+
+ // 2. Task to download assets.
+ var downloadAssets = tasks.register("downloadAssets", DownloadAssets.class, task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.getNeoFormArtifact().set(mcAndNeoFormVersion.map(v -> "net.neoforged:neoform:" + v + "@zip"));
+ task.getAssetPropertiesFile().set(neoDevBuildDir.map(dir -> dir.file("minecraft_assets.properties")));
+ });
+
+ // FML needs Minecraft resources on the classpath to find it. Add to runtimeOnly so subprojects also get it at runtime.
+ var runtimeClasspath = project.getConfigurations().getByName(JavaPlugin.RUNTIME_ONLY_CONFIGURATION_NAME);
+ runtimeClasspath.getDependencies().add(
+ dependencyFactory.create(
+ project.files(createSourceArtifacts.flatMap(CreateMinecraftArtifacts::getResourcesArtifact))
+ )
+ );
+ // 3. Let MDG do the rest of the setup. :)
+ NeoDevFacade.setupRuns(
+ project,
+ neoDevBuildDir,
+ extension.getRuns(),
+ writeNeoDevConfig,
+ modulePath -> {
+ modulePath.extendsFrom(configurations.moduleLibraries);
+ },
+ legacyClassPath -> {
+ legacyClassPath.getDependencies().addLater(mcAndNeoFormVersion.map(v -> dependencyFactory.create("net.neoforged:neoform:" + v).capabilities(caps -> {
+ caps.requireCapability("net.neoforged:neoform-dependencies");
+ })));
+ legacyClassPath.extendsFrom(configurations.libraries, configurations.moduleLibraries, configurations.userdevCompileOnly);
+ },
+ downloadAssets.flatMap(DownloadAssets::getAssetPropertiesFile)
+ );
+ // TODO: Gradle run tasks should be moved to gradle group GROUP
+
+ /*
+ * OTHER TASKS
+ */
+
+ // Generate source patches into a patch archive.
+ var genSourcePatches = tasks.register("generateSourcePatches", GenerateSourcePatches.class, task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.getOriginalJar().set(applyAt.flatMap(ApplyAccessTransformer::getOutputJar));
+ task.getModifiedSources().set(project.file("src/main/java"));
+ task.getPatchesJar().set(neoDevBuildDir.map(dir -> dir.file("source-patches.zip")));
+ });
+
+ // Update the patch/ folder with the current patches.
+ tasks.register("genPatches", Sync.class, task -> {
+ task.setGroup(GROUP);
+ task.from(project.zipTree(genSourcePatches.flatMap(GenerateSourcePatches::getPatchesJar)));
+ task.into(project.getRootProject().file("patches"));
+ });
+
+ // Universal jar = the jar that contains NeoForge classes
+ // TODO: signing?
+ var universalJar = tasks.register("universalJar", Jar.class, task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.getArchiveClassifier().set("universal");
+
+ task.from(project.zipTree(
+ tasks.named("jar", Jar.class).flatMap(AbstractArchiveTask::getArchiveFile)));
+ task.exclude("net/minecraft/**");
+ task.exclude("com/**");
+ task.exclude("mcp/**");
+
+ task.manifest(manifest -> {
+ manifest.attributes(Map.of("FML-System-Mods", "neoforge"));
+ // These attributes are used from NeoForgeVersion.java to find the NF version without command line arguments.
+ manifest.attributes(
+ Map.of(
+ "Specification-Title", "NeoForge",
+ "Specification-Vendor", "NeoForge",
+ "Specification-Version", project.getVersion().toString().substring(0, project.getVersion().toString().lastIndexOf(".")),
+ "Implementation-Title", project.getGroup(),
+ "Implementation-Version", project.getVersion(),
+ "Implementation-Vendor", "NeoForged"),
+ "net/neoforged/neoforge/internal/versions/neoforge/");
+ manifest.attributes(
+ Map.of(
+ "Specification-Title", "Minecraft",
+ "Specification-Vendor", "Mojang",
+ "Specification-Version", minecraftVersion,
+ "Implementation-Title", "MCP",
+ "Implementation-Version", mcAndNeoFormVersion,
+ "Implementation-Vendor", "NeoForged"),
+ "net/neoforged/neoforge/versions/neoform/");
+ });
+ });
+
+ var jarJarTask = JarJar.registerWithConfiguration(project, "jarJar");
+ jarJarTask.configure(task -> task.setGroup(INTERNAL_GROUP));
+ universalJar.configure(task -> task.from(jarJarTask));
+
+ var createCleanArtifacts = tasks.register("createCleanArtifacts", CreateCleanArtifacts.class, task -> {
+ task.setGroup(INTERNAL_GROUP);
+ var cleanArtifactsDir = neoDevBuildDir.map(dir -> dir.dir("artifacts/clean"));
+ task.getCleanClientJar().set(cleanArtifactsDir.map(dir -> dir.file("client.jar")));
+ task.getRawServerJar().set(cleanArtifactsDir.map(dir -> dir.file("raw-server.jar")));
+ task.getCleanServerJar().set(cleanArtifactsDir.map(dir -> dir.file("server.jar")));
+ task.getCleanJoinedJar().set(cleanArtifactsDir.map(dir -> dir.file("joined.jar")));
+ task.getMergedMappings().set(cleanArtifactsDir.map(dir -> dir.file("merged-mappings.txt")));
+ task.getNeoFormArtifact().set(mcAndNeoFormVersion.map(version -> "net.neoforged:neoform:" + version + "@zip"));
+ });
+
+ var binaryPatchOutputs = configureBinaryPatchCreation(
+ project,
+ configurations,
+ createCleanArtifacts,
+ neoDevBuildDir,
+ patchesFolder
+ );
+
+ // Launcher profile = the version.json file used by the Minecraft launcher.
+ var createLauncherProfile = tasks.register("createLauncherProfile", CreateLauncherProfile.class, task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.getFmlVersion().set(fmlVersion);
+ task.getMinecraftVersion().set(minecraftVersion);
+ task.getNeoForgeVersion().set(neoForgeVersion);
+ task.getRawNeoFormVersion().set(rawNeoFormVersion);
+ task.setLibraries(configurations.launcherProfileClasspath);
+ task.getRepositoryURLs().set(project.provider(() -> {
+ List repos = new ArrayList<>();
+ for (var repo : project.getRepositories().withType(MavenArtifactRepository.class)) {
+ var uri = repo.getUrl();
+ if (!uri.toString().endsWith("/")) {
+ uri = URI.create(uri + "/");
+ }
+ repos.add(uri);
+ }
+ return repos;
+ }));
+ task.getIgnoreList().addAll("client-extra", "neoforge-");
+ task.setModules(configurations.modulePath);
+ task.getLauncherProfile().set(neoDevBuildDir.map(dir -> dir.file("launcher-profile.json")));
+ });
+
+ // Installer profile = the .json file used by the NeoForge installer.
+ var createInstallerProfile = tasks.register("createInstallerProfile", CreateInstallerProfile.class, task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.getMinecraftVersion().set(minecraftVersion);
+ task.getNeoForgeVersion().set(neoForgeVersion);
+ task.getMcAndNeoFormVersion().set(mcAndNeoFormVersion);
+ task.getIcon().set(project.getRootProject().file("docs/assets/neoforged.ico"));
+ // Anything that is on the launcher classpath should be downloaded by the installer.
+ // (At least on the server side).
+ task.addLibraries(configurations.launcherProfileClasspath);
+ // We need the NeoForm zip for the SRG mappings.
+ task.addLibraries(configurations.neoFormDataOnly);
+ task.getRepositoryURLs().set(project.provider(() -> {
+ List repos = new ArrayList<>();
+ for (var repo : project.getRepositories().withType(MavenArtifactRepository.class)) {
+ var uri = repo.getUrl();
+ if (!uri.toString().endsWith("/")) {
+ uri = URI.create(uri + "/");
+ }
+ repos.add(uri);
+ }
+ return repos;
+ }));
+ task.getUniversalJar().set(universalJar.flatMap(AbstractArchiveTask::getArchiveFile));
+ task.getInstallerProfile().set(neoDevBuildDir.map(dir -> dir.file("installer-profile.json")));
+
+ // Make all installer processor tools available to the profile
+ for (var installerProcessor : InstallerProcessor.values()) {
+ var configuration = configurations.getExecutableTool(installerProcessor.tool);
+ // Different processors might use different versions of the same library,
+ // but that is fine because each processor gets its own classpath.
+ task.addLibraries(configuration);
+ task.getProcessorClasspaths().put(installerProcessor, DependencyUtils.configurationToGavList(configuration));
+ task.getProcessorGavs().put(installerProcessor, installerProcessor.tool.asGav(project));
+ }
+ });
+
+ var createWindowsServerArgsFile = tasks.register("createWindowsServerArgsFile", CreateArgsFile.class, task -> {
+ task.setLibraries(";", configurations.launcherProfileClasspath, configurations.modulePath);
+ task.getArgsFile().set(neoDevBuildDir.map(dir -> dir.file("windows-server-args.txt")));
+ });
+ var createUnixServerArgsFile = tasks.register("createUnixServerArgsFile", CreateArgsFile.class, task -> {
+ task.setLibraries(":", configurations.launcherProfileClasspath, configurations.modulePath);
+ task.getArgsFile().set(neoDevBuildDir.map(dir -> dir.file("unix-server-args.txt")));
+ });
+
+ for (var taskProvider : List.of(createWindowsServerArgsFile, createUnixServerArgsFile)) {
+ taskProvider.configure(task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.getTemplate().set(project.getRootProject().file("server_files/args.txt"));
+ task.getFmlVersion().set(fmlVersion);
+ task.getMinecraftVersion().set(minecraftVersion);
+ task.getNeoForgeVersion().set(neoForgeVersion);
+ task.getRawNeoFormVersion().set(rawNeoFormVersion);
+ // In theory, new BootstrapLauncher shouldn't need the module path in the ignore list anymore.
+ // However, in server installs libraries are passed as relative paths here.
+ // Module path detection doesn't currently work with relative paths (BootstrapLauncher #20).
+ task.getIgnoreList().set(configurations.modulePath.getIncoming().getArtifacts().getResolvedArtifacts().map(results -> {
+ return results.stream().map(r -> r.getFile().getName()).toList();
+ }));
+ task.getRawServerJar().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getRawServerJar));
+ });
+ }
+
+ var installerConfig = configurations.getExecutableTool(Tools.LEGACYINSTALLER);
+ // TODO: signing?
+ // We want to inherit the executable JAR manifest from LegacyInstaller.
+ // - Jar tasks have special manifest handling, so use Zip.
+ // - The manifest must be the first entry in the jar so LegacyInstaller has to be the first input.
+ var installerJar = tasks.register("installerJar", Zip.class, task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.getArchiveClassifier().set("installer");
+ task.getArchiveExtension().set("jar");
+ task.setMetadataCharset("UTF-8");
+ task.getDestinationDirectory().convention(project.getExtensions().getByType(BasePluginExtension.class).getLibsDirectory());
+
+ task.from(project.zipTree(project.provider(installerConfig::getSingleFile)), spec -> {
+ spec.exclude("big_logo.png");
+ });
+ task.from(createLauncherProfile.flatMap(CreateLauncherProfile::getLauncherProfile), spec -> {
+ spec.rename(s -> "version.json");
+ });
+ task.from(createInstallerProfile.flatMap(CreateInstallerProfile::getInstallerProfile), spec -> {
+ spec.rename(s -> "install_profile.json");
+ });
+ task.from(project.getRootProject().file("src/main/resources/url.png"));
+ task.from(project.getRootProject().file("src/main/resources/neoforged_logo.png"), spec -> {
+ spec.rename(s -> "big_logo.png");
+ });
+ task.from(createUnixServerArgsFile.flatMap(CreateArgsFile::getArgsFile), spec -> {
+ spec.into("data");
+ spec.rename(s -> "unix_args.txt");
+ });
+ task.from(createWindowsServerArgsFile.flatMap(CreateArgsFile::getArgsFile), spec -> {
+ spec.into("data");
+ spec.rename(s -> "win_args.txt");
+ });
+ task.from(binaryPatchOutputs.binaryPatchesForClient(), spec -> {
+ spec.into("data");
+ spec.rename(s -> "client.lzma");
+ });
+ task.from(binaryPatchOutputs.binaryPatchesForServer(), spec -> {
+ spec.into("data");
+ spec.rename(s -> "server.lzma");
+ });
+ var mavenPath = neoForgeVersion.map(v -> "net/neoforged/neoforge/" + v);
+ task.getInputs().property("mavenPath", mavenPath);
+ task.from(project.getRootProject().files("server_files"), spec -> {
+ spec.into("data");
+ spec.exclude("args.txt");
+ spec.filter(s -> {
+ return s.replaceAll("@MAVEN_PATH@", mavenPath.get());
+ });
+ });
+
+ // This is true by default (see gradle.properties), and needs to be disabled explicitly when building (see release.yml).
+ String installerDebugProperty = "neogradle.runtime.platform.installer.debug";
+ if (project.getProperties().containsKey(installerDebugProperty) && Boolean.parseBoolean(project.getProperties().get(installerDebugProperty).toString())) {
+ task.from(universalJar.flatMap(AbstractArchiveTask::getArchiveFile), spec -> {
+ spec.into(String.format("/maven/net/neoforged/neoforge/%s/", neoForgeVersion.get()));
+ spec.rename(name -> String.format("neoforge-%s-universal.jar", neoForgeVersion.get()));
+ });
+ }
+ });
+
+ var userdevJar = tasks.register("userdevJar", Jar.class, task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.getArchiveClassifier().set("userdev");
+
+ task.from(writeUserDevConfig.flatMap(CreateUserDevConfig::getUserDevConfig), spec -> {
+ spec.rename(s -> "config.json");
+ });
+ task.from(atFile, spec -> {
+ spec.into("ats/");
+ });
+ task.from(binaryPatchOutputs.binaryPatchesForMerged(), spec -> {
+ spec.rename(s -> "joined.lzma");
+ });
+ task.from(project.zipTree(genSourcePatches.flatMap(GenerateSourcePatches::getPatchesJar)), spec -> {
+ spec.into("patches/");
+ });
+ });
+
+ project.getExtensions().getByType(JavaPluginExtension.class).withSourcesJar();
+ var sourcesJarProvider = project.getTasks().named("sourcesJar", Jar.class);
+ sourcesJarProvider.configure(task -> {
+ task.exclude("net/minecraft/**");
+ task.exclude("com/**");
+ task.exclude("mcp/**");
+ });
+
+ tasks.named("assemble", task -> {
+ task.dependsOn(installerJar);
+ task.dependsOn(universalJar);
+ task.dependsOn(userdevJar);
+ task.dependsOn(sourcesJarProvider);
+ });
+ }
+
+ private static TaskProvider configureAccessTransformer(
+ Project project,
+ NeoDevConfigurations configurations,
+ TaskProvider createSourceArtifacts,
+ Provider neoDevBuildDir,
+ File atFile) {
+
+ // Pass -PvalidateAccessTransformers to validate ATs.
+ var validateAts = project.getProviders().gradleProperty("validateAccessTransformers").map(p -> true).orElse(false);
+ return project.getTasks().register("applyAccessTransformer", ApplyAccessTransformer.class, task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.classpath(configurations.getExecutableTool(Tools.JST));
+ task.getInputJar().set(createSourceArtifacts.flatMap(CreateMinecraftArtifacts::getSourcesArtifact));
+ task.getAccessTransformer().set(atFile);
+ task.getValidate().set(validateAts);
+ task.getOutputJar().set(neoDevBuildDir.map(dir -> dir.file("artifacts/access-transformed-sources.jar")));
+ task.getLibraries().from(configurations.neoFormClasspath);
+ task.getLibrariesFile().set(neoDevBuildDir.map(dir -> dir.file("minecraft-libraries-for-jst.txt")));
+ });
+ }
+
+ private static BinaryPatchOutputs configureBinaryPatchCreation(Project project,
+ NeoDevConfigurations configurations,
+ TaskProvider createCleanArtifacts,
+ Provider neoDevBuildDir,
+ File sourcesPatchesFolder) {
+ var tasks = project.getTasks();
+
+ var artConfig = configurations.getExecutableTool(Tools.AUTO_RENAMING_TOOL);
+ var remapClientJar = tasks.register("remapClientJar", RemapJar.class, task -> {
+ task.setDescription("Creates a Minecraft client jar with the official mappings applied. Used as the base for generating binary patches for the client.");
+ task.getInputJar().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getCleanClientJar));
+ task.getOutputJar().set(neoDevBuildDir.map(dir -> dir.file("remapped-client.jar")));
+ });
+ var remapServerJar = tasks.register("remapServerJar", RemapJar.class, task -> {
+ task.setDescription("Creates a Minecraft dedicated server jar with the official mappings applied. Used as the base for generating binary patches for the client.");
+ task.getInputJar().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getCleanServerJar));
+ task.getOutputJar().set(neoDevBuildDir.map(dir -> dir.file("remapped-server.jar")));
+ });
+ for (var remapTask : List.of(remapClientJar, remapServerJar)) {
+ remapTask.configure(task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.classpath(artConfig);
+ task.getMappings().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getMergedMappings));
+ });
+ }
+
+ var binpatcherConfig = configurations.getExecutableTool(Tools.BINPATCHER);
+ var generateMergedBinPatches = tasks.register("generateMergedBinPatches", GenerateBinaryPatches.class, task -> {
+ task.setDescription("Creates binary patch files by diffing a merged client/server jar-file and the compiled Minecraft classes in this project.");
+ task.getCleanJar().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getCleanJoinedJar));
+ task.getOutputFile().set(neoDevBuildDir.map(dir -> dir.file("merged-binpatches.lzma")));
+ });
+ var generateClientBinPatches = tasks.register("generateClientBinPatches", GenerateBinaryPatches.class, task -> {
+ task.setDescription("Creates binary patch files by diffing a merged client jar-file and the compiled Minecraft classes in this project.");
+ task.getCleanJar().set(remapClientJar.flatMap(RemapJar::getOutputJar));
+ task.getOutputFile().set(neoDevBuildDir.map(dir -> dir.file("client-binpatches.lzma")));
+ });
+ var generateServerBinPatches = tasks.register("generateServerBinPatches", GenerateBinaryPatches.class, task -> {
+ task.setDescription("Creates binary patch files by diffing a merged server jar-file and the compiled Minecraft classes in this project.");
+ task.getCleanJar().set(remapServerJar.flatMap(RemapJar::getOutputJar));
+ task.getOutputFile().set(neoDevBuildDir.map(dir -> dir.file("server-binpatches.lzma")));
+ });
+ for (var generateBinPatchesTask : List.of(generateMergedBinPatches, generateClientBinPatches, generateServerBinPatches)) {
+ generateBinPatchesTask.configure(task -> {
+ task.setGroup(INTERNAL_GROUP);
+ task.classpath(binpatcherConfig);
+ task.getPatchedJar().set(tasks.named("jar", Jar.class).flatMap(Jar::getArchiveFile));
+ task.getSourcePatchesFolder().set(sourcesPatchesFolder);
+ task.getMappings().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getMergedMappings));
+ });
+ }
+
+ return new BinaryPatchOutputs(
+ generateMergedBinPatches.flatMap(GenerateBinaryPatches::getOutputFile),
+ generateClientBinPatches.flatMap(GenerateBinaryPatches::getOutputFile),
+ generateServerBinPatches.flatMap(GenerateBinaryPatches::getOutputFile)
+ );
+ }
+
+ private record BinaryPatchOutputs(
+ Provider binaryPatchesForMerged,
+ Provider binaryPatchesForClient,
+ Provider binaryPatchesForServer
+ ) {
+ }
+
+ /**
+ * Sets up NFRT, and creates the sources and resources artifacts.
+ */
+ static TaskProvider configureMinecraftDecompilation(Project project) {
+ project.getPlugins().apply(NeoFormRuntimePlugin.class);
+
+ var configurations = project.getConfigurations();
+ var dependencyFactory = project.getDependencyFactory();
+ var tasks = project.getTasks();
+ var neoDevBuildDir = project.getLayout().getBuildDirectory().dir("neodev");
+
+ var rawNeoFormVersion = project.getProviders().gradleProperty("neoform_version");
+ var minecraftVersion = project.getProviders().gradleProperty("minecraft_version");
+ var mcAndNeoFormVersion = minecraftVersion.zip(rawNeoFormVersion, (mc, nf) -> mc + "-" + nf);
+
+ // Configuration for all artifacts that should be passed to NFRT to prevent repeated downloads
+ var neoFormRuntimeArtifactManifestNeoForm = configurations.create("neoFormRuntimeArtifactManifestNeoForm", spec -> {
+ spec.setCanBeConsumed(false);
+ spec.setCanBeResolved(true);
+ spec.getDependencies().addLater(mcAndNeoFormVersion.map(version -> {
+ return dependencyFactory.create("net.neoforged:neoform:" + version);
+ }));
+ });
+
+ tasks.withType(NeoFormRuntimeTask.class, task -> {
+ task.addArtifactsToManifest(neoFormRuntimeArtifactManifestNeoForm);
+ });
+
+ return tasks.register("createSourceArtifacts", CreateMinecraftArtifacts.class, task -> {
+ var minecraftArtifactsDir = neoDevBuildDir.map(dir -> dir.dir("artifacts"));
+ task.getSourcesArtifact().set(minecraftArtifactsDir.map(dir -> dir.file("base-sources.jar")));
+ task.getResourcesArtifact().set(minecraftArtifactsDir.map(dir -> dir.file("minecraft-resources.jar")));
+ task.getNeoFormArtifact().set(mcAndNeoFormVersion.map(version -> "net.neoforged:neoform:" + version + "@zip"));
+ });
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/NeoDevTestExtension.java b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevTestExtension.java
new file mode 100644
index 0000000000..57d3f7be33
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/NeoDevTestExtension.java
@@ -0,0 +1,30 @@
+package net.neoforged.neodev;
+
+import net.neoforged.moddevgradle.dsl.ModModel;
+import org.gradle.api.provider.Property;
+import org.gradle.api.provider.SetProperty;
+
+import javax.inject.Inject;
+
+public abstract class NeoDevTestExtension {
+ public static final String NAME = "neoDevTest";
+
+ @Inject
+ public NeoDevTestExtension() {
+ }
+
+ /**
+ * The mod that will be loaded in JUnit tests.
+ * The compiled classes from {@code src/test/java} and the resources from {@code src/test/resources}
+ * will be added to that mod at runtime.
+ */
+ public abstract Property getTestedMod();
+
+ /**
+ * The mods to load when running unit tests. Defaults to all mods registered in the project.
+ * This must contain {@link #getTestedMod()}.
+ *
+ * @see ModModel
+ */
+ public abstract SetProperty getLoadedMods();
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/RemapJar.java b/buildSrc/src/main/java/net/neoforged/neodev/RemapJar.java
new file mode 100644
index 0000000000..e93ccb8b94
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/RemapJar.java
@@ -0,0 +1,53 @@
+package net.neoforged.neodev;
+
+import org.gradle.api.GradleException;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.JavaExec;
+import org.gradle.api.tasks.OutputFile;
+
+import javax.inject.Inject;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Produces a remapped jar-file that has almost no other changes applied with the intent of being
+ * the base against which we {@link GenerateBinaryPatches generate binary patches}.
+ *
+ * The installer produces the same Jar file as this task does and then applies the patches against that.
+ *
+ * Any changes to the options used here have to be reflected in the {@link net.neoforged.neodev.installer.CreateInstallerProfile installer profile}
+ * and vice versa, to ensure the patches are generated against the same binary files as they are applied to later.
+ */
+abstract class RemapJar extends JavaExec {
+ @Inject
+ public RemapJar() {}
+
+ @InputFile
+ abstract RegularFileProperty getInputJar();
+
+ @InputFile
+ abstract RegularFileProperty getMappings();
+
+ @OutputFile
+ abstract RegularFileProperty getOutputJar();
+
+ @Override
+ public void exec() {
+ args("--input", getInputJar().get().getAsFile().getAbsolutePath());
+ args("--output", getOutputJar().get().getAsFile().getAbsolutePath());
+ args("--names", getMappings().get().getAsFile().getAbsolutePath());
+ args("--ann-fix", "--ids-fix", "--src-fix", "--record-fix");
+
+ var logFile = new File(getTemporaryDir(), "console.log");
+ try (var out = new BufferedOutputStream(new FileOutputStream(logFile))) {
+ getLogger().info("Logging ART console output to {}", logFile.getAbsolutePath());
+ setStandardOutput(out);
+ super.exec();
+ } catch (IOException e) {
+ throw new GradleException("Failed to remap jar.", e);
+ }
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/Tools.java b/buildSrc/src/main/java/net/neoforged/neodev/Tools.java
new file mode 100644
index 0000000000..a9f9106697
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/Tools.java
@@ -0,0 +1,53 @@
+package net.neoforged.neodev;
+
+import org.gradle.api.Project;
+
+// If a GAV is changed, make sure to change the corresponding renovate comment in gradle.properties.
+public enum Tools {
+ // Fatjar jst-cli-bundle instead of jst-cli because publication of the latter is currently broken.
+ JST("net.neoforged.jst:jst-cli-bundle:%s", "jst_version", "toolJstClasspath", true),
+ // Fatjar because the contents are copy/pasted into the installer jar which must be standalone.
+ LEGACYINSTALLER("net.neoforged:legacyinstaller:%s:shrunk", "legacyinstaller_version", "toolLegacyinstallerClasspath", true),
+ // Fatjar because the slim jar currently does not have the main class set in its manifest.
+ AUTO_RENAMING_TOOL("net.neoforged:AutoRenamingTool:%s:all", "art_version", "toolAutoRenamingToolClasspath", true),
+ INSTALLERTOOLS("net.neoforged.installertools:installertools:%s", "installertools_version", "toolInstallertoolsClasspath", false),
+ JARSPLITTER("net.neoforged.installertools:jarsplitter:%s", "installertools_version", "toolJarsplitterClasspath", false),
+ // Fatjar because it was like that in the userdev json in the past.
+ // To reconsider, we need to get in touch with 3rd party plugin developers or wait for a BC window.
+ BINPATCHER("net.neoforged.installertools:binarypatcher:%s:fatjar", "installertools_version", "toolBinpatcherClasspath", true);
+
+ private final String gavPattern;
+ private final String versionProperty;
+ private final String gradleConfigurationName;
+ private final boolean requestFatJar;
+
+ Tools(String gavPattern, String versionProperty, String gradleConfigurationName, boolean requestFatJar) {
+ this.gavPattern = gavPattern;
+ this.versionProperty = versionProperty;
+ this.gradleConfigurationName = gradleConfigurationName;
+ this.requestFatJar = requestFatJar;
+ }
+
+ /**
+ * The name of the Gradle {@link org.gradle.api.artifacts.Configuration} used to resolve this particular tool.
+ */
+ public String getGradleConfigurationName() {
+ return gradleConfigurationName;
+ }
+
+ /**
+ * Some tools may be incorrectly packaged and declare transitive dependencies even for their "fatjar" variants.
+ * Gradle will not run these, so we ignore them.
+ */
+ public boolean isRequestFatJar() {
+ return requestFatJar;
+ }
+
+ public String asGav(Project project) {
+ var version = project.property(versionProperty);
+ if (version == null) {
+ throw new IllegalStateException("Could not find property " + versionProperty);
+ }
+ return gavPattern.formatted(version);
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/installer/CreateArgsFile.java b/buildSrc/src/main/java/net/neoforged/neodev/installer/CreateArgsFile.java
new file mode 100644
index 0000000000..6b403d5b94
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/installer/CreateArgsFile.java
@@ -0,0 +1,126 @@
+package net.neoforged.neodev.installer;
+
+import net.neoforged.neodev.utils.DependencyUtils;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.file.ArchiveOperations;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.provider.ListProperty;
+import org.gradle.api.provider.Property;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+
+import javax.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Creates the JVM/program argument files used by the dedicated server launcher.
+ */
+public abstract class CreateArgsFile extends DefaultTask {
+ @Inject
+ public CreateArgsFile() {}
+
+ @InputFile
+ public abstract RegularFileProperty getTemplate();
+
+ @Input
+ public abstract Property getFmlVersion();
+
+ @Input
+ public abstract Property getMinecraftVersion();
+
+ @Input
+ public abstract Property getNeoForgeVersion();
+
+ @Input
+ public abstract Property getRawNeoFormVersion();
+
+ @Input
+ protected abstract Property getPathSeparator();
+
+ @Input
+ protected abstract Property getModules();
+
+ @Input
+ public abstract ListProperty getIgnoreList();
+
+ @Input
+ protected abstract Property getClasspath();
+
+ public void setLibraries(String separator, Configuration classpath, Configuration modulePath) {
+ getPathSeparator().set(separator);
+ getClasspath().set(DependencyUtils.configurationToClasspath(classpath, "libraries/", separator));
+ getModules().set(DependencyUtils.configurationToClasspath(modulePath, "libraries/", separator));
+ }
+
+ @InputFile
+ public abstract RegularFileProperty getRawServerJar();
+
+ @OutputFile
+ public abstract RegularFileProperty getArgsFile();
+
+ @Inject
+ protected abstract ArchiveOperations getArchiveOperations();
+
+ private String resolveClasspath() throws IOException {
+ var ourClasspath = getClasspath().get() + getPathSeparator().get()
+ + "libraries/net/minecraft/server/%s/server-%s-extra.jar".formatted(
+ getRawNeoFormVersion().get(), getRawNeoFormVersion().get());
+
+ // The raw server jar also contains its own classpath.
+ // We want to make sure that our versions of the libraries are used when there is a conflict.
+ var ourClasspathEntries = Stream.of(ourClasspath.split(getPathSeparator().get()))
+ .map(CreateArgsFile::stripVersionSuffix)
+ .collect(Collectors.toSet());
+
+ var serverClasspath = getArchiveOperations().zipTree(getRawServerJar())
+ .filter(spec -> spec.getPath().endsWith("META-INF" + File.separator + "classpath-joined"))
+ .getSingleFile();
+
+ var filteredServerClasspath = Stream.of(Files.readString(serverClasspath.toPath()).split(";"))
+ .filter(path -> !ourClasspathEntries.contains(stripVersionSuffix(path)))
+ // Exclude the actual MC server jar, which is under versions/
+ .filter(path -> path.startsWith("libraries/"))
+ .collect(Collectors.joining(getPathSeparator().get()));
+
+ return ourClasspath + getPathSeparator().get() + filteredServerClasspath;
+ }
+
+ // Example:
+ // Convert "libraries/com/github/oshi/oshi-core/6.4.10/oshi-core-6.4.10.jar"
+ // to "libraries/com/github/oshi/oshi-core".
+ private static String stripVersionSuffix(String classpathEntry) {
+ var parts = classpathEntry.split("/");
+ return String.join("/", List.of(parts).subList(0, parts.length - 2));
+ }
+
+ @TaskAction
+ public void createArgsFile() throws IOException {
+ var replacements = new HashMap();
+ replacements.put("@MODULE_PATH@", getModules().get());
+ replacements.put("@MODULES@", "ALL-MODULE-PATH");
+ replacements.put("@IGNORE_LIST@", String.join(",", getIgnoreList().get()));
+ replacements.put("@PLUGIN_LAYER_LIBRARIES@", "");
+ replacements.put("@GAME_LAYER_LIBRARIES@", "");
+ replacements.put("@CLASS_PATH@", resolveClasspath());
+ replacements.put("@TASK@", "forgeserver");
+ replacements.put("@FORGE_VERSION@", getNeoForgeVersion().get());
+ replacements.put("@FML_VERSION@", getFmlVersion().get());
+ replacements.put("@MC_VERSION@", getMinecraftVersion().get());
+ replacements.put("@MCP_VERSION@", getRawNeoFormVersion().get());
+
+ var contents = Files.readString(getTemplate().get().getAsFile().toPath());
+ for (var entry : replacements.entrySet()) {
+ contents = contents.replaceAll(entry.getKey(), entry.getValue());
+ }
+ Files.writeString(getArgsFile().get().getAsFile().toPath(), contents);
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/installer/CreateInstallerProfile.java b/buildSrc/src/main/java/net/neoforged/neodev/installer/CreateInstallerProfile.java
new file mode 100644
index 0000000000..3b745f744a
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/installer/CreateInstallerProfile.java
@@ -0,0 +1,230 @@
+package net.neoforged.neodev.installer;
+
+import com.google.gson.GsonBuilder;
+import net.neoforged.neodev.utils.FileUtils;
+import net.neoforged.neodev.utils.MavenIdentifier;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.provider.ListProperty;
+import org.gradle.api.provider.MapProperty;
+import org.gradle.api.provider.Property;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.Nested;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+import org.jetbrains.annotations.Nullable;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+/**
+ * Creates the JSON profile used by legacyinstaller for installing the client into the vanilla launcher,
+ * or installing a dedicated server.
+ */
+public abstract class CreateInstallerProfile extends DefaultTask {
+ @Inject
+ public CreateInstallerProfile() {}
+
+ @Input
+ public abstract Property getMinecraftVersion();
+
+ @Input
+ public abstract Property getNeoForgeVersion();
+
+ @Input
+ public abstract Property getMcAndNeoFormVersion();
+
+ @InputFile
+ public abstract RegularFileProperty getIcon();
+
+ @Nested
+ protected abstract ListProperty getLibraryFiles();
+
+ public void addLibraries(Configuration libraries) {
+ getLibraryFiles().addAll(IdentifiedFile.listFromConfiguration(getProject(), libraries));
+ }
+
+ @Input
+ public abstract ListProperty getRepositoryURLs();
+
+ @Input
+ public abstract MapProperty> getProcessorClasspaths();
+
+ @Input
+ public abstract MapProperty getProcessorGavs();
+
+ @InputFile
+ public abstract RegularFileProperty getUniversalJar();
+
+ @OutputFile
+ public abstract RegularFileProperty getInstallerProfile();
+
+ private void addProcessor(List processors, @Nullable List sides, InstallerProcessor processor, List args) {
+ var classpath = getProcessorClasspaths().get().get(processor);
+ var mainJar = getProcessorGavs().get().get(processor);
+ if (!classpath.contains(mainJar)) {
+ throw new IllegalStateException("Processor %s is not included in its own classpath %s".formatted(mainJar, classpath));
+ }
+ processors.add(new ProcessorEntry(sides, mainJar, classpath, args));
+ }
+
+ @TaskAction
+ public void createInstallerProfile() throws IOException {
+ var icon = "data:image/png;base64," + Base64.getEncoder().encodeToString(Files.readAllBytes(getIcon().getAsFile().get().toPath()));
+
+ var data = new LinkedHashMap();
+ var neoFormVersion = getMcAndNeoFormVersion().get();
+ data.put("MAPPINGS", new LauncherDataEntry(String.format("[net.neoforged:neoform:%s:mappings@txt]", neoFormVersion), String.format("[net.neoforged:neoform:%s:mappings@txt]", neoFormVersion)));
+ data.put("MOJMAPS", new LauncherDataEntry(String.format("[net.minecraft:client:%s:mappings@txt]", neoFormVersion), String.format("[net.minecraft:server:%s:mappings@txt]", neoFormVersion)));
+ data.put("MERGED_MAPPINGS", new LauncherDataEntry(String.format("[net.neoforged:neoform:%s:mappings-merged@txt]", neoFormVersion), String.format("[net.neoforged:neoform:%s:mappings-merged@txt]", neoFormVersion)));
+ data.put("BINPATCH", new LauncherDataEntry("/data/client.lzma", "/data/server.lzma"));
+ data.put("MC_UNPACKED", new LauncherDataEntry(String.format("[net.minecraft:client:%s:unpacked]", neoFormVersion), String.format("[net.minecraft:server:%s:unpacked]", neoFormVersion)));
+ data.put("MC_SLIM", new LauncherDataEntry(String.format("[net.minecraft:client:%s:slim]", neoFormVersion), String.format("[net.minecraft:server:%s:slim]", neoFormVersion)));
+ data.put("MC_EXTRA", new LauncherDataEntry(String.format("[net.minecraft:client:%s:extra]", neoFormVersion), String.format("[net.minecraft:server:%s:extra]", neoFormVersion)));
+ data.put("MC_SRG", new LauncherDataEntry(String.format("[net.minecraft:client:%s:srg]", neoFormVersion), String.format("[net.minecraft:server:%s:srg]", neoFormVersion)));
+ data.put("PATCHED", new LauncherDataEntry(String.format("[%s:%s:%s:client]", "net.neoforged", "neoforge", getNeoForgeVersion().get()), String.format("[%s:%s:%s:server]", "net.neoforged", "neoforge", getNeoForgeVersion().get())));
+ data.put("MCP_VERSION", new LauncherDataEntry(String.format("'%s'", neoFormVersion), String.format("'%s'", neoFormVersion)));
+
+ var processors = new ArrayList();
+ BiConsumer> commonProcessor = (processor, args) -> addProcessor(processors, null, processor, args);
+ BiConsumer> clientProcessor = (processor, args) -> addProcessor(processors, List.of("client"), processor, args);
+ BiConsumer> serverProcessor = (processor, args) -> addProcessor(processors, List.of("server"), processor, args);
+
+ serverProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
+ List.of("--task", "EXTRACT_FILES", "--archive", "{INSTALLER}",
+
+ "--from", "data/run.sh", "--to", "{ROOT}/run.sh", "--exec", "{ROOT}/run.sh",
+
+ "--from", "data/run.bat", "--to", "{ROOT}/run.bat",
+
+ "--from", "data/user_jvm_args.txt", "--to", "{ROOT}/user_jvm_args.txt", "--optional", "{ROOT}/user_jvm_args.txt",
+
+ "--from", "data/win_args.txt", "--to", "{ROOT}/libraries/net/neoforged/neoforge/%s/win_args.txt".formatted(getNeoForgeVersion().get()),
+
+ "--from", "data/unix_args.txt", "--to", "{ROOT}/libraries/net/neoforged/neoforge/%s/unix_args.txt".formatted(getNeoForgeVersion().get()))
+ );
+ serverProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
+ List.of("--task", "BUNDLER_EXTRACT", "--input", "{MINECRAFT_JAR}", "--output", "{ROOT}/libraries/", "--libraries")
+ );
+ serverProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
+ List.of("--task", "BUNDLER_EXTRACT", "--input", "{MINECRAFT_JAR}", "--output", "{MC_UNPACKED}", "--jar-only")
+ );
+ var neoformDependency = "net.neoforged:neoform:" + getMcAndNeoFormVersion().get() + "@zip";;
+ commonProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
+ List.of("--task", "MCP_DATA", "--input", String.format("[%s]", neoformDependency), "--output", "{MAPPINGS}", "--key", "mappings")
+ );
+ commonProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
+ List.of("--task", "DOWNLOAD_MOJMAPS", "--version", getMinecraftVersion().get(), "--side", "{SIDE}", "--output", "{MOJMAPS}")
+ );
+ commonProcessor.accept(InstallerProcessor.INSTALLERTOOLS,
+ List.of("--task", "MERGE_MAPPING", "--left", "{MAPPINGS}", "--right", "{MOJMAPS}", "--output", "{MERGED_MAPPINGS}", "--classes", "--fields", "--methods", "--reverse-right")
+ );
+ clientProcessor.accept(InstallerProcessor.JARSPLITTER,
+ List.of("--input", "{MINECRAFT_JAR}", "--slim", "{MC_SLIM}", "--extra", "{MC_EXTRA}", "--srg", "{MERGED_MAPPINGS}")
+ );
+ serverProcessor.accept(InstallerProcessor.JARSPLITTER,
+ List.of("--input", "{MC_UNPACKED}", "--slim", "{MC_SLIM}", "--extra", "{MC_EXTRA}", "--srg", "{MERGED_MAPPINGS}")
+ );
+ // Note that the options supplied here have to match the ones used in the RemapJar task used to generate the binary patches
+ commonProcessor.accept(InstallerProcessor.FART,
+ List.of("--input", "{MC_SLIM}", "--output", "{MC_SRG}", "--names", "{MERGED_MAPPINGS}", "--ann-fix", "--ids-fix", "--src-fix", "--record-fix")
+ );
+ commonProcessor.accept(InstallerProcessor.BINPATCHER,
+ List.of("--clean", "{MC_SRG}", "--output", "{PATCHED}", "--apply", "{BINPATCH}")
+ );
+
+ getLogger().info("Collecting libraries for Installer Profile");
+ // Remove potential duplicates.
+ var libraryFilesToResolve = new LinkedHashMap(getLibraryFiles().get().size());
+ for (var libraryFile : getLibraryFiles().get()) {
+ var existingFile = libraryFilesToResolve.putIfAbsent(libraryFile.getIdentifier().get(), libraryFile);
+ if (existingFile != null) {
+ var existing = existingFile.getFile().getAsFile().get();
+ var duplicate = libraryFile.getFile().getAsFile().get();
+ if (!existing.equals(duplicate)) {
+ throw new IllegalArgumentException("Cannot resolve installer profile! Library %s has different files: %s and %s.".formatted(
+ libraryFile.getIdentifier().get(),
+ existing,
+ duplicate));
+ }
+ }
+ }
+ var libraries = new ArrayList<>(
+ LibraryCollector.resolveLibraries(getRepositoryURLs().get(), libraryFilesToResolve.values()));
+
+ var universalJar = getUniversalJar().getAsFile().get().toPath();
+ libraries.add(new Library(
+ "net.neoforged:neoforge:%s:universal".formatted(getNeoForgeVersion().get()),
+ new LibraryDownload(new LibraryArtifact(
+ LibraryCollector.sha1Hash(universalJar),
+ Files.size(universalJar),
+ "https://maven.neoforged.net/releases/net/neoforged/neoforge/%s/neoforge-%s-universal.jar".formatted(
+ getNeoForgeVersion().get(),
+ getNeoForgeVersion().get()),
+ "net/neoforged/neoforge/%s/neoforge-%s-universal.jar".formatted(
+ getNeoForgeVersion().get(),
+ getNeoForgeVersion().get())
+ ))));
+
+ var profile = new InstallerProfile(
+ "1",
+ "NeoForge",
+ "neoforge-%s".formatted(getNeoForgeVersion().get()),
+ icon,
+ getMinecraftVersion().get(),
+ "/version.json",
+ "/big_logo.png",
+ "Welcome to the simple NeoForge installer",
+ "https://mirrors.neoforged.net",
+ true,
+ data,
+ processors,
+ libraries,
+ "{LIBRARY_DIR}/net/minecraft/server/{MINECRAFT_VERSION}/server-{MINECRAFT_VERSION}.jar"
+ );
+
+ FileUtils.writeStringSafe(
+ getInstallerProfile().getAsFile().get().toPath(),
+ new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(profile),
+ StandardCharsets.UTF_8
+ );
+ }
+}
+
+record InstallerProfile(
+ String spec,
+ String profile,
+ String version,
+ String icon,
+ String minecraft,
+ String json,
+ String logo,
+ String welcome,
+ String mirrorList,
+ boolean hideExtract,
+ Map data,
+ List processors,
+ List libraries,
+ String serverJarPath) {}
+
+record LauncherDataEntry(
+ String client,
+ String server) {}
+
+record ProcessorEntry(
+ @Nullable
+ List sides,
+ String jar,
+ List classpath,
+ List args) {}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/installer/CreateLauncherProfile.java b/buildSrc/src/main/java/net/neoforged/neodev/installer/CreateLauncherProfile.java
new file mode 100644
index 0000000000..5968190304
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/installer/CreateLauncherProfile.java
@@ -0,0 +1,130 @@
+package net.neoforged.neodev.installer;
+
+import com.google.gson.GsonBuilder;
+import net.neoforged.neodev.utils.DependencyUtils;
+import net.neoforged.neodev.utils.FileUtils;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.provider.ListProperty;
+import org.gradle.api.provider.Property;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.Nested;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Creates the JSON file for running NeoForge via the Vanilla launcher.
+ */
+public abstract class CreateLauncherProfile extends DefaultTask {
+ @Inject
+ public CreateLauncherProfile() {}
+
+ @Input
+ public abstract Property getFmlVersion();
+
+ @Input
+ public abstract Property getMinecraftVersion();
+
+ @Input
+ public abstract Property getNeoForgeVersion();
+
+ @Input
+ public abstract Property getRawNeoFormVersion();
+
+ @Nested
+ protected abstract ListProperty getLibraryFiles();
+
+ public void setLibraries(Configuration libraries) {
+ getLibraryFiles().set(IdentifiedFile.listFromConfiguration(getProject(), libraries));
+ }
+
+ @Input
+ public abstract ListProperty getRepositoryURLs();
+
+ @Input
+ public abstract ListProperty getIgnoreList();
+
+ @Input
+ protected abstract Property getModulePath();
+
+ public void setModules(Configuration modules) {
+ getModulePath().set(DependencyUtils.configurationToClasspath(modules, "${library_directory}/", "${classpath_separator}"));
+ }
+
+ @OutputFile
+ public abstract RegularFileProperty getLauncherProfile();
+
+ @TaskAction
+ public void createLauncherProfile() throws IOException {
+ var time = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
+
+ getLogger().info("Collecting libraries for Launcher Profile");
+ var libraries = LibraryCollector.resolveLibraries(getRepositoryURLs().get(), getLibraryFiles().get());
+
+ var gameArguments = new ArrayList<>(List.of(
+ "--fml.neoForgeVersion", getNeoForgeVersion().get(),
+ "--fml.fmlVersion", getFmlVersion().get(),
+ "--fml.mcVersion", getMinecraftVersion().get(),
+ "--fml.neoFormVersion", getRawNeoFormVersion().get(),
+ "--launchTarget", "forgeclient"));
+
+ var jvmArguments = new ArrayList<>(List.of(
+ "-Djava.net.preferIPv6Addresses=system",
+ "-DignoreList=" + String.join(",", getIgnoreList().get()),
+ "-DlibraryDirectory=${library_directory}"));
+
+ jvmArguments.add("-p");
+ jvmArguments.add(getModulePath().get());
+
+ jvmArguments.addAll(List.of(
+ "--add-modules", "ALL-MODULE-PATH",
+ "--add-opens", "java.base/java.util.jar=cpw.mods.securejarhandler",
+ "--add-opens", "java.base/java.lang.invoke=cpw.mods.securejarhandler",
+ "--add-exports", "java.base/sun.security.util=cpw.mods.securejarhandler",
+ "--add-exports", "jdk.naming.dns/com.sun.jndi.dns=java.naming"));
+
+ var arguments = new LinkedHashMap>();
+ arguments.put("game", gameArguments);
+ arguments.put("jvm", jvmArguments);
+
+ var profile = new LauncherProfile(
+ "neoforge-%s".formatted(getNeoForgeVersion().get()),
+ time,
+ time,
+ "release",
+ "cpw.mods.bootstraplauncher.BootstrapLauncher",
+ getMinecraftVersion().get(),
+ arguments,
+ libraries
+ );
+
+ FileUtils.writeStringSafe(
+ getLauncherProfile().getAsFile().get().toPath(),
+ new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(profile),
+ StandardCharsets.UTF_8
+ );
+ }
+}
+
+record LauncherProfile(
+ String id,
+ String time,
+ String releaseTime,
+ String type,
+ String mainClass,
+ String inheritsFrom,
+ Map> arguments,
+ List libraries) {}
+
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/installer/IdentifiedFile.java b/buildSrc/src/main/java/net/neoforged/neodev/installer/IdentifiedFile.java
new file mode 100644
index 0000000000..a7f685236a
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/installer/IdentifiedFile.java
@@ -0,0 +1,48 @@
+package net.neoforged.neodev.installer;
+
+import net.neoforged.neodev.utils.DependencyUtils;
+import net.neoforged.neodev.utils.MavenIdentifier;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.result.ResolvedArtifactResult;
+import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.provider.Property;
+import org.gradle.api.provider.Provider;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.PathSensitive;
+import org.gradle.api.tasks.PathSensitivity;
+
+import javax.inject.Inject;
+import java.io.File;
+import java.util.List;
+
+/**
+ * Combines a {@link File} and its {@link MavenIdentifier maven identifier},
+ * for usage as task inputs that will be passed to {@link LibraryCollector}.
+ */
+abstract class IdentifiedFile {
+ static Provider> listFromConfiguration(Project project, Configuration configuration) {
+ return configuration.getIncoming().getArtifacts().getResolvedArtifacts().map(
+ artifacts -> artifacts.stream()
+ .map(artifact -> IdentifiedFile.of(project, artifact))
+ .toList());
+ }
+
+ private static IdentifiedFile of(Project project, ResolvedArtifactResult resolvedArtifact) {
+ var identifiedFile = project.getObjects().newInstance(IdentifiedFile.class);
+ identifiedFile.getFile().set(resolvedArtifact.getFile());
+ identifiedFile.getIdentifier().set(DependencyUtils.guessMavenIdentifier(resolvedArtifact));
+ return identifiedFile;
+ }
+
+ @Inject
+ public IdentifiedFile() {}
+
+ @InputFile
+ @PathSensitive(PathSensitivity.NONE)
+ protected abstract RegularFileProperty getFile();
+
+ @Input
+ protected abstract Property getIdentifier();
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/installer/InstallerProcessor.java b/buildSrc/src/main/java/net/neoforged/neodev/installer/InstallerProcessor.java
new file mode 100644
index 0000000000..970fa6d1cb
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/installer/InstallerProcessor.java
@@ -0,0 +1,19 @@
+package net.neoforged.neodev.installer;
+
+import net.neoforged.neodev.Tools;
+
+/**
+ * Identifies the tools used by the {@link InstallerProfile} to install NeoForge.
+ */
+public enum InstallerProcessor {
+ BINPATCHER(Tools.BINPATCHER),
+ FART(Tools.AUTO_RENAMING_TOOL),
+ INSTALLERTOOLS(Tools.INSTALLERTOOLS),
+ JARSPLITTER(Tools.JARSPLITTER);
+
+ public final Tools tool;
+
+ InstallerProcessor(Tools tool) {
+ this.tool = tool;
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/installer/Library.java b/buildSrc/src/main/java/net/neoforged/neodev/installer/Library.java
new file mode 100644
index 0000000000..46669967f4
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/installer/Library.java
@@ -0,0 +1,3 @@
+package net.neoforged.neodev.installer;
+
+record Library(String name, LibraryDownload downloads) {}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/installer/LibraryArtifact.java b/buildSrc/src/main/java/net/neoforged/neodev/installer/LibraryArtifact.java
new file mode 100644
index 0000000000..1e345980e9
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/installer/LibraryArtifact.java
@@ -0,0 +1,7 @@
+package net.neoforged.neodev.installer;
+
+record LibraryArtifact(
+ String sha1,
+ long size,
+ String url,
+ String path) {}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/installer/LibraryCollector.java b/buildSrc/src/main/java/net/neoforged/neodev/installer/LibraryCollector.java
new file mode 100644
index 0000000000..7417ac14c3
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/installer/LibraryCollector.java
@@ -0,0 +1,174 @@
+package net.neoforged.neodev.installer;
+
+import net.neoforged.neodev.utils.MavenIdentifier;
+import org.gradle.api.logging.Logger;
+import org.gradle.api.logging.Logging;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.function.Function;
+
+/**
+ * For each file in a collection, finds the repository that the file came from.
+ */
+class LibraryCollector {
+ public static List resolveLibraries(List repositoryUrls, Collection libraries) throws IOException {
+ var collector = new LibraryCollector(repositoryUrls);
+ for (var library : libraries) {
+ collector.addLibrary(library.getFile().getAsFile().get(), library.getIdentifier().get());
+ }
+
+ var result = collector.libraries.stream().map(future -> {
+ try {
+ return future.get();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }).toList();
+ LOGGER.info("Collected %d libraries".formatted(result.size()));
+ return result;
+ }
+
+ private static final Logger LOGGER = Logging.getLogger(LibraryCollector.class);
+ /**
+ * Hosts from which we allow the installer to download.
+ * We whitelist here to avoid redirecting player download traffic to anyone not affiliated with Mojang or us.
+ */
+ private static final List HOST_WHITELIST = List.of(
+ "minecraft.net",
+ "neoforged.net",
+ "mojang.com"
+ );
+
+ private static final URI MOJANG_MAVEN = URI.create("https://libraries.minecraft.net");
+ private static final URI NEOFORGED_MAVEN = URI.create("https://maven.neoforged.net/releases");
+
+ private final List repositoryUrls;
+
+ private final List> libraries = new ArrayList<>();
+
+ private final HttpClient httpClient = HttpClient.newBuilder().build();
+
+ private LibraryCollector(List repoUrl) {
+ this.repositoryUrls = new ArrayList<>(repoUrl);
+
+ // Only remote repositories make sense (no maven local)
+ repositoryUrls.removeIf(it -> {
+ var lowercaseScheme = it.getScheme().toLowerCase(Locale.ROOT);
+ return !lowercaseScheme.equals("https") && !lowercaseScheme.equals("http");
+ });
+ // Allow only URLs from whitelisted hosts
+ repositoryUrls.removeIf(uri -> {
+ var lowercaseHost = uri.getHost().toLowerCase(Locale.ROOT);
+ return HOST_WHITELIST.stream().noneMatch(it -> lowercaseHost.equals(it) || lowercaseHost.endsWith("." + it));
+ });
+ // Always try Mojang Maven first, then our installer Maven
+ repositoryUrls.removeIf(it -> it.getHost().equals(MOJANG_MAVEN.getHost()));
+ repositoryUrls.removeIf(it -> it.getHost().equals(NEOFORGED_MAVEN.getHost()) && it.getPath().startsWith(NEOFORGED_MAVEN.getPath()));
+ repositoryUrls.add(0, NEOFORGED_MAVEN);
+ repositoryUrls.add(0, MOJANG_MAVEN);
+
+ LOGGER.info("Collecting libraries from:");
+ for (var repo : repositoryUrls) {
+ LOGGER.info(" - " + repo);
+ }
+ }
+
+ private void addLibrary(File file, MavenIdentifier identifier) throws IOException {
+ final String name = identifier.artifactNotation();
+ final String path = identifier.repositoryPath();
+
+ var sha1 = sha1Hash(file.toPath());
+ var fileSize = Files.size(file.toPath());
+
+ // Try each configured repository in-order to find the file
+ CompletableFuture libraryFuture = null;
+ for (var repositoryUrl : repositoryUrls) {
+ var artifactUri = joinUris(repositoryUrl, path);
+ var request = HttpRequest.newBuilder(artifactUri)
+ .method("HEAD", HttpRequest.BodyPublishers.noBody())
+ .build();
+
+ Function> makeRequest = (String previousError) -> {
+ return httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding())
+ .thenApply(response -> {
+ if (response.statusCode() != 200) {
+ LOGGER.info(" Got %d for %s".formatted(response.statusCode(), artifactUri));
+ String message = "Could not find %s: %d".formatted(artifactUri, response.statusCode());
+ // Prepend error message from previous repo if they all fail
+ if (previousError != null) {
+ message = previousError + "\n" + message;
+ }
+ throw new RuntimeException(message);
+ }
+ LOGGER.info(" Found %s -> %s".formatted(name, artifactUri));
+ return new Library(
+ name,
+ new LibraryDownload(new LibraryArtifact(
+ sha1,
+ fileSize,
+ artifactUri.toString(),
+ path)));
+ });
+ };
+
+ if (libraryFuture == null) {
+ libraryFuture = makeRequest.apply(null);
+ } else {
+ libraryFuture = libraryFuture.exceptionallyCompose(error -> {
+ return makeRequest.apply(error.getMessage());
+ });
+ }
+ }
+
+ libraries.add(libraryFuture);
+ }
+
+ static String sha1Hash(Path path) throws IOException {
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+
+ try (var in = Files.newInputStream(path);
+ var din = new DigestInputStream(in, digest)) {
+ byte[] buffer = new byte[8192];
+ while (din.read(buffer) != -1) {
+ }
+ }
+
+ return HexFormat.of().formatHex(digest.digest());
+ }
+
+ private static URI joinUris(URI repositoryUrl, String path) {
+ var baseUrl = repositoryUrl.toString();
+ if (baseUrl.endsWith("/") && path.startsWith("/")) {
+ while (path.startsWith("/")) {
+ path = path.substring(1);
+ }
+ return URI.create(baseUrl + path);
+ } else if (!baseUrl.endsWith("/") && !path.startsWith("/")) {
+ return URI.create(baseUrl + "/" + path);
+ } else {
+ return URI.create(baseUrl + path);
+ }
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/installer/LibraryDownload.java b/buildSrc/src/main/java/net/neoforged/neodev/installer/LibraryDownload.java
new file mode 100644
index 0000000000..38afe5d057
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/installer/LibraryDownload.java
@@ -0,0 +1,4 @@
+package net.neoforged.neodev.installer;
+
+record LibraryDownload(
+ LibraryArtifact artifact) {}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/utils/DependencyUtils.java b/buildSrc/src/main/java/net/neoforged/neodev/utils/DependencyUtils.java
new file mode 100644
index 0000000000..ee1bb65175
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/utils/DependencyUtils.java
@@ -0,0 +1,84 @@
+package net.neoforged.neodev.utils;
+
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.result.ResolvedArtifactResult;
+import org.gradle.api.provider.Provider;
+import org.gradle.internal.component.external.model.ModuleComponentArtifactIdentifier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public final class DependencyUtils {
+ private DependencyUtils() {
+ }
+
+ /**
+ * Given a resolved artifact, try to guess which Maven GAV it was resolved from.
+ */
+ public static MavenIdentifier guessMavenIdentifier(ResolvedArtifactResult result) {
+ String group;
+ String artifact;
+ String version;
+ String ext = "";
+ String classifier = "";
+
+ var filename = result.getFile().getName();
+ var startOfExt = filename.lastIndexOf('.');
+ if (startOfExt != -1) {
+ ext = filename.substring(startOfExt + 1);
+ filename = filename.substring(0, startOfExt);
+ }
+
+ if (result.getId() instanceof ModuleComponentArtifactIdentifier moduleId) {
+ group = moduleId.getComponentIdentifier().getGroup();
+ artifact = moduleId.getComponentIdentifier().getModule();
+ version = moduleId.getComponentIdentifier().getVersion();
+ var expectedBasename = artifact + "-" + version;
+
+ if (filename.startsWith(expectedBasename + "-")) {
+ classifier = filename.substring((expectedBasename + "-").length());
+ }
+ } else {
+ // When we encounter a project reference, the component identifier does not expose the group or module name.
+ // But we can access the list of capabilities associated with the published variant the artifact originates from.
+ // If the capability was not overridden, this will be the project GAV. If it is *not* the project GAV,
+ // it will be at least in valid GAV format, not crashing NFRT when it parses the manifest. It will just be ignored.
+ var capabilities = result.getVariant().getCapabilities();
+ if (capabilities.size() == 1) {
+ var capability = capabilities.get(0);
+ group = capability.getGroup();
+ artifact = capability.getName();
+ version = capability.getVersion();
+ } else {
+ throw new IllegalArgumentException("Don't know how to break " + result.getId().getComponentIdentifier() + " into Maven components.");
+ }
+ }
+ return new MavenIdentifier(group, artifact, version, classifier, ext);
+ }
+
+ /**
+ * Turns a configuration into a list of GAV entries.
+ */
+ public static Provider> configurationToGavList(Configuration configuration) {
+ return configuration.getIncoming().getArtifacts().getResolvedArtifacts().map(results -> {
+ // Using .toList() fails with the configuration cache - looks like Gradle can't deserialize the resulting list?
+ return results.stream().map(artifact -> guessMavenIdentifier(artifact).artifactNotation()).collect(Collectors.toCollection(ArrayList::new));
+ });
+ }
+
+ /**
+ * Turns a configuration into a classpath string,
+ * assuming that the contents of the configuration are installed following the Maven directory layout.
+ *
+ * @param prefix string to add in front of each classpath entry
+ * @param separator separator to add between each classpath entry
+ */
+ public static Provider configurationToClasspath(Configuration configuration, String prefix, String separator) {
+ return configuration.getIncoming().getArtifacts().getResolvedArtifacts().map(
+ results -> results.stream()
+ .map(artifact -> prefix + guessMavenIdentifier(artifact).repositoryPath())
+ .collect(Collectors.joining(separator))
+ );
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/utils/FileUtils.java b/buildSrc/src/main/java/net/neoforged/neodev/utils/FileUtils.java
new file mode 100644
index 0000000000..d91996d32d
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/utils/FileUtils.java
@@ -0,0 +1,72 @@
+package net.neoforged.neodev.utils;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+
+public final class FileUtils {
+ private FileUtils() {
+ }
+
+ public static void writeStringSafe(Path destination, String content, Charset charset) throws IOException {
+ if (!charset.newEncoder().canEncode(content)) {
+ throw new IllegalArgumentException("The given character set " + charset
+ + " cannot represent this string: " + content);
+ }
+
+ try (var out = newSafeFileOutputStream(destination)) {
+ var encodedContent = content.getBytes(charset);
+ out.write(encodedContent);
+ }
+ }
+
+ public static void writeLinesSafe(Path destination, List lines, Charset charset) throws IOException {
+ writeStringSafe(destination, String.join("\n", lines), charset);
+ }
+
+ public static OutputStream newSafeFileOutputStream(Path destination) throws IOException {
+ var uniqueId = ProcessHandle.current().pid() + "." + Thread.currentThread().getId();
+
+ var tempFile = destination.resolveSibling(destination.getFileName().toString() + "." + uniqueId + ".tmp");
+ var closed = new boolean[1];
+ return new FilterOutputStream(Files.newOutputStream(tempFile)) {
+ @Override
+ public void close() throws IOException {
+ try {
+ super.close();
+ if (!closed[0]) {
+ atomicMoveIfPossible(tempFile, destination);
+ }
+ } finally {
+ try {
+ Files.deleteIfExists(tempFile);
+ } catch (IOException ignored) {
+ }
+ closed[0] = true;
+ }
+ }
+ };
+ }
+
+ /**
+ * Atomically moves the given source file to the given destination file.
+ * If the atomic move is not supported, the file will be moved normally.
+ *
+ * @param source The source file
+ * @param destination The destination file
+ * @throws IOException If an I/O error occurs
+ */
+ private static void atomicMoveIfPossible(final Path source, final Path destination) throws IOException {
+ try {
+ Files.move(source, destination, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
+ } catch (AtomicMoveNotSupportedException ex) {
+ Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+}
diff --git a/buildSrc/src/main/java/net/neoforged/neodev/utils/MavenIdentifier.java b/buildSrc/src/main/java/net/neoforged/neodev/utils/MavenIdentifier.java
new file mode 100644
index 0000000000..6a22312fd0
--- /dev/null
+++ b/buildSrc/src/main/java/net/neoforged/neodev/utils/MavenIdentifier.java
@@ -0,0 +1,13 @@
+package net.neoforged.neodev.utils;
+
+import java.io.Serializable;
+
+public record MavenIdentifier(String group, String artifact, String version, String classifier, String extension) implements Serializable {
+ public String artifactNotation() {
+ return group + ":" + artifact + ":" + version + (classifier.isEmpty() ? "" : ":" + classifier) + ("jar".equals(extension) ? "" : "@" + extension);
+ }
+
+ public String repositoryPath() {
+ return group.replace(".", "/") + "/" + artifact + "/" + version + "/" + artifact + "-" + version + (classifier.isEmpty() ? "" : "-" + classifier) + "." + extension;
+ }
+}
diff --git a/coremods/build.gradle b/coremods/build.gradle
index 3d942a4701..127e7ec9df 100644
--- a/coremods/build.gradle
+++ b/coremods/build.gradle
@@ -5,15 +5,6 @@ plugins {
id 'neoforge.formatting-conventions'
}
-repositories {
- maven { url = 'https://maven.neoforged.net/releases' }
- maven {
- name 'Mojang'
- url 'https://libraries.minecraft.net'
- }
- mavenCentral()
-}
-
jar {
manifest {
attributes(
diff --git a/coremods/settings.gradle b/coremods/settings.gradle
new file mode 100644
index 0000000000..06c2cf68e4
--- /dev/null
+++ b/coremods/settings.gradle
@@ -0,0 +1,8 @@
+repositories {
+ maven { url = 'https://maven.neoforged.net/releases' }
+ maven {
+ name 'Mojang'
+ url 'https://libraries.minecraft.net'
+ }
+ mavenCentral()
+}
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 80f55cebf7..00f2af575d 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -19,8 +19,11 @@ Contributing to NeoForge
8. Modify the patched Minecraft sources in `projects/neoforge/src/main/java` as needed. The unmodified sources are available in `projects/base/src/main/java` for your reference. Do not modify these.
9. Test your changes
- Run the game (Runs are available in the IDE)
- - Run `gradlew :tests:runGameTestServer` or `Tests: GameTestServer` from IDE
- - Run `gradlew :tests:runGameTestClient` or `Tests: GameTestClient` from IDE
+ - Runs starting with `base -` run Vanilla without NeoForge or its patches.
+ - Runs starting with `neoforge -` run NeoForge.
+ - Runs starting with `tests -` run NeoForge along with the test mods in the `tests` project.
+ - Run `gradlew :tests:runGameTestServer` or `tests - GameTestServer` from IDE
+ - Run `gradlew :tests:runClient` or `tests - Client` from IDE
- If possible, write an automated test under the tests project. See [NEOGAMETESTS.md](NEOGAMETESTS.md) for more info.
10. Run `gradlew genPatches` to generate patch-files from the patched sources
11. Run `gradlew applyAllFormatting` to automatically format sources
diff --git a/gradle.properties b/gradle.properties
index ee9ba5e243..1c1d602cd3 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,8 +4,14 @@ org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
-org.gradle.configuration-cache=false
+org.gradle.configuration-cache=true
org.gradle.debug=false
+#org.gradle.warning.mode=fail
+
+# renovate: net.neoforged:moddev-gradle
+moddevgradle_plugin_version=2.0.47-beta
+# renovate: io.codechicken:DiffPatch
+diffpatch_version=2.0.0.35
java_version=21
@@ -14,6 +20,14 @@ neoform_version=20241023.131943
# on snapshot versions, used to prefix the version
neoforge_snapshot_next_stable=21.4
+# renovate: net.neoforged.jst:jst-cli-bundle
+jst_version=1.0.45
+legacyinstaller_version=3.0.+
+# renovate: net.neoforged:AutoRenamingTool
+art_version=2.0.3
+# renovate: net.neoforged.installertools:installertools
+installertools_version=2.1.2
+
mergetool_version=2.0.0
accesstransformers_version=11.0.1
coremods_version=6.0.4
@@ -22,7 +36,6 @@ modlauncher_version=11.0.4
securejarhandler_version=3.0.8
bootstraplauncher_version=2.0.2
asm_version=9.7
-installer_version=2.1.+
mixin_version=0.15.2+mixin.0.8.7
terminalconsoleappender_version=1.3.0
nightconfig_version=3.8.0
@@ -43,12 +56,8 @@ nashorn_core_version=15.3
lwjgl_glfw_version=3.3.2
mixin_extras_version=0.4.1
-jupiter_api_version=5.7.0
+jupiter_api_version=5.10.2
vintage_engine_version=5.+
assertj_core=3.25.1
neogradle.runtime.platform.installer.debug=true
-# We want to be able to have a junit run disconnected from the test and main sourcesets
-neogradle.subsystems.conventions.sourcesets.automatic-inclusion=false
-neogradle.subsystems.conventions.enabled=false
-neogradle.subsystems.tools.jst=net.neoforged.jst:jst-cli-bundle:1.0.45
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 9355b41557..0aaefbcaf0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/patches/net/minecraft/client/renderer/block/LiquidBlockRenderer.java.patch b/patches/net/minecraft/client/renderer/block/LiquidBlockRenderer.java.patch
index f2ca146f9c..72aee1fb3e 100644
--- a/patches/net/minecraft/client/renderer/block/LiquidBlockRenderer.java.patch
+++ b/patches/net/minecraft/client/renderer/block/LiquidBlockRenderer.java.patch
@@ -1,6 +1,6 @@
--- a/net/minecraft/client/renderer/block/LiquidBlockRenderer.java
+++ b/net/minecraft/client/renderer/block/LiquidBlockRenderer.java
-@@ -37,6 +_,7 @@
+@@ -37,12 +_,17 @@
this.waterIcons[0] = Minecraft.getInstance().getModelManager().getBlockModelShaper().getBlockModel(Blocks.WATER.defaultBlockState()).getParticleIcon();
this.waterIcons[1] = ModelBakery.WATER_FLOW.sprite();
this.waterOverlay = ModelBakery.WATER_OVERLAY.sprite();
@@ -8,8 +8,29 @@
}
private static boolean isNeighborSameFluid(FluidState p_203186_, FluidState p_203187_) {
-@@ -70,8 +_,9 @@
+ return p_203187_.getType().isSame(p_203186_.getType());
+ }
+
++ private static boolean isNeighborStateHidingOverlay(FluidState selfState, BlockState otherState, Direction neighborFace) {
++ return otherState.shouldHideAdjacentFluidFace(neighborFace, selfState);
++ }
++
+ private static boolean isFaceOccludedByState(Direction p_110980_, float p_110981_, BlockState p_110983_) {
+ VoxelShape voxelshape = p_110983_.getFaceOcclusionShape(p_110980_.getOpposite());
+ if (voxelshape == Shapes.empty()) {
+@@ -64,14 +_,20 @@
+ return isFaceOccludedByState(p_110963_.getOpposite(), 1.0F, p_110962_);
+ }
++ /** @deprecated Neo: use overload that accepts BlockState */
+ public static boolean shouldRenderFace(FluidState p_203169_, BlockState p_203170_, Direction p_203171_, FluidState p_203172_) {
+ return !isFaceOccludedBySelf(p_203170_, p_203171_) && !isNeighborSameFluid(p_203169_, p_203172_);
+ }
+
++ public static boolean shouldRenderFace(FluidState fluidState, BlockState selfState, Direction direction, BlockState otherState) {
++ return !isFaceOccludedBySelf(selfState, direction) && !isNeighborStateHidingOverlay(fluidState, otherState, direction.getOpposite());
++ }
++
public void tesselate(BlockAndTintGetter p_234370_, BlockPos p_234371_, VertexConsumer p_234372_, BlockState p_234373_, FluidState p_234374_) {
boolean flag = p_234374_.is(FluidTags.LAVA);
- TextureAtlasSprite[] atextureatlassprite = flag ? this.lavaIcons : this.waterIcons;
@@ -20,6 +41,25 @@
float f = (float)(i >> 16 & 0xFF) / 255.0F;
float f1 = (float)(i >> 8 & 0xFF) / 255.0F;
float f2 = (float)(i & 0xFF) / 255.0F;
+@@ -87,12 +_,12 @@
+ FluidState fluidstate4 = blockstate4.getFluidState();
+ BlockState blockstate5 = p_234370_.getBlockState(p_234371_.relative(Direction.EAST));
+ FluidState fluidstate5 = blockstate5.getFluidState();
+- boolean flag1 = !isNeighborSameFluid(p_234374_, fluidstate1);
+- boolean flag2 = shouldRenderFace(p_234374_, p_234373_, Direction.DOWN, fluidstate) && !isFaceOccludedByNeighbor(Direction.DOWN, 0.8888889F, blockstate);
+- boolean flag3 = shouldRenderFace(p_234374_, p_234373_, Direction.NORTH, fluidstate2);
+- boolean flag4 = shouldRenderFace(p_234374_, p_234373_, Direction.SOUTH, fluidstate3);
+- boolean flag5 = shouldRenderFace(p_234374_, p_234373_, Direction.WEST, fluidstate4);
+- boolean flag6 = shouldRenderFace(p_234374_, p_234373_, Direction.EAST, fluidstate5);
++ boolean flag1 = !isNeighborStateHidingOverlay(p_234374_, blockstate1, Direction.DOWN);
++ boolean flag2 = shouldRenderFace(p_234374_, p_234373_, Direction.DOWN, blockstate) && !isFaceOccludedByNeighbor(Direction.DOWN, 0.8888889F, blockstate);
++ boolean flag3 = shouldRenderFace(p_234374_, p_234373_, Direction.NORTH, blockstate2);
++ boolean flag4 = shouldRenderFace(p_234374_, p_234373_, Direction.SOUTH, blockstate3);
++ boolean flag5 = shouldRenderFace(p_234374_, p_234373_, Direction.WEST, blockstate4);
++ boolean flag6 = shouldRenderFace(p_234374_, p_234373_, Direction.EAST, blockstate5);
+ if (flag1 || flag2 || flag6 || flag5 || flag3 || flag4) {
+ float f3 = p_234370_.getShade(Direction.DOWN, true);
+ float f4 = p_234370_.getShade(Direction.UP, true);
@@ -180,15 +_,15 @@
float f57 = f4 * f;
float f29 = f4 * f1;
diff --git a/projects/base/.gitignore b/projects/base/.gitignore
index 1b10a46d0a..cb56563da3 100644
--- a/projects/base/.gitignore
+++ b/projects/base/.gitignore
@@ -1,3 +1,4 @@
src
build
.gradle
+run
diff --git a/projects/base/build.gradle b/projects/base/build.gradle
index 7a78b9aa79..04cca6fe08 100644
--- a/projects/base/build.gradle
+++ b/projects/base/build.gradle
@@ -1,3 +1,34 @@
-dynamicProject {
- neoform("${project.minecraft_version}-${project.neoform_version}")
+
+plugins {
+ id 'java-library'
+}
+
+apply plugin: net.neoforged.neodev.NeoDevBasePlugin
+
+dependencies {
+ implementation("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
+ capabilities {
+ requireCapability 'net.neoforged:neoform-dependencies'
+ }
+ endorseStrictVersions()
+ }
+}
+
+neoDev {
+ runs {
+ configureEach {
+ gameDirectory = layout.projectDir.dir("run/$name")
+ }
+ client {
+ client()
+ }
+ server {
+ server()
+ }
+ // Generated files are in run/data/generated
+ data {
+ data()
+ programArgument "--all"
+ }
+ }
}
diff --git a/projects/neoforge/build.gradle b/projects/neoforge/build.gradle
index bc642bd0c9..3912a5bd30 100644
--- a/projects/neoforge/build.gradle
+++ b/projects/neoforge/build.gradle
@@ -10,6 +10,11 @@ plugins {
id 'neoforge.versioning'
}
+apply plugin : net.neoforged.neodev.NeoDevPlugin
+
+// Because of the source set reference.
+evaluationDependsOn(":neoforge-coremods")
+
gradleutils.setupSigning(project: project, signAllPublications: true)
changelog {
@@ -17,10 +22,15 @@ changelog {
disableAutomaticPublicationRegistration()
}
-dynamicProject {
- runtime("${project.minecraft_version}-${project.neoform_version}",
- rootProject.layout.projectDirectory.dir('patches'),
- rootProject.layout.projectDirectory.dir('rejects'))
+sourceSets {
+ main {
+ java {
+ srcDirs rootProject.file('src/main/java')
+ }
+ resources {
+ srcDirs rootProject.file('src/main/resources'), rootProject.file('src/generated/resources')
+ }
+ }
}
final checkVersion = JCCPlugin.providePreviousVersion(
@@ -29,7 +39,7 @@ final checkVersion = JCCPlugin.providePreviousVersion(
)
final createCompatJar = tasks.register('createCompatibilityCheckJar', ProvideNeoForgeJarTask) {
// Use the same jar that the patches were generated against
- cleanJar.set(tasks.generateClientBinaryPatches.clean)
+ cleanJar.set(tasks.generateClientBinPatches.cleanJar)
maven.set('https://maven.neoforged.net/releases')
artifact.set('net.neoforged:neoforge')
version.set(checkVersion)
@@ -42,47 +52,37 @@ checkJarCompatibility {
baseJar = createCompatJar.flatMap { it.output }
}
-installerProfile {
- profile = 'NeoForge'
-}
-
-minecraft {
- // FML looks for this mod id to find the minecraft classes
- modIdentifier 'minecraft'
-
- accessTransformers {
- file rootProject.file('src/main/resources/META-INF/accesstransformer.cfg')
- }
-}
-
-tasks.configureEach { tsk ->
- if (tsk.name == 'neoFormApplyUserAccessTransformer' && project.hasProperty('validateAccessTransformers')) {
- tsk.inputs.property('validation', 'error')
- tsk.logLevel('ERROR')
- tsk.doFirst {
- tsk.getRuntimeProgramArguments().addAll(tsk.getRuntimeProgramArguments().get())
- tsk.getRuntimeProgramArguments().add('--access-transformer-validation=error')
+neoDev {
+ mods {
+ minecraft {
+ sourceSet sourceSets.main
+ }
+ "neoforge-coremods" {
+ sourceSet project(":neoforge-coremods").sourceSets.main
}
}
}
-sourceSets {
- main {
- java {
- srcDirs rootProject.file('src/main/java')
+dependencies {
+ // For an overview of what the nonstandard configurations do,
+ // have a look at NeoDevConfigurations.java in the buildSrc folder.
+
+ neoFormData("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
+ capabilities {
+ requireCapability 'net.neoforged:neoform'
}
- resources {
- srcDirs rootProject.file('src/main/resources'), rootProject.file('src/generated/resources')
+ endorseStrictVersions()
+ }
+ neoFormDependencies("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
+ capabilities {
+ requireCapability 'net.neoforged:neoform-dependencies'
}
+ endorseStrictVersions()
}
-}
-
-dependencies {
- runtimeOnly "cpw.mods:bootstraplauncher:${project.bootstraplauncher_version}"
- moduleOnly "cpw.mods:securejarhandler:${project.securejarhandler_version}"
+ moduleLibraries "cpw.mods:securejarhandler:${project.securejarhandler_version}"
for (var asmModule : ["org.ow2.asm:asm", "org.ow2.asm:asm-commons", "org.ow2.asm:asm-tree", "org.ow2.asm:asm-util", "org.ow2.asm:asm-analysis"]) {
- moduleOnly(asmModule) {
+ moduleLibraries(asmModule) {
// Vanilla ships with ASM 9.3 transitively (via their OpenID connect library dependency), we require
// ASM in a more recent version and have to strictly require this to override the strict Minecraft version.
version {
@@ -90,166 +90,75 @@ dependencies {
}
}
}
- moduleOnly "cpw.mods:bootstraplauncher:${project.bootstraplauncher_version}"
- moduleOnly "net.neoforged:JarJarFileSystems:${project.jarjar_version}"
+ moduleLibraries "cpw.mods:bootstraplauncher:${project.bootstraplauncher_version}"
+ moduleLibraries "net.neoforged:JarJarFileSystems:${project.jarjar_version}"
- installer ("net.neoforged.fancymodloader:loader:${project.fancy_mod_loader_version}") {
+ libraries ("net.neoforged.fancymodloader:loader:${project.fancy_mod_loader_version}") {
exclude group: 'org.slf4j'
exclude group: 'net.fabricmc'
}
- installer ("net.neoforged.fancymodloader:earlydisplay:${project.fancy_mod_loader_version}") {
+ libraries ("net.neoforged.fancymodloader:earlydisplay:${project.fancy_mod_loader_version}") {
exclude group: 'org.lwjgl'
exclude group: 'org.slf4j'
exclude group: 'net.fabricmc'
}
- installer "cpw.mods:securejarhandler:${project.securejarhandler_version}"
- installer "org.ow2.asm:asm:${project.asm_version}"
- installer "org.ow2.asm:asm-commons:${project.asm_version}"
- installer "org.ow2.asm:asm-tree:${project.asm_version}"
- installer "org.ow2.asm:asm-util:${project.asm_version}"
- installer "org.ow2.asm:asm-analysis:${project.asm_version}"
- installer "net.neoforged:accesstransformers:${project.accesstransformers_version}"
- installer "net.neoforged:bus:${project.eventbus_version}"
- installer "net.neoforged:coremods:${project.coremods_version}"
- installer "cpw.mods:modlauncher:${project.modlauncher_version}"
- installer "net.neoforged:mergetool:${project.mergetool_version}:api"
- installer "com.electronwill.night-config:core:${project.nightconfig_version}"
- installer "com.electronwill.night-config:toml:${project.nightconfig_version}"
- installer "org.apache.maven:maven-artifact:${project.apache_maven_artifact_version}"
- installer "net.jodah:typetools:${project.typetools_version}"
- installer "net.minecrell:terminalconsoleappender:${project.terminalconsoleappender_version}"
- installer("net.fabricmc:sponge-mixin:${project.mixin_version}") { transitive = false }
- installer "org.openjdk.nashorn:nashorn-core:${project.nashorn_core_version}"
- installer ("net.neoforged:JarJarSelector:${project.jarjar_version}") {
+ libraries "net.neoforged:accesstransformers:${project.accesstransformers_version}"
+ libraries "net.neoforged:bus:${project.eventbus_version}"
+ libraries "net.neoforged:coremods:${project.coremods_version}"
+ libraries "cpw.mods:modlauncher:${project.modlauncher_version}"
+ libraries "net.neoforged:mergetool:${project.mergetool_version}:api"
+ libraries "com.electronwill.night-config:core:${project.nightconfig_version}"
+ libraries "com.electronwill.night-config:toml:${project.nightconfig_version}"
+ libraries "org.apache.maven:maven-artifact:${project.apache_maven_artifact_version}"
+ libraries "net.jodah:typetools:${project.typetools_version}"
+ libraries "net.minecrell:terminalconsoleappender:${project.terminalconsoleappender_version}"
+ libraries("net.fabricmc:sponge-mixin:${project.mixin_version}") { transitive = false }
+ libraries "org.openjdk.nashorn:nashorn-core:${project.nashorn_core_version}"
+ libraries ("net.neoforged:JarJarSelector:${project.jarjar_version}") {
exclude group: 'org.slf4j'
}
// We depend on apache commons directly as there is a difference between the version the server uses and the one the client does
- installer "org.apache.commons:commons-lang3:${project.apache_commons_lang3_version}"
- installer ("net.neoforged:JarJarMetadata:${project.jarjar_version}") {
+ libraries "org.apache.commons:commons-lang3:${project.apache_commons_lang3_version}"
+ libraries ("net.neoforged:JarJarMetadata:${project.jarjar_version}") {
exclude group: 'org.slf4j'
}
- // Manually override log4j since the version coming from other `installer` dependencies is outdated
- installer "org.apache.logging.log4j:log4j-api:${project.log4j_version}"
- installer "org.apache.logging.log4j:log4j-core:${project.log4j_version}"
compileOnly "org.jetbrains:annotations:${project.jetbrains_annotations_version}"
- userdevCompileOnly jarJar("io.github.llamalad7:mixinextras-neoforge:${project.mixin_extras_version}"), {
- jarJar.ranged(it, "[${project.mixin_extras_version},)")
- }
-
- userdevTestImplementation("net.neoforged.fancymodloader:junit-fml:${project.fancy_mod_loader_version}")
- compileOnly(jarJar(project(":neoforge-coremods")))
-}
-
-runTypes {
- client {
- singleInstance false
- client true
-
- arguments.addAll '--fml.neoForgeVersion', project.version
- arguments.addAll '--fml.fmlVersion', project.fancy_mod_loader_version
- arguments.addAll '--fml.mcVersion', project.minecraft_version
- arguments.addAll '--fml.neoFormVersion', project.neoform_version
- }
-
- server {
- server true
-
- arguments.addAll '--fml.neoForgeVersion', project.version
- arguments.addAll '--fml.fmlVersion', project.fancy_mod_loader_version
- arguments.addAll '--fml.mcVersion', project.minecraft_version
- arguments.addAll '--fml.neoFormVersion', project.neoform_version
- }
-
- gameTestServer {
- from project.runTypes.server
+ userdevCompileOnly jarJar("io.github.llamalad7:mixinextras-neoforge:${project.mixin_extras_version}")
- gameTest true
- }
-
- gameTestClient {
- from project.runTypes.client
-
- gameTest true
- }
-
- data {
- dataGenerator true
-
- // Don't set modid here so we can reuse this runType for test datagen
- arguments.addAll '--fml.neoForgeVersion', project.version
- arguments.addAll '--fml.fmlVersion', project.fancy_mod_loader_version
- arguments.addAll '--fml.mcVersion', project.minecraft_version
- arguments.addAll '--fml.neoFormVersion', project.neoform_version
+ userdevTestFixtures("net.neoforged.fancymodloader:junit-fml:${project.fancy_mod_loader_version}") {
+ endorseStrictVersions()
}
- junit {
- junit true
- arguments.addAll '--fml.neoForgeVersion', project.version
- arguments.addAll '--fml.fmlVersion', project.fancy_mod_loader_version
- arguments.addAll '--fml.mcVersion', project.minecraft_version
- arguments.addAll '--fml.neoFormVersion', project.neoform_version
- }
+ // Must be implementation instead of compileOnly so that running dependent projects such as tests will trigger (re)compilation of coremods.
+ // (Only needed when compiling through IntelliJ non-delegated builds - otherwise `compileOnly` would work).
+ implementation(jarJar(project(":neoforge-coremods")))
}
-runs {
- client { }
- server { }
- gameTestServer { }
- gameTestClient { }
- data {
- arguments.addAll '--mod', 'neoforge'
-
- modSources.add project.sourceSets.main
-
- idea {
- primarySourceSet project.sourceSets.main
+neoDev {
+ runs {
+ configureEach {
+ gameDirectory = layout.projectDir.dir("run/$name")
+ }
+ client {
+ client()
+ }
+ server {
+ server()
+ }
+ gameTestServer {
+ type = "gameTestServer"
+ }
+ data {
+ data()
+ programArguments.addAll '--mod', 'neoforge', '--flat', '--all', '--validate',
+ '--existing', rootProject.file("src/main/resources").absolutePath,
+ '--output', rootProject.file("src/generated/resources").absolutePath
}
}
}
-runs.configureEach { it ->
- modSources.add project(":neoforge-coremods").sourceSets.main
-
- final File gameDir = project.file("run/${it.name}") as File
- gameDir.mkdirs();
-
- it.workingDirectory.set gameDir
- it.arguments.addAll '--gameDir', gameDir.absolutePath
-}
-
-tasks.register("genPatches") {
- dependsOn tasks.unpackSourcePatches
-}
-
-launcherProfile {
- arguments {
- game '--fml.neoForgeVersion'
- game project.version
- game '--fml.fmlVersion'
- game project.fancy_mod_loader_version
- game '--fml.mcVersion'
- game project.minecraft_version
- game '--fml.neoFormVersion'
- game project.neoform_version
- }
-}
-
-userdevProfile {
- runTypes.configureEach {
- argument '--fml.neoForgeVersion'
- argument project.version
- argument '--fml.fmlVersion'
- argument project.fancy_mod_loader_version
- argument '--fml.mcVersion'
- argument project.minecraft_version
- argument '--fml.neoFormVersion'
- argument project.neoform_version
- }
- additionalTestDependencyArtifactCoordinate "net.neoforged:testframework:${project.version}"
-}
-
tasks.withType(Javadoc.class).configureEach {
options.tags = [
'apiNote:a:API Note:',
@@ -259,27 +168,6 @@ tasks.withType(Javadoc.class).configureEach {
options.addStringOption('Xdoclint:all,-missing', '-public')
}
-configurations {
- forValidation {
- canBeConsumed = true
- canBeResolved = false
- attributes {
- attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
- attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
- attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
- attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements, LibraryElements.JAR))
- }
-
- extendsFrom api, runtimeOnly
- }
-}
-
-artifacts {
- forValidation(jar.archiveFile) {
- builtBy(jar)
- }
-}
-
AdhocComponentWithVariants javaComponent = (AdhocComponentWithVariants) project.components.findByName("java")
// Ensure the two default variants are not published, since they
// contain Minecraft classes
@@ -290,10 +178,12 @@ javaComponent.withVariantsFromConfiguration(configurations.runtimeElements) {
it.skip()
}
+// Resolvable configurations only
configurations {
modDevBundle {
+ canBeDeclared = false
canBeResolved = false
- canBeConsumed = true
+ extendsFrom neoFormData
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, "data"))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@@ -304,8 +194,8 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {} // Publish it
}
modDevConfig {
+ canBeDeclared = false
canBeResolved = false
- canBeConsumed = true
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, "data"))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@@ -316,8 +206,8 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {} // Publish it
}
installerJar {
+ canBeDeclared = false
canBeResolved = false
- canBeConsumed = true
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
@@ -332,8 +222,8 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {}
}
universalJar {
+ canBeDeclared = false
canBeResolved = false
- canBeConsumed = true
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
@@ -345,8 +235,8 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {}
}
changelog {
+ canBeDeclared = false
canBeResolved = false
- canBeConsumed = true
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, "changelog"))
@@ -357,11 +247,9 @@ configurations {
}
}
modDevApiElements {
+ canBeDeclared = false
canBeResolved = false
- canBeConsumed = true
- afterEvaluate {
- extendsFrom userdevCompileOnly, installerLibraries, moduleOnly
- }
+ extendsFrom libraries, moduleLibraries, userdevCompileOnly, neoFormDependencies
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@@ -373,11 +261,9 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {}
}
modDevRuntimeElements {
+ canBeDeclared = false
canBeResolved = false
- canBeConsumed = true
- afterEvaluate {
- extendsFrom installerLibraries, moduleOnly
- }
+ extendsFrom libraries, moduleLibraries, neoFormDependencies
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@@ -389,11 +275,9 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {}
}
modDevModulePath {
+ canBeDeclared = false
canBeResolved = false
- canBeConsumed = true
- afterEvaluate {
- extendsFrom moduleOnly
- }
+ extendsFrom moduleLibraries
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@@ -404,8 +288,9 @@ configurations {
javaComponent.addVariantsFromConfiguration(it) {}
}
modDevTestFixtures {
+ canBeDeclared = false
canBeResolved = false
- canBeConsumed = true
+ extendsFrom userdevTestFixtures
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.LIBRARY))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling, Bundling.EXTERNAL))
@@ -418,63 +303,35 @@ configurations {
}
}
-dependencies {
- modDevBundle("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
- capabilities {
- requireCapability 'net.neoforged:neoform'
- }
- endorseStrictVersions()
- }
- modDevApiElements("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
- capabilities {
- requireCapability 'net.neoforged:neoform-dependencies'
- }
- endorseStrictVersions()
- }
- modDevRuntimeElements("net.neoforged:neoform:${project.minecraft_version}-${project.neoform_version}") {
- capabilities {
- requireCapability 'net.neoforged:neoform-dependencies'
- }
- endorseStrictVersions()
- }
- modDevTestFixtures("net.neoforged.fancymodloader:junit-fml:${project.fancy_mod_loader_version}") {
- endorseStrictVersions()
- }
-}
-
processResources {
inputs.property("version", project.version)
+ final version = project.version
filesMatching("META-INF/neoforge.mods.toml") {
expand([
"global": [
- "neoForgeVersion": project.version
+ "neoForgeVersion": version
]
])
}
}
-afterEvaluate {
- artifacts {
- modDevBundle(userdevJar) {
- setClassifier("userdev") // Legacy
- }
- modDevConfig(createUserdevJson.output) {
- builtBy(createUserdevJson)
- setClassifier("moddev-config")
- }
- universalJar(signUniversalJar.output) {
- builtBy(signUniversalJar)
- setClassifier("universal")
- }
- installerJar(signInstallerJar.output) {
- builtBy(signInstallerJar)
- setClassifier("installer")
- }
- changelog(createChangelog.outputFile) {
- builtBy(createChangelog)
- setClassifier("changelog")
- setExtension("txt")
- }
+artifacts {
+ modDevBundle(userdevJar) {
+ setClassifier("userdev") // Legacy
+ }
+ modDevConfig(writeUserDevConfig.userDevConfig) {
+ setClassifier("moddev-config")
+ }
+ universalJar(universalJar) {
+ setClassifier("universal")
+ }
+ installerJar(installerJar) {
+ setClassifier("installer")
+ }
+ changelog(createChangelog.outputFile) {
+ builtBy(createChangelog)
+ setClassifier("changelog")
+ setExtension("txt")
}
}
diff --git a/settings.gradle b/settings.gradle
index 2e8f7b6541..aff93fdd4c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,13 +1,28 @@
pluginManagement {
repositories {
gradlePluginPortal()
- mavenLocal()
maven { url = 'https://maven.neoforged.net/releases' }
+ mavenLocal()
}
}
plugins {
- id 'net.neoforged.gradle.platform' version '7.0.171'
+ id 'net.neoforged.moddev.repositories' version "${moddevgradle_plugin_version}"
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
+}
+
+// This makes the version available to buildSrc
+gradle.ext.moddevgradle_plugin_version = moddevgradle_plugin_version
+gradle.ext.gson_version = gson_version
+gradle.ext.diffpatch_version = diffpatch_version
+
+dependencyResolutionManagement {
+ repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
+ rulesMode = RulesMode.FAIL_ON_PROJECT_RULES
+ repositories {
+ mavenCentral()
+ mavenLocal()
+ }
}
if (rootProject.name.toLowerCase() == "neoforge") {
@@ -15,13 +30,10 @@ if (rootProject.name.toLowerCase() == "neoforge") {
rootProject.name = "NeoForge-Root"
}
-dynamicProjects {
- include ':base'
- include ':neoforge'
-
- project(":base").projectDir = file("projects/base")
- project(":neoforge").projectDir = file("projects/neoforge")
-}
+include ':base'
+project(':base').projectDir = file('projects/base')
+include ':neoforge'
+project(':neoforge').projectDir = file('projects/neoforge')
include ':tests'
project(":tests").projectDir = file("tests")
diff --git a/src/main/java/net/neoforged/neoforge/common/extensions/IBlockExtension.java b/src/main/java/net/neoforged/neoforge/common/extensions/IBlockExtension.java
index 813b8a398f..f20dcdec2f 100644
--- a/src/main/java/net/neoforged/neoforge/common/extensions/IBlockExtension.java
+++ b/src/main/java/net/neoforged/neoforge/common/extensions/IBlockExtension.java
@@ -1019,4 +1019,16 @@ default BubbleColumnDirection getBubbleColumnDirection(BlockState state) {
return BubbleColumnDirection.NONE;
}
}
+
+ /**
+ * Determines if a fluid adjacent to the block on the given side should not be rendered.
+ *
+ * @param state the block state of the block
+ * @param selfFace the face of this block that the fluid is adjacent to
+ * @param adjacentFluid the fluid that is touching that face
+ * @return true if this block should cause the fluid's face to not render
+ */
+ default boolean shouldHideAdjacentFluidFace(BlockState state, Direction selfFace, FluidState adjacentFluid) {
+ return state.getFluidState().getType().isSame(adjacentFluid.getType());
+ }
}
diff --git a/src/main/java/net/neoforged/neoforge/common/extensions/IBlockStateExtension.java b/src/main/java/net/neoforged/neoforge/common/extensions/IBlockStateExtension.java
index 3e08a8b5b0..ce05f6783f 100644
--- a/src/main/java/net/neoforged/neoforge/common/extensions/IBlockStateExtension.java
+++ b/src/main/java/net/neoforged/neoforge/common/extensions/IBlockStateExtension.java
@@ -755,4 +755,15 @@ default boolean isEmpty() {
default BubbleColumnDirection getBubbleColumnDirection() {
return self().getBlock().getBubbleColumnDirection(self());
}
+
+ /**
+ * Determines if a fluid adjacent to the block on the given side should not be rendered.
+ *
+ * @param selfFace the face of this block that the fluid is adjacent to
+ * @param adjacentFluid the fluid that is touching that face
+ * @return true if this block should cause the fluid's face to not render
+ */
+ default boolean shouldHideAdjacentFluidFace(Direction selfFace, FluidState adjacentFluid) {
+ return self().getBlock().shouldHideAdjacentFluidFace(self(), selfFace, adjacentFluid);
+ }
}
diff --git a/testframework/build.gradle b/testframework/build.gradle
index 91a01c1c76..d5d98b4059 100644
--- a/testframework/build.gradle
+++ b/testframework/build.gradle
@@ -3,25 +3,19 @@ plugins {
id 'maven-publish'
id 'com.diffplug.spotless'
id 'net.neoforged.licenser'
- id 'net.neoforged.gradle.platform'
id 'neoforge.formatting-conventions'
}
java.withSourcesJar()
-repositories {
- maven {
- name 'Mojang'
- url 'https://libraries.minecraft.net'
- }
- maven {
- name 'NeoForged'
- url 'https://maven.neoforged.net/releases'
- }
-}
+apply plugin : net.neoforged.minecraftdependencies.MinecraftDependenciesPlugin
dependencies {
- implementation project(path: ':neoforge', configuration: 'runtimeElements')
+ // TODO: is this leaking in the POM? (most likely yes)
+ // TODO: does this need to be changed back to runtimeDependencies?
+ // TODO: should use attributes to resolve the right variant instead of hardcoding
+ compileOnly project(path: ':neoforge', configuration: 'apiElements')
+ runtimeOnly project(path: ':neoforge', configuration: 'runtimeElements')
compileOnly(platform("org.junit:junit-bom:${project.jupiter_api_version}"))
compileOnly "org.junit.jupiter:junit-jupiter-params"
@@ -30,6 +24,14 @@ dependencies {
compileOnly "com.google.code.findbugs:jsr305:3.0.2"
}
+sourceSets {
+ main {
+ // TODO: cursed
+ compileClasspath += project(':neoforge').sourceSets.main.compileClasspath
+ runtimeClasspath += project(':neoforge').sourceSets.main.runtimeClasspath
+ }
+}
+
license {
header = rootProject.file('codeformat/HEADER.txt')
include '**/*.java'
@@ -39,6 +41,7 @@ tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}
+def version = project.version
tasks.withType(ProcessResources).configureEach {
inputs.properties version: version
diff --git a/tests/build.gradle b/tests/build.gradle
index 2da20a4e40..37ea199d5b 100644
--- a/tests/build.gradle
+++ b/tests/build.gradle
@@ -1,27 +1,18 @@
plugins {
id 'java'
- id 'net.neoforged.gradle.platform'
id 'com.diffplug.spotless'
id 'net.neoforged.licenser'
id 'neoforge.formatting-conventions'
}
+apply plugin : net.neoforged.neodev.NeoDevExtraPlugin
+
+evaluationDependsOn(":neoforge")
+
def neoforgeProject = project(':neoforge')
def testframeworkProject = project(':testframework')
def coremodsProject = project(':neoforge-coremods')
-repositories {
- mavenLocal()
- maven {
- name 'Mojang'
- url 'https://libraries.minecraft.net'
- }
- maven {
- name 'NeoForged'
- url 'https://maven.neoforged.net/releases'
- }
-}
-
sourceSets {
main {
resources {
@@ -31,10 +22,6 @@ sourceSets {
junit {}
}
-configurations {
- junitImplementation.extendsFrom(implementation)
-}
-
dependencies {
implementation project(path: ':neoforge', configuration: 'runtimeElements')
implementation(testframeworkProject)
@@ -45,74 +32,82 @@ dependencies {
junitImplementation("org.assertj:assertj-core:${project.assertj_core}")
junitImplementation "net.neoforged.fancymodloader:junit-fml:${project.fancy_mod_loader_version}"
+ junitImplementation project(path: ':neoforge', configuration: 'runtimeElements')
+ junitImplementation(testframeworkProject)
compileOnly "org.jetbrains:annotations:${project.jetbrains_annotations_version}"
}
-runs {
- client {
- configure neoforgeProject.runTypes.client
- }
- junit {
- configure neoforgeProject.runTypes.junit
- unitTestSource sourceSets.junit
- }
- server {
- configure neoforgeProject.runTypes.server
- }
- gameTestServer {
- configure neoforgeProject.runTypes.gameTestServer
- }
- gameTestClient {
- configure neoforgeProject.runTypes.gameTestClient
+junitTest {
+ useJUnitPlatform()
+ classpath = sourceSets.junit.output + sourceSets.junit.runtimeClasspath
+ testClassesDirs = sourceSets.junit.output.classesDirs
+ outputs.upToDateWhen { false }
+}
+
+neoDev {
+ mods {
+ neotests {
+ sourceSet sourceSets.main
+ }
+ testframework {
+ sourceSet project(":testframework").sourceSets.main
+ }
+ junit {
+ sourceSet sourceSets.junit
+ }
+ coremods {
+ sourceSet coremodsProject.sourceSets.main
+ }
}
- data {
- configure neoforgeProject.runTypes.data
-
- arguments.addAll '--flat', '--all', '--validate',
- '--mod', 'data_gen_test',
- '--mod', 'global_loot_test',
- '--mod', 'scaffolding_test',
- '--mod', 'custom_tag_types_test',
- '--mod', 'new_model_loader_test',
- '--mod', 'remove_tag_datagen_test',
- '--mod', 'tag_based_tool_types',
- '--mod', 'custom_transformtype_test',
- '--mod', 'data_pack_registries_test',
- '--mod', 'biome_modifiers_test',
- '--mod', 'structure_modifiers_test',
- '--mod', 'custom_preset_editor_test',
- '--mod', 'custom_predicate_test',
- '--mod', 'neotests',
- '--existing-mod', 'testframework',
- '--existing', sourceSets.main.resources.srcDirs[0].absolutePath
-
- final File gameDir = project.file("runs/${name}") as File
- gameDir.mkdirs();
-
- workingDirectory.set gameDir
- arguments.addAll '--gameDir', gameDir.absolutePath
+
+ runs {
+ configureEach {
+ gameDirectory = layout.projectDir.dir("run/$name")
+ }
+ client {
+ client()
+ }
+ server {
+ server()
+ }
+ gameTestServer {
+ type = "gameTestServer"
+ }
+ data {
+ data()
+
+ programArguments.addAll '--flat', '--all', '--validate',
+ '--mod', 'data_gen_test',
+ '--mod', 'global_loot_test',
+ '--mod', 'scaffolding_test',
+ '--mod', 'custom_tag_types_test',
+ '--mod', 'new_model_loader_test',
+ '--mod', 'remove_tag_datagen_test',
+ '--mod', 'tag_based_tool_types',
+ '--mod', 'custom_transformtype_test',
+ '--mod', 'data_pack_registries_test',
+ '--mod', 'biome_modifiers_test',
+ '--mod', 'structure_modifiers_test',
+ '--mod', 'custom_preset_editor_test',
+ '--mod', 'custom_predicate_test',
+ '--mod', 'neotests',
+ '--existing-mod', 'testframework',
+ '--existing', project.file("src/main/resources").absolutePath,
+ '--output', project.file("src/generated/resources").absolutePath
+ }
}
-}
-//We need the assets and natives tasks from the forge project.
-runs.configureEach {
- dependsOn.add(neoforgeProject.runtime.assets)
- dependsOn.add(neoforgeProject.runtime.natives)
- modSource neoforgeProject.sourceSets.main
- modSource coremodsProject.sourceSets.main
- modSource testframeworkProject.sourceSets.main
-}
+ runs.configureEach {
+ // Add NeoForge and Minecraft (both under the "minecraft" mod), and exclude junit.
+ loadedMods = [neoforgeProject.neoDev.mods.minecraft, mods.neotests, mods.testframework, mods.coremods]
-afterEvaluate {
- runs.data {
- // Override --output that forge already has
- def args = new ArrayList(arguments.get());
- def outputIndex = args.indexOf('--output');
- args.set(outputIndex+1, file('src/generated/resources/').absolutePath);
- arguments.set(args);
+ gameDirectory.set project.file("runs/${it.name}") as File
}
- runs.junit.modSources.all().get().values().remove(sourceSets.main)
+}
+
+neoDevTest {
+ loadedMods = [ project(":neoforge").neoDev.mods.minecraft, neoDev.mods.testframework, neoDev.mods.coremods, neoDev.mods.junit ]
}
license {
diff --git a/tests/src/generated/resources/assets/neotests_test_water_glass_face_removal/blockstates/water_glass.json b/tests/src/generated/resources/assets/neotests_test_water_glass_face_removal/blockstates/water_glass.json
new file mode 100644
index 0000000000..ed35a1c6c0
--- /dev/null
+++ b/tests/src/generated/resources/assets/neotests_test_water_glass_face_removal/blockstates/water_glass.json
@@ -0,0 +1,7 @@
+{
+ "variants": {
+ "": {
+ "model": "neotests_test_water_glass_face_removal:block/water_glass"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/src/generated/resources/assets/neotests_test_water_glass_face_removal/lang/en_us.json b/tests/src/generated/resources/assets/neotests_test_water_glass_face_removal/lang/en_us.json
new file mode 100644
index 0000000000..e016ca7125
--- /dev/null
+++ b/tests/src/generated/resources/assets/neotests_test_water_glass_face_removal/lang/en_us.json
@@ -0,0 +1,3 @@
+{
+ "block.neotests_test_water_glass_face_removal.water_glass": "Water Glass"
+}
\ No newline at end of file
diff --git a/tests/src/generated/resources/assets/neotests_test_water_glass_face_removal/models/block/water_glass.json b/tests/src/generated/resources/assets/neotests_test_water_glass_face_removal/models/block/water_glass.json
new file mode 100644
index 0000000000..10740130a9
--- /dev/null
+++ b/tests/src/generated/resources/assets/neotests_test_water_glass_face_removal/models/block/water_glass.json
@@ -0,0 +1,7 @@
+{
+ "parent": "minecraft:block/cube_all",
+ "render_type": "minecraft:cutout",
+ "textures": {
+ "all": "minecraft:block/glass"
+ }
+}
\ No newline at end of file
diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/fluid/ClientFluidTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/fluid/ClientFluidTests.java
new file mode 100644
index 0000000000..cc40433fce
--- /dev/null
+++ b/tests/src/main/java/net/neoforged/neoforge/debug/fluid/ClientFluidTests.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) NeoForged and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.neoforged.neoforge.debug.fluid;
+
+import net.minecraft.client.renderer.block.LiquidBlockRenderer;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.Direction;
+import net.minecraft.gametest.framework.GameTest;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.TransparentBlock;
+import net.minecraft.world.level.block.state.BlockBehaviour;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.material.FluidState;
+import net.minecraft.world.level.material.Fluids;
+import net.neoforged.api.distmarker.Dist;
+import net.neoforged.neoforge.client.model.generators.BlockStateProvider;
+import net.neoforged.testframework.DynamicTest;
+import net.neoforged.testframework.annotation.ForEachTest;
+import net.neoforged.testframework.annotation.TestHolder;
+import net.neoforged.testframework.gametest.EmptyTemplate;
+import net.neoforged.testframework.registration.RegistrationHelper;
+
+@ForEachTest(groups = ClientFluidTests.GROUP, side = Dist.CLIENT)
+public class ClientFluidTests {
+ public static final String GROUP = "level.fluid.client";
+
+ static class WaterGlassBlock extends TransparentBlock {
+ private static final Direction HIDE_DIRECTION = Direction.NORTH;
+
+ public WaterGlassBlock(Properties p_309186_) {
+ super(p_309186_);
+ }
+
+ @Override
+ public boolean shouldHideAdjacentFluidFace(BlockState state, Direction selfFace, FluidState adjacentFluid) {
+ if (selfFace == HIDE_DIRECTION) {
+ return adjacentFluid.getFluidType() == Fluids.WATER.getFluidType();
+ } else {
+ return super.shouldHideAdjacentFluidFace(state, selfFace, adjacentFluid);
+ }
+ }
+ }
+
+ @GameTest
+ @EmptyTemplate
+ @TestHolder(description = "Tests if blocks can prevent neighboring fluids from rendering against them")
+ static void testWaterGlassFaceRemoval(final DynamicTest test, final RegistrationHelper reg) {
+ final var glass = reg.blocks().registerBlock("water_glass", WaterGlassBlock::new, BlockBehaviour.Properties.ofFullCopy(Blocks.GLASS)).withLang("Water Glass").withBlockItem();
+ reg.provider(BlockStateProvider.class, prov -> prov.simpleBlock(glass.get(), prov.models()
+ .cubeAll("water_glass", ResourceLocation.withDefaultNamespace("block/glass"))
+ .renderType("cutout")));
+ final var waterPosition = new BlockPos(1, 1, 2);
+ final var glassDirection = WaterGlassBlock.HIDE_DIRECTION.getOpposite();
+ final var glassPosition = waterPosition.relative(glassDirection);
+ test.onGameTest(helper -> helper.startSequence()
+ .thenExecute(() -> helper.setBlock(glassPosition, glass.get().defaultBlockState()))
+ .thenExecute(() -> helper.setBlock(waterPosition, Blocks.WATER.defaultBlockState()))
+ // Check that the north side of the water is not rendered
+ .thenExecute(() -> helper.assertFalse(
+ LiquidBlockRenderer.shouldRenderFace(
+ helper.getBlockState(waterPosition).getFluidState(),
+ helper.getBlockState(waterPosition),
+ glassDirection,
+ helper.getBlockState(glassPosition)),
+ "Fluid face rendering is not skipped"))
+ .thenSucceed());
+ }
+}