diff --git a/README.md b/README.md index ae299905..02c78cb9 100644 --- a/README.md +++ b/README.md @@ -181,8 +181,10 @@ neoForge { type = "gameTestServer" // Client, data and server runs can use a shorthand instead: // client() + // vanillaClient() // data() // server() + // vanillaServer() // Changes the working directory used for this run. // The default is the 'run' subdirectory of your project diff --git a/src/main/java/net/neoforged/moddevgradle/dsl/RunModel.java b/src/main/java/net/neoforged/moddevgradle/dsl/RunModel.java index ce73f3f8..5f3d9ee0 100644 --- a/src/main/java/net/neoforged/moddevgradle/dsl/RunModel.java +++ b/src/main/java/net/neoforged/moddevgradle/dsl/RunModel.java @@ -1,6 +1,7 @@ package net.neoforged.moddevgradle.dsl; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; @@ -57,6 +58,7 @@ public RunModel(String name, Project project, Iterable defaultMods) { getLogLevel().convention(Level.INFO); getDevLogin().convention(false); + getUseVanillaRunTemplates().convention(false); // Build a nicer name for the IDE run configuration boolean isSubProject = project.getRootProject() != project; @@ -164,6 +166,15 @@ public void client() { getType().set("client"); } + /** + * Equivalent to setting {@code type = "client"} and using the vanilla client run template. + */ + public void vanillaClient() { + getType().set("client"); + getUseVanillaRunTemplates().set(true); + getLoadedMods().set(Collections.emptySet()); + } + /** * Equivalent to setting {@code type = "clientData"}. * @@ -191,6 +202,15 @@ public void server() { getType().set("server"); } + /** + * Equivalent to setting {@code type = "server"} and using the vanilla server run template. + */ + public void vanillaServer() { + getType().set("server"); + getUseVanillaRunTemplates().set(true); + getLoadedMods().set(Collections.emptySet()); + } + /** * Equivalent to setting {@code type = "serverData"}. * @@ -273,6 +293,11 @@ public Configuration getAdditionalRuntimeClasspathConfiguration() { */ public abstract Property getDevLogin(); + /** + * Use ModDevGradle's built-in vanilla run templates instead of the configured loader's run templates. + */ + public abstract Property getUseVanillaRunTemplates(); + @Override public String toString() { return "Run[" + getName() + "]"; diff --git a/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java b/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java index 40e97557..e628af4a 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java @@ -20,7 +20,6 @@ import org.gradle.api.file.Directory; import org.gradle.api.file.RegularFile; import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.TaskProvider; import org.gradle.plugins.ide.eclipse.EclipsePlugin; import org.gradle.plugins.ide.eclipse.model.Classpath; import org.gradle.plugins.ide.eclipse.model.EclipseModel; @@ -70,13 +69,18 @@ public void attachSources(Map, Provider> jarT } @Override - public void configureRuns(Map> prepareRunTasks, + public void configureRuns(Map ideRunConfigurations, Iterable runs) { // Set up runs if running under buildship and in VS Code project.afterEvaluate(ignored -> { for (var run : runs) { - var prepareTask = prepareRunTasks.get(run).get(); - addEclipseLaunchConfiguration(project, run, prepareTask); + var ideRunConfiguration = ideRunConfigurations.get(run); + var prepareTask = ideRunConfiguration.prepareRunTask().get(); + if (ideRunConfiguration.usesDedicatedVanillaRuntime().get()) { + addEclipseGradleLaunchConfiguration(project, run, prepareTask, ideRunConfiguration.runTask().get()); + } else { + addEclipseLaunchConfiguration(project, run, prepareTask); + } } }); } @@ -166,6 +170,27 @@ private void addEclipseLaunchConfiguration(Project project, writeEclipseLaunchConfig(project, launchConfigName, config); } + private void addEclipseGradleLaunchConfiguration(Project project, + RunModel run, + PrepareRun prepareTask, + RunGameTask runTask) { + if (!prepareTask.getEnabled()) { + LOG.info("Not creating Eclipse run {} since its prepare task {} is disabled", run, prepareTask); + return; + } + if (!shouldGenerateConfigFor(run)) { + LOG.info("Not creating Eclipse run {} since it's explicitly disabled", run); + return; + } + + var runIdeName = run.getIdeName().get(); + var eclipseProjectName = Objects.requireNonNullElse(eclipseModel.getProject().getName(), project.getName()); + var config = GradleLaunchConfig.builder(eclipseProjectName) + .tasks(runTask.getPath()) + .build(); + writeEclipseLaunchConfig(project, runIdeName, config); + } + protected static ModFoldersProvider getModFoldersProvider(Project project, Provider> modsProvider, @Nullable Provider testedMod) { diff --git a/src/main/java/net/neoforged/moddevgradle/internal/IdeIntegration.java b/src/main/java/net/neoforged/moddevgradle/internal/IdeIntegration.java index 8e38adea..bfc84f58 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/IdeIntegration.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/IdeIntegration.java @@ -89,7 +89,7 @@ public final void runTaskOnProjectSync(Object task) { ideSyncTask.configure(ideSyncTask -> ideSyncTask.dependsOn(task)); } - void configureRuns(Map> prepareRunTasks, Iterable runs) {} + void configureRuns(Map ideRunConfigurations, Iterable runs) {} void configureTesting(Provider> loadedMods, Provider testedMod, diff --git a/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java b/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java index 50f08e80..9c175d4d 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java @@ -24,13 +24,13 @@ import org.gradle.api.plugins.ExtensionAware; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.TaskProvider; import org.gradle.internal.DefaultTaskExecutionRequest; import org.gradle.plugins.ide.idea.IdeaPlugin; import org.gradle.plugins.ide.idea.model.IdeaModel; import org.jetbrains.annotations.Nullable; import org.jetbrains.gradle.ext.Application; import org.jetbrains.gradle.ext.BeforeRunTask; +import org.jetbrains.gradle.ext.Gradle; import org.jetbrains.gradle.ext.IdeaExtPlugin; import org.jetbrains.gradle.ext.JUnit; import org.jetbrains.gradle.ext.ModuleRef; @@ -81,7 +81,7 @@ public void attachSources(Map, Provider> jarT } @Override - public void configureRuns(Map> prepareRunTasks, Iterable runs) { + public void configureRuns(Map ideRunConfigurations, Iterable runs) { // IDEA Sync has no real notion of tasks or providers or similar project.afterEvaluate(ignored -> { @@ -93,7 +93,8 @@ public void configureRuns(Map> prepareRunTask var outputDirectory = IntelliJOutputDirectoryValueSource.getIntellijOutputDirectory(project); for (var run : runs) { - var prepareTask = prepareRunTasks.get(run).get(); + var ideRunConfiguration = ideRunConfigurations.get(run); + var prepareTask = ideRunConfiguration.prepareRunTask().get(); if (!prepareTask.getEnabled()) { LOG.info("Not creating IntelliJ run {} since its prepare task {} is disabled", run, prepareTask); continue; @@ -102,7 +103,11 @@ public void configureRuns(Map> prepareRunTask LOG.info("Not creating IntelliJ run {} since it's explicitly disabled", run); continue; } - addIntelliJRunConfiguration(project, runConfigurations, outputDirectory, run, prepareTask); + if (ideRunConfiguration.usesDedicatedVanillaRuntime().get()) { + addIntelliJGradleRunConfiguration(project, runConfigurations, run, ideRunConfiguration.runTask().get()); + } else { + addIntelliJRunConfiguration(project, runConfigurations, outputDirectory, run, prepareTask); + } } } }); @@ -220,6 +225,16 @@ class GradleTasks extends BeforeRunTask { runConfigurations.add(appRun); } + private static void addIntelliJGradleRunConfiguration(Project project, + RunConfigurationContainer runConfigurations, + RunModel run, + RunGameTask runTask) { + var gradleRun = new ExtendedGradle(run.getIdeName().get(), getExtraIntelijRunProperties(run)); + gradleRun.setProjectPath(project.getProjectDir().getAbsolutePath()); + gradleRun.setTaskNames(List.of(runTask.getPath())); + runConfigurations.add(gradleRun); + } + private static String buildRelativePath(Provider file, File workingDirectory) { return workingDirectory.toPath().relativize(file.get().getAsFile().toPath()).toString().replace("\\", "/"); } @@ -295,4 +310,21 @@ public ExtendedApplication(String name, Project project, Map ext return m; } } + + private static class ExtendedGradle extends Gradle { + private final Map extraProperties; + + public ExtendedGradle(String name, Map extraProperties) { + super(name); + this.extraProperties = extraProperties; + } + + @Override + public Map toMap() { + @SuppressWarnings("unchecked") + var m = (Map) super.toMap(); + m.putAll(extraProperties); + return m; + } + } } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java index 40b1723d..bebb9c6d 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java @@ -44,13 +44,16 @@ public record ModDevArtifactsWorkflow( VersionCapabilitiesInternal versionCapabilities, TaskProvider createArtifacts, Provider minecraftClassesDependency, + Provider vanillaMinecraftClassesArtifact, TaskProvider downloadAssets, Configuration runtimeDependencies, Configuration compileDependencies, + Configuration vanillaRuntimeDependencies, Provider modDevBuildDir, Provider artifactsBuildDir) { private static final String EXTENSION_NAME = "__internal_modDevArtifactsWorkflow"; + private static final String VANILLA_CLASSES_RESULT_ID = "vanillaDeobfuscated"; public static ModDevArtifactsWorkflow get(Project project) { var result = ExtensionUtils.findExtension(project, EXTENSION_NAME, ModDevArtifactsWorkflow.class); if (result == null) { @@ -193,6 +196,7 @@ public static ModDevArtifactsWorkflow create(Project project, } else { minecraftClassesDependency = createArtifacts.map(task -> project.files(task.getGameJarArtifact())).map(dependencyFactory::create); } + var vanillaMinecraftClassesArtifact = artifactsBuildDir.map(dir -> dir.file("vanilla-runtime-" + versionCapabilities.minecraftVersion() + ".jar")); // 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. @@ -225,6 +229,18 @@ public static ModDevArtifactsWorkflow create(Project project, } }); + var vanillaRuntimeDependencies = configurations.create("modDevVanillaRuntimeDependencies", config -> { + config.setDescription("The runtime dependencies to run vanilla Minecraft."); + config.setCanBeResolved(false); + config.setCanBeConsumed(false); + if (moddingDependencies.neoFormDependency() != null) { + config.getDependencies().add(moddingDependencies.neoFormDependency().copy() + .capabilities(c -> c.requireCapability("net.neoforged:neoform-dependencies"))); + } else { + config.getDependencies().add(dependencyFactory.create("net.neoforged:minecraft-dependencies:" + versionCapabilities.minecraftVersion())); + } + }); + // For IDEs that support it, link the source/binary artifacts if we use separated ones if (!disableRecompilation && !ideIntegration.shouldUseCombinedSourcesAndClassesArtifact()) { ideIntegration.attachSources( @@ -239,9 +255,11 @@ public static ModDevArtifactsWorkflow create(Project project, versionCapabilities, createArtifacts, minecraftClassesDependency, + vanillaMinecraftClassesArtifact, downloadAssets, runtimeDependencies, compileDependencies, + vanillaRuntimeDependencies, modDevBuildDir, artifactsBuildDir); @@ -371,6 +389,11 @@ public Provider requestAdditionalMinecraftArtifact(String id, Provi createArtifacts.flatMap(task -> task.getAdditionalResults().getting(id))); } + public Provider requestVanillaMinecraftClassesDependency() { + createArtifacts.configure(task -> task.getAdditionalResults().put(VANILLA_CLASSES_RESULT_ID, vanillaMinecraftClassesArtifact.map(RegularFile::getAsFile))); + return vanillaMinecraftClassesArtifact.map(path -> project.getDependencyFactory().create(project.files(path).builtBy(createArtifacts))); + } + private static void setNamedAttribute(Project project, AttributeContainer attributes, Attribute attribute, String value) { attributes.attribute(attribute, project.getObjects().named(attribute.getType(), value)); } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java index d6be0f49..258c2948 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java @@ -128,6 +128,7 @@ private ModDevRunWorkflow(Project project, setupRuns( project, branding, + artifactsWorkflow, artifactsWorkflow.modDevBuildDir(), runs, userDevConfigOnly, @@ -242,6 +243,7 @@ private static void addClientResources(Project project, Configuration spec, Task public static void setupRuns( Project project, Branding branding, + @Nullable ModDevArtifactsWorkflow artifactsWorkflow, Provider argFileDir, DomainObjectCollection runs, Object runTemplatesSourceFile, @@ -267,16 +269,17 @@ public static void setupRuns( task.setDescription("Creates batch files/shell scripts to launch the game from outside of Gradle (i.e. Renderdoc, NVidia Nsight, etc.)"); }); - Map> prepareRunTasks = new IdentityHashMap<>(); + Map ideRunConfigurations = new IdentityHashMap<>(); runs.all(run -> { if (!versionCapabilities.modLocatorRework()) { // TODO: do this properly now that we have a flag in the version capabilities // This will explicitly be replaced in RunUtils to make this work for IDEs run.getEnvironment().put("MOD_CLASSES", RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null).getClassesArgument()); } - var prepareRunTask = setupRunInGradle( + var ideRunConfiguration = setupRunInGradle( project, branding, + artifactsWorkflow, argFileDir, run, runTemplatesSourceFile, @@ -286,9 +289,9 @@ public static void setupRuns( devLaunchConfig, versionCapabilities, createLaunchScriptsTask); - prepareRunTasks.put(run, prepareRunTask); + ideRunConfigurations.put(run, ideRunConfiguration); }); - ideIntegration.configureRuns(prepareRunTasks, runs); + ideIntegration.configureRuns(ideRunConfigurations, runs); } /** @@ -297,9 +300,10 @@ public static void setupRuns( * @param configureLegacyClasspath Callback to add entries to the legacy classpath. * @param assetPropertiesFile File that contains the asset properties file produced by NFRT. */ - private static TaskProvider setupRunInGradle( + private static IdeRunConfiguration setupRunInGradle( Project project, Branding branding, + @Nullable ModDevArtifactsWorkflow artifactsWorkflow, Provider argFileDir, RunModel run, Object runTemplatesFile, @@ -317,19 +321,13 @@ private static TaskProvider setupRunInGradle( var runtimeClasspathConfig = run.getSourceSet().map(SourceSet::getRuntimeClasspathConfigurationName) .map(configurations::getByName); - // Sucks, but what can you do... Only at the end do we actually know which source set this run will use - project.afterEvaluate(ignored -> { - runtimeClasspathConfig.get().extendsFrom(devLaunchConfig); - }); - var type = RunUtils.getRequiredType(project, run); + var runtimeClasspath = project.files(); var modulePathConfiguration = project.getConfigurations().create(InternalModelHelper.nameOfRun(run, "", "modulesOnly"), spec -> { spec.setDescription("Libraries that should be placed on the JVMs boot module path for run " + run.getName() + "."); spec.setCanBeResolved(true); spec.setCanBeConsumed(false); - spec.shouldResolveConsistentlyWith(runtimeClasspathConfig.get()); - configureModulePath.accept(spec); }); Provider legacyClasspathFile; @@ -338,7 +336,6 @@ private static TaskProvider setupRunInGradle( spec.setDescription("Contains all dependencies of the " + run.getName() + " run that should not be considered boot classpath modules."); spec.setCanBeResolved(true); spec.setCanBeConsumed(false); - spec.shouldResolveConsistentlyWith(runtimeClasspathConfig.get()); spec.attributes(attributes -> { attributes.attributeProvider(MinecraftDistribution.ATTRIBUTE, type.map(t -> { var name = t.equals("client") || t.equals("data") || t.equals("clientData") ? MinecraftDistribution.CLIENT : MinecraftDistribution.SERVER; @@ -346,8 +343,6 @@ private static TaskProvider setupRunInGradle( })); setNamedAttribute(project, attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME); }); - configureLegacyClasspath.accept(spec); - spec.extendsFrom(run.getAdditionalRuntimeClasspathConfiguration()); }); var writeLcpTask = tasks.register(InternalModelHelper.nameOfRun(run, "write", "legacyClasspath"), WriteLegacyClasspath.class, writeLcp -> { @@ -357,12 +352,38 @@ private static TaskProvider setupRunInGradle( writeLcp.addEntries(legacyClasspathConfiguration); }); legacyClasspathFile = writeLcpTask.get().getLegacyClasspathFile(); + + project.afterEvaluate(ignored -> { + if (!shouldUseDedicatedVanillaRuntime(artifactsWorkflow, run)) { + legacyClasspathConfiguration.shouldResolveConsistentlyWith(runtimeClasspathConfig.get()); + configureLegacyClasspath.accept(legacyClasspathConfiguration); + legacyClasspathConfiguration.extendsFrom(run.getAdditionalRuntimeClasspathConfiguration()); + } + }); } else { // Disallow adding dependencies to the additional classpath configuration since it would have no effect. forbidAdditionalRuntimeDependencies(run.getAdditionalRuntimeClasspathConfiguration(), versionCapabilities); legacyClasspathFile = null; } + project.afterEvaluate(ignored -> { + if (shouldUseDedicatedVanillaRuntime(artifactsWorkflow, run)) { + runtimeClasspath.from(getDedicatedVanillaRuntimeClasspath( + project, + artifactsWorkflow, + run, + type, + devLaunchConfig)); + } else { + runtimeClasspathConfig.get().extendsFrom(devLaunchConfig); + modulePathConfiguration.shouldResolveConsistentlyWith(runtimeClasspathConfig.get()); + configureModulePath.accept(modulePathConfiguration); + // Note: this contains both the runtimeClasspath configuration and the sourceset's outputs. + // This records a dependency on compiling and processing the resources of the source set. + runtimeClasspath.from(run.getSourceSet().map(SourceSet::getRuntimeClasspath)); + } + }); + var prepareRunTask = tasks.register(InternalModelHelper.nameOfRun(run, "prepare", "run"), PrepareRun.class, task -> { task.setGroup(branding.internalTaskGroup()); task.setDescription("Prepares all files needed to launch the " + run.getName() + " Minecraft run."); @@ -388,6 +409,7 @@ private static TaskProvider setupRunInGradle( task.getJvmArguments().set(run.getJvmArguments()); task.getGameLogLevel().set(run.getLogLevel()); task.getDevLogin().set(run.getDevLogin()); + task.getUseVanillaRunTemplates().set(run.getUseVanillaRunTemplates()); task.getVersionCapabilities().set(versionCapabilities); }); ideIntegration.runTaskOnProjectSync(prepareRunTask); @@ -397,9 +419,7 @@ private static TaskProvider setupRunInGradle( task.setDescription("Creates a bash/shell-script to launch the " + run.getName() + " Minecraft run from outside Gradle or your IDE."); task.getWorkingDirectory().set(run.getGameDirectory().map(d -> d.getAsFile().getAbsolutePath())); - // Note: this contains both the runtimeClasspath configuration and the sourceset's outputs. - // This records a dependency on compiling and processing the resources of the source set. - task.getRuntimeClasspath().from(run.getSourceSet().map(SourceSet::getRuntimeClasspath)); + task.getRuntimeClasspath().from(runtimeClasspath); task.getLaunchScript().set(RunUtils.getLaunchScript(argFileDir, run)); task.getClasspathArgsFile().set(RunUtils.getArgFile(argFileDir, run, RunUtils.RunArgFile.CLASSPATH)); task.getVmArgsFile().set(prepareRunTask.get().getVmArgsFile().map(d -> d.getAsFile().getAbsolutePath())); @@ -409,16 +429,14 @@ private static TaskProvider setupRunInGradle( }); createLaunchScriptsTask.configure(task -> task.dependsOn(launchScriptTask)); - tasks.register(InternalModelHelper.nameOfRun(run, "run", ""), RunGameTask.class, task -> { + var runTask = tasks.register(InternalModelHelper.nameOfRun(run, "run", ""), RunGameTask.class, task -> { task.setGroup(branding.publicTaskGroup()); task.setDescription("Runs the " + run.getName() + " Minecraft run configuration."); // Launch with the Java version used in the project var toolchainService = ExtensionUtils.findExtension(project, "javaToolchains", JavaToolchainService.class); task.getJavaLauncher().set(toolchainService.launcherFor(spec -> spec.getLanguageVersion().set(javaExtension.getToolchain().getLanguageVersion()))); - // Note: this contains both the runtimeClasspath configuration and the sourceset's outputs. - // This records a dependency on compiling and processing the resources of the source set. - task.getClasspathProvider().from(run.getSourceSet().map(SourceSet::getRuntimeClasspath)); + task.getClasspathProvider().from(runtimeClasspath); task.getGameDirectory().set(run.getGameDirectory()); task.getEnvironmentProperty().set(run.getEnvironment()); @@ -432,7 +450,46 @@ private static TaskProvider setupRunInGradle( task.getJvmArgumentProviders().add(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null)); }); - return prepareRunTask; + return new IdeRunConfiguration( + prepareRunTask, + runTask, + launchScriptTask, + project.provider(() -> shouldUseDedicatedVanillaRuntime(artifactsWorkflow, run))); + } + + private static ConfigurableFileCollection getDedicatedVanillaRuntimeClasspath( + Project project, + @Nullable ModDevArtifactsWorkflow artifactsWorkflow, + RunModel run, + Provider type, + Configuration devLaunchConfig) { + if (artifactsWorkflow == null) { + throw new IllegalStateException("A vanilla runtime classpath requires the ModDev artifacts workflow."); + } + + var vanillaRuntimeClasspath = project.getConfigurations().create(InternalModelHelper.nameOfRun(run, "", "vanillaRuntimeClasspath"), spec -> { + spec.setDescription("Contains the vanilla runtime classpath for run " + run.getName() + "."); + spec.setCanBeResolved(true); + spec.setCanBeConsumed(false); + spec.getDependencies().addLater(artifactsWorkflow.requestVanillaMinecraftClassesDependency()); + spec.extendsFrom(artifactsWorkflow.vanillaRuntimeDependencies()); + spec.extendsFrom(devLaunchConfig); + spec.attributes(attributes -> { + attributes.attributeProvider(MinecraftDistribution.ATTRIBUTE, type.map(t -> { + var name = t.equals("client") || t.equals("data") || t.equals("clientData") ? MinecraftDistribution.CLIENT : MinecraftDistribution.SERVER; + return project.getObjects().named(MinecraftDistribution.class, name); + })); + setNamedAttribute(project, attributes, Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME); + }); + }); + + return project.files(vanillaRuntimeClasspath); + } + + private static boolean shouldUseDedicatedVanillaRuntime(@Nullable ModDevArtifactsWorkflow artifactsWorkflow, RunModel run) { + return artifactsWorkflow != null + && artifactsWorkflow.dependencies().neoForgeDependency() != null + && run.getUseVanillaRunTemplates().getOrElse(false); } /** @@ -540,3 +597,9 @@ private static void setNamedAttribute(Project project, Attribu attributes.attribute(attribute, project.getObjects().named(attribute.getType(), value)); } } + +record IdeRunConfiguration( + TaskProvider prepareRunTask, + TaskProvider runTask, + TaskProvider launchScriptTask, + Provider usesDedicatedVanillaRuntime) {} diff --git a/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java b/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java index 9192640d..5b146cd4 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/NeoDevFacade.java @@ -35,6 +35,7 @@ public static void setupRuns(Project project, ModDevRunWorkflow.setupRuns( project, Branding.NEODEV, + null, argFileDir, runs, runTemplatesSourceFile, diff --git a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java index 116bb72f..1ed72f3f 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/PrepareRunOrTest.java @@ -124,12 +124,19 @@ abstract class PrepareRunOrTest extends DefaultTask { @Input public abstract Property getDevLogin(); + /** + * Use the built-in vanilla run templates even when loader-specific templates are available. + */ + @Input + public abstract Property getUseVanillaRunTemplates(); + private final ProgramArgsFormat programArgsFormat; protected PrepareRunOrTest(ProgramArgsFormat programArgsFormat) { this.programArgsFormat = programArgsFormat; getVersionCapabilities().convention(VersionCapabilitiesInternal.latest()); getDevLogin().convention(false); + getUseVanillaRunTemplates().convention(false); } protected abstract UserDevRunType resolveRunType(UserDevConfig userDevConfig); @@ -167,7 +174,7 @@ public void prepareRun() throws IOException { // If no NeoForge userdev config is set, we only support Vanilla run types UserDevRunType runConfig; - if (getRunTypeTemplatesSource().isEmpty()) { + if (getUseVanillaRunTemplates().get() || getRunTypeTemplatesSource().isEmpty()) { runConfig = resolveRunType(getSimulatedUserDevConfigForVanilla()); } else { var userDevConfig = loadUserDevConfig(getRunTypeTemplatesSource().getSingleFile()); diff --git a/src/main/java/net/neoforged/moddevgradle/internal/VsCodeIntegration.java b/src/main/java/net/neoforged/moddevgradle/internal/VsCodeIntegration.java index 342f042b..1328e52b 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/VsCodeIntegration.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/VsCodeIntegration.java @@ -1,17 +1,18 @@ package net.neoforged.moddevgradle.internal; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import net.neoforged.moddevgradle.dsl.RunModel; import net.neoforged.vsclc.BatchedLaunchWriter; import net.neoforged.vsclc.attribute.ConsoleType; +import net.neoforged.vsclc.attribute.LocatorPathLike; import net.neoforged.vsclc.attribute.PathLike; import net.neoforged.vsclc.attribute.ShortCmdBehaviour; import net.neoforged.vsclc.writer.WritingMode; import org.gradle.api.Project; -import org.gradle.api.tasks.TaskProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,15 +27,16 @@ final class VsCodeIntegration extends EclipseIntegration { } @Override - public void configureRuns(Map> prepareRunTasks, + public void configureRuns(Map ideRunConfigurations, Iterable runs) { // Set up runs if running under buildship and in VS Code project.afterEvaluate(ignored -> { var launchWriter = new BatchedLaunchWriter(WritingMode.MODIFY_CURRENT); for (var run : runs) { - var prepareTask = prepareRunTasks.get(run).get(); - addVscodeLaunchConfiguration(project, run, prepareTask, launchWriter); + var ideRunConfiguration = ideRunConfigurations.get(run); + var prepareTask = ideRunConfiguration.prepareRunTask().get(); + addVscodeLaunchConfiguration(project, run, prepareTask, ideRunConfiguration, launchWriter); } try { @@ -48,6 +50,7 @@ public void configureRuns(Map> prepareRunTask private void addVscodeLaunchConfiguration(Project project, RunModel run, PrepareRun prepareTask, + IdeRunConfiguration ideRunConfiguration, BatchedLaunchWriter launchWriter) { if (!prepareTask.getEnabled()) { LOG.info("Not creating VSCode run {} since its prepare task {} is disabled", run, prepareTask); @@ -60,6 +63,15 @@ private void addVscodeLaunchConfiguration(Project project, var runIdeName = run.getIdeName().get(); var eclipseProjectName = Objects.requireNonNullElse(eclipseModel.getProject().getName(), project.getName()); + var modFoldersProvider = getModFoldersProvider(project, run.getLoadedMods(), null); + var usesDedicatedVanillaRuntime = ideRunConfiguration.usesDedicatedVanillaRuntime().get(); + var additionalJvmArgs = new ArrayList(); + if (usesDedicatedVanillaRuntime) { + ideSyncTask.configure(task -> task.dependsOn(ideRunConfiguration.launchScriptTask())); + additionalJvmArgs.add(RunUtils.getArgFileParameter(ideRunConfiguration.launchScriptTask().get().getClasspathArgsFile().get())); + } + additionalJvmArgs.add(RunUtils.getArgFileParameter(prepareTask.getVmArgsFile().get())); + additionalJvmArgs.add(modFoldersProvider.getArgument()); // If the user wants to run tasks before the actual execution, we attach them to autoBuildTasks // Missing proper support - https://github.com/microsoft/vscode-java-debug/issues/1106 @@ -67,18 +79,19 @@ private void addVscodeLaunchConfiguration(Project project, eclipseModel.autoBuildTasks(run.getTasksBefore().toArray()); } - var modFoldersProvider = getModFoldersProvider(project, run.getLoadedMods(), null); - launchWriter.createGroup("Mod Development - " + project.getName(), WritingMode.REMOVE_EXISTING) + var launchConfiguration = launchWriter.createGroup("Mod Development - " + project.getName(), WritingMode.REMOVE_EXISTING) .createLaunchConfiguration() .withName(runIdeName) .withProjectName(eclipseProjectName) .withArguments(List.of(RunUtils.getArgFileParameter(prepareTask.getProgramArgsFile().get()))) - .withAdditionalJvmArgs(List.of(RunUtils.getArgFileParameter(prepareTask.getVmArgsFile().get()), - modFoldersProvider.getArgument())) + .withAdditionalJvmArgs(additionalJvmArgs) .withEnvironmentVariables(RunUtils.replaceModClassesEnv(run, modFoldersProvider)) .withMainClass(RunUtils.DEV_LAUNCH_MAIN_CLASS) .withShortenCommandLine(ShortCmdBehaviour.NONE) .withConsoleType(ConsoleType.INTERNAL_CONSOLE) .withCurrentWorkingDirectory(PathLike.ofNio(run.getGameDirectory().get().getAsFile().toPath())); + if (usesDedicatedVanillaRuntime) { + launchConfiguration.withClassPathsOverride(List.of()); + } } } diff --git a/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java b/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java index c1a4c86e..8ae2b5ec 100644 --- a/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java +++ b/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; @@ -38,6 +39,15 @@ protected final void writeProjectFile(String relativePath, String content) throw Files.writeString(destination, content); } + protected final String readTestResource(String resourcePath) throws IOException { + try (var stream = getClass().getClassLoader().getResourceAsStream(resourcePath)) { + if (stream == null) { + throw new IOException("Missing test resource: " + resourcePath); + } + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + } + void writeGroovySettingsScript(@Language("gradle") String text, Object... args) throws IOException { writeFile(settingsFile, interpolateTemplate(text, args)); } diff --git a/src/test/java/net/neoforged/moddevgradle/functional/VanillaRunFunctionalTest.java b/src/test/java/net/neoforged/moddevgradle/functional/VanillaRunFunctionalTest.java new file mode 100644 index 00000000..e7c57f8a --- /dev/null +++ b/src/test/java/net/neoforged/moddevgradle/functional/VanillaRunFunctionalTest.java @@ -0,0 +1,39 @@ +package net.neoforged.moddevgradle.functional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import org.gradle.testkit.runner.GradleRunner; +import org.gradle.testkit.runner.TaskOutcome; +import org.junit.jupiter.api.Test; + +public class VanillaRunFunctionalTest extends AbstractFunctionalTest { + @Test + public void testVanillaRunsUseDedicatedRuntimeClasspath() throws IOException { + writeFile(settingsFile, "rootProject.name = 'vanilla-runs'"); + writeGroovyBuildScript(readTestResource("net/neoforged/moddevgradle/functional/vanilla-runs.gradle")); + + var result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(testProjectDir) + .withArguments("assertVanillaRuns", "--stacktrace") + .build(); + + assertEquals(TaskOutcome.SUCCESS, result.task(":assertVanillaRuns").getOutcome()); + } + + @Test + public void testVanillaRunTaskGraphCanBeScheduled() throws IOException { + writeFile(settingsFile, "rootProject.name = 'vanilla-runs'"); + writeGroovyBuildScript(readTestResource("net/neoforged/moddevgradle/functional/vanilla-runs.gradle")); + + var result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(testProjectDir) + .withArguments("runVanillaClient", "--dry-run", "--stacktrace") + .build(); + + assertThat(result.getOutput()).contains(":runVanillaClient SKIPPED"); + } +} diff --git a/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java b/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java index 09acc348..64f57ff6 100644 --- a/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java +++ b/src/test/java/net/neoforged/moddevgradle/internal/ModDevPluginTest.java @@ -4,17 +4,31 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; import java.util.Set; +import java.util.function.Supplier; import net.neoforged.moddevgradle.AbstractProjectBuilderTest; import net.neoforged.moddevgradle.dsl.NeoForgeExtension; import net.neoforged.moddevgradle.internal.utils.ExtensionUtils; import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal; +import net.neoforged.nfrtgradle.CreateMinecraftArtifacts; import org.gradle.api.InvalidUserCodeException; +import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.artifacts.ExternalModuleDependency; +import org.gradle.api.artifacts.FileCollectionDependency; +import org.gradle.api.internal.project.ProjectInternal; +import org.gradle.api.plugins.ExtensionAware; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.SourceSet; import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.plugins.ide.idea.model.IdeaModel; import org.gradle.testfixtures.ProjectBuilder; +import org.jetbrains.gradle.ext.Gradle; +import org.jetbrains.gradle.ext.ProjectSettings; +import org.jetbrains.gradle.ext.RunConfigurationContainer; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -33,9 +47,7 @@ public ModDevPluginTest() { mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); testSourceSet = sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME); - // Set the Java version to the currently running Java to make it use that - var java = ExtensionUtils.getExtension(project, "java", JavaPluginExtension.class); - java.getToolchain().getLanguageVersion().set(JavaLanguageVersion.current()); + configureCurrentJavaToolchain(project); } @Test @@ -78,6 +90,109 @@ void testAddModdingDependenciesTo() { assertContainsModdingRuntimeDependencies("21.11.0", testSourceSet.getRuntimeClasspathConfigurationName()); } + @Test + void testVanillaRuntimeDependencyUsesKnownArtifactPath() { + extension.setVersion("21.11.0"); + + var dependency = ModDevArtifactsWorkflow.get(project) + .requestVanillaMinecraftClassesDependency() + .get(); + + assertThat(describeDependency(dependency)) + .isEqualTo("build/moddev/artifacts/vanilla-runtime-1.21.11.jar"); + assertThat(dependency).isInstanceOf(FileCollectionDependency.class); + assertThat(((FileCollectionDependency) dependency).getFiles().getBuildDependencies().getDependencies(null)) + .extracting(Task::getName) + .containsOnly("createMinecraftArtifacts"); + + var createArtifacts = project.getTasks().named("createMinecraftArtifacts", CreateMinecraftArtifacts.class).get(); + assertThat(createArtifacts.getAdditionalResults().get()) + .containsOnlyKeys("vanillaDeobfuscated"); + } + + @Test + void testVanillaRuntimeDependenciesDoNotUseNeoForge() { + extension.setVersion("21.11.0"); + + assertThatDependencies("modDevVanillaRuntimeDependencies") + .containsOnly("net.neoforged:minecraft-dependencies:1.21.11"); + assertDoesNotContainNeoForgeDependency("modDevVanillaRuntimeDependencies"); + } + + @Test + void testVanillaServerRunDefaultsToDedicatedVanillaRuntime() { + extension.getMods().create("mainmod", mod -> mod.sourceSet(mainSourceSet)); + extension.getRuns().create("vanilla", run -> run.vanillaServer()); + + var run = extension.getRuns().getByName("vanilla"); + assertThat(run.getType().get()).isEqualTo("server"); + assertThat(run.getUseVanillaRunTemplates().get()).isTrue(); + assertThat(run.getLoadedMods().get()).isEmpty(); + } + + @Test + void testVanillaClientRunDefaultsToDedicatedVanillaRuntime() { + extension.getMods().create("mainmod", mod -> mod.sourceSet(mainSourceSet)); + extension.getRuns().create("vanilla", run -> run.vanillaClient()); + + var run = extension.getRuns().getByName("vanilla"); + assertThat(run.getType().get()).isEqualTo("client"); + assertThat(run.getUseVanillaRunTemplates().get()).isTrue(); + assertThat(run.getLoadedMods().get()).isEmpty(); + } + + @Test + void testVanillaIntelliJRunUsesGradleTaskClasspath() { + withSystemProperty("idea.sync.active", "true", () -> { + var ideProject = createProjectWithModDevPlugin(); + createVanillaServerRun(ideProject); + + evaluateProject(ideProject); + + var runConfiguration = getIntelliJRunConfigurations(ideProject).getByName("Vanilla"); + assertThat(runConfiguration).isInstanceOf(Gradle.class); + + var runConfigurationData = runConfiguration.toMap(); + assertThat(runConfigurationData.get("type")).isEqualTo("gradle"); + assertThat(runConfigurationData.get("taskNames")).isEqualTo(List.of(":runVanilla")); + }); + } + + @Test + void testVanillaEclipseRunUsesGradleTaskClasspath() throws IOException { + Project ideProject = withSystemProperty("eclipse.application", "org.eclipse.buildship.core", () -> { + var result = createProjectWithModDevPlugin(); + createVanillaServerRun(result); + + evaluateProject(result); + return result; + }); + + var launchConfig = Files.readString(ideProject.file(".eclipse/configurations/Vanilla.launch").toPath()); + assertThat(launchConfig) + .contains("org.eclipse.buildship.core.launch.runconfiguration") + .contains(":runVanilla") + .doesNotContain("org.eclipse.jdt.launching.localJavaApplication"); + } + + @Test + void testVanillaVsCodeRunUsesGeneratedClasspathArgfile() throws IOException { + var ideProject = createProjectWithVsCodeIntegration(); + createVanillaServerRun(ideProject); + + evaluateProject(ideProject); + + var launchJson = Files.readString(ideProject.file(".vscode/launch.json").toPath()); + assertThat(launchJson) + .contains("\"classPaths\": []") + .contains("vanillaRunClasspath.txt") + .doesNotContain("\"$Runtime\""); + + assertThat(ideProject.getTasks().named("neoForgeIdeSync").get().getTaskDependencies().getDependencies(null)) + .extracting(Task::getName) + .contains("createVanillaLaunchScript"); + } + @Test void testGetVersion() { extension.setVersion("2.3.0"); @@ -188,4 +303,79 @@ private void assertContainsModdingRuntimeDependencies(String version, String con "build/moddev/artifacts/neoforge-" + version + "-client-extra-aka-minecraft-resources.jar", "net.neoforged:neoforge:" + version + "[net.neoforged:neoforge-dependencies]"); } + + private void assertDoesNotContainNeoForgeDependency(String configurationName) { + var dependencies = project.getConfigurations().getByName(configurationName).getAllDependencies(); + assertThat(dependencies).allSatisfy(dependency -> { + if (dependency instanceof ExternalModuleDependency moduleDependency) { + assertThat(moduleDependency.getName()).isNotEqualTo("neoforge"); + assertThat(moduleDependency.getRequestedCapabilities()) + .extracting("name") + .doesNotContain("neoforge-dependencies", "neoforge-moddev-bundle", "neoforge-moddev-config", "neoforge-moddev-module-path"); + } + }); + } + + private static Project createProjectWithModDevPlugin() { + var result = ProjectBuilder.builder().build(); + result.getPlugins().apply(ModDevPlugin.class); + configureCurrentJavaToolchain(result); + return result; + } + + private static Project createProjectWithVsCodeIntegration() { + var result = ProjectBuilder.builder().build(); + result.getExtensions().add(IdeIntegration.class, "mdgInternalIdeIntegration", new VsCodeIntegration(result, Branding.MDG)); + result.getPlugins().apply(ModDevPlugin.class); + configureCurrentJavaToolchain(result); + return result; + } + + private static void configureCurrentJavaToolchain(Project project) { + var java = ExtensionUtils.getExtension(project, "java", JavaPluginExtension.class); + java.getToolchain().getLanguageVersion().set(JavaLanguageVersion.current()); + } + + private static NeoForgeExtension getNeoForgeExtension(Project project) { + return ExtensionUtils.getExtension(project, "neoForge", NeoForgeExtension.class); + } + + private static void createVanillaServerRun(Project project) { + var extension = getNeoForgeExtension(project); + extension.setVersion("21.11.0"); + extension.getRuns().create("vanilla", run -> run.vanillaServer()); + } + + private static void evaluateProject(Project project) { + ((ProjectInternal) project).evaluate(); + } + + private static RunConfigurationContainer getIntelliJRunConfigurations(Project project) { + var rootIdeaModel = ExtensionUtils.getExtension(project.getRootProject(), "idea", IdeaModel.class); + var projectSettings = ((ExtensionAware) rootIdeaModel.getProject()).getExtensions().getByType(ProjectSettings.class); + var runConfigurations = ExtensionUtils.findExtension((ExtensionAware) projectSettings, "runConfigurations", RunConfigurationContainer.class); + assertThat(runConfigurations).isNotNull(); + return runConfigurations; + } + + private static void withSystemProperty(String name, String value, Runnable runnable) { + withSystemProperty(name, value, () -> { + runnable.run(); + return null; + }); + } + + private static T withSystemProperty(String name, String value, Supplier supplier) { + var previousValue = System.getProperty(name); + System.setProperty(name, value); + try { + return supplier.get(); + } finally { + if (previousValue == null) { + System.clearProperty(name); + } else { + System.setProperty(name, previousValue); + } + } + } } diff --git a/src/test/resources/net/neoforged/moddevgradle/functional/vanilla-runs.gradle b/src/test/resources/net/neoforged/moddevgradle/functional/vanilla-runs.gradle new file mode 100644 index 00000000..d57e8589 --- /dev/null +++ b/src/test/resources/net/neoforged/moddevgradle/functional/vanilla-runs.gradle @@ -0,0 +1,65 @@ +import org.gradle.api.artifacts.ExternalModuleDependency + +plugins { + id "net.neoforged.moddev" +} + +neoForge { + version = "{DEFAULT_NEOFORGE_VERSION}" + + mods { + testmod { + sourceSet sourceSets.main + } + } + + runs { + regularServer { + server() + sourceSet = sourceSets.test + } + vanillaClient { + vanillaClient() + } + vanillaServer { + vanillaServer() + } + } +} + +def assertNoNeoForgeDependencies = { dependencies -> + def neoForgeDependencies = dependencies.findAll { dependency -> + dependency instanceof ExternalModuleDependency + && (dependency.name == "neoforge" + || dependency.requestedCapabilities.any { it.name.startsWith("neoforge-") }) + } + if (!neoForgeDependencies.empty) { + throw new GradleException("Unexpected NeoForge dependencies: ${neoForgeDependencies}") + } +} + +tasks.register("assertVanillaRuns") { + doLast { + assert configurations.modDevVanillaRuntimeDependencies.allDependencies*.name == ["minecraft-dependencies"] + assertNoNeoForgeDependencies(configurations.modDevVanillaRuntimeDependencies.allDependencies) + + ["vanillaClient", "vanillaServer"].each { runName -> + def run = neoForge.runs.named(runName).get() + assert run.loadedMods.get().empty + + def vanillaRuntimeClasspath = configurations.getByName("${runName}VanillaRuntimeClasspath") + assert vanillaRuntimeClasspath.extendsFrom*.name.contains("modDevVanillaRuntimeDependencies") + assert vanillaRuntimeClasspath.extendsFrom*.name.contains("devLaunchConfig") + assert !vanillaRuntimeClasspath.extendsFrom*.name.contains("modDevRuntimeDependencies") + assert !vanillaRuntimeClasspath.extendsFrom*.name.contains("${runName}AdditionalRuntimeClasspath") + assertNoNeoForgeDependencies(vanillaRuntimeClasspath.allDependencies) + + def legacyClasspath = configurations.getByName("${runName}LegacyClasspath") + assert legacyClasspath.dependencies.empty + assert legacyClasspath.extendsFrom.empty + } + + def regularRuntimeClasspath = configurations.getByName(sourceSets.test.runtimeClasspathConfigurationName) + assert regularRuntimeClasspath.extendsFrom*.name.contains("devLaunchConfig") + } +}