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 a3f7ef17..64e1c129 100644 --- a/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeModDevPlugin.java +++ b/src/legacy/java/net/neoforged/moddevgradle/legacyforge/internal/LegacyForgeModDevPlugin.java @@ -154,7 +154,8 @@ public void enable(Project project, LegacyForgeModdingSettings settings, LegacyF configurations.getByName(DataFileCollections.CONFIGURATION_ACCESS_TRANSFORMERS), configurations.getByName(DataFileCollections.CONFIGURATION_INTERFACE_INJECTION_DATA), versionCapabilities, - settings.isDisableRecompilation()); + settings.isDisableRecompilation(), + false); var runs = ModDevRunWorkflow.create( project, diff --git a/src/main/java/net/neoforged/moddevgradle/dsl/ModModel.java b/src/main/java/net/neoforged/moddevgradle/dsl/ModModel.java index bc259127..7ef1e12d 100644 --- a/src/main/java/net/neoforged/moddevgradle/dsl/ModModel.java +++ b/src/main/java/net/neoforged/moddevgradle/dsl/ModModel.java @@ -15,6 +15,8 @@ public ModModel() { // TODO: We could potentially do a bit of name validation getModSourceSets().convention(List.of()); getModSourceSets().finalizeValueOnRead(); + getModClientSourceSets().convention(List.of()); + getModClientSourceSets().finalizeValueOnRead(); } @Override @@ -23,7 +25,13 @@ public ModModel() { // Do not name getSourceSets or it will conflict with project.sourceSets in scripts. public abstract ListProperty getModSourceSets(); + public abstract ListProperty getModClientSourceSets(); + public void sourceSet(SourceSet sourceSet) { getModSourceSets().add(sourceSet); } + + public void clientSourceSet(SourceSet sourceSet) { + getModClientSourceSets().add(sourceSet); + } } diff --git a/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java b/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java index d6a1da9b..dc1a80e8 100644 --- a/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java +++ b/src/main/java/net/neoforged/moddevgradle/dsl/ModdingVersionSettings.java @@ -15,6 +15,8 @@ public abstract class ModdingVersionSettings { @Nullable private String neoFormVersion; + private boolean splitDist = false; + private Set enabledSourceSets = new HashSet<>(); private boolean disableRecompilation = "true".equals(System.getenv("CI")); @@ -77,4 +79,12 @@ public boolean isDisableRecompilation() { public void setDisableRecompilation(boolean disableRecompilation) { this.disableRecompilation = disableRecompilation; } + + public boolean isSplitDist() { + return splitDist; + } + + public void setSplitDist(boolean enable) { + this.splitDist = enable; + } } diff --git a/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java b/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java index 40e97557..32cef5e0 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/EclipseIntegration.java @@ -154,7 +154,7 @@ private void addEclipseLaunchConfiguration(Project project, } // This is the actual main launch configuration that launches the game - var modFoldersProvider = getModFoldersProvider(project, run.getLoadedMods(), null); + var modFoldersProvider = getModFoldersProvider(project, run.getLoadedMods(), null, run.getType()); var config = JavaApplicationLaunchConfig.builder(eclipseProjectName) .vmArgs( RunUtils.escapeJvmArg(RunUtils.getArgFileParameter(prepareTask.getVmArgsFile().get())), @@ -168,12 +168,13 @@ private void addEclipseLaunchConfiguration(Project project, protected static ModFoldersProvider getModFoldersProvider(Project project, Provider> modsProvider, - @Nullable Provider testedMod) { + @Nullable Provider testedMod, + Provider runType) { var folders = RunUtils.buildModFolders(project, modsProvider, testedMod, (sourceSet, output) -> { output.from(RunUtils.findSourceSetProject(project, sourceSet).getProjectDir().toPath() .resolve("bin") .resolve(sourceSet.getName())); - }); + }, runType); var modFoldersProvider = project.getObjects().newInstance(ModFoldersProvider.class); modFoldersProvider.getModFolders().set(folders); diff --git a/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java b/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java index 50f08e80..d1a4dcb7 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/IntelliJIntegration.java @@ -122,7 +122,7 @@ public void configureTesting(Provider> loadedMods, var intellijVmArgsFile = runArgsDir.map(dir -> dir.file("intellijVmArgs.txt")); var outputDirectory = IntelliJOutputDirectoryValueSource.getIntellijOutputDirectory(project); - var ideSpecificVmArgs = RunUtils.escapeJvmArg(getModFoldersProvider(project, outputDirectory, loadedMods, testedMod).getArgument()); + var ideSpecificVmArgs = RunUtils.escapeJvmArg(getModFoldersProvider(project, outputDirectory, loadedMods, testedMod, project.provider(() -> "client")).getArgument()); try { var vmArgsFilePath = intellijVmArgsFile.get().getAsFile().toPath(); Files.createDirectories(vmArgsFilePath.getParent()); @@ -185,7 +185,7 @@ private static void addIntelliJRunConfiguration(Project project, } appRun.setModuleName(getIntellijModuleName(project, sourceSet)); appRun.setWorkingDirectory(run.getGameDirectory().get().getAsFile().getAbsolutePath()); - var modFoldersProvider = getModFoldersProvider(project, outputDirectory, run.getLoadedMods(), null); + var modFoldersProvider = getModFoldersProvider(project, outputDirectory, run.getLoadedMods(), null, run.getType()); appRun.setEnvs(RunUtils.replaceModClassesEnv(run, modFoldersProvider)); appRun.setJvmArgs( RunUtils.escapeJvmArg(RunUtils.getArgFileParameter(prepareTask.getVmArgsFile().get())) @@ -232,15 +232,16 @@ public boolean shouldUseCombinedSourcesAndClassesArtifact() { private static ModFoldersProvider getModFoldersProvider(Project project, @Nullable Function outputDirectory, Provider> modsProvider, - @Nullable Provider testedMod) { + @Nullable Provider testedMod, + Provider runType) { Provider> folders; if (outputDirectory != null) { folders = RunUtils.buildModFolders(project, modsProvider, testedMod, (sourceSet, output) -> { var sourceSetDir = outputDirectory.apply(RunUtils.findSourceSetProject(project, sourceSet)).toPath().resolve(getIdeaOutName(sourceSet)); output.from(sourceSetDir.resolve("classes"), sourceSetDir.resolve("resources")); - }); + }, runType); } else { - folders = RunUtils.getModFoldersForGradle(project, modsProvider, testedMod); + folders = RunUtils.getModFoldersForGradle(project, modsProvider, testedMod, runType); } var modFoldersProvider = project.getObjects().newInstance(ModFoldersProvider.class); diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java index 40b1723d..a8465d84 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevArtifactsWorkflow.java @@ -11,6 +11,7 @@ import net.neoforged.moddevgradle.internal.utils.VersionCapabilitiesInternal; import net.neoforged.nfrtgradle.CreateMinecraftArtifacts; import net.neoforged.nfrtgradle.DownloadAssets; +import net.neoforged.nfrtgradle.SplitMergedJar; import org.gradle.api.GradleException; import org.gradle.api.InvalidUserCodeException; import org.gradle.api.Named; @@ -28,6 +29,7 @@ import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.toolchain.JavaLanguageVersion; import org.gradle.jvm.toolchain.JavaToolchainService; @@ -47,6 +49,7 @@ public record ModDevArtifactsWorkflow( TaskProvider downloadAssets, Configuration runtimeDependencies, Configuration compileDependencies, + Configuration clientExtraCompileDependencies, Provider modDevBuildDir, Provider artifactsBuildDir) { @@ -68,7 +71,8 @@ public static ModDevArtifactsWorkflow create(Project project, Configuration accessTransformers, Configuration interfaceInjectionData, VersionCapabilitiesInternal versionCapabilities, - boolean disableRecompilation) { + boolean disableRecompilation, + boolean splitDist) { if (project.getExtensions().findByName(EXTENSION_NAME) != null) { throw new InvalidUserCodeException("You cannot enable modding in the same project twice."); } @@ -115,6 +119,8 @@ public static ModDevArtifactsWorkflow create(Project project, spec.getDependencies().addLater(parchment.getParchmentArtifact().map(dependencyFactory::create)); }); + Function> artifactPathStrategy = artifact -> artifactsBuildDir.map(dir -> dir.file(artifactNamingStrategy.getFilename(artifact))); + // 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()); @@ -151,8 +157,6 @@ public static ModDevArtifactsWorkflow create(Project project, task.getParchmentEnabled().set(parchment.getEnabled()); task.getParchmentConflictResolutionPrefix().set(parchment.getConflictResolutionPrefix()); - Function> artifactPathStrategy = artifact -> artifactsBuildDir.map(dir -> dir.file(artifactNamingStrategy.getFilename(artifact))); - task.getIncludeNeoForgeInGameJar().set(versionCapabilities.needsNeoForgeInMinecraftJar()); task.getGameJarArtifact().set(artifactPathStrategy.apply(WorkflowArtifact.COMPILED)); if (disableRecompilation) { @@ -185,6 +189,24 @@ public static ModDevArtifactsWorkflow create(Project project, task.getNeoFormArtifact().set(moddingDependencies.neoFormDependencyNotation()); }); + var splitMergedJar = tasks.register("splitMergedJar", SplitMergedJar.class, task -> { + task.getClientResourcesJar().set(createArtifacts.flatMap(CreateMinecraftArtifacts::getResourcesArtifact)); + task.getClientJar().set(artifactPathStrategy.apply(WorkflowArtifact.CLIENT)); + task.getCommonJar().set(artifactPathStrategy.apply(WorkflowArtifact.COMMON)); + if (!disableRecompilation) { + task.getMergedJar().set(createArtifacts.flatMap(CreateMinecraftArtifacts::getGameJarWithSourcesArtifact)); + task.getClientSourcesJar().set(artifactPathStrategy.apply(WorkflowArtifact.CLIENT_SOURCES)); + task.getCommonSourcesJar().set(artifactPathStrategy.apply(WorkflowArtifact.COMMON_SOURCES)); + } else { + task.getMergedJar().set(createArtifacts.flatMap(CreateMinecraftArtifacts::getGameJarArtifact)); + } + + }); + + if (splitDist) { + ideIntegration.runTaskOnProjectSync(splitMergedJar); + } + // 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; @@ -218,13 +240,26 @@ 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); + if (!splitDist) { + config.getDependencies().addLater(minecraftClassesDependency); + } else { + config.getDependencies().addLater(splitMergedJar.map(task -> project.files(task.getCommonJar())).map(dependencyFactory::create)); + } config.getDependencies().add(moddingDependencies.gameLibrariesDependency()); if (!versionCapabilities.needsNeoForgeInMinecraftJar() && moddingDependencies.neoForgeDependency() != null) { config.getDependencies().add(moddingDependencies.neoForgeDependency()); } }); + var clientExtraCompileDependencies = configurations.create("modDevClientCompileDependencies", config -> { + config.setDescription("The extra client compile-time dependencies to develop a mod, including Minecraft and modding platform classes."); + config.setCanBeResolved(false); + config.setCanBeConsumed(false); + if (splitDist) { + config.getDependencies().addLater(splitMergedJar.map(task -> project.files(task.getClientJar())).map(dependencyFactory::create)); + } + }); + // For IDEs that support it, link the source/binary artifacts if we use separated ones if (!disableRecompilation && !ideIntegration.shouldUseCombinedSourcesAndClassesArtifact()) { ideIntegration.attachSources( @@ -242,13 +277,22 @@ public static ModDevArtifactsWorkflow create(Project project, downloadAssets, runtimeDependencies, compileDependencies, + clientExtraCompileDependencies, modDevBuildDir, artifactsBuildDir); project.getExtensions().add(ModDevArtifactsWorkflow.class, EXTENSION_NAME, result); for (var sourceSets : enabledSourceSets) { - result.addToSourceSet(sourceSets); + result.addToSourceSet(sourceSets, !splitDist); + } + + if (splitDist) { + SourceSetContainer sourceSets = ExtensionUtils.getSourceSets(project); + var main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); + SourceSet client = sourceSets.create("client"); + client.setCompileClasspath(client.getCompileClasspath().plus(main.getOutput())); + result.addToSourceSet(client, true); } return result; @@ -351,14 +395,23 @@ private static List configureArtifactManifestConfigurations( * Adds the compile-time and runtime-dependencies needed to compile mod code to the source-set of the given name. */ public void addToSourceSet(SourceSet sourceSet) { + addToSourceSet(sourceSet, true); + } + + public void addToSourceSet(SourceSet sourceSet, boolean includeClient) { var configurations = project.getConfigurations(); var sourceSets = ExtensionUtils.getSourceSets(project); if (!sourceSets.contains(sourceSet)) { throw new GradleException("Cannot add to the source set in another project: " + sourceSet); } - configurations.getByName(sourceSet.getRuntimeClasspathConfigurationName()).extendsFrom(runtimeDependencies); - configurations.getByName(sourceSet.getCompileClasspathConfigurationName()).extendsFrom(compileDependencies); + Configuration runtime = configurations.getByName(sourceSet.getRuntimeClasspathConfigurationName()); + runtime.extendsFrom(runtimeDependencies); + Configuration compile = configurations.getByName(sourceSet.getCompileClasspathConfigurationName()); + compile.extendsFrom(compileDependencies); + if (includeClient) { + compile.extendsFrom(clientExtraCompileDependencies); + } } public Provider requestAdditionalMinecraftArtifact(String id, String filename) { diff --git a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java index 97f0e7f5..2646c33a 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevPlugin.java @@ -98,7 +98,8 @@ public void enable( configurations.getByName(DataFileCollections.CONFIGURATION_ACCESS_TRANSFORMERS), configurations.getByName(DataFileCollections.CONFIGURATION_INTERFACE_INJECTION_DATA), versionCapabilities, - settings.isDisableRecompilation()); + settings.isDisableRecompilation(), + settings.isSplitDist()); 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 d6be0f49..db574df6 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/ModDevRunWorkflow.java @@ -272,7 +272,7 @@ public static void setupRuns( 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()); + run.getEnvironment().put("MOD_CLASSES", RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null, run.getType()).getClassesArgument()); } var prepareRunTask = setupRunInGradle( project, @@ -405,7 +405,7 @@ private static TaskProvider setupRunInGradle( task.getVmArgsFile().set(prepareRunTask.get().getVmArgsFile().map(d -> d.getAsFile().getAbsolutePath())); task.getProgramArgsFile().set(prepareRunTask.get().getProgramArgsFile().map(d -> d.getAsFile().getAbsolutePath())); task.getEnvironment().set(run.getEnvironment()); - task.getModFolders().set(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null)); + task.getModFolders().set(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null, run.getType())); }); createLaunchScriptsTask.configure(task -> task.dependsOn(launchScriptTask)); @@ -428,8 +428,7 @@ private static TaskProvider setupRunInGradle( // Of course we need the arg files to be up-to-date ;) task.dependsOn(prepareRunTask); task.dependsOn(run.getTasksBefore()); - - task.getJvmArgumentProviders().add(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null)); + task.getJvmArgumentProviders().add(RunUtils.getGradleModFoldersProvider(project, run.getLoadedMods(), null, run.getType())); }); return prepareRunTask; @@ -524,7 +523,7 @@ static void setupTestTask(Project project, task.systemProperty("fml.junit.argsfile", programArgsFile.get().getAsFile().getAbsolutePath()); task.jvmArgs(RunUtils.getArgFileParameter(vmArgsFile.get())); - var modFoldersProvider = RunUtils.getGradleModFoldersProvider(project, loadedMods, testedMod); + var modFoldersProvider = RunUtils.getGradleModFoldersProvider(project, loadedMods, testedMod, project.provider(() -> "client")); task.getJvmArgumentProviders().add(modFoldersProvider); }); diff --git a/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java b/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java index c913ce82..c0bbedbc 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/RunUtils.java @@ -171,9 +171,9 @@ public static String getArgFileParameter(RegularFile argFile) { return "@" + argFile.getAsFile().getAbsolutePath(); } - public static ModFoldersProvider getGradleModFoldersProvider(Project project, Provider> modsProvider, Provider testedMod) { + public static ModFoldersProvider getGradleModFoldersProvider(Project project, Provider> modsProvider, Provider testedMod, Provider runType) { var modFoldersProvider = project.getObjects().newInstance(ModFoldersProvider.class); - modFoldersProvider.getModFolders().set(getModFoldersForGradle(project, modsProvider, testedMod)); + modFoldersProvider.getModFolders().set(getModFoldersForGradle(project, modsProvider, testedMod, runType)); return modFoldersProvider; } @@ -215,16 +215,18 @@ public static Map replaceModClassesEnv(RunModel model, ModFolder public static Provider> getModFoldersForGradle(Project project, Provider> modsProvider, - @Nullable Provider testedMod) { + @Nullable Provider testedMod, + Provider runType) { return buildModFolders(project, modsProvider, testedMod, (sourceSet, output) -> { output.from(sourceSet.getOutput()); - }); + }, runType); } public static Provider> buildModFolders(Project project, Provider> modsProvider, @Nullable Provider testedModProvider, - BiConsumer outputFolderResolver) { + BiConsumer outputFolderResolver, + Provider runType) { // Convert it to optional to ensure zip will be called even if no mod under test is present. if (testedModProvider == null) { testedModProvider = project.provider(() -> null); @@ -253,6 +255,18 @@ public static Provider> buildModFolders(Project project, outputFolderResolver.accept(sourceSet, modFolder.getFolders()); } + boolean isClient = runType.get().startsWith("client"); + if (isClient) { + var clientSourceSets = mod.getModClientSourceSets().get(); + for (int i = 0; i < clientSourceSets.size(); ++i) { + var sourceSet = clientSourceSets.get(i); + if (clientSourceSets.subList(0, i).contains(sourceSet)) { + throw new InvalidUserCodeException("Duplicate source set '%s' in mod '%s'".formatted(sourceSet.getName(), mod.getName())); + } + outputFolderResolver.accept(sourceSet, modFolder.getFolders()); + } + } + // Add the test source set to the mod under test and if unit tests are enabled if (testedMod.isPresent() && testedMod.get() == mod) { var testSourceSet = ExtensionUtils.getSourceSets(project).findByName(SourceSet.TEST_SOURCE_SET_NAME); diff --git a/src/main/java/net/neoforged/moddevgradle/internal/VsCodeIntegration.java b/src/main/java/net/neoforged/moddevgradle/internal/VsCodeIntegration.java index 342f042b..67d8c1d9 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/VsCodeIntegration.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/VsCodeIntegration.java @@ -67,7 +67,7 @@ private void addVscodeLaunchConfiguration(Project project, eclipseModel.autoBuildTasks(run.getTasksBefore().toArray()); } - var modFoldersProvider = getModFoldersProvider(project, run.getLoadedMods(), null); + var modFoldersProvider = getModFoldersProvider(project, run.getLoadedMods(), null, run.getType()); launchWriter.createGroup("Mod Development - " + project.getName(), WritingMode.REMOVE_EXISTING) .createLaunchConfiguration() .withName(runIdeName) diff --git a/src/main/java/net/neoforged/moddevgradle/internal/WorkflowArtifact.java b/src/main/java/net/neoforged/moddevgradle/internal/WorkflowArtifact.java index 2d16251d..89496478 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/WorkflowArtifact.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/WorkflowArtifact.java @@ -6,6 +6,10 @@ public enum WorkflowArtifact { COMPILED(""), COMPILED_WITH_SOURCES("-merged"), + COMMON("-common"), + COMMON_SOURCES("-common-sources"), + CLIENT("-client"), + CLIENT_SOURCES("-client-sources"), SOURCES("-sources"), CLIENT_RESOURCES("-client-extra-aka-minecraft-resources"); diff --git a/src/main/java/net/neoforged/nfrtgradle/SplitMergedJar.java b/src/main/java/net/neoforged/nfrtgradle/SplitMergedJar.java new file mode 100644 index 00000000..deefec3c --- /dev/null +++ b/src/main/java/net/neoforged/nfrtgradle/SplitMergedJar.java @@ -0,0 +1,113 @@ +package net.neoforged.nfrtgradle; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import javax.inject.Inject; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.jetbrains.annotations.Nullable; + +public abstract class SplitMergedJar extends DefaultTask { + @Inject + public SplitMergedJar() {} + + @InputFile + public abstract RegularFileProperty getClientResourcesJar(); + + @InputFile + public abstract RegularFileProperty getMergedJar(); + + @OutputFile + public abstract RegularFileProperty getCommonJar(); + + @OutputFile + @Optional + public abstract RegularFileProperty getCommonSourcesJar(); + + @OutputFile + public abstract RegularFileProperty getClientJar(); + + @OutputFile + @Optional + public abstract RegularFileProperty getClientSourcesJar(); + + @TaskAction + public void splitMergedJar() throws IOException { + if (!getClientResourcesJar().isPresent()) { + throw new IllegalStateException("Can't request split dist result when splitDist is disabled!"); + } + try ( + var clientResources = new JarFile(getClientResourcesJar().get().getAsFile()); + var merged = new ZipInputStream(new BufferedInputStream(Files.newInputStream(getMergedJar().get().getAsFile().toPath()))); + var common = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(getCommonJar().get().getAsFile().toPath()))); + var client = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(getClientJar().get().getAsFile().toPath())))) { + + var manifest = clientResources.getManifest(); + + if (getCommonSourcesJar().isPresent() && getClientSourcesJar().isPresent()) { + try ( + var commonSources = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(getCommonSourcesJar().get().getAsFile().toPath()))); + var clientSources = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(getClientSourcesJar().get().getAsFile().toPath())))) { + spiltHelper(manifest, merged, common, client, commonSources, clientSources); + } + } else { + spiltHelper(manifest, merged, common, client, null, null); + } + + } + } + + private static void spiltHelper( + Manifest manifest, + ZipInputStream merged, ZipOutputStream common, ZipOutputStream client, + @Nullable ZipOutputStream commonSources, @Nullable ZipOutputStream clientSources) throws IOException { + var sourceDistName = new Attributes.Name("Minecraft-Dist"); + for (var entry = merged.getNextEntry(); entry != null; entry = merged.getNextEntry()) { + if (entry.isDirectory()) { + continue; + } + + var name = entry.getName(); + ZipOutputStream commonTarget = common; + ZipOutputStream clientTarget = client; + Attributes fileEntry = null; + if (name.endsWith(".class")) { + fileEntry = manifest.getEntries().get(name); + } else if (name.endsWith(".java")) { + fileEntry = manifest.getEntries().get(name.replace(".java", ".class")); + if (commonSources != null && clientSources != null) { + commonTarget = commonSources; + clientTarget = clientSources; + } + } + String dist = null; + + if (fileEntry != null) { + dist = fileEntry.getValue(sourceDistName); + } else if (name.startsWith("net/neoforged/neoforge/client")) { + dist = "client"; + } + + if ("client".equals(dist)) { + clientTarget.putNextEntry(entry); + merged.transferTo(clientTarget); + clientTarget.closeEntry(); + } else { + commonTarget.putNextEntry(entry); + merged.transferTo(commonTarget); + commonTarget.closeEntry(); + } + } + } +} diff --git a/testproject/settings.gradle b/testproject/settings.gradle index 952a551e..56ce33d8 100644 --- a/testproject/settings.gradle +++ b/testproject/settings.gradle @@ -21,5 +21,6 @@ include 'subproject' include 'common' include 'jijtest' include 'coremod' +include 'split' enableFeaturePreview "STABLE_CONFIGURATION_CACHE" diff --git a/testproject/split/build.gradle b/testproject/split/build.gradle new file mode 100644 index 00000000..94b509f3 --- /dev/null +++ b/testproject/split/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'net.neoforged.moddev' + id 'maven-publish' +} + +neoForge { + enable { + version = project.neoforge_version + splitDist = true + } + runs { + "client" { + client() + } + + "server" { + server() + } + } + mods { + splittest { + sourceSet sourceSets.main + clientSourceSet sourceSets.client + } + } +} \ No newline at end of file diff --git a/testproject/split/src/client/java/split/client/ClientMain.java b/testproject/split/src/client/java/split/client/ClientMain.java new file mode 100644 index 00000000..97344fa2 --- /dev/null +++ b/testproject/split/src/client/java/split/client/ClientMain.java @@ -0,0 +1,17 @@ +package split.client; + +import net.minecraft.client.Minecraft; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.fml.common.Mod; +import split.SplitMain; + +@Mod(value = "splittest", dist = Dist.CLIENT) +public class ClientMain { + + public ClientMain() { + Minecraft instance = Minecraft.getInstance(); + System.out.println("from client"); + System.out.println(SplitMain.class); + } + +} diff --git a/testproject/split/src/main/java/split/SplitMain.java b/testproject/split/src/main/java/split/SplitMain.java new file mode 100644 index 00000000..fe8c40d3 --- /dev/null +++ b/testproject/split/src/main/java/split/SplitMain.java @@ -0,0 +1,10 @@ +package split; + +import net.neoforged.fml.common.Mod; + +@Mod("splittest") +public class SplitMain { + public SplitMain() { + + } +} diff --git a/testproject/split/src/main/resources/META-INF/neoforge.mods.toml b/testproject/split/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..a65ab0dc --- /dev/null +++ b/testproject/split/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,96 @@ +# This is an example neoforge.mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml + +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license="CC0" + +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional + + +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory + +# The modid of the mod +modId="splittest" #mandatory + +# The version number of the mod +version="0.0.0" + +# A display name for the mod +displayName="Test Split Project" #mandatory + +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional + +# A URL for the "homepage" for this mod, displayed in the mod UI +#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional + +# A file name (in the root of the mod JAR) containing a logo for display +#logoFile="examplemod.png" #optional + +# A text field displayed in the mod UI +#credits="" #optional + +# A text field displayed in the mod UI +#authors="${mod_authors}" #optional + +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description='''A test project.''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +#[[mixins]] +#config="${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies.splittest]] #optional +# the modid of the dependency +modId="neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +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.10.0-beta,)" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering="NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side="BOTH" + +# Here's another dependency +[[dependencies.splittest]] +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.10]" +ordering="NONE" +side="BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features.${mod_id}] +#openGLVersion="[3.2,)"