diff --git a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeModDevPlugin.java b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeModDevPlugin.java index d9663c21..02937db3 100644 --- a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeModDevPlugin.java +++ b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeModDevPlugin.java @@ -153,7 +153,9 @@ public void enable(Project project, LegacyForgeModdingSettings settings, LegacyF artifactNamingStrategy, configurations.getByName(DataFileCollections.CONFIGURATION_ACCESS_TRANSFORMERS), configurations.getByName(DataFileCollections.CONFIGURATION_INTERFACE_INJECTION_DATA), - versionCapabilities); + versionCapabilities, + false + ); var runs = ModDevRunWorkflow.create( project, diff --git a/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java b/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java index bc36311b..5e4d159c 100644 --- a/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java +++ b/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java @@ -17,6 +17,8 @@ public abstract class ModdingVersionSettings { private Set enabledSourceSets = new HashSet<>(); + private boolean binaryPatches = false; + @Inject public ModdingVersionSettings(Project project) { // By default, enable modding deps only for the main source set @@ -60,4 +62,12 @@ public Set getEnabledSourceSets() { public void setEnabledSourceSets(Set enabledSourceSets) { this.enabledSourceSets = enabledSourceSets; } + + public boolean isBinaryPatches() { + return binaryPatches; + } + + public void setBinaryPatches(boolean binaryPatches) { + this.binaryPatches = binaryPatches; + } } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java index f5a14cc3..c9618094 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java @@ -1,20 +1,18 @@ package net.neoforged.moddevgradle.internal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.function.Function; import net.neoforged.minecraftdependencies.MinecraftDistribution; import net.neoforged.moddevgradle.dsl.ModDevExtension; import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal; +import net.neoforged.moddevgradle.tasks.CreateMinecraftJar; import net.neoforged.nfrtgradle.CreateMinecraftArtifacts; import net.neoforged.nfrtgradle.DownloadAssets; import org.gradle.api.GradleException; import org.gradle.api.InvalidUserCodeException; import org.gradle.api.Named; +import org.gradle.api.NamedDomainObjectProvider; import org.gradle.api.Project; +import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.ModuleDependency; @@ -34,6 +32,14 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + /** * The workflow needed to produce artifacts and assets for compiling and running a mod. */ @@ -42,8 +48,9 @@ public record ModDevArtifactsWorkflow( Project project, ModdingDependencies dependencies, VersionCapabilitiesInternal versionCapabilities, - TaskProvider createArtifacts, - Provider minecraftClassesDependency, + TaskProvider createArtifacts, + List> minecraftCompileDependencies, + List> minecraftRuntimeDependencies, TaskProvider downloadAssets, Configuration runtimeDependencies, Configuration compileDependencies, @@ -51,6 +58,7 @@ public record ModDevArtifactsWorkflow( Provider artifactsBuildDir) { private static final String EXTENSION_NAME = "__internal_modDevArtifactsWorkflow"; + public static ModDevArtifactsWorkflow get(Project project) { var result = ExtensionUtils.findExtension(project, EXTENSION_NAME, ModDevArtifactsWorkflow.class); if (result == null) { @@ -60,14 +68,15 @@ public static ModDevArtifactsWorkflow get(Project project) { } public static ModDevArtifactsWorkflow create(Project project, - Collection enabledSourceSets, - Branding branding, - ModDevExtension extension, - ModdingDependencies moddingDependencies, - ArtifactNamingStrategy artifactNamingStrategy, - Configuration accessTransformers, - Configuration interfaceInjectionData, - VersionCapabilitiesInternal versionCapabilities) { + Collection enabledSourceSets, + Branding branding, + ModDevExtension extension, + ModdingDependencies moddingDependencies, + ArtifactNamingStrategy artifactNamingStrategy, + Configuration accessTransformers, + Configuration interfaceInjectionData, + VersionCapabilitiesInternal versionCapabilities, + boolean useBinaryPatches) { if (project.getExtensions().findByName(EXTENSION_NAME) != null) { throw new InvalidUserCodeException("You cannot enable modding in the same project twice."); } @@ -111,45 +120,117 @@ public static ModDevArtifactsWorkflow create(Project project, }); // it has to contain client-extra to be loaded by FML, and it must be added to the legacy CP - var createArtifacts = tasks.register("createMinecraftArtifacts", CreateMinecraftArtifacts.class, task -> { - task.setGroup(branding.internalTaskGroup()); - task.setDescription("Creates the NeoForge and Minecraft artifacts by invoking NFRT."); - for (var configuration : createManifestConfigurations) { - task.addArtifactsToManifest(configuration); - } + List> minecraftRuntimeDependencies = new ArrayList<>(); + List> minecraftCompileDependencies = new ArrayList<>(); + TaskProvider createArtifacts; + Provider compiledMinecraftJar; + Optional> sourceArtifact; + if (useBinaryPatches) { + // Gradle prevents us from having dependencies with "incompatible attributes" in the same configuration. + // What constitutes incompatible cannot be overridden on a per-configuration basis. + var modDevDataConfig = configurations.dependencyScope("modDevData", spec -> { + spec.setDescription("Mod development bundle dependencies for NeoForge/NeoForm"); + if (moddingDependencies.neoForgeDependency() != null) { + spec.getDependencies().add(moddingDependencies.neoForgeDependency().copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoforge-moddev-bundle"))); + } + + // This dependency is used when the NeoForm version is overridden or when we run in Vanilla-only mode + if (moddingDependencies.neoFormDependency() != null) { + spec.getDependencies().add(moddingDependencies.neoFormDependency().copy() + .capabilities(caps -> caps.requireCapability("net.neoforged:neoform"))); + } + }); + var modDevDataConfigResolvable = configurations.resolvable("modDevDataResolvable", spec -> { + spec.setDescription("Mod development bundle dependencies for NeoForge/NeoForm"); + spec.extendsFrom(modDevDataConfig.get()); + }); + var installerttools = configurations.create("modDevInstallerTools", spec -> { + spec.setTransitive(false); + spec.getDependencies().add(dependencyFactory.create("net.neoforged.installertools:installertools:3.0.25-moddev-support:fatjar")); + }); - // NFRT needs access to a JDK of the right version to be able to correctly decompile and recompile the code - task.getToolsJavaExecutable().set(javaToolchainService - .launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(versionCapabilities.javaVersion()))) - .map(javaLauncher -> javaLauncher.getExecutablePath().getAsFile().getAbsolutePath())); - - task.getAccessTransformers().from(accessTransformers); - // If AT validation is enabled, add the user-supplied AT paths as files to be validated, - // they're also part of the normal AT collection, so if AT validation is disabled, just return an empty list. - task.getValidatedAccessTransformers().from( - extension.getValidateAccessTransformers().map(validate -> { - if (validate) { - return extension.getAccessTransformers().getFiles(); - } else { - return project.files(); - } - })); - task.getInterfaceInjectionData().from(interfaceInjectionData); - task.getParchmentData().from(parchmentData); - task.getParchmentEnabled().set(parchment.getEnabled()); - task.getParchmentConflictResolutionPrefix().set(parchment.getConflictResolutionPrefix()); - - Function> artifactPathStrategy = artifact -> artifactsBuildDir.map(dir -> dir.file(artifactNamingStrategy.getFilename(artifact))); - - task.getCompiledArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.COMPILED)); - task.getCompiledWithSourcesArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.COMPILED_WITH_SOURCES)); - task.getSourcesArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.SOURCES)); - task.getResourcesArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.CLIENT_RESOURCES)); + var createMinecraftJar = tasks.register("createMinecraftArtifacts", CreateMinecraftJar.class, task -> { + task.setGroup(branding.internalTaskGroup()); + task.setDescription("Creates the Minecraft jar."); - task.getNeoForgeArtifact().set(moddingDependencies.neoForgeDependencyNotation()); - task.getNeoFormArtifact().set(moddingDependencies.neoFormDependencyNotation()); - task.getAdditionalResults().putAll(extension.getAdditionalMinecraftArtifacts()); - }); + task.getAccessTransformers().from(accessTransformers); + task.getInterfaceInjectionData().from(interfaceInjectionData); + task.getInstallerTools().from(installerttools); + + Function> artifactPathStrategy = artifact -> artifactsBuildDir.map(dir -> dir.file(artifactNamingStrategy.getFilename(artifact))); + + task.getMinecraftJar().set(artifactPathStrategy.apply(WorkflowArtifact.COMPILED)); + + task.getNeoForgeUserDevConfig().from(findArtifactWithCapability(modDevDataConfigResolvable, "net.neoforged:neoforge-moddev-bundle")); + task.getNeoFormConfig().fileProvider(findArtifactWithCapability(modDevDataConfigResolvable, "net.neoforged:neoform")); + }); + + compiledMinecraftJar = createMinecraftJar.flatMap(CreateMinecraftJar::getMinecraftJar); + sourceArtifact = Optional.empty(); + createArtifacts = createMinecraftJar; + + minecraftRuntimeDependencies.add(createMinecraftJar.map(task -> project.files(task.getMinecraftJar())).map(dependencyFactory::create)); + minecraftRuntimeDependencies.add(project.provider(moddingDependencies::neoForgeDependency)); + minecraftCompileDependencies.add(createMinecraftJar.map(task -> project.files(task.getMinecraftJar())).map(dependencyFactory::create)); + minecraftCompileDependencies.add(project.provider(moddingDependencies::neoForgeDependency)); + } else { + var neoformTask = tasks.register("createMinecraftArtifacts", CreateMinecraftArtifacts.class, task -> { + task.setGroup(branding.internalTaskGroup()); + task.setDescription("Creates the NeoForge and Minecraft artifacts by invoking NFRT."); + for (var configuration : createManifestConfigurations) { + task.addArtifactsToManifest(configuration); + } + + // NFRT needs access to a JDK of the right version to be able to correctly decompile and recompile the code + task.getToolsJavaExecutable().set(javaToolchainService + .launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(versionCapabilities.javaVersion()))) + .map(javaLauncher -> javaLauncher.getExecutablePath().getAsFile().getAbsolutePath())); + + task.getAccessTransformers().from(accessTransformers); + // If AT validation is enabled, add the user-supplied AT paths as files to be validated, + // they're also part of the normal AT collection, so if AT validation is disabled, just return an empty list. + task.getValidatedAccessTransformers().from( + extension.getValidateAccessTransformers().map(validate -> { + if (validate) { + return extension.getAccessTransformers().getFiles(); + } else { + return project.files(); + } + })); + task.getInterfaceInjectionData().from(interfaceInjectionData); + task.getParchmentData().from(parchmentData); + task.getParchmentEnabled().set(parchment.getEnabled()); + task.getParchmentConflictResolutionPrefix().set(parchment.getConflictResolutionPrefix()); + + Function> artifactPathStrategy = artifact -> artifactsBuildDir.map(dir -> dir.file(artifactNamingStrategy.getFilename(artifact))); + + task.getCompiledArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.COMPILED)); + task.getCompiledWithSourcesArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.COMPILED_WITH_SOURCES)); + task.getSourcesArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.SOURCES)); + task.getResourcesArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.CLIENT_RESOURCES)); + + task.getNeoForgeArtifact().set(moddingDependencies.neoForgeDependencyNotation()); + task.getNeoFormArtifact().set(moddingDependencies.neoFormDependencyNotation()); + task.getAdditionalResults().putAll(extension.getAdditionalMinecraftArtifacts()); + }); + + compiledMinecraftJar = neoformTask.flatMap(CreateMinecraftArtifacts::getCompiledArtifact); + sourceArtifact = Optional.of(neoformTask.flatMap(CreateMinecraftArtifacts::getSourcesArtifact)); + createArtifacts = neoformTask; + + // For IntelliJ, we attach a combined sources+classes artifact which enables an "Attach Sources..." link for IJ users + // Otherwise, attaching sources is a pain for IJ users. + Provider minecraftClassesDependency; + if (ideIntegration.shouldUseCombinedSourcesAndClassesArtifact()) { + minecraftClassesDependency = neoformTask.map(task -> project.files(task.getCompiledWithSourcesArtifact())).map(dependencyFactory::create); + } else { + minecraftClassesDependency = neoformTask.map(task -> project.files(task.getCompiledArtifact())).map(dependencyFactory::create); + } + minecraftRuntimeDependencies.add(minecraftClassesDependency); + minecraftCompileDependencies.add(minecraftClassesDependency); + minecraftRuntimeDependencies.add(neoformTask.map(task -> project.files(task.getResourcesArtifact())).map(dependencyFactory::create)); + } ideIntegration.runTaskOnProjectSync(createArtifacts); var downloadAssets = tasks.register("downloadAssets", DownloadAssets.class, task -> { @@ -166,15 +247,6 @@ public static ModDevArtifactsWorkflow create(Project project, task.getNeoFormArtifact().set(moddingDependencies.neoFormDependencyNotation()); }); - // For IntelliJ, we attach a combined sources+classes artifact which enables an "Attach Sources..." link for IJ users - // Otherwise, attaching sources is a pain for IJ users. - Provider minecraftClassesDependency; - if (ideIntegration.shouldUseCombinedSourcesAndClassesArtifact()) { - minecraftClassesDependency = createArtifacts.map(task -> project.files(task.getCompiledWithSourcesArtifact())).map(dependencyFactory::create); - } else { - minecraftClassesDependency = createArtifacts.map(task -> project.files(task.getCompiledArtifact())).map(dependencyFactory::create); - } - // Name of the configuration in which we place the required dependencies to develop mods for use in the runtime-classpath. // We cannot use "runtimeOnly", since the contents of that are published. var runtimeDependencies = configurations.create("modDevRuntimeDependencies", config -> { @@ -182,8 +254,8 @@ public static ModDevArtifactsWorkflow create(Project project, config.setCanBeResolved(false); config.setCanBeConsumed(false); - config.getDependencies().addLater(minecraftClassesDependency); - config.getDependencies().addLater(createArtifacts.map(task -> project.files(task.getResourcesArtifact())).map(dependencyFactory::create)); + minecraftRuntimeDependencies.forEach(config.getDependencies()::addLater); + // Technically, the Minecraft dependencies do not strictly need to be on the classpath because they are pulled from the legacy class path. // However, we do it anyway because this matches production environments, and allows launch proxies such as DevLogin to use Minecraft's libraries. config.getDependencies().add(moddingDependencies.gameLibrariesDependency()); @@ -195,16 +267,14 @@ public static ModDevArtifactsWorkflow create(Project project, config.setDescription("The compile-time dependencies to develop a mod, including Minecraft and modding platform classes."); config.setCanBeResolved(false); config.setCanBeConsumed(false); - config.getDependencies().addLater(minecraftClassesDependency); + + minecraftCompileDependencies.forEach(config.getDependencies()::addLater); config.getDependencies().add(moddingDependencies.gameLibrariesDependency()); }); // For IDEs that support it, link the source/binary artifacts if we use separated ones - if (!ideIntegration.shouldUseCombinedSourcesAndClassesArtifact()) { - ideIntegration.attachSources( - Map.of( - createArtifacts.get().getCompiledArtifact(), - createArtifacts.get().getSourcesArtifact())); + if (!ideIntegration.shouldUseCombinedSourcesAndClassesArtifact() && sourceArtifact.isPresent()) { + ideIntegration.attachSources(Map.of(compiledMinecraftJar, sourceArtifact.get())); } var result = new ModDevArtifactsWorkflow( @@ -212,7 +282,8 @@ public static ModDevArtifactsWorkflow create(Project project, moddingDependencies, versionCapabilities, createArtifacts, - minecraftClassesDependency, + minecraftCompileDependencies, + minecraftRuntimeDependencies, downloadAssets, runtimeDependencies, compileDependencies, @@ -228,6 +299,26 @@ public static ModDevArtifactsWorkflow create(Project project, return result; } + /** + * Searches the resolved artifacts of a configuration for the first artifact that was resolved for a given capability. + */ + private static Provider findArtifactWithCapability(NamedDomainObjectProvider modDevDataConfigResolvable, String capability) { + String[] parts = capability.split(":", 2); + String capabilityGroup = parts[0]; + String capabilityName = parts[1]; + + return modDevDataConfigResolvable.flatMap(configuration -> { + return configuration.getIncoming().getArtifacts().getResolvedArtifacts().map(resolvedArtifacts -> { + for (var resolvedArtifact : resolvedArtifacts) { + if (resolvedArtifact.getVariant().getCapabilities().stream().anyMatch(cap -> cap.getGroup().equals(capabilityGroup) && cap.getName().equals(capabilityName))) { + return resolvedArtifact.getFile(); + } + } + return null; + }); + }); + } + /** * Collects all dependencies needed by the NeoFormRuntime */ @@ -340,9 +431,21 @@ public Provider requestAdditionalMinecraftArtifact(String id, Strin } public Provider requestAdditionalMinecraftArtifact(String id, Provider path) { - createArtifacts.configure(task -> task.getAdditionalResults().put(id, path.map(RegularFile::getAsFile))); + createArtifacts.configure(task -> { + if (task instanceof CreateMinecraftArtifacts createMinecraftArtifacts) { + createMinecraftArtifacts.getAdditionalResults().put(id, path.map(RegularFile::getAsFile)); + } else { + throw new InvalidUserCodeException("Cannot request additional Minecraft artifact '" + id + "' from task " + task.getName() + " when using binary patches."); + } + }); return project.getLayout().file( - createArtifacts.flatMap(task -> task.getAdditionalResults().getting(id))); + createArtifacts.flatMap(task -> { + if (task instanceof CreateMinecraftArtifacts createMinecraftArtifacts) { + return createMinecraftArtifacts.getAdditionalResults().getting(id); + } else { + throw new InvalidUserCodeException("Cannot request additional Minecraft artifact '" + id + "' from task " + task.getName() + " when using binary patches."); + } + })); } private static void setNamedAttribute(Project project, AttributeContainer attributes, Attribute attribute, String value) { diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java index 26a7e3dd..275de446 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java @@ -95,7 +95,8 @@ public void enable( artifactNamingStrategy, configurations.getByName(DataFileCollections.CONFIGURATION_ACCESS_TRANSFORMERS), configurations.getByName(DataFileCollections.CONFIGURATION_INTERFACE_INJECTION_DATA), - versionCapabilities); + versionCapabilities, + settings.isBinaryPatches()); ModDevRunWorkflow.create( project, diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java index 6c0fdbbb..9c63745b 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java @@ -1,12 +1,5 @@ package net.neoforged.moddevgradle.internal; -import java.io.File; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; import net.neoforged.minecraftdependencies.MinecraftDistribution; import net.neoforged.moddevgradle.dsl.InternalModelHelper; import net.neoforged.moddevgradle.dsl.ModModel; @@ -40,6 +33,15 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.event.Level; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + /** * After modding has been enabled, this will be attached as an extension to the project. */ @@ -69,14 +71,14 @@ public class ModDevRunWorkflow { * (apiElements vs. runtimeElements). */ private ModDevRunWorkflow(Project project, - Branding branding, - ModDevArtifactsWorkflow artifactsWorkflow, - @Nullable ModuleDependency modulePathDependency, - @Nullable ModuleDependency runTypesConfigDependency, - @Nullable ModuleDependency testFixturesDependency, - ModuleDependency gameLibrariesDependency, - DomainObjectCollection runs, - VersionCapabilitiesInternal versionCapabilities) { + Branding branding, + ModDevArtifactsWorkflow artifactsWorkflow, + @Nullable ModuleDependency modulePathDependency, + @Nullable ModuleDependency runTypesConfigDependency, + @Nullable ModuleDependency testFixturesDependency, + ModuleDependency gameLibrariesDependency, + DomainObjectCollection runs, + VersionCapabilitiesInternal versionCapabilities) { this.project = project; this.branding = branding; this.modulePathDependency = modulePathDependency; @@ -107,7 +109,7 @@ private ModDevRunWorkflow(Project project, if (!versionCapabilities.modLocatorRework()) { // Forge expects to find the Forge and client-extra jar on the legacy classpath // Newer FML versions also search for it on the java.class.path. - spec.getDependencies().addLater(artifactsWorkflow.minecraftClassesDependency()); + artifactsWorkflow.minecraftRuntimeDependencies().forEach(spec.getDependencies()::addLater); } }); @@ -136,9 +138,9 @@ public static ModDevRunWorkflow get(Project project) { } public static ModDevRunWorkflow create(Project project, - Branding branding, - ModDevArtifactsWorkflow artifactsWorkflow, - DomainObjectCollection runs) { + Branding branding, + ModDevArtifactsWorkflow artifactsWorkflow, + DomainObjectCollection runs) { var dependencies = artifactsWorkflow.dependencies(); var versionCapabilites = artifactsWorkflow.versionCapabilities(); @@ -203,10 +205,17 @@ public void configureTesting(Provider testedMod, Provider createArtifacts) { + private static void addClientResources(Project project, Configuration spec, TaskProvider createArtifacts) { spec.getDependencies().add( project.getDependencyFactory().create( - project.files(createArtifacts.flatMap(CreateMinecraftArtifacts::getResourcesArtifact)))); + project.files(createArtifacts.flatMap(task -> { + if (task instanceof CreateMinecraftArtifacts createMinecraftArtifacts) { + return createMinecraftArtifacts.getResourcesArtifact(); + } else { + return project.provider(() -> project.files()); + } + }))) + ); } public static void setupRuns( @@ -397,15 +406,15 @@ private static TaskProvider setupRunInGradle( * @see #setupRunInGradle for a description of the parameters */ static void setupTestTask(Project project, - Branding branding, - Object runTemplatesSourceFile, - TaskProvider testTask, - Provider> loadedMods, - Provider testedMod, - Provider argFileDir, - Consumer configureModulePath, - Consumer configureLegacyClasspath, - Provider assetPropertiesFile) { + Branding branding, + Object runTemplatesSourceFile, + TaskProvider testTask, + Provider> loadedMods, + Provider testedMod, + Provider argFileDir, + Consumer configureModulePath, + Consumer configureLegacyClasspath, + Provider assetPropertiesFile) { var gameDirectory = new File(project.getProjectDir(), JUNIT_GAME_DIR); var ideIntegration = IdeIntegration.of(project, branding); diff --git a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java index 0543e684..a005902a 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java @@ -231,7 +231,7 @@ private UserDevConfig getSimulatedUserDevConfigForVanilla() { true, "net.minecraft.data.Main", commonArgs, List.of(), Map.of(), Map.of())); } - return new UserDevConfig(runTypes); + return new UserDevConfig(runTypes, null); } private void writeJvmArguments(UserDevRunType runConfig, Map additionalProperties) throws IOException { diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java index ec296a1a..15c3813f 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RepositoriesPlugin.java @@ -57,6 +57,7 @@ private void applyRepositories(RepositoryHandler repositories) { repo.setName("NeoForged Releases"); repo.setUrl(URI.create("https://maven.neoforged.net/releases/")); }); + repositories.mavenLocal(); } private static void sortFirst(RepositoryHandler repositories, MavenArtifactRepository repo) { diff --git a/src/main/java/net/neoforged/moddevgradle/internal/UserDevConfig.java b/src/main/java/net/neoforged/moddevgradle/internal/UserDevConfig.java index d62e210a..14433d3e 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/UserDevConfig.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/UserDevConfig.java @@ -1,17 +1,23 @@ package net.neoforged.moddevgradle.internal; import com.google.gson.Gson; +import org.gradle.api.GradleException; + import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Serializable; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Map; +import java.util.zip.ZipFile; /** * Sourced from the userdev config json. The run templates are the only thing that we use. */ -public record UserDevConfig(Map runs) implements Serializable { +public record UserDevConfig(Map runs, String binpatches) implements Serializable { public static UserDevConfig from(InputStream in) { return new Gson().fromJson(new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)), UserDevConfig.class); } diff --git a/src/main/java/net/neoforged/moddevgradle/tasks/CreateMinecraftJar.java b/src/main/java/net/neoforged/moddevgradle/tasks/CreateMinecraftJar.java new file mode 100644 index 00000000..368985bd --- /dev/null +++ b/src/main/java/net/neoforged/moddevgradle/tasks/CreateMinecraftJar.java @@ -0,0 +1,234 @@ +package net.neoforged.moddevgradle.tasks; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import net.neoforged.moddevgradle.internal.UserDevConfig; +import org.gradle.api.DefaultTask; +import org.gradle.api.InvalidUserCodeException; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.process.ExecOperations; +import org.jetbrains.annotations.ApiStatus; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.concurrent.CompletableFuture; +import java.util.zip.ZipFile; + +/** + * The primary task for creating the Minecraft artifacts that mods will be compiled against, + * using the NFRT CLI. + */ +@ApiStatus.Experimental +@ApiStatus.NonExtendable +public abstract class CreateMinecraftJar extends DefaultTask { + @Inject + public CreateMinecraftJar() { + } + + /** + * The installertools Jar needed to create the Minecraft jar. + */ + @Classpath + public abstract ConfigurableFileCollection getInstallerTools(); + + /** + * Files added to this collection will be applied to the Minecraft jar. + */ + @InputFiles + public abstract ConfigurableFileCollection getAccessTransformers(); + + /** + * Interface injection data files to apply to the Minecraft jar. + */ + @InputFiles + public abstract ConfigurableFileCollection getInterfaceInjectionData(); + + /** + * This must contain a single file, which has one of the following supported formats: + *
    + *
  • NeoForge Userdev jar file, containing a Userdev config.json file
  • + *
+ */ + @Classpath + public abstract ConfigurableFileCollection getNeoForgeUserDevConfig(); + + /** + * This must contain a single file, which has one of the following supported formats: + *
    + *
  • NeoForm config.json file.
  • + *
  • NeoForm data zip file.
  • + *
+ */ + @Classpath + public abstract RegularFileProperty getNeoFormConfig(); + + /** + * The Minecraft jar. + */ + @OutputFile + public abstract RegularFileProperty getMinecraftJar(); + + @Inject + @ApiStatus.Internal + protected abstract ExecOperations getExecOperations(); + + @TaskAction + public void createArtifacts() throws IOException { + + File neoFormConfig = getNeoFormConfig().getAsFile().get(); + + // Parse the neoFormConfig JSON and extract the minecraft field + String minecraftVersion; + try (var neoFormZip = new ZipFile(neoFormConfig)) { + var configEntry = neoFormZip.getEntry("config.json"); + if (configEntry == null) { + throw new InvalidUserCodeException("NeoForm config file does not contain a config.json entry."); + } + try (var inputStream = neoFormZip.getInputStream(configEntry); + var reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { + Gson gson = new Gson(); + JsonObject jsonObject = gson.fromJson(reader, JsonObject.class); + minecraftVersion = jsonObject.get("version").getAsString(); + } + } + + HttpClient client = HttpClient.newHttpClient(); + Gson gson = new Gson(); + + // Download version manifest + HttpRequest manifestRequest = HttpRequest.newBuilder() + .uri(URI.create("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json")) + .GET() + .build(); + + String manifestResponse; + try { + manifestResponse = client.send(manifestRequest, HttpResponse.BodyHandlers.ofString()).body(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while downloading version manifest", e); + } + + // Parse manifest and find the matching version + JsonObject manifest = gson.fromJson(manifestResponse, JsonObject.class); + JsonArray versions = manifest.getAsJsonArray("versions"); + + String versionUrl = null; + for (var versionElement : versions) { + JsonObject versionObj = versionElement.getAsJsonObject(); + if (minecraftVersion.equals(versionObj.get("id").getAsString())) { + versionUrl = versionObj.get("url").getAsString(); + break; + } + } + + if (versionUrl == null) { + throw new InvalidUserCodeException("Could not find Minecraft version " + minecraftVersion + " in version manifest"); + } + + // Download the specific version JSON + HttpRequest versionRequest = HttpRequest.newBuilder() + .uri(URI.create(versionUrl)) + .GET() + .build(); + + String versionResponse; + try { + versionResponse = client.send(versionRequest, HttpResponse.BodyHandlers.ofString()).body(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while downloading version data", e); + } + + JsonObject versionData = gson.fromJson(versionResponse, JsonObject.class); + getLogger().info("Downloaded Minecraft {} version data", minecraftVersion); + + var clientUrl = versionData.getAsJsonObject("downloads").getAsJsonObject("client").getAsJsonPrimitive("url").getAsString(); + var clientMappingsUrl = versionData.getAsJsonObject("downloads").getAsJsonObject("client_mappings").getAsJsonPrimitive("url").getAsString(); + var serverUrl = versionData.getAsJsonObject("downloads").getAsJsonObject("server").getAsJsonPrimitive("url").getAsString(); + + var clientFile = new File(getTemporaryDir(), "client.jar"); + var clientMappingsFile = new File(getTemporaryDir(), "client_mappings.txt"); + var serverFile = new File(getTemporaryDir(), "server.jar"); + + CompletableFuture.allOf( + client.sendAsync(HttpRequest.newBuilder(URI.create(clientUrl)).build(), HttpResponse.BodyHandlers.ofFile(clientFile.toPath())), + client.sendAsync(HttpRequest.newBuilder(URI.create(clientMappingsUrl)).build(), HttpResponse.BodyHandlers.ofFile(clientMappingsFile.toPath())), + client.sendAsync(HttpRequest.newBuilder(URI.create(serverUrl)).build(), HttpResponse.BodyHandlers.ofFile(serverFile.toPath())) + ).join(); + + var args = new ArrayList(); + args.add("--task"); + args.add("PROCESS_MINECRAFT_JAR"); + + args.add("--input"); + args.add(clientFile.getAbsolutePath()); + args.add("--input"); + args.add(serverFile.getAbsolutePath()); + args.add("--input-mappings"); + args.add(clientMappingsFile.getAbsolutePath()); + + args.add("--output"); + args.add(getMinecraftJar().getAsFile().get().getAbsolutePath()); + + // If an AT path is added twice, the validated variant takes precedence + var accessTransformersAdded = new HashSet(); + for (var accessTransformer : getAccessTransformers().getFiles()) { + if (accessTransformersAdded.add(accessTransformer)) { + args.add("--access-transformer"); + args.add(accessTransformer.getAbsolutePath()); + } + } + + for (var interfaceInjectionFile : getInterfaceInjectionData().getFiles()) { + args.add("--interface-injection-data"); + args.add(interfaceInjectionFile.getAbsolutePath()); + } + + args.add("--neoform-data"); + args.add(neoFormConfig.getAbsolutePath()); + + if (!getNeoForgeUserDevConfig().isEmpty()) { + try (var zip = new ZipFile(getNeoForgeUserDevConfig().getSingleFile())) { + var configEntry = zip.getEntry("config.json"); + if (configEntry == null) { + throw new InvalidUserCodeException("NeoForge UserDev config file does not contain a config.json entry."); + } + try (var inputStream = zip.getInputStream(configEntry)) { + var userDevConfig = UserDevConfig.from(inputStream); + if (userDevConfig.binpatches() != null) { + var binpatches = new File(getTemporaryDir(), "neoforge_binpatches.lzma"); + try (var binpatchesIn = zip.getInputStream(zip.getEntry(userDevConfig.binpatches()))) { + getLogger().info("Extracting NeoForge UserDev binpatches to {}", binpatches.getAbsolutePath()); + Files.copy(binpatchesIn, binpatches.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + args.add("--apply-patches"); + args.add(binpatches.getAbsolutePath()); + } + } + } + } + + getExecOperations().javaexec(spec -> { + spec.args(args); + spec.setClasspath(getInstallerTools()); + }); + } +} diff --git a/testproject/build.gradle b/testproject/build.gradle index fddd51e9..ffaa4825 100644 --- a/testproject/build.gradle +++ b/testproject/build.gradle @@ -30,7 +30,10 @@ test { } neoForge { - version = project.neoforge_version + enable { + version = project.neoforge_version + // binaryPatches = true + } addModdingDependenciesTo(sourceSets.api) validateAccessTransformers = true @@ -62,7 +65,7 @@ neoForge { disableIdeRun() } data { - data() + clientData() } server { server() diff --git a/testproject/gradle.properties b/testproject/gradle.properties index 38a79804..02127378 100644 --- a/testproject/gradle.properties +++ b/testproject/gradle.properties @@ -1,4 +1,4 @@ org.gradle.configuration-cache=true # Dependency versions -neoforge_version=21.0.61-beta +neoforge_version=21.8.38-pr-2547-features-one-step-install diff --git a/testproject/jijtest/build.gradle b/testproject/jijtest/build.gradle index 028146e3..20f9e2d3 100644 --- a/testproject/jijtest/build.gradle +++ b/testproject/jijtest/build.gradle @@ -44,7 +44,7 @@ neoForge { runs { data { - data() + clientData() } } diff --git a/testproject/settings.gradle b/testproject/settings.gradle index 952a551e..658e350f 100644 --- a/testproject/settings.gradle +++ b/testproject/settings.gradle @@ -23,3 +23,32 @@ include 'jijtest' include 'coremod' enableFeaturePreview "STABLE_CONFIGURATION_CACHE" + +gradle.lifecycle.afterProject { + it.repositories.maven { + name = "Maven for PR #2547" // https://github.com/neoforged/NeoForge/pull/2547 + url = uri("https://prmaven.neoforged.net/NeoForge/pr2547") + content { + includeModule("net.neoforged", "neoforge") + includeModule("net.neoforged", "testframework") + } + } + it.repositories.maven { + name = "Maven for PR #13" // https://github.com/neoforged/NeoForm/pull/13 + url = uri("https://prmaven.neoforged.net/NeoForm/pr13") + content { + includeModule("net.neoforged", "neoform") + } + } + it.repositories.maven { + name = "Maven for PR #337" // https://github.com/neoforged/FancyModLoader/pull/337 + url = uri("https://prmaven.neoforged.net/FancyModLoader/pr337") + content { + includeModule("net.neoforged.fancymodloader", "bootstraplauncher") + includeModule("net.neoforged.fancymodloader", "earlydisplay") + includeModule("net.neoforged.fancymodloader", "junit-fml") + includeModule("net.neoforged.fancymodloader", "loader") + includeModule("net.neoforged.fancymodloader", "securejarhandler") + } + } +} \ No newline at end of file diff --git a/testproject/src/main/java/testproject/TestProject.java b/testproject/src/main/java/testproject/TestProject.java index 88c01485..3cd90fff 100644 --- a/testproject/src/main/java/testproject/TestProject.java +++ b/testproject/src/main/java/testproject/TestProject.java @@ -11,8 +11,8 @@ @Mod("testproject") public class TestProject { public TestProject() { - System.out.println(DetectedVersion.tryDetectVersion().getName()); - System.out.println("Top-Level: " + ((DetectedVersion) DetectedVersion.BUILT_IN).buildTime); + System.out.println(DetectedVersion.tryDetectVersion().id()); + System.out.println("Top-Level: " + DetectedVersion.LOGGER.getName()); System.out.println(SubProject.class.getName()); new ApiTest(); // access something from the api source set diff --git a/testproject/src/main/resources/META-INF/accesstransformer.cfg b/testproject/src/main/resources/META-INF/accesstransformer.cfg index b03a0543..73d54b0d 100644 --- a/testproject/src/main/resources/META-INF/accesstransformer.cfg +++ b/testproject/src/main/resources/META-INF/accesstransformer.cfg @@ -1 +1 @@ -public net.minecraft.DetectedVersion buildTime +public net.minecraft.DetectedVersion LOGGER diff --git a/testproject/src/main/resources/META-INF/neoforge.mods.toml b/testproject/src/main/resources/META-INF/neoforge.mods.toml index 63c5c92f..f01e8da2 100644 --- a/testproject/src/main/resources/META-INF/neoforge.mods.toml +++ b/testproject/src/main/resources/META-INF/neoforge.mods.toml @@ -79,7 +79,7 @@ type="required" #mandatory # Optional field describing why the dependency is required or why it is incompatible # reason="..." # The version range of the dependency -versionRange="[21.0.0-beta,)" #mandatory +versionRange="[21.8.0,)" #mandatory # An ordering relationship for the dependency. # BEFORE - This mod is loaded BEFORE the dependency # AFTER - This mod is loaded AFTER the dependency @@ -92,7 +92,7 @@ side="BOTH" modId="minecraft" type="required" # This version range declares a minimum of the current minecraft version up to but not including the next major version -versionRange="[1.21]" +versionRange="[1.21.8]" ordering="NONE" side="BOTH" diff --git a/testproject/src/test/java/testproject/TestTest.java b/testproject/src/test/java/testproject/TestTest.java index f704de1f..ae351aba 100644 --- a/testproject/src/test/java/testproject/TestTest.java +++ b/testproject/src/test/java/testproject/TestTest.java @@ -1,5 +1,6 @@ package testproject; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.MinecraftServer; import net.minecraft.tags.ItemTags; import net.minecraft.world.item.Items; @@ -14,7 +15,7 @@ public class TestTest { @Test public void testIngredient(MinecraftServer server) { // required to load tags Assertions.assertTrue( - Ingredient.of(ItemTags.AXES).test(Items.DIAMOND_AXE.getDefaultInstance()) + Ingredient.of(BuiltInRegistries.ITEM.get(ItemTags.AXES).get()).test(Items.DIAMOND_AXE.getDefaultInstance()) ); } }