diff --git a/.github/workflows/build-prs.yml b/.github/workflows/build-prs.yml index 91e34b4f2..c383d133c 100644 --- a/.github/workflows/build-prs.yml +++ b/.github/workflows/build-prs.yml @@ -20,5 +20,6 @@ jobs: uses: neoforged/actions/.github/workflows/build-prs.yml@main with: java: 21 - gradle_tasks: test + # --info allows seeing STDOUT of tests + gradle_tasks: check --info jar_compatibility: true diff --git a/.run/Template JUnit.run.xml b/.run/Template JUnit.run.xml new file mode 100644 index 000000000..f572f687a --- /dev/null +++ b/.run/Template JUnit.run.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..d90c74cfc --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Fancy Mod Loader + +The mod loader used by [NeoForge](https://github.com/neoforged/NeoForge). + +## Extension Points + +### Mod File Candidate Locators + +Responsible for locating potential mod files. Filesystem locations, virtual jars or even full mod-files can be reported to the discovery pipeline for inclusion in the mod loading process. +The pipeline also offers a way for locators to add issues (warnings & errors) that will later be shown to the user when mod loading concludes. + +Interface: `net.neoforged.neoforgespi.locating.IModFileCandidateLocator` + +Resolved via Java ServiceLoader. + +You can construct a basic locator to scan a folder for mods by using `IModFileCandidateLocator.forFolder`. This can be +useful if your locator generates a folder on-disk and wants to delegate to default behavior for it (For example used +by [ServerPackLocator](https://github.com/marchermans/serverpacklocator/)). + +### Mod File Readers + +Responsible for creating a `IModFile` for mod file candidates. + +The default implementation will resolve the type of the mod file by inspecting the Jar manifest or the mod metadata +file (`neoforge.mods.toml`) and return an `IModFile` instance if this succeeds. + +Interface: `net.neoforged.neoforgespi.locating.IModFileReader` + +Resolved via Java ServiceLoader. + +Mod file instances can be created using the static methods on `IModFile`. diff --git a/build.gradle b/build.gradle index 586bb3a29..763627a95 100644 --- a/build.gradle +++ b/build.gradle @@ -53,9 +53,6 @@ allprojects { name = 'Minecraft' url = 'https://libraries.minecraft.net' } - - // TODO remove - mavenLocal() } dependencyUpdates.rejectVersionIf { isNonStable(it.candidate.version) } @@ -69,6 +66,9 @@ allprojects { test { useJUnitPlatform() + + // Needed by UnionFS + jvmArgs("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED") } } diff --git a/earlydisplay/build.gradle b/earlydisplay/build.gradle index dae247b0e..9c796f245 100644 --- a/earlydisplay/build.gradle +++ b/earlydisplay/build.gradle @@ -26,8 +26,7 @@ dependencies { implementation("net.sf.jopt-simple:jopt-simple:${jopt_simple_version}") testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiter_version}") - testImplementation("org.powermock:powermock-core:${powermock_version}") - + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${jupiter_version}") testRuntimeOnly("org.slf4j:slf4j-jdk14:${slf4j_api_version}") testRuntimeOnly("org.lwjgl:lwjgl::${lwjglNatives}") diff --git a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java index d2f77f372..d88e7d90c 100644 --- a/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java +++ b/earlydisplay/src/main/java/net/neoforged/fml/earlydisplay/DisplayWindow.java @@ -45,7 +45,6 @@ import java.util.stream.Collectors; import joptsimple.OptionParser; import net.neoforged.fml.loading.FMLConfig; -import net.neoforged.fml.loading.FMLLoader; import net.neoforged.fml.loading.FMLPaths; import net.neoforged.fml.loading.ImmediateWindowHandler; import net.neoforged.fml.loading.progress.StartupNotificationManager; @@ -600,7 +599,7 @@ public Supplier loadingOverlay(final Supplier mc, final Supplier ri public void updateModuleReads(final ModuleLayer layer) { var fm = layer.findModule("neoforge").orElseThrow(); getClass().getModule().addReads(fm); - var clz = FMLLoader.getGameLayer().findModule("neoforge").map(l -> Class.forName(l, "net.neoforged.neoforge.client.loading.NeoForgeLoadingOverlay")).orElseThrow(); + var clz = Class.forName(fm, "net.neoforged.neoforge.client.loading.NeoForgeLoadingOverlay"); var methods = Arrays.stream(clz.getMethods()).filter(m -> Modifier.isStatic(m.getModifiers())).collect(Collectors.toMap(Method::getName, Function.identity())); loadingOverlay = methods.get("newInstance"); } diff --git a/gradle.properties b/gradle.properties index 79a1bd62a..aae078cc3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,19 +5,19 @@ accesstransformers_version=10.0.1 coremods_version=7.0.3 eventbus_version=7.0.16 modlauncher_version=10.1.10 -securejarhandler_version=2.1.31 +securejarhandler_version=3.0.4 bootstraplauncher_version=1.1.8 asm_version=9.5 -mixin_version=0.12.5+mixin.0.8.5 +mixin_version=0.13.1+mixin.0.8.5 terminalconsoleappender_version=1.2.0 nightconfig_version=3.6.4 jetbrains_annotations_version=24.0.1 slf4j_api_version=1.8.0-beta4 apache_maven_artifact_version=3.8.5 -jarjar_version=0.4.0 +jarjar_version=0.4.1 lwjgl_version=3.3.1 -jupiter_version=5.8.2 -powermock_version=2.0.9 +jupiter_version=5.10.2 +mockito_version=5.11.0 mojang_logging_version=1.1.1 log4j_version=2.19.0 diff --git a/loader/build.gradle b/loader/build.gradle index 9b536ae23..7576dda26 100644 --- a/loader/build.gradle +++ b/loader/build.gradle @@ -44,9 +44,11 @@ dependencies { testCompileOnly("org.jetbrains:annotations:${jetbrains_annotations_version}") testRuntimeOnly("cpw.mods:bootstraplauncher:${bootstraplauncher_version}") testRuntimeOnly("org.apache.logging.log4j:log4j-core:$log4j_version") + testRuntimeOnly("net.neoforged:JarJarFileSystems:$jarjar_version") testImplementation("org.junit.jupiter:junit-jupiter-api:$jupiter_version") - testImplementation("org.powermock:powermock-core:$powermock_version") - testImplementation("org.hamcrest:hamcrest-core:2.2+") + testImplementation("org.junit.jupiter:junit-jupiter-params:$jupiter_version") + testImplementation("org.mockito:mockito-junit-jupiter:$mockito_version") + testImplementation("org.assertj:assertj-core:3.25.3") testImplementation("org.junit.jupiter:junit-jupiter-engine:$jupiter_version") } @@ -68,7 +70,3 @@ spotless { bumpThisNumberIfACustomStepChanges(1) } } - -test { - useJUnitPlatform() -} diff --git a/loader/src/main/java/net/neoforged/fml/LoadingFailedException.java b/loader/src/main/java/net/neoforged/fml/LoadingFailedException.java deleted file mode 100644 index aaf2f4d76..000000000 --- a/loader/src/main/java/net/neoforged/fml/LoadingFailedException.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml; - -import java.util.List; -import java.util.stream.Collectors; - -public class LoadingFailedException extends RuntimeException { - private final List loadingExceptions; - - public LoadingFailedException(final List loadingExceptions) { - this.loadingExceptions = loadingExceptions; - } - - public List getErrors() { - return this.loadingExceptions; - } - - @Override - public String getMessage() { - return "Loading errors encountered: " + this.loadingExceptions.stream().map(ModLoadingException::getMessage).collect(Collectors.joining(",\n\t", "[\n\t", "\n]")); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/ModContainer.java b/loader/src/main/java/net/neoforged/fml/ModContainer.java index 5293e5b92..5a941c72a 100644 --- a/loader/src/main/java/net/neoforged/fml/ModContainer.java +++ b/loader/src/main/java/net/neoforged/fml/ModContainer.java @@ -185,7 +185,7 @@ public final void acceptEvent(T e) { LOGGER.trace(LOADING, "Fired event for modid {} : {}", this.getModId(), e); } catch (Throwable t) { LOGGER.error(LOADING, "Caught exception during event {} dispatch for modid {}", e, this.getModId(), t); - throw new ModLoadingException(modInfo, "fml.modloading.errorduringevent", t, e.getClass().getName()); + throw new ModLoadingException(ModLoadingIssue.error("fml.modloading.errorduringevent", e.getClass().getName()).withAffectedMod(modInfo).withCause(t)); } } @@ -204,7 +204,7 @@ public final void acceptEvent(EventPriority pha LOGGER.trace(LOADING, "Fired event for phase {} for modid {} : {}", phase, this.getModId(), e); } catch (Throwable t) { LOGGER.error(LOADING, "Caught exception during event {} dispatch for modid {}", e, this.getModId(), t); - throw new ModLoadingException(modInfo, "fml.modloading.errorduringevent", t, e.getClass().getName()); + throw new ModLoadingException(ModLoadingIssue.error("fml.modloading.errorduringevent", e.getClass().getName()).withAffectedMod(modInfo).withCause(t)); } } } diff --git a/loader/src/main/java/net/neoforged/fml/ModList.java b/loader/src/main/java/net/neoforged/fml/ModList.java index 09c2b7ab5..4e54a2ca9 100644 --- a/loader/src/main/java/net/neoforged/fml/ModList.java +++ b/loader/src/main/java/net/neoforged/fml/ModList.java @@ -18,8 +18,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ForkJoinWorkerThread; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; @@ -32,15 +30,12 @@ import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.language.ModFileScanData; import net.neoforged.neoforgespi.locating.IModFile; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** * Master list of all mods - game-side version. This is classloaded in the game scope and * can dispatch game level events as a result. */ public class ModList { - private static Logger LOGGER = LogManager.getLogger(); private static ModList INSTANCE; private final List modFiles; private final List sortedList; @@ -58,10 +53,11 @@ private ModList(final List modFiles, final List sortedList) { } private String fileToLine(IModFile mf) { + var mainMod = mf.getModInfos().getFirst(); return String.format(Locale.ENGLISH, "%-50.50s|%-30.30s|%-30.30s|%-20.20s|Manifest: %s", mf.getFileName(), - mf.getModInfos().get(0).getDisplayName(), - mf.getModInfos().get(0).getModId(), - mf.getModInfos().get(0).getVersion(), + mainMod.getDisplayName(), + mainMod.getModId(), + mainMod.getVersion(), ((ModFileInfo) mf.getModFileInfo()).getCodeSigningFingerprint().orElse("NOSIGNATURE")); } @@ -78,14 +74,6 @@ public static ModList get() { return INSTANCE; } - private static ForkJoinWorkerThread newForkJoinWorkerThread(ForkJoinPool pool) { - ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); - thread.setName("modloading-worker-" + thread.getPoolIndex()); - // The default sets it to the SystemClassloader, so copy the current one. - thread.setContextClassLoader(Thread.currentThread().getContextClassLoader()); - return thread; - } - public List getModFiles() { return modFiles; } diff --git a/loader/src/main/java/net/neoforged/fml/ModLoader.java b/loader/src/main/java/net/neoforged/fml/ModLoader.java index fa3cf3f78..75efe1804 100644 --- a/loader/src/main/java/net/neoforged/fml/ModLoader.java +++ b/loader/src/main/java/net/neoforged/fml/ModLoader.java @@ -8,9 +8,7 @@ import static net.neoforged.fml.Logging.CORE; import static net.neoforged.fml.Logging.LOADING; -import com.google.common.collect.ImmutableList; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -36,10 +34,9 @@ import net.neoforged.fml.loading.FMLLoader; import net.neoforged.fml.loading.ImmediateWindowHandler; import net.neoforged.fml.loading.LoadingModList; -import net.neoforged.fml.loading.moddiscovery.AbstractModProvider; -import net.neoforged.fml.loading.moddiscovery.InvalidModIdentifier; import net.neoforged.fml.loading.moddiscovery.ModFileInfo; import net.neoforged.fml.loading.moddiscovery.ModInfo; +import net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader; import net.neoforged.fml.loading.progress.StartupNotificationManager; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.language.IModLanguageProvider; @@ -47,6 +44,7 @@ import net.neoforged.neoforgespi.locating.IModFile; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; /** @@ -62,9 +60,7 @@ private ModLoader() {} private static final Logger LOGGER = LogManager.getLogger(); - private static final List loadingExceptions = new ArrayList<>(); - private static final List loadingWarnings = new ArrayList<>(); - private static boolean loadingStateValid; + private static final List loadingIssues = new ArrayList<>(); private static ModList modList; static { @@ -74,25 +70,6 @@ private ModLoader() {} CrashReportCallables.registerCrashCallable("FML Language Providers", ModLoader::computeLanguageList); } - private static void collectLoadingErrors(LoadingModList loadingModList) { - loadingModList.getErrors().stream() - .flatMap(ModLoadingException::fromEarlyException) - .forEach(loadingExceptions::add); - loadingModList.getBrokenFiles().stream() - .map(file -> new ModLoadingWarning(null, InvalidModIdentifier.identifyJarProblem(file.getFilePath()).orElse("fml.modloading.brokenfile"), file.getFileName())) - .forEach(loadingWarnings::add); - - loadingModList.getWarnings().stream() - .flatMap(ModLoadingWarning::fromEarlyException) - .forEach(loadingWarnings::add); - - loadingModList.getModFiles().stream() - .filter(ModFileInfo::missingLicense) - .filter(modFileInfo -> modFileInfo.getMods().stream().noneMatch(thisModInfo -> loadingExceptions.stream().map(ModLoadingException::getModInfo).anyMatch(otherInfo -> otherInfo == thisModInfo))) //Ignore files where any other mod already encountered an error - .map(modFileInfo -> new ModLoadingException(null, "fml.modloading.missinglicense", null, modFileInfo.getFile())) - .forEach(loadingExceptions::add); - } - private static String computeLanguageList() { return "\n" + FMLLoader.getLanguageLoadingProvider().applyForEach(lp -> lp.name() + "@" + lp.getClass().getPackage().getImplementationVersion()).collect(Collectors.joining("\n\t\t", "\t\t", "")); } @@ -107,27 +84,29 @@ private static String computeModLauncherServiceList() { /** * Run on the primary starting thread by ClientModLoader and ServerModLoader - * + * * @param syncExecutor An executor to run tasks on the main thread * @param parallelExecutor An executor to run tasks on a parallel loading thread pool * @param periodicTask Optional periodic task to perform on the main thread while other activities run */ public static void gatherAndInitializeMods(final Executor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) { - LoadingModList loadingModList = FMLLoader.getLoadingModList(); - collectLoadingErrors(loadingModList); + var loadingModList = FMLLoader.getLoadingModList(); + loadingIssues.clear(); + loadingIssues.addAll(loadingModList.getModLoadingIssues()); ForgeFeature.registerFeature("javaVersion", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.BOTH, System.getProperty("java.version"))); ForgeFeature.registerFeature("openGLVersion", ForgeFeature.VersionFeatureTest.forVersionString(IModInfo.DependencySide.CLIENT, ImmediateWindowHandler.getGLVersion())); - loadingStateValid = true; FMLLoader.backgroundScanHandler.waitForScanToComplete(periodicTask); final ModList modList = ModList.of(loadingModList.getModFiles().stream().map(ModFileInfo::getFile).toList(), loadingModList.getMods()); - if (!loadingExceptions.isEmpty()) { - LOGGER.fatal(CORE, "Error during pre-loading phase", loadingExceptions.get(0)); - StartupNotificationManager.modLoaderMessage("ERROR DURING MOD LOADING"); - modList.setLoadedMods(Collections.emptyList()); - loadingStateValid = false; - throw new LoadingFailedException(loadingExceptions); + + if (hasErrors()) { + var loadingErrors = getLoadingErrors(); + for (var loadingError : loadingErrors) { + LOGGER.fatal(CORE, "Error during pre-loading phase: {}", loadingError, loadingError.cause()); + } + cancelLoading(modList); + throw new ModLoadingException(loadingIssues); } List failedBounds = loadingModList.getMods().stream() .map(ModInfo::getForgeFeatures) @@ -137,25 +116,24 @@ public static void gatherAndInitializeMods(final Executor syncExecutor, final Ex if (!failedBounds.isEmpty()) { LOGGER.fatal(CORE, "Failed to validate feature bounds for mods: {}", failedBounds); - StartupNotificationManager.modLoaderMessage("ERROR DURING MOD LOADING"); - modList.setLoadedMods(Collections.emptyList()); - loadingStateValid = false; - throw new LoadingFailedException(failedBounds.stream() - .map(fb -> new ModLoadingException(fb.modInfo(), "fml.modloading.feature.missing", null, fb, ForgeFeature.featureValue(fb))) - .toList()); + for (var fb : failedBounds) { + loadingIssues.add(ModLoadingIssue.error("fml.modloading.feature.missing", null, fb, ForgeFeature.featureValue(fb)).withAffectedMod(fb.modInfo())); + } + cancelLoading(modList); + throw new ModLoadingException(loadingIssues); } - final List modContainers = loadingModList.getModFiles().stream() + var modContainers = loadingModList.getModFiles().stream() .map(ModFileInfo::getFile) .map(ModLoader::buildMods) .mapMulti(Iterable::forEach) .toList(); - if (!loadingExceptions.isEmpty()) { - LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.get(0)); - StartupNotificationManager.modLoaderMessage("ERROR DURING MOD LOADING"); - modList.setLoadedMods(Collections.emptyList()); - loadingStateValid = false; - throw new LoadingFailedException(loadingExceptions); + if (hasErrors()) { + for (var loadingError : getLoadingErrors()) { + LOGGER.fatal(CORE, "Failed to initialize mod containers: {}", loadingError, loadingError.cause()); + } + cancelLoading(modList); + throw new ModLoadingException(loadingIssues); } modList.setLoadedMods(modContainers); ModLoader.modList = modList; @@ -163,6 +141,16 @@ public static void gatherAndInitializeMods(final Executor syncExecutor, final Ex constructMods(syncExecutor, parallelExecutor, periodicTask); } + private static void cancelLoading(ModList modList) { + StartupNotificationManager.modLoaderMessage("ERROR DURING MOD LOADING"); + modList.setLoadedMods(Collections.emptyList()); + } + + @Deprecated(forRemoval = true, since = "20.5") + public static boolean isLoadingStateValid() { + return !hasErrors(); + } + private static void constructMods(Executor syncExecutor, Executor parallelExecutor, Runnable periodicTask) { var workQueue = new DeferredWorkQueue("Mod Construction"); dispatchParallelTask("Mod Construction", parallelExecutor, periodicTask, modContainer -> { @@ -235,24 +223,19 @@ private static void waitForFuture(String name, Runnable periodicTask, Completabl future.get(50, TimeUnit.MILLISECONDS); return; } catch (ExecutionException e) { - loadingStateValid = false; - Throwable t = e.getCause(); - final List notModLoading = Arrays.stream(t.getSuppressed()) - .filter(obj -> !(obj instanceof ModLoadingException)) - .collect(Collectors.toList()); - if (!notModLoading.isEmpty()) { - LOGGER.fatal("Encountered non-modloading exceptions!", e); - StartupNotificationManager.modLoaderMessage("ERROR DURING MOD LOADING"); - throw new RuntimeException("Encountered non-modloading exception in future " + name, e); + // Merge all potential modloading issues + var errorCount = 0; + for (var error : e.getCause().getSuppressed()) { + if (error instanceof ModLoadingException modLoadingException) { + loadingIssues.addAll(modLoadingException.getIssues()); + } else { + loadingIssues.add(ModLoadingIssue.error("fml.modloading.uncaughterror", name).withCause(e)); + } + errorCount++; } - - final List modLoadingExceptions = Arrays.stream(t.getSuppressed()) - .filter(ModLoadingException.class::isInstance) - .map(ModLoadingException.class::cast) - .collect(Collectors.toList()); - LOGGER.fatal(LOADING, "Failed to wait for future {}, {} errors found", name, modLoadingExceptions.size()); - StartupNotificationManager.modLoaderMessage("ERROR DURING MOD LOADING"); - throw new LoadingFailedException(modLoadingExceptions); + LOGGER.fatal(LOADING, "Failed to wait for future {}, {} errors found", name, errorCount); + cancelLoading(modList); + throw new ModLoadingException(loadingIssues); } catch (Exception ignored) {} } } @@ -266,7 +249,7 @@ private static List buildMods(final IModFile modFile) { .stream() .map(e -> buildModContainerFromTOML(modFile, modInfoMap, e)) .filter(Objects::nonNull) - .collect(Collectors.toList()); + .toList(); if (containers.size() != modInfoMap.size()) { var modIds = modInfoMap.values().stream().map(IModInfo::getModId).sorted().collect(Collectors.toList()); var containerIds = containers.stream().map(c -> c != null ? c.getModId() : "(null)").sorted().collect(Collectors.toList()); @@ -278,13 +261,13 @@ private static List buildMods(final IModFile modFile) { var missingClasses = new ArrayList<>(modIds); missingClasses.removeAll(containerIds); - LOGGER.fatal(LOADING, "The following classes are missing, but are reported in the {}: {}", AbstractModProvider.MODS_TOML, missingClasses); + LOGGER.fatal(LOADING, "The following classes are missing, but are reported in the {}: {}", JarModsDotTomlModFileReader.MODS_TOML, missingClasses); var missingMods = new ArrayList<>(containerIds); missingMods.removeAll(modIds); LOGGER.fatal(LOADING, "The following mods are missing, but have classes in the jar: {}", missingMods); - loadingExceptions.add(new ModLoadingException(null, "fml.modloading.missingclasses", null, modFile.getFilePath())); + loadingIssues.add(ModLoadingIssue.error("fml.modloading.missingclasses", modFile.getFilePath()).withAffectedModFile(modFile)); } // remove errored mod containers return containers.stream().filter(mc -> !(mc instanceof ErroredModContainer)).collect(Collectors.toList()); @@ -296,26 +279,18 @@ private static ModContainer buildModContainerFromTOML(final IModFile modFile, fi final IModLanguageProvider.IModLanguageLoader languageLoader = idToProviderEntry.getValue(); IModInfo info = Optional.ofNullable(modInfoMap.get(modId)). // throw a missing metadata error if there is no matching modid in the modInfoMap from the mods.toml file - orElseThrow(() -> new ModLoadingException(null, "fml.modloading.missingmetadata", null, modId)); + orElseThrow(() -> new ModLoadingException(ModLoadingIssue.error("fml.modloading.missingmetadata", modId))); return languageLoader.loadMod(info, modFile.getScanResult(), FMLLoader.getGameLayer()); } catch (ModLoadingException mle) { // exceptions are caught and added to the error list for later handling - loadingExceptions.add(mle); + loadingIssues.addAll(mle.getIssues()); // return an errored container instance here, because we tried and failed building a container. return new ErroredModContainer(); } } - /** - * @return If the current mod loading state is valid. Use if you interact with vanilla systems directly during loading - * and don't want to cause extraneous crashes due to trying to do things that aren't possible in a "broken load" - */ - public static boolean isLoadingStateValid() { - return loadingStateValid; - } - public static void runEventGenerator(Function generator) { - if (!loadingStateValid) { + if (hasErrors()) { LOGGER.error("Cowardly refusing to send event generator to a broken mod state"); return; } @@ -334,7 +309,7 @@ public static void runEventGenerator(Function void postEvent(T e) { - if (!loadingStateValid) { + if (hasErrors()) { LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName()); return; } @@ -353,7 +328,7 @@ public static void postEventWrapContainerInModO } public static void postEventWithWrapInModOrder(T e, BiConsumer pre, BiConsumer post) { - if (!loadingStateValid) { + if (hasErrors()) { LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName()); return; } @@ -366,18 +341,33 @@ public static void postEventWithWrapInModOrder( } } - public static List getWarnings() { - return ImmutableList.copyOf(loadingWarnings); + /** + * @return If the errors occurred during mod loading. Use if you interact with vanilla systems directly during loading + * and don't want to cause extraneous crashes due to trying to do things that aren't possible. + * If you are running in a Mixin before mod loading has actually started, check {@link LoadingModList#hasErrors()} instead. + */ + public static boolean hasErrors() { + return loadingIssues.stream().anyMatch(issue -> issue.severity() == ModLoadingIssue.Severity.ERROR); } - public static void addWarning(ModLoadingWarning warning) { - loadingWarnings.add(warning); + @ApiStatus.Internal + public static List getLoadingErrors() { + return loadingIssues.stream().filter(issue -> issue.severity() == ModLoadingIssue.Severity.ERROR).toList(); } - private static boolean runningDataGen = false; + @ApiStatus.Internal + public static List getLoadingWarnings() { + return loadingIssues.stream().filter(issue -> issue.severity() == ModLoadingIssue.Severity.WARNING).toList(); + } + + @ApiStatus.Internal + public static List getLoadingIssues() { + return List.copyOf(loadingIssues); + } - public static boolean isDataGenRunning() { - return runningDataGen; + @ApiStatus.Internal + public static void addLoadingIssue(ModLoadingIssue issue) { + loadingIssues.add(issue); } private static class ErroredModContainer extends ModContainer { diff --git a/loader/src/main/java/net/neoforged/fml/ModLoadingException.java b/loader/src/main/java/net/neoforged/fml/ModLoadingException.java index 6029a5a3b..f564fe507 100644 --- a/loader/src/main/java/net/neoforged/fml/ModLoadingException.java +++ b/loader/src/main/java/net/neoforged/fml/ModLoadingException.java @@ -5,67 +5,27 @@ package net.neoforged.fml; -import com.google.common.collect.Streams; -import java.util.Arrays; import java.util.List; -import java.util.stream.Stream; -import net.neoforged.fml.loading.EarlyLoadingException; -import net.neoforged.neoforgespi.language.IModInfo; +import java.util.stream.Collectors; -/** - * General purpose mod loading error message - */ public class ModLoadingException extends RuntimeException { - private static final long serialVersionUID = 2048947398536935507L; - /** - * Mod Info for mod with issue - */ - private final IModInfo modInfo; - - /** - * I18N message to use for display - */ - private final String i18nMessage; - - /** - * Context for message display - */ - private final List context; + private final List issues; - public ModLoadingException(final IModInfo modInfo, final String i18nMessage, final Throwable originalException, Object... context) { - super("Mod Loading Exception", originalException); - this.modInfo = modInfo; - this.i18nMessage = i18nMessage; - this.context = Arrays.asList(context); + public ModLoadingException(ModLoadingIssue issue) { + this(List.of(issue)); } - static Stream fromEarlyException(final EarlyLoadingException e) { - return e.getAllData().stream().map(ed -> new ModLoadingException(ed.getModInfo(), ed.getI18message(), e.getCause(), ed.getArgs())); + public ModLoadingException(List issues) { + this.issues = issues; } - public String getI18NMessage() { - return i18nMessage; - } - - public Object[] getContext() { - return context.toArray(); - } - - public String formatToString() { - // TODO: cleanup null here - this requires moving all indices in the translations - return Bindings.parseMessage(i18nMessage, Streams.concat(Stream.of(modInfo, null, getCause()), context.stream()).toArray()); + public List getIssues() { + return this.issues; } @Override public String getMessage() { - return formatToString(); - } - - public IModInfo getModInfo() { - return modInfo; - } - - public String getCleanMessage() { - return Bindings.stripControlCodes(formatToString()); + return "Loading errors encountered: " + this.issues.stream().map(ModLoadingIssue::translationKey) + .collect(Collectors.joining(",\n\t", "[\n\t", "\n]")); } } diff --git a/loader/src/main/java/net/neoforged/fml/ModLoadingIssue.java b/loader/src/main/java/net/neoforged/fml/ModLoadingIssue.java new file mode 100644 index 000000000..8c2093670 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/ModLoadingIssue.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml; + +import com.google.common.collect.Streams; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import net.neoforged.neoforgespi.language.IModInfo; +import net.neoforged.neoforgespi.locating.IModFile; +import org.jetbrains.annotations.Nullable; + +public record ModLoadingIssue( + Severity severity, + String translationKey, + List translationArgs, + @Nullable Throwable cause, + @Nullable Path affectedPath, + @Nullable IModFile affectedModFile, + @Nullable IModInfo affectedMod) { + + public ModLoadingIssue(Severity severity, String translationKey, List translationArgs) { + this(severity, translationKey, translationArgs, null, null, null, null); + } + + public static ModLoadingIssue error(String translationKey, Object... args) { + return new ModLoadingIssue(Severity.ERROR, translationKey, List.of(args)); + } + + public static ModLoadingIssue warning(String translationKey, Object... args) { + return new ModLoadingIssue(Severity.WARNING, translationKey, List.of(args)); + } + + public ModLoadingIssue withAffectedPath(Path affectedPath) { + return new ModLoadingIssue(severity, translationKey, translationArgs, cause, affectedPath, null, null); + } + + public ModLoadingIssue withAffectedModFile(IModFile affectedModFile) { + var affectedPath = affectedModFile.getFilePath(); + return new ModLoadingIssue(severity, translationKey, translationArgs, cause, affectedPath, affectedModFile, null); + } + + public ModLoadingIssue withAffectedMod(IModInfo affectedMod) { + var affectedModFile = affectedMod.getOwningFile().getFile(); + var affectedPath = affectedModFile.getFilePath(); + return new ModLoadingIssue(severity, translationKey, translationArgs, cause, affectedPath, affectedModFile, affectedMod); + } + + public ModLoadingIssue withCause(Throwable cause) { + return new ModLoadingIssue(severity, translationKey, translationArgs, cause, affectedPath, affectedModFile, affectedMod); + } + + public String getTranslatedMessage() { + Object[] formattingArgs; + // TODO: cleanup null here - this requires moving all indices in the translations + if (severity == Severity.ERROR) { + // Error translations included a "cause" in position 2 + formattingArgs = Streams.concat(Stream.of(affectedMod, null, cause), translationArgs.stream()).toArray(); + } else { + formattingArgs = Streams.concat(Stream.of(affectedMod, null), translationArgs.stream()).toArray(); + } + + return Bindings.parseMessage(translationKey, formattingArgs); + } + + public String toString() { + var result = new StringBuilder(severity + ": " + translationKey); + + for (var arg : translationArgs) { + result.append(", "); + result.append(arg); + } + + return result.toString(); + } + public enum Severity { + WARNING, + ERROR + } +} diff --git a/loader/src/main/java/net/neoforged/fml/ModLoadingWarning.java b/loader/src/main/java/net/neoforged/fml/ModLoadingWarning.java deleted file mode 100644 index 7982cea90..000000000 --- a/loader/src/main/java/net/neoforged/fml/ModLoadingWarning.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml; - -import com.google.common.collect.Streams; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Stream; -import net.neoforged.fml.loading.EarlyLoadingException; -import net.neoforged.neoforgespi.language.IModInfo; - -public class ModLoadingWarning { - /** - * Mod Info for mod with warning - */ - private final IModInfo modInfo; - - /** - * I18N message to use for display - */ - private final String i18nMessage; - - /** - * Context for message display - */ - private final List context; - - public ModLoadingWarning(final IModInfo modInfo, final String i18nMessage, Object... context) { - this.modInfo = modInfo; - this.i18nMessage = i18nMessage; - this.context = Arrays.asList(context); - } - - public String formatToString() { - // TODO: cleanup null here - this requires moving all indices in the translations - return Bindings.parseMessage(i18nMessage, Streams.concat(Stream.of(modInfo, null), context.stream()).toArray()); - } - - static Stream fromEarlyException(final EarlyLoadingException e) { - return e.getAllData().stream().map(ed -> new ModLoadingWarning(ed.getModInfo(), ed.getI18message(), ed.getArgs())); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/javafmlmod/AutomaticEventSubscriber.java b/loader/src/main/java/net/neoforged/fml/javafmlmod/AutomaticEventSubscriber.java index c9b69de8a..10daf6df7 100644 --- a/loader/src/main/java/net/neoforged/fml/javafmlmod/AutomaticEventSubscriber.java +++ b/loader/src/main/java/net/neoforged/fml/javafmlmod/AutomaticEventSubscriber.java @@ -20,7 +20,7 @@ import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLEnvironment; -import net.neoforged.fml.loading.moddiscovery.ModAnnotation; +import net.neoforged.fml.loading.modscan.ModAnnotation; import net.neoforged.neoforgespi.language.ModFileScanData; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLModContainer.java b/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLModContainer.java index 3b9900b44..1ab97bf43 100644 --- a/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLModContainer.java +++ b/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLModContainer.java @@ -18,6 +18,7 @@ import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.event.IModBusEvent; import net.neoforged.fml.loading.FMLLoader; import net.neoforged.neoforgespi.language.IModInfo; @@ -52,7 +53,7 @@ public FMLModContainer(IModInfo info, String className, ModFileScanData modFileS LOGGER.trace(LOADING, "Loaded modclass {} with {}", modClass.getName(), modClass.getClassLoader()); } catch (Throwable e) { LOGGER.error(LOADING, "Failed to load class {}", className, e); - throw new ModLoadingException(info, "fml.modloading.failedtoloadmodclass", e); + throw new ModLoadingException(ModLoadingIssue.error("fml.modloading.failedtoloadmodclass").withCause(e).withAffectedMod(info)); } } @@ -104,7 +105,7 @@ protected void constructMod() { } catch (Throwable e) { if (e instanceof InvocationTargetException) e = e.getCause(); // exceptions thrown when a reflected method call throws are wrapped in an InvocationTargetException. However, this isn't useful for the end user who has to dig through the logs to find the actual cause. LOGGER.error(LOADING, "Failed to create mod instance. ModID: {}, class {}", getModId(), modClass.getName(), e); - throw new ModLoadingException(modInfo, "fml.modloading.failedtoloadmod", e, modClass); + throw new ModLoadingException(ModLoadingIssue.error("fml.modloading.failedtoloadmod", e, modClass).withCause(e).withAffectedMod(modInfo)); } try { LOGGER.trace(LOADING, "Injecting Automatic event subscribers for {}", getModId()); @@ -112,7 +113,7 @@ protected void constructMod() { LOGGER.trace(LOADING, "Completed Automatic event subscribers for {}", getModId()); } catch (Throwable e) { LOGGER.error(LOADING, "Failed to register automatic subscribers. ModID: {}, class {}", getModId(), modClass.getName(), e); - throw new ModLoadingException(modInfo, "fml.modloading.failedtoloadmod", e, modClass); + throw new ModLoadingException(ModLoadingIssue.error("fml.modloading.failedtoloadmod", e, modClass).withCause(e).withAffectedMod(modInfo)); } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/ClasspathTransformerDiscoverer.java b/loader/src/main/java/net/neoforged/fml/loading/ClasspathTransformerDiscoverer.java index da3c61f57..a5c362d19 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/ClasspathTransformerDiscoverer.java +++ b/loader/src/main/java/net/neoforged/fml/loading/ClasspathTransformerDiscoverer.java @@ -16,10 +16,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; +import java.util.Collection; import java.util.Enumeration; +import java.util.HashSet; import java.util.List; -import java.util.Map; import net.neoforged.fml.loading.targets.CommonLaunchHandler; import org.apache.logging.log4j.LogManager; @@ -28,52 +28,51 @@ public class ClasspathTransformerDiscoverer implements ITransformerDiscoveryServ @Override public List candidates(Path gameDirectory) { - return Collections.emptyList(); + throw new UnsupportedOperationException(); } @Override public List candidates(final Path gameDirectory, final String launchTarget) { if (launchTarget != null && launchTarget.contains("dev")) { - this.scan(gameDirectory); + return scan(); } - return List.copyOf(found); + return List.of(); } - private final static List found = new ArrayList<>(); - - public static List allExcluded() { - return found.stream().map(np -> np.paths()[0]).toList(); - } - - private void scan(final Path gameDirectory) { + private List scan() { try { - for (final String serviceClass : TransformerDiscovererConstants.SERVICES) { - locateTransformers("META-INF/services/" + serviceClass); + var result = new HashSet(); + for (var serviceClass : TransformerDiscovererConstants.SERVICES) { + locateTransformers("META-INF/services/" + serviceClass, result); } - scanModClasses(); + scanModClasses(result); + return new ArrayList<>(result); } catch (IOException e) { LogManager.getLogger().error("Error during discovery of transform services from the classpath", e); + return List.of(); } } - private void locateTransformers(String resource) throws IOException { - final Enumeration resources = ClassLoader.getSystemClassLoader().getResources(resource); + private void locateTransformers(String resource, Collection result) throws IOException { + final Enumeration resources = Thread.currentThread().getContextClassLoader().getResources(resource); while (resources.hasMoreElements()) { URL url = resources.nextElement(); Path path = ClasspathLocatorUtils.findJarPathFor(resource, url.toString(), url); if (legacyClasspath.stream().anyMatch(path::equals) || !Files.exists(path) || Files.isDirectory(path)) continue; - found.add(new NamedPath(path.toUri().toString(), path)); + result.add(new NamedPath(path.toUri().toString(), path)); } } - private void scanModClasses() { - final Map> modClassPaths = CommonLaunchHandler.getModClasses(); - modClassPaths.forEach((modid, paths) -> { - if (shouldLoadInServiceLayer(paths.toArray(Path[]::new))) { - found.add(new NamedPath(modid, paths.toArray(Path[]::new))); + private void scanModClasses(Collection result) { + var modClassPaths = CommonLaunchHandler.getGroupedModFolders(); + for (var entry : modClassPaths.entrySet()) { + String modid = entry.getKey(); + List paths = entry.getValue(); + if (shouldLoadInServiceLayer(paths)) { + result.add(new NamedPath(modid, paths.toArray(Path[]::new))); } - }); + } } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/EarlyLoadingException.java b/loader/src/main/java/net/neoforged/fml/loading/EarlyLoadingException.java deleted file mode 100644 index e1179f866..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/EarlyLoadingException.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading; - -import java.util.List; -import net.neoforged.neoforgespi.language.IModInfo; - -/** - * Thrown during early loading phase, and collected by the LoadingModList for handoff to the client - * or server. - */ -public class EarlyLoadingException extends RuntimeException { - public static class ExceptionData { - private final IModInfo modInfo; - private final String i18message; - private final Object[] args; - - public ExceptionData(final String message, Object... args) { - this(message, null, args); - } - - public ExceptionData(final String message, final IModInfo modInfo, Object... args) { - this.i18message = message; - this.modInfo = modInfo; - this.args = args; - } - - public String getI18message() { - return i18message; - } - - public Object[] getArgs() { - return args; - } - - public IModInfo getModInfo() { - return modInfo; - } - } - - private final List errorMessages; - - public List getAllData() { - return errorMessages; - } - - public EarlyLoadingException(final String message, final Throwable originalException, List errorMessages) { - super(message, originalException); - this.errorMessages = errorMessages; - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLEnvironment.java b/loader/src/main/java/net/neoforged/fml/loading/FMLEnvironment.java index 5277f69e9..5dfb4bea0 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLEnvironment.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLEnvironment.java @@ -6,21 +6,14 @@ package net.neoforged.fml.loading; import cpw.mods.modlauncher.api.IEnvironment; -import cpw.mods.modlauncher.api.TypesafeMap; -import java.util.function.Supplier; import net.neoforged.api.distmarker.Dist; import net.neoforged.neoforgespi.Environment; public class FMLEnvironment { public static final Dist dist = FMLLoader.getDist(); public static final boolean production = FMLLoader.isProduction() || System.getProperties().containsKey("production"); - public static final boolean secureJarsEnabled = FMLLoader.isSecureJarEnabled(); static void setupInteropEnvironment(IEnvironment environment) { environment.computePropertyIfAbsent(Environment.Keys.DIST.get(), v -> dist); } - - public static class Keys { - public static final Supplier> LOCATORCLASSLOADER = IEnvironment.buildKey("LOCATORCLASSLOADER", ClassLoader.class); - } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index f2d7d0cd0..bff58e9b5 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -9,36 +9,37 @@ import cpw.mods.modlauncher.Launcher; import cpw.mods.modlauncher.api.IEnvironment; import cpw.mods.modlauncher.api.ILaunchHandlerService; -import cpw.mods.modlauncher.api.IModuleLayerManager; import cpw.mods.modlauncher.api.ITransformationService; import cpw.mods.modlauncher.api.IncompatibleEnvironmentException; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import net.neoforged.accesstransformer.api.AccessTransformerEngine; import net.neoforged.accesstransformer.ml.AccessTransformerService; import net.neoforged.api.distmarker.Dist; import net.neoforged.coremod.CoreModScriptingEngine; import net.neoforged.fml.common.asm.RuntimeDistCleaner; import net.neoforged.fml.loading.mixin.DeferredMixinConfigRegistration; -import net.neoforged.fml.loading.moddiscovery.BackgroundScanHandler; import net.neoforged.fml.loading.moddiscovery.ModDiscoverer; import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.fml.loading.moddiscovery.ModValidator; +import net.neoforged.fml.loading.modscan.BackgroundScanHandler; import net.neoforged.fml.loading.targets.CommonLaunchHandler; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; public class FMLLoader { private static final Logger LOGGER = LogUtils.getLogger(); private static AccessTransformerEngine accessTransformer; - private static ModDiscoverer modDiscoverer; private static CoreModScriptingEngine coreModEngine; - private static LanguageLoadingProvider languageLoadingProvider; + private static LanguageProviderLoader languageProviderLoader; private static Dist dist; private static LoadingModList loadingModList; private static RuntimeDistCleaner runtimeDistCleaner; @@ -50,9 +51,10 @@ public class FMLLoader { private static ModValidator modValidator; public static BackgroundScanHandler backgroundScanHandler; private static boolean production; - private static IModuleLayerManager moduleLayerManager; + @Nullable + private static ModuleLayer gameLayer; - static void onInitialLoad(IEnvironment environment, Set otherServices) throws IncompatibleEnvironmentException { + static void onInitialLoad(IEnvironment environment) throws IncompatibleEnvironmentException { final String version = LauncherVersion.getVersion(); LOGGER.debug(LogMarkers.CORE, "FML {} loading", version); final Package modLauncherPackage = ITransformationService.class.getPackage(); @@ -100,9 +102,8 @@ static void onInitialLoad(IEnvironment environment, Set otherServices) t } } - static void setupLaunchHandler(final IEnvironment environment, final Map arguments) { - final String launchTarget = environment.getProperty(IEnvironment.Keys.LAUNCHTARGET.get()).orElse("MISSING"); - arguments.put("launchTarget", launchTarget); + static void setupLaunchHandler(IEnvironment environment, VersionInfo versionInfo) { + var launchTarget = environment.getProperty(IEnvironment.Keys.LAUNCHTARGET.get()).orElse("MISSING"); final Optional launchHandler = environment.findLaunchHandler(launchTarget); LOGGER.debug(LogMarkers.CORE, "Using {} as launch service", launchTarget); if (launchHandler.isEmpty()) { @@ -117,31 +118,29 @@ static void setupLaunchHandler(final IEnvironment environment, final Map beginModScan(final Map arguments) { - LOGGER.debug(LogMarkers.SCAN, "Scanning for Mod Locators"); - modDiscoverer = new ModDiscoverer(arguments); + public static List beginModScan(ILaunchContext launchContext) { + var additionalLocators = new ArrayList(); + commonLaunchHandler.collectAdditionalModFileLocators(versionInfo, additionalLocators::add); + + var modDiscoverer = new ModDiscoverer(launchContext, additionalLocators); modValidator = modDiscoverer.discoverMods(); var pluginResources = modValidator.getPluginResources(); return List.of(pluginResources); } - public static List completeScan(IModuleLayerManager layerManager, List extraMixinConfigs) { - moduleLayerManager = layerManager; - languageLoadingProvider = new LanguageLoadingProvider(); + public static List completeScan(ILaunchContext launchContext, List extraMixinConfigs) { + languageProviderLoader = new LanguageProviderLoader(launchContext); backgroundScanHandler = modValidator.stage2Validation(); loadingModList = backgroundScanHandler.getLoadingModList(); - if (loadingModList.getErrors().isEmpty()) { + if (!loadingModList.hasErrors()) { // Add extra mixin configs extraMixinConfigs.forEach(DeferredMixinConfigRegistration::addMixinConfig); } @@ -152,16 +151,8 @@ static CoreModScriptingEngine getCoreModEngine() { return coreModEngine; } - public static LanguageLoadingProvider getLanguageLoadingProvider() { - return languageLoadingProvider; - } - - static ModDiscoverer getModDiscoverer() { - return modDiscoverer; - } - - public static CommonLaunchHandler getLaunchHandler() { - return commonLaunchHandler; + public static LanguageProviderLoader getLanguageLoadingProvider() { + return languageProviderLoader; } public static void addAccessTransformer(Path atPath, ModFile modName) { @@ -178,6 +169,7 @@ public static Dist getDist() { } public static void beforeStart(ModuleLayer gameLayer) { + FMLLoader.gameLayer = gameLayer; ImmediateWindowHandler.acceptGameLayer(gameLayer); ImmediateWindowHandler.updateProgress("Launching minecraft"); progressWindowTick.run(); @@ -207,12 +199,11 @@ public static boolean isProduction() { return production; } - public static boolean isSecureJarEnabled() { - return true; - } - public static ModuleLayer getGameLayer() { - return moduleLayerManager.getLayer(IModuleLayerManager.Layer.GAME).orElseThrow(); + if (gameLayer == null) { + throw new IllegalStateException("This can only be called after mod discovery is completed"); + } + return gameLayer; } public static VersionInfo versionInfo() { diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLServiceProvider.java b/loader/src/main/java/net/neoforged/fml/loading/FMLServiceProvider.java index d141b3df8..e9fd84b7b 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLServiceProvider.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLServiceProvider.java @@ -14,16 +14,15 @@ import cpw.mods.modlauncher.api.ITransformer; import cpw.mods.modlauncher.api.IncompatibleEnvironmentException; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Supplier; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionSpecBuilder; -import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.neoforgespi.Environment; +import net.neoforged.neoforgespi.ILaunchContext; import org.slf4j.Logger; public class FMLServiceProvider implements ITransformationService { @@ -36,17 +35,12 @@ public class FMLServiceProvider implements ITransformationService { private ArgumentAcceptingOptionSpec forgeOption; private ArgumentAcceptingOptionSpec mcOption; private ArgumentAcceptingOptionSpec mcpOption; - private ArgumentAcceptingOptionSpec mappingsOption; private List modsArgumentList; private List modListsArgumentList; private List mavenRootsArgumentList; private List mixinConfigsArgumentList; - private String targetForgeVersion; - private String targetFMLVersion; - private String targetMcVersion; - private String targetMcpVersion; - private String targetMcpMappings; - private Map arguments; + private VersionInfo versionInfo; + private ILaunchContext launchContext; public FMLServiceProvider() { final String markerselection = System.getProperty("forge.logging.markers", ""); @@ -64,19 +58,14 @@ public void initialize(IEnvironment environment) { FMLPaths.setup(environment); LOGGER.debug(CORE, "Loading configuration"); FMLConfig.load(); - LOGGER.debug(CORE, "Preparing ModFile"); - environment.computePropertyIfAbsent(Environment.Keys.MODFILEFACTORY.get(), k -> ModFile::new); - arguments = new HashMap<>(); - arguments.put("modLists", modListsArgumentList); - arguments.put("mods", modsArgumentList); - arguments.put("mavenRoots", mavenRootsArgumentList); - arguments.put("neoForgeVersion", targetForgeVersion); - arguments.put("fmlVersion", targetFMLVersion); - arguments.put("mcVersion", targetMcVersion); - arguments.put("neoFormVersion", targetMcpVersion); - arguments.put("mcpMappings", targetMcpMappings); + var moduleLayerManager = environment.findModuleLayerManager().orElseThrow(); + launchContext = new LaunchContext(environment, + moduleLayerManager, + modListsArgumentList, + modsArgumentList, + mavenRootsArgumentList); LOGGER.debug(CORE, "Preparing launch handler"); - FMLLoader.setupLaunchHandler(environment, arguments); + FMLLoader.setupLaunchHandler(environment, versionInfo); FMLEnvironment.setupInteropEnvironment(environment); Environment.build(environment); } @@ -84,29 +73,26 @@ public void initialize(IEnvironment environment) { @Override public List beginScanning(final IEnvironment environment) { LOGGER.debug(CORE, "Initiating mod scan"); - return FMLLoader.beginModScan(arguments); + return FMLLoader.beginModScan(launchContext); } @Override public List completeScan(final IModuleLayerManager layerManager) { - return FMLLoader.completeScan(layerManager, mixinConfigsArgumentList); + Supplier gameLayerSupplier = () -> layerManager.getLayer(IModuleLayerManager.Layer.GAME).orElseThrow(); + return FMLLoader.completeScan(launchContext, mixinConfigsArgumentList); } @Override public void onLoad(IEnvironment environment, Set otherServices) throws IncompatibleEnvironmentException { -// LOGGER.debug("Injecting tracing printstreams for STDOUT/STDERR."); -// System.setOut(new TracingPrintStream(LogManager.getLogger("STDOUT"), System.out)); -// System.setErr(new TracingPrintStream(LogManager.getLogger("STDERR"), System.err)); - FMLLoader.onInitialLoad(environment, otherServices); + FMLLoader.onInitialLoad(environment); } @Override public void arguments(BiFunction argumentBuilder) { - forgeOption = argumentBuilder.apply("neoForgeVersion", "Forge Version number").withRequiredArg().ofType(String.class).required(); + forgeOption = argumentBuilder.apply("neoForgeVersion", "NeoForge Version number").withRequiredArg().ofType(String.class).required(); fmlOption = argumentBuilder.apply("fmlVersion", "FML Version number").withRequiredArg().ofType(String.class).required(); mcOption = argumentBuilder.apply("mcVersion", "Minecraft Version number").withRequiredArg().ofType(String.class).required(); - mcpOption = argumentBuilder.apply("neoFormVersion", "MCP Version number").withRequiredArg().ofType(String.class).required(); - mappingsOption = argumentBuilder.apply("mcpMappings", "MCP Mappings Channel and Version").withRequiredArg().ofType(String.class); + mcpOption = argumentBuilder.apply("neoFormVersion", "Neoform Version number").withRequiredArg().ofType(String.class).required(); modsOption = argumentBuilder.apply("mods", "List of mods to add").withRequiredArg().ofType(String.class).withValuesSeparatedBy(","); modListsOption = argumentBuilder.apply("modLists", "JSON modlists").withRequiredArg().ofType(String.class).withValuesSeparatedBy(","); mavenRootsOption = argumentBuilder.apply("mavenRoots", "Maven root directories").withRequiredArg().ofType(String.class).withValuesSeparatedBy(","); @@ -119,11 +105,12 @@ public void argumentValues(OptionResult option) { modListsArgumentList = option.values(modListsOption); mavenRootsArgumentList = option.values(mavenRootsOption); mixinConfigsArgumentList = option.values(mixinConfigsOption); - targetFMLVersion = option.value(fmlOption); - targetForgeVersion = option.value(forgeOption); - targetMcVersion = option.value(mcOption); - targetMcpVersion = option.value(mcpOption); - targetMcpMappings = option.value(mappingsOption); + versionInfo = new VersionInfo( + option.value(forgeOption), + option.value(fmlOption), + option.value(mcOption), + option.value(mcpOption)); + LOGGER.debug(LogMarkers.CORE, "Received command line version data : {}", versionInfo); } @Override diff --git a/loader/src/main/java/net/neoforged/fml/loading/JarVersionLookupHandler.java b/loader/src/main/java/net/neoforged/fml/loading/JarVersionLookupHandler.java index cb8d426de..3ade42cc5 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/JarVersionLookupHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/JarVersionLookupHandler.java @@ -11,33 +11,10 @@ * Finds Version data from a package, with possible default values */ public class JarVersionLookupHandler { - public static Optional getImplementationVersion(final String pkgName) { - // Note that with Java 9, you'll probably want the module's version data, hence pulling this out - final String pkgVersion = Package.getPackage(pkgName).getImplementationVersion(); - return Optional.ofNullable(pkgVersion); - } - - public static Optional getSpecificationVersion(final String pkgName) { - // Note that with Java 9, you'll probably want the module's version data, hence pulling this out - final String pkgVersion = Package.getPackage(pkgName).getSpecificationVersion(); - return Optional.ofNullable(pkgVersion); - } - - public static Optional getImplementationVersion(final Class clazz) { - // With java 9 we'll use the module's version if it exists in preference. - final String pkgVersion = clazz.getPackage().getImplementationVersion(); - return Optional.ofNullable(pkgVersion); - } - - public static Optional getImplementationTitle(final Class clazz) { - // With java 9 we'll use the module's version if it exists in preference. - final String pkgVersion = clazz.getPackage().getImplementationTitle(); - return Optional.ofNullable(pkgVersion); - } - - public static Optional getSpecificationVersion(final Class clazz) { - // With java 9 we'll use the module's version if it exists in preference. - final String pkgVersion = clazz.getPackage().getSpecificationVersion(); - return Optional.ofNullable(pkgVersion); + public static Optional getVersion(final Class clazz) { + if (clazz.getModule() != null && clazz.getModule().getDescriptor() != null) { + return clazz.getModule().getDescriptor().rawVersion(); + } + return Optional.empty(); } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/LanguageLoadingProvider.java b/loader/src/main/java/net/neoforged/fml/loading/LanguageLoadingProvider.java deleted file mode 100644 index 40b8ad5bc..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/LanguageLoadingProvider.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading; - -import com.mojang.logging.LogUtils; -import cpw.mods.modlauncher.Launcher; -import cpw.mods.modlauncher.api.IModuleLayerManager; -import cpw.mods.modlauncher.util.ServiceLoaderUtils; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Stream; -import net.neoforged.fml.loading.moddiscovery.ExplodedDirectoryLocator; -import net.neoforged.fml.loading.moddiscovery.ModFile; -import net.neoforged.neoforgespi.language.IModLanguageProvider; -import org.apache.maven.artifact.versioning.ArtifactVersion; -import org.apache.maven.artifact.versioning.DefaultArtifactVersion; -import org.apache.maven.artifact.versioning.VersionRange; -import org.slf4j.Logger; - -public class LanguageLoadingProvider { - private static final Logger LOGGER = LogUtils.getLogger(); - private final List languageProviders = new ArrayList<>(); - private final ServiceLoader serviceLoader; - private final Map languageProviderMap = new HashMap<>(); - private List languagePaths = new ArrayList<>(); - - public void forEach(final Consumer consumer) { - languageProviders.forEach(consumer); - } - - public Stream applyForEach(final Function function) { - return languageProviders.stream().map(function); - } - - private static class ModLanguageWrapper { - private final IModLanguageProvider modLanguageProvider; - - private final ArtifactVersion version; - - public ModLanguageWrapper(IModLanguageProvider modLanguageProvider, ArtifactVersion version) { - this.modLanguageProvider = modLanguageProvider; - this.version = version; - } - - public ArtifactVersion getVersion() { - return version; - } - - public IModLanguageProvider getModLanguageProvider() { - return modLanguageProvider; - } - } - - LanguageLoadingProvider() { - var sl = Launcher.INSTANCE.environment().findModuleLayerManager().flatMap(lm -> lm.getLayer(IModuleLayerManager.Layer.PLUGIN)).orElseThrow(); - serviceLoader = ServiceLoader.load(sl, IModLanguageProvider.class); - loadLanguageProviders(); - } - - private void loadLanguageProviders() { - LOGGER.debug(LogMarkers.CORE, "Found {} language providers", ServiceLoaderUtils.streamServiceLoader(() -> serviceLoader, sce -> LOGGER.error("Problem with language loaders")).count()); - serviceLoader.forEach(languageProviders::add); - ImmediateWindowHandler.updateProgress("Loading language providers"); - languageProviders.forEach(lp -> { - final Path lpPath; - try { - lpPath = Paths.get(lp.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()); - } catch (URISyntaxException e) { - throw new RuntimeException("Huh?", e); - } - Optional implementationVersion = JarVersionLookupHandler.getImplementationVersion(lp.getClass()); - String impl = implementationVersion.orElse(Files.isDirectory(lpPath) ? FMLLoader.versionInfo().fmlVersion().split("\\.")[0] : null); - if (impl == null) { - LOGGER.error(LogMarkers.CORE, "Found unversioned language provider {}", lp.name()); - throw new RuntimeException("Failed to find implementation version for language provider " + lp.name()); - } - LOGGER.debug(LogMarkers.CORE, "Found language provider {}, version {}", lp.name(), impl); - ImmediateWindowHandler.updateProgress("Loaded language provider " + lp.name() + " " + impl); - languageProviderMap.put(lp.name(), new ModLanguageWrapper(lp, new DefaultArtifactVersion(impl))); - }); - } - - void addForgeLanguage(final Path forgePath) { - if (!languageProviderMap.containsKey("javafml")) { - LOGGER.debug(LogMarkers.CORE, "Adding forge as a language from {}", forgePath.toString()); - addLanguagePaths(Stream.of(forgePath)); - serviceLoader.reload(); - loadLanguageProviders(); - } else { - LOGGER.debug(LogMarkers.CORE, "Skipping adding forge jar - javafml is already present"); - } - } - - private void addLanguagePaths(final Stream langPaths) { - languageProviders.clear(); - languageProviderMap.clear(); -// langPaths.peek(languagePaths::add).map(Path::toFile).map(File::toURI).map(rethrowFunction(URI::toURL)).forEach(languageClassLoader::addURL); - } - - public void addAdditionalLanguages(List modFiles) { - if (modFiles == null) return; - Stream langPaths = modFiles.stream().map(ModFile::getFilePath); - addLanguagePaths(langPaths); - serviceLoader.reload(); - loadLanguageProviders(); - } - - Stream getLibraries() { - return languagePaths.stream(); - } - - public IModLanguageProvider findLanguage(ModFile mf, String modLoader, VersionRange modLoaderVersion) { - final String languageFileName = mf.getProvider() instanceof ExplodedDirectoryLocator ? "in-development" : mf.getFileName(); - final ModLanguageWrapper mlw = languageProviderMap.get(modLoader); - if (mlw == null) { - LOGGER.error(LogMarkers.LOADING, "Missing language {} version {} wanted by {}", modLoader, modLoaderVersion, languageFileName); - throw new EarlyLoadingException("Missing language " + modLoader, null, Collections.singletonList(new EarlyLoadingException.ExceptionData("fml.language.missingversion", modLoader, modLoaderVersion, languageFileName, "null"))); - } - if (!VersionSupportMatrix.testVersionSupportMatrix(modLoaderVersion, modLoader, "languageloader", (llid, range) -> range.containsVersion(mlw.getVersion()))) { - LOGGER.error(LogMarkers.LOADING, "Missing language {} version {} wanted by {}, found {}", modLoader, modLoaderVersion, languageFileName, mlw.getVersion()); - throw new EarlyLoadingException("Missing language " + modLoader + " matching range " + modLoaderVersion + " found " + mlw.getVersion(), null, Collections.singletonList(new EarlyLoadingException.ExceptionData("fml.language.missingversion", modLoader, modLoaderVersion, languageFileName, mlw.getVersion()))); - } - - return mlw.getModLanguageProvider(); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/LanguageProviderLoader.java b/loader/src/main/java/net/neoforged/fml/loading/LanguageProviderLoader.java new file mode 100644 index 000000000..2ad96caf0 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/LanguageProviderLoader.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import com.mojang.logging.LogUtils; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.ModLoadingIssue; +import net.neoforged.fml.loading.moddiscovery.ModFile; +import net.neoforged.fml.util.ServiceLoaderUtil; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.language.IModLanguageProvider; +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.artifact.versioning.VersionRange; +import org.slf4j.Logger; + +public class LanguageProviderLoader { + private static final Logger LOGGER = LogUtils.getLogger(); + private final List languageProviders; + private final Map languageProviderMap = new HashMap<>(); + + public void forEach(final Consumer consumer) { + languageProviders.forEach(consumer); + } + + public Stream applyForEach(final Function function) { + return languageProviders.stream().map(function); + } + + private record ModLanguageWrapper(IModLanguageProvider modLanguageProvider, ArtifactVersion version) {} + + LanguageProviderLoader(ILaunchContext launchContext) { + languageProviders = ServiceLoaderUtil.loadServices(launchContext, IModLanguageProvider.class); + ImmediateWindowHandler.updateProgress("Loading language providers"); + languageProviders.forEach(lp -> { + final Path lpPath; + try { + lpPath = Paths.get(lp.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException("Huh?", e); + } + Optional version = JarVersionLookupHandler.getVersion(lp.getClass()); + String impl = version.orElse(Files.isDirectory(lpPath) ? FMLLoader.versionInfo().fmlVersion().split("\\.")[0] : null); + if (impl == null) { + LOGGER.error(LogMarkers.CORE, "Found unversioned language provider {}", lp.name()); + throw new RuntimeException("Failed to find implementation version for language provider " + lp.name()); + } + LOGGER.debug(LogMarkers.CORE, "Found language provider {}, version {}", lp.name(), impl); + ImmediateWindowHandler.updateProgress("Loaded language provider " + lp.name() + " " + impl); + languageProviderMap.put(lp.name(), new ModLanguageWrapper(lp, new DefaultArtifactVersion(impl))); + }); + } + + public IModLanguageProvider findLanguage(ModFile mf, String modLoader, VersionRange modLoaderVersion) { + final String languageFileName = mf.getFileName(); + final ModLanguageWrapper mlw = languageProviderMap.get(modLoader); + if (mlw == null) { + LOGGER.error(LogMarkers.LOADING, "Missing language {} version {} wanted by {}", modLoader, modLoaderVersion, languageFileName); + throw new ModLoadingException(ModLoadingIssue.error("fml.language.missingversion", modLoader, modLoaderVersion, languageFileName, "null").withAffectedModFile(mf)); + } + if (!VersionSupportMatrix.testVersionSupportMatrix(modLoaderVersion, modLoader, "languageloader", (llid, range) -> range.containsVersion(mlw.version()))) { + LOGGER.error(LogMarkers.LOADING, "Missing language {} version {} wanted by {}, found {}", modLoader, modLoaderVersion, languageFileName, mlw.version()); + throw new ModLoadingException(ModLoadingIssue.error("fml.language.missingversion", modLoader, modLoaderVersion, languageFileName, mlw.version()).withAffectedModFile(mf)); + } + + return mlw.modLanguageProvider(); + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/LaunchContext.java b/loader/src/main/java/net/neoforged/fml/loading/LaunchContext.java new file mode 100644 index 000000000..3302debcb --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/LaunchContext.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import cpw.mods.modlauncher.api.IEnvironment; +import cpw.mods.modlauncher.api.IModuleLayerManager; +import cpw.mods.niofs.union.UnionFileSystem; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import net.neoforged.neoforgespi.ILaunchContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class LaunchContext implements ILaunchContext { + private static final Logger LOG = LoggerFactory.getLogger(LaunchContext.class); + private final IEnvironment environment; + private final IModuleLayerManager moduleLayerManager; + private final List modLists; + private final List mods; + private final List mavenRoots; + private final Set locatedPaths = new HashSet<>(); + + LaunchContext( + IEnvironment environment, + IModuleLayerManager moduleLayerManager, + List modLists, + List mods, + List mavenRoots) { + this.environment = environment; + this.moduleLayerManager = moduleLayerManager; + this.modLists = modLists; + this.mods = mods; + this.mavenRoots = mavenRoots; + + // Index current layers of the module layer manager + for (var layerId : IModuleLayerManager.Layer.values()) { + moduleLayerManager.getLayer(layerId).ifPresent(layer -> { + for (var resolvedModule : layer.configuration().modules()) { + resolvedModule.reference().location().ifPresent(moduleUri -> { + try { + locatedPaths.add(unpackPath(Paths.get(moduleUri))); + } catch (Exception ignored) {} + }); + } + }); + } + LOG.debug(LogMarkers.SCAN, "Located paths when launch context was created: {}", locatedPaths); + } + + private Path unpackPath(Path path) { + if (path.getFileSystem() instanceof UnionFileSystem unionFileSystem) { + return unionFileSystem.getPrimaryPath(); + } + return path; + } + + @Override + public ServiceLoader createServiceLoader(Class serviceClass) { + var moduleLayer = moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(); + return ServiceLoader.load(moduleLayer, serviceClass); + } + + @Override + public boolean isLocated(Path path) { + return locatedPaths.contains(unpackPath(path)); + } + + public boolean addLocated(Path path) { + return locatedPaths.add(unpackPath(path)); + } + + @Override + public IEnvironment environment() { + return environment; + } + + public IModuleLayerManager moduleLayerManager() { + return moduleLayerManager; + } + + @Override + public List modLists() { + return modLists; + } + + @Override + public List mods() { + return mods; + } + + @Override + public List mavenRoots() { + return mavenRoots; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/LauncherVersion.java b/loader/src/main/java/net/neoforged/fml/loading/LauncherVersion.java index 8a8aaa9aa..a182897f8 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/LauncherVersion.java +++ b/loader/src/main/java/net/neoforged/fml/loading/LauncherVersion.java @@ -5,20 +5,11 @@ package net.neoforged.fml.loading; -import static net.neoforged.fml.loading.LogMarkers.CORE; - -import com.mojang.logging.LogUtils; -import org.slf4j.Logger; - public class LauncherVersion { - private static final Logger LOGGER = LogUtils.getLogger(); private static final String launcherVersion; static { - String vers = JarVersionLookupHandler.getImplementationVersion(LauncherVersion.class).orElse(System.getenv("LAUNCHER_VERSION")); - if (vers == null) throw new RuntimeException("Missing FMLLauncher version, cannot continue"); - launcherVersion = vers; - LOGGER.debug(CORE, "Found FMLLauncher version {}", launcherVersion); + launcherVersion = JarVersionLookupHandler.getVersion(LauncherVersion.class).orElse(""); } public static String getVersion() { diff --git a/loader/src/main/java/net/neoforged/fml/loading/LibraryFinder.java b/loader/src/main/java/net/neoforged/fml/loading/LibraryFinder.java index 2bf895a43..b0cf6a89d 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/LibraryFinder.java +++ b/loader/src/main/java/net/neoforged/fml/loading/LibraryFinder.java @@ -5,50 +5,27 @@ package net.neoforged.fml.loading; -import com.mojang.logging.LogUtils; import java.nio.file.Files; import java.nio.file.Path; -import org.slf4j.Logger; public class LibraryFinder { - private static final Logger LOGGER = LogUtils.getLogger(); - private static Path libsPath; - static Path findLibsPath() { - if (libsPath == null) { - libsPath = Path.of(System.getProperty("libraryDirectory", "crazysnowmannonsense/cheezwhizz")); - if (!Files.isDirectory(libsPath)) { - throw new IllegalStateException("Missing libraryDirectory system property, cannot continue"); - } + var libraryDirectoryProp = System.getProperty("libraryDirectory"); + if (libraryDirectoryProp == null) { + throw new IllegalStateException("Missing libraryDirectory system property"); + } + var libsPath = Path.of(libraryDirectoryProp); + if (!Files.isDirectory(libsPath)) { + throw new IllegalStateException("libraryDirectory system property refers to a non-directory: " + libsPath); } return libsPath; } - static Path getForgeLibraryPath(final String mcVersion, final String forgeVersion, final String forgeGroup) { - Path forgePath = findLibsPath().resolve(MavenCoordinateResolver.get(forgeGroup, "forge", "", "universal", mcVersion + "-" + forgeVersion)); - LOGGER.debug(LogMarkers.CORE, "Found forge path {} is {}", forgePath, pathStatus(forgePath)); - return forgePath; - } - - static String pathStatus(final Path path) { - return Files.exists(path) ? "present" : "missing"; - } - - static Path[] getMCPaths(final String mcVersion, final String mcpVersion, final String forgeVersion, final String forgeGroup, final String type) { - Path srgMcPath = findLibsPath().resolve(MavenCoordinateResolver.get("net.minecraft", type, "", "srg", mcVersion + "-" + mcpVersion)); - Path mcExtrasPath = findLibsPath().resolve(MavenCoordinateResolver.get("net.minecraft", type, "", "extra", mcVersion + "-" + mcpVersion)); - Path patchedBinariesPath = findLibsPath().resolve(MavenCoordinateResolver.get(forgeGroup, "forge", "", type, mcVersion + "-" + forgeVersion)); - LOGGER.debug(LogMarkers.CORE, "SRG MC at {} is {}", srgMcPath.toString(), pathStatus(srgMcPath)); - LOGGER.debug(LogMarkers.CORE, "MC Extras at {} is {}", mcExtrasPath.toString(), pathStatus(mcExtrasPath)); - LOGGER.debug(LogMarkers.CORE, "Forge patches at {} is {}", patchedBinariesPath.toString(), pathStatus(patchedBinariesPath)); - return new Path[] { patchedBinariesPath, mcExtrasPath, srgMcPath }; - } - - public static Path findPathForMaven(final String group, final String artifact, final String extension, final String classifier, final String version) { - return findLibsPath().resolve(MavenCoordinateResolver.get(group, artifact, extension, classifier, version)); + public static Path findPathForMaven(String group, String artifact, String extension, String classifier, String version) { + return findPathForMaven(new MavenCoordinate(group, artifact, extension, classifier, version)); } - public static Path findPathForMaven(final String maven) { - return findLibsPath().resolve(MavenCoordinateResolver.get(maven)); + public static Path findPathForMaven(MavenCoordinate artifact) { + return findLibsPath().resolve(artifact.toRelativeRepositoryPath()); } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java b/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java index 7da76d8e5..0a7c58e24 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java +++ b/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java @@ -17,12 +17,12 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.stream.Collectors; +import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.loading.mixin.DeferredMixinConfigRegistration; -import net.neoforged.fml.loading.moddiscovery.BackgroundScanHandler; import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.fml.loading.moddiscovery.ModFileInfo; import net.neoforged.fml.loading.moddiscovery.ModInfo; -import net.neoforged.neoforgespi.locating.IModFile; +import net.neoforged.fml.loading.modscan.BackgroundScanHandler; /** * Master list of all mods in the loading context. This class cannot refer outside the @@ -33,9 +33,7 @@ public class LoadingModList { private final List modFiles; private final List sortedList; private final Map fileById; - private final List preLoadErrors; - private final List preLoadWarnings; - private List brokenFiles; + private final List modLoadingIssues; private LoadingModList(final List modFiles, final List sortedList) { this.modFiles = modFiles.stream() @@ -50,15 +48,12 @@ private LoadingModList(final List modFiles, final List sortedL .flatMap(Collection::stream) .map(ModInfo.class::cast) .collect(Collectors.toMap(ModInfo::getModId, ModInfo::getOwningFile)); - this.preLoadErrors = new ArrayList<>(); - this.preLoadWarnings = new ArrayList<>(); + this.modLoadingIssues = new ArrayList<>(); } - public static LoadingModList of(List modFiles, List sortedList, final EarlyLoadingException earlyLoadingException) { + public static LoadingModList of(List modFiles, List sortedList, List issues) { INSTANCE = new LoadingModList(modFiles, sortedList); - if (earlyLoadingException != null) { - INSTANCE.preLoadErrors.add(earlyLoadingException); - } + INSTANCE.modLoadingIssues.addAll(issues); return INSTANCE; } @@ -159,19 +154,11 @@ public List getMods() { return this.sortedList; } - public List getErrors() { - return preLoadErrors; - } - - public List getWarnings() { - return preLoadWarnings; - } - - public void setBrokenFiles(final List brokenFiles) { - this.brokenFiles = brokenFiles; + public boolean hasErrors() { + return modLoadingIssues.stream().noneMatch(issue -> issue.severity() == ModLoadingIssue.Severity.ERROR); } - public List getBrokenFiles() { - return this.brokenFiles; + public List getModLoadingIssues() { + return modLoadingIssues; } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/MavenCoordinate.java b/loader/src/main/java/net/neoforged/fml/loading/MavenCoordinate.java new file mode 100644 index 000000000..09d532519 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/MavenCoordinate.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; + +/** + * Models the Maven coordinates for an artifact. + */ +public record MavenCoordinate(String groupId, String artifactId, String extension, String classifier, String version) { + public MavenCoordinate { + Objects.requireNonNull(groupId); + Objects.requireNonNull(artifactId); + Objects.requireNonNull(version); + if (extension == null) { + extension = ""; + } + if (classifier == null) { + classifier = ""; + } + } + + /** + * Valid forms: + *
    + *
  • {@code groupId:artifactId:version}
  • + *
  • {@code groupId:artifactId:version:classifier}
  • + *
  • {@code groupId:artifactId:version:classifier@extension}
  • + *
  • {@code groupId:artifactId:version@extension}
  • + *
+ */ + public static MavenCoordinate parse(String coordinate) { + var coordinateAndExt = coordinate.split("@"); + String extension = ""; + if (coordinateAndExt.length > 2) { + throw new IllegalArgumentException("Malformed Maven coordinate: " + coordinate); + } else if (coordinateAndExt.length == 2) { + extension = coordinateAndExt[1]; + coordinate = coordinateAndExt[0]; + } + + var parts = coordinate.split(":"); + if (parts.length != 3 && parts.length != 4) { + throw new IllegalArgumentException("Malformed Maven coordinate: " + coordinate); + } + + var groupId = parts[0]; + var artifactId = parts[1]; + var version = parts[2]; + var classifier = parts.length == 4 ? parts[3] : ""; + return new MavenCoordinate(groupId, artifactId, extension, classifier, version); + } + + /** + * Constructs a path relative to the root of a Maven repository pointing to the artifact expressed through + * these coordinates. + */ + public Path toRelativeRepositoryPath() { + final String fileName = artifactId + "-" + version + + (!classifier.isEmpty() ? "-" + classifier : "") + + (!extension.isEmpty() ? "." + extension : ".jar"); + + String[] groups = groupId.split("\\."); + Path result = Paths.get(groups[0]); + for (int i = 1; i < groups.length; i++) { + result = result.resolve(groups[i]); + } + + return result.resolve(artifactId).resolve(version).resolve(fileName); + } + + @Override + public String toString() { + var result = new StringBuilder(); + result.append(groupId).append(":").append(artifactId).append(":").append(version); + if (!classifier.isEmpty()) { + result.append(":").append(classifier); + } + if (!extension.isEmpty()) { + result.append("@").append(extension); + } + return result.toString(); + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/MavenCoordinateResolver.java b/loader/src/main/java/net/neoforged/fml/loading/MavenCoordinateResolver.java index f5ab00059..6707bb1be 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/MavenCoordinateResolver.java +++ b/loader/src/main/java/net/neoforged/fml/loading/MavenCoordinateResolver.java @@ -6,38 +6,20 @@ package net.neoforged.fml.loading; import java.nio.file.Path; -import java.nio.file.Paths; /** * Convert a maven coordinate into a Path. - * + *

* This is gradle standard not maven standard coordinate formatting * {@code :[:]:[@extension]}, must not be {@code null}. */ public class MavenCoordinateResolver { - public static Path get(final String coordinate) { - final String[] parts = coordinate.split(":"); - final String groupId = parts[0]; - final String artifactId = parts[1]; - final String classifier = parts.length > 3 ? parts[2] : ""; - final String[] versext = parts[parts.length - 1].split("@"); - final String version = versext[0]; - final String extension = versext.length > 1 ? versext[1] : ""; - return get(groupId, artifactId, extension, classifier, version); + public static Path get(String coordinate) { + return MavenCoordinate.parse(coordinate).toRelativeRepositoryPath(); } - public static Path get(final String groupId, final String artifactId, final String extension, final String classifier, final String version) { - final String fileName = artifactId + "-" + version + - (!classifier.isEmpty() ? "-" + classifier : "") + - (!extension.isEmpty() ? "." + extension : ".jar"); - - String[] groups = groupId.split("\\."); - Path result = Paths.get(groups[0]); - for (int i = 1; i < groups.length; i++) { - result = result.resolve(groups[i]); - } - - return result.resolve(artifactId).resolve(version).resolve(fileName); + public static Path get(String groupId, String artifactId, String extension, String classifier, String version) { + return new MavenCoordinate(groupId, artifactId, extension, classifier, version).toRelativeRepositoryPath(); } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/ModDirTransformerDiscoverer.java b/loader/src/main/java/net/neoforged/fml/loading/ModDirTransformerDiscoverer.java index 43cdf7c78..44be5bd95 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/ModDirTransformerDiscoverer.java +++ b/loader/src/main/java/net/neoforged/fml/loading/ModDirTransformerDiscoverer.java @@ -56,7 +56,7 @@ public void earlyInitialization(final String launchTarget, final String[] argume @Override public List candidates(final Path gameDirectory) { - ModDirTransformerDiscoverer.scan(gameDirectory); + scan(gameDirectory); return List.copyOf(found); } diff --git a/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java b/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java index 0771fdb73..162089711 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java +++ b/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java @@ -29,7 +29,8 @@ import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.stream.Stream; -import net.neoforged.fml.loading.moddiscovery.MinecraftLocator; +import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.fml.loading.moddiscovery.ModFileInfo; import net.neoforged.fml.loading.moddiscovery.ModInfo; @@ -52,13 +53,13 @@ private ModSorter(final List modFiles) { this.uniqueModListBuilder = new UniqueModListBuilder(modFiles); } - public static LoadingModList sort(List mods, final List errors) { + public static LoadingModList sort(List mods, final List issues) { final ModSorter ms = new ModSorter(mods); try { ms.buildUniqueList(); - } catch (EarlyLoadingException e) { + } catch (ModLoadingException e) { // We cannot build any list with duped mods. We have to abort immediately and report it - return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf -> (ModInfo) mf.getModInfos().get(0)).collect(toList()), e); + return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf -> (ModInfo) mf.getModInfos().get(0)).collect(toList()), e.getIssues()); } // try and validate dependencies @@ -68,28 +69,40 @@ public static LoadingModList sort(List mods, final List (ModInfo) mf.getModInfos().get(0)).collect(toList()), new EarlyLoadingException("failure to validate mod list", null, resolutionResult.buildErrorMessages())); + list = LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf -> (ModInfo) mf.getModInfos().get(0)).collect(toList()), concat(issues, resolutionResult.buildErrorMessages())); } else { // Otherwise, lets try and sort the modlist and proceed - EarlyLoadingException earlyLoadingException = null; + ModLoadingException modLoadingException = null; try { ms.sort(); - } catch (EarlyLoadingException e) { - earlyLoadingException = e; + } catch (ModLoadingException e) { + modLoadingException = e; + } + if (modLoadingException == null) { + list = LoadingModList.of(ms.modFiles, ms.sortedList, issues); + } else { + list = LoadingModList.of(ms.modFiles, ms.sortedList, concat(issues, modLoadingException.getIssues())); } - list = LoadingModList.of(ms.modFiles, ms.sortedList, earlyLoadingException); } // If we have conflicts those are considered warnings if (!resolutionResult.discouraged.isEmpty()) { - list.getWarnings().add(new EarlyLoadingException( + list.getModLoadingIssues().add(ModLoadingIssue.warning( "found mod conflicts", - null, resolutionResult.buildWarningMessages())); } return list; } + @SafeVarargs + private static List concat(List... lists) { + var lst = new ArrayList(); + for (List list : lists) { + lst.addAll(list); + } + return lst; + } + @SuppressWarnings("UnstableApiUsage") private void sort() { // lambdas are identity based, so sorting them is impossible unless you hold reference to them @@ -119,9 +132,9 @@ private void sort() { .mapMulti(Iterable::forEach) .mapMulti((mf, c) -> mf.getMods().forEach(c)) .map(IModInfo::getModId) - .map(list -> new EarlyLoadingException.ExceptionData("fml.modloading.cycle", list)) + .map(list -> ModLoadingIssue.error("fml.modloading.cycle", list).withCause(e)) .toList(); - throw new EarlyLoadingException("Sorting error", e, dataList); + throw new ModLoadingException(dataList); } this.sortedList = sorted.stream() .map(ModFileInfo::getMods) @@ -166,9 +179,8 @@ private void detectSystemMods(final Map> modFilesByFirstId final Set systemMods = new HashSet<>(); // The minecraft mod is always a system mod systemMods.add("minecraft"); - // Find mod file from MinecraftLocator to define the system mods + // Find system mod files and scan them for system mods modFiles.stream() - .filter(modFile -> modFile.getProvider().getClass() == MinecraftLocator.class) .map(ModFile::getSecureJar) .map(SecureJar::moduleDataProvider) .map(SecureJar.ModuleDataProvider::getManifest) @@ -184,9 +196,7 @@ private void detectSystemMods(final Map> modFilesByFirstId var container = modFilesByFirstId.get(systemMod); if (container != null && !container.isEmpty()) { LOGGER.debug("Found system mod: {}", systemMod); - this.systemMods.add((ModFile) container.get(0)); - } else { - throw new IllegalStateException("Failed to find system mod: " + systemMod); + this.systemMods.add(container.getFirst()); } } } @@ -196,26 +206,26 @@ public record DependencyResolutionResult( Collection discouraged, Collection versionResolution, Map modVersions) { - public List buildWarningMessages() { + public List buildWarningMessages() { return Stream.concat(discouraged.stream() - .map(mv -> new EarlyLoadingException.ExceptionData("fml.modloading.discouragedmod", - mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), - modVersions.get(mv.getModId()), mv.getReason().orElse("fml.modloading.discouragedmod.noreason"))), + .map(mv -> ModLoadingIssue.warning("fml.modloading.discouragedmod", + mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), + modVersions.get(mv.getModId()), mv.getReason().orElse("fml.modloading.discouragedmod.noreason")).withAffectedMod(mv.getOwner())), - Stream.of(new EarlyLoadingException.ExceptionData("fml.modloading.discouragedmod.proceed"))) + Stream.of(ModLoadingIssue.warning("fml.modloading.discouragedmod.proceed"))) .toList(); } - public List buildErrorMessages() { + public List buildErrorMessages() { return Stream.concat( versionResolution.stream() - .map(mv -> new EarlyLoadingException.ExceptionData(mv.getType() == IModInfo.DependencyType.REQUIRED ? "fml.modloading.missingdependency" : "fml.modloading.missingdependency.optional", - mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), - modVersions.getOrDefault(mv.getModId(), new DefaultArtifactVersion("null")), mv.getReason())), + .map(mv -> ModLoadingIssue.error(mv.getType() == IModInfo.DependencyType.REQUIRED ? "fml.modloading.missingdependency" : "fml.modloading.missingdependency.optional", + mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), + modVersions.getOrDefault(mv.getModId(), new DefaultArtifactVersion("null")), mv.getReason()).withAffectedMod(mv.getOwner())), incompatibilities.stream() - .map(mv -> new EarlyLoadingException.ExceptionData("fml.modloading.incompatiblemod", - mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), - modVersions.get(mv.getModId()), mv.getReason().orElse("fml.modloading.incompatiblemod.noreason")))) + .map(mv -> ModLoadingIssue.error("fml.modloading.incompatiblemod", + mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), + modVersions.get(mv.getModId()), mv.getReason().orElse("fml.modloading.incompatiblemod.noreason")).withAffectedMod(mv.getOwner()))) .toList(); } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/TransformerDiscovererConstants.java b/loader/src/main/java/net/neoforged/fml/loading/TransformerDiscovererConstants.java index af1ec5e8c..09a9cc22c 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/TransformerDiscovererConstants.java +++ b/loader/src/main/java/net/neoforged/fml/loading/TransformerDiscovererConstants.java @@ -5,12 +5,13 @@ package net.neoforged.fml.loading; -import cpw.mods.jarhandling.JarContentsBuilder; +import cpw.mods.jarhandling.JarContents; import cpw.mods.jarhandling.JarMetadata; import cpw.mods.jarhandling.SecureJar; import cpw.mods.modlauncher.api.IModuleLayerManager.Layer; import cpw.mods.modlauncher.serviceapi.ITransformerDiscoveryService; import java.nio.file.Path; +import java.util.Collection; import java.util.Set; /** @@ -32,8 +33,16 @@ private TransformerDiscovererConstants() {} "net.neoforged.fml.loading.ImmediateWindowProvider", // FIXME: remove this when removing the legacy ImmediateWindowProvider "net.neoforged.neoforgespi.earlywindow.ImmediateWindowProvider"); - public static boolean shouldLoadInServiceLayer(Path... path) { - JarMetadata metadata = JarMetadata.from(new JarContentsBuilder().paths(path).build()); + public static boolean shouldLoadInServiceLayer(Collection paths) { + return shouldLoadInServiceLayer(JarContents.of(paths)); + } + + public static boolean shouldLoadInServiceLayer(Path path) { + return shouldLoadInServiceLayer(JarContents.of(path)); + } + + public static boolean shouldLoadInServiceLayer(JarContents jarContents) { + JarMetadata metadata = JarMetadata.from(jarContents); return metadata.providers().stream() .map(SecureJar.Provider::serviceName) .anyMatch(SERVICES::contains); diff --git a/loader/src/main/java/net/neoforged/fml/loading/UniqueModListBuilder.java b/loader/src/main/java/net/neoforged/fml/loading/UniqueModListBuilder.java index b25357e06..a156b51b9 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/UniqueModListBuilder.java +++ b/loader/src/main/java/net/neoforged/fml/loading/UniqueModListBuilder.java @@ -17,6 +17,8 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; +import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.neoforgespi.language.IModInfo; import org.apache.maven.artifact.versioning.ArtifactVersion; @@ -79,10 +81,8 @@ public UniqueModListData buildUniqueList() { .toList(); if (!dupedModErrors.isEmpty()) { - LOGGER.error(LOADING, "Found duplicate mods:\n{}", dupedModErrors.stream().collect(joining("\n"))); - throw new EarlyLoadingException("Duplicate mods found", null, dupedModErrors.stream() - .map(s -> new EarlyLoadingException.ExceptionData(s)) - .toList()); + LOGGER.error(LOADING, "Found duplicate mods:\n{}", String.join("\n", dupedModErrors)); + throw new ModLoadingException(dupedModErrors.stream().map(ModLoadingIssue::error).toList()); } final List dupedLibErrors = versionedLibIds.values().stream() @@ -94,10 +94,8 @@ public UniqueModListData buildUniqueList() { .toList(); if (!dupedLibErrors.isEmpty()) { - LOGGER.error(LOADING, "Found duplicate plugins or libraries:\n{}", dupedLibErrors.stream().collect(joining("\n"))); - throw new EarlyLoadingException("Duplicate plugins or libraries found", null, dupedLibErrors.stream() - .map(s -> new EarlyLoadingException.ExceptionData(s)) - .toList()); + LOGGER.error(LOADING, "Found duplicate plugins or libraries:\n{}", String.join("\n", dupedLibErrors)); + throw new ModLoadingException(dupedLibErrors.stream().map(ModLoadingIssue::error).toList()); } // Collect unique mod files by module name. This will be used for deduping purposes diff --git a/loader/src/main/java/net/neoforged/fml/loading/VersionInfo.java b/loader/src/main/java/net/neoforged/fml/loading/VersionInfo.java index 98a917810..8255b5a1e 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/VersionInfo.java +++ b/loader/src/main/java/net/neoforged/fml/loading/VersionInfo.java @@ -5,13 +5,7 @@ package net.neoforged.fml.loading; -import java.util.Map; - public record VersionInfo(String neoForgeVersion, String fmlVersion, String mcVersion, String neoFormVersion) { - VersionInfo(Map arguments) { - this((String) arguments.get("neoForgeVersion"), (String) arguments.get("fmlVersion"), (String) arguments.get("mcVersion"), (String) arguments.get("neoFormVersion")); - } - public String mcAndFmlVersion() { return mcVersion + "-" + fmlVersion; } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractJarFileDependencyLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractJarFileDependencyLocator.java deleted file mode 100644 index 0fe4ca55c..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractJarFileDependencyLocator.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery; - -import com.mojang.logging.LogUtils; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.util.Optional; -import net.neoforged.neoforgespi.locating.IDependencyLocator; -import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.ModFileLoadingException; -import org.slf4j.Logger; - -public abstract class AbstractJarFileDependencyLocator extends AbstractJarFileModProvider implements IDependencyLocator { - private static final Logger LOGGER = LogUtils.getLogger(); - - protected Optional loadResourceFromModFile(final IModFile modFile, final Path path) { - try { - return Optional.of(Files.newInputStream(modFile.findResource(path.toString()))); - } catch (final NoSuchFileException e) { - LOGGER.trace("Failed to load resource {} from {}, it does not contain dependency information.", path, modFile.getFileName()); - return Optional.empty(); - } catch (final Exception e) { - LOGGER.error("Failed to load resource {} from mod {}, cause {}", path, modFile.getFileName(), e); - return Optional.empty(); - } - } - - protected Optional loadModFileFrom(final IModFile file, final Path path) { - try { - final Path pathInModFile = file.findResource(path.toString()); - return Optional.of(createMod(pathInModFile).file()); - } catch (Exception e) { - LOGGER.error("Failed to load mod file {} from {}", path, file.getFileName()); - throw new ModFileLoadingException("Failed to load mod file " + file.getFileName()); - } - } - - protected String identifyMod(final IModFile modFile) { - return modFile.getFileName(); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractJarFileModLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractJarFileModLocator.java deleted file mode 100644 index 8ac424901..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractJarFileModLocator.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery; - -import java.io.File; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Stream; -import net.neoforged.neoforgespi.locating.IModLocator; - -public abstract class AbstractJarFileModLocator extends AbstractJarFileModProvider implements IModLocator { - @Override - public List scanMods() { - return scanCandidates().map(this::createMod).toList(); - } - - public abstract Stream scanCandidates(); - - protected static List getLegacyClasspath() { - return Arrays.stream(System.getProperty("legacyClassPath", "").split(File.pathSeparator)).map(Path::of).toList(); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractJarFileModProvider.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractJarFileModProvider.java deleted file mode 100644 index b392bfc80..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractJarFileModProvider.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery; - -import com.mojang.logging.LogUtils; -import cpw.mods.jarhandling.SecureJar; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Stream; -import net.neoforged.fml.loading.LogMarkers; -import net.neoforged.neoforgespi.locating.IModFile; -import org.slf4j.Logger; - -public abstract class AbstractJarFileModProvider extends AbstractModProvider { - private static final Logger LOGGER = LogUtils.getLogger(); - - @Override - public void scanFile(final IModFile file, final Consumer pathConsumer) { - LOGGER.debug(LogMarkers.SCAN, "Scan started: {}", file); - final Function status = p -> file.getSecureJar().verifyPath(p); - try (Stream files = Files.find(file.getSecureJar().getRootPath(), Integer.MAX_VALUE, (p, a) -> p.getNameCount() > 0 && p.getFileName().toString().endsWith(".class"))) { - file.setSecurityStatus(files.peek(pathConsumer).map(status).reduce((s1, s2) -> SecureJar.Status.values()[Math.min(s1.ordinal(), s2.ordinal())]).orElse(SecureJar.Status.INVALID)); - } catch (IOException e) { - e.printStackTrace(); - } - LOGGER.debug(LogMarkers.SCAN, "Scan finished: {}", file); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/BuiltinGameLibraryLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/BuiltinGameLibraryLocator.java deleted file mode 100644 index aaadda4c1..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/BuiltinGameLibraryLocator.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery; - -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class BuiltinGameLibraryLocator extends AbstractJarFileModLocator { - private final List legacyClasspath = AbstractJarFileModLocator.getLegacyClasspath(); - - @Override - public String name() { - return "builtin game layer libraries"; - } - - @Override - public void initArguments(Map arguments) {} - - @Override - public Stream scanCandidates() { - String gameLibrariesStr = System.getProperty("fml.gameLayerLibraries"); - if (gameLibrariesStr == null || gameLibrariesStr.isBlank()) - return Stream.of(); - - Set targets = Arrays.stream(gameLibrariesStr.split(",")).map(Path::of).collect(Collectors.toSet()); - var paths = Stream.builder(); - - for (Path path : this.legacyClasspath) { - if (targets.contains(path.getFileName())) - paths.add(path); - } - - return paths.build(); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ClasspathLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ClasspathLocator.java deleted file mode 100644 index 2f9ac8f62..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ClasspathLocator.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery; - -import com.mojang.logging.LogUtils; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; -import net.neoforged.fml.loading.ClasspathLocatorUtils; -import net.neoforged.fml.loading.LogMarkers; -import org.slf4j.Logger; - -public class ClasspathLocator extends AbstractJarFileModLocator { - private static final Logger LOGGER = LogUtils.getLogger(); - private final List legacyClasspath = AbstractJarFileModLocator.getLegacyClasspath(); - private boolean enabled = false; - - @Override - public String name() { - return "userdev classpath"; - } - - @Override - public Stream scanCandidates() { - if (!enabled) - return Stream.of(); - - try { - var claimed = new ArrayList<>(legacyClasspath); - var paths = Stream.builder(); - - findPaths(claimed, MODS_TOML).forEach(paths::add); - findPaths(claimed, MANIFEST).forEach(paths::add); - - return paths.build(); - } catch (IOException e) { - LOGGER.error(LogMarkers.SCAN, "Error trying to find resources", e); - throw new RuntimeException(e); - } - } - - private List findPaths(List claimed, String resource) throws IOException { - var ret = new ArrayList(); - final Enumeration resources = ClassLoader.getSystemClassLoader().getResources(resource); - while (resources.hasMoreElements()) { - URL url = resources.nextElement(); - Path path = ClasspathLocatorUtils.findJarPathFor(resource, resource, url); - if (claimed.stream().anyMatch(path::equals) || !Files.exists(path) || Files.isDirectory(path)) - continue; - ret.add(path); - } - return ret; - } - - @Override - public void initArguments(Map arguments) { - var launchTarget = (String) arguments.get("launchTarget"); - enabled = launchTarget != null && launchTarget.contains("dev"); - } - - private Path findJarPathFor(final String resourceName, final String jarName, final URL resource) { - try { - Path path; - final URI uri = resource.toURI(); - if (uri.getScheme().equals("jar") && uri.getRawSchemeSpecificPart().contains("!/")) { - int lastExcl = uri.getRawSchemeSpecificPart().lastIndexOf("!/"); - path = Paths.get(new URI(uri.getRawSchemeSpecificPart().substring(0, lastExcl))); - } else { - path = Paths.get(new URI("file://" + uri.getRawSchemeSpecificPart().substring(0, uri.getRawSchemeSpecificPart().length() - resourceName.length()))); - } - //LOGGER.debug(CORE, "Found JAR {} at path {}", jarName, path.toString()); - return path; - } catch (NullPointerException | URISyntaxException e) { - LOGGER.error(LogMarkers.SCAN, "Failed to find JAR for class {} - {}", resourceName, jarName); - throw new RuntimeException("Unable to locate " + resourceName + " - " + jarName, e); - } - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ExplodedDirectoryLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ExplodedDirectoryLocator.java deleted file mode 100644 index f748c58f9..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ExplodedDirectoryLocator.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery; - -import com.mojang.logging.LogUtils; -import cpw.mods.jarhandling.JarContentsBuilder; -import cpw.mods.jarhandling.SecureJar; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.stream.Stream; -import net.neoforged.fml.loading.LogMarkers; -import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IModLocator; -import org.slf4j.Logger; - -public class ExplodedDirectoryLocator implements IModLocator { - private static final Logger LOGGER = LogUtils.getLogger(); - - public record ExplodedMod(String modid, List paths) {} - - private final List explodedMods = new ArrayList<>(); - private final Map mods = new HashMap<>(); - - @Override - public List scanMods() { - explodedMods.forEach(explodedMod -> { - var jarContents = new JarContentsBuilder().paths(explodedMod.paths().toArray(Path[]::new)).build(); - if (jarContents.findFile(AbstractModProvider.MODS_TOML).isPresent()) { - var mjm = new ModJarMetadata(jarContents); - var mf = new ModFile(SecureJar.from(jarContents, mjm), this, ModFileParser::modsTomlParser); - mjm.setModFile(mf); - mods.put(explodedMod, mf); - } else { - LOGGER.warn(LogMarkers.LOADING, "Failed to find exploded resource {} in directory {}", AbstractModProvider.MODS_TOML, explodedMod.paths().get(0).toString()); - } - }); - return mods.values().stream().map(mf -> new IModLocator.ModFileOrException(mf, null)).toList(); - } - - @Override - public String name() { - return "exploded directory"; - } - - @Override - public void scanFile(final IModFile file, final Consumer pathConsumer) { - LOGGER.debug(LogMarkers.SCAN, "Scanning exploded directory {}", file.getFilePath().toString()); - try (Stream files = Files.find(file.getSecureJar().getRootPath(), Integer.MAX_VALUE, (p, a) -> p.getNameCount() > 0 && p.getFileName().toString().endsWith(".class"))) { - files.forEach(pathConsumer); - } catch (IOException e) { - e.printStackTrace(); - } - LOGGER.debug(LogMarkers.SCAN, "Exploded directory scan complete {}", file.getFilePath().toString()); - } - - @Override - public String toString() { - return "{ExplodedDir locator}"; - } - - @SuppressWarnings("unchecked") - @Override - public void initArguments(final Map arguments) { - final var explodedTargets = ((Map>) arguments).get("explodedTargets"); - if (explodedTargets != null && !explodedTargets.isEmpty()) { - explodedMods.addAll(explodedTargets); - } - } - - @Override - public boolean isValid(final IModFile modFile) { - return true; - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/IncompatibleModReason.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/IncompatibleModReason.java new file mode 100644 index 000000000..27a863edb --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/IncompatibleModReason.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery; + +import cpw.mods.jarhandling.JarContents; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Predicate; +import net.neoforged.fml.loading.StringUtils; + +/** + * When we find a jar file that no {@link net.neoforged.neoforgespi.locating.IModFileReader} can handle, + * we try to detect if the mod potentially came from another modding system and warn the user about it + * not being compatible. + */ +public enum IncompatibleModReason { + OLDFORGE(filePresent("mcmod.info")), + MINECRAFT_FORGE(filePresent("META-INF/mods.toml")), + FABRIC(filePresent("fabric.mod.json")), + QUILT(filePresent("quilt.mod.json")), + LITELOADER(filePresent("litemod.json")), + OPTIFINE(filePresent("optifine/Installer.class")), + BUKKIT(filePresent("plugin.yml")); + + private final Predicate ident; + + IncompatibleModReason(Predicate identifier) { + this.ident = identifier; + } + + public String getReason() { + return "fml.modloading.brokenfile." + StringUtils.toLowerCase(name()); + } + + public static Optional detect(JarContents jar) { + return Arrays.stream(values()) + .filter(i -> i.ident.test(jar)) + .findAny(); + } + + private static Predicate filePresent(String filename) { + return jarContents -> jarContents.findFile(filename).isPresent(); + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/InvalidModIdentifier.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/InvalidModIdentifier.java deleted file mode 100644 index 9f8aa80f8..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/InvalidModIdentifier.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery; - -import cpw.mods.modlauncher.api.LambdaExceptionUtils; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Optional; -import java.util.function.BiPredicate; -import java.util.zip.ZipFile; -import net.neoforged.fml.loading.StringUtils; - -public enum InvalidModIdentifier { - OLDFORGE(filePresent("mcmod.info")), - FABRIC(filePresent("fabric.mod.json")), - LITELOADER(filePresent("litemod.json")), - OPTIFINE(filePresent("optifine/Installer.class")), - BUKKIT(filePresent("plugin.yml")), - INVALIDZIP((f, zf) -> !zf.isPresent()); - - private BiPredicate> ident; - - InvalidModIdentifier(BiPredicate> identifier) { - this.ident = identifier; - } - - private String getReason() { - return "fml.modloading.brokenfile." + StringUtils.toLowerCase(name()); - } - - public static Optional identifyJarProblem(Path path) { - Optional zfo = tryOpenFile(path); - Optional result = Arrays.stream(values()).filter(i -> i.ident.test(path, zfo)).map(InvalidModIdentifier::getReason).findAny(); - zfo.ifPresent(LambdaExceptionUtils.rethrowConsumer(ZipFile::close)); - return result; - } - - private static BiPredicate> filePresent(String filename) { - return (f, zfo) -> zfo.map(zf -> zf.getEntry(filename) != null).orElse(false); - } - - private static Optional tryOpenFile(Path path) { - try { - return Optional.of(new ZipFile(path.toFile())); - } catch (Exception ignored) { - return Optional.empty(); - } - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/MavenDirectoryLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/MavenDirectoryLocator.java deleted file mode 100644 index f3d6c7486..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/MavenDirectoryLocator.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import net.neoforged.fml.loading.FMLPaths; -import net.neoforged.fml.loading.MavenCoordinateResolver; - -public class MavenDirectoryLocator extends AbstractJarFileModLocator { - private List modCoords; - - @Override - public Stream scanCandidates() { - return modCoords.stream(); - } - - @Override - public String name() { - return "maven libs"; - } - - public String toString() { - return "{Maven Directory locator for mods " + this.modCoords + "}"; - } - - @SuppressWarnings("unchecked") - @Override - public void initArguments(final Map arguments) { - final List mavenRoots = (List) arguments.get("mavenRoots"); - final List mavenRootPaths = mavenRoots.stream().map(n -> FMLPaths.GAMEDIR.get().resolve(n)).collect(Collectors.toList()); - final List mods = (List) arguments.get("mods"); - final List listedMods = ModListHandler.processModLists((List) arguments.get("modLists"), mavenRootPaths); - - List localModCoords = Stream.concat(mods.stream(), listedMods.stream()).map(MavenCoordinateResolver::get).collect(Collectors.toList()); - // find the modCoords path in each supplied maven path, and turn it into a mod file. (skips not found files) - - this.modCoords = localModCoords.stream().map(mc -> mavenRootPaths.stream().map(root -> root.resolve(mc)).filter(path -> Files.exists(path)).findFirst().orElseThrow(() -> new IllegalArgumentException("Failed to locate requested mod coordinate " + mc))).collect(Collectors.toList()); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/MinecraftLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/MinecraftLocator.java deleted file mode 100644 index d1ae069f8..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/MinecraftLocator.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery; - -import com.electronwill.nightconfig.core.Config; -import com.mojang.logging.LogUtils; -import cpw.mods.jarhandling.JarContentsBuilder; -import cpw.mods.jarhandling.SecureJar; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import net.neoforged.fml.loading.ClasspathTransformerDiscoverer; -import net.neoforged.fml.loading.FMLLoader; -import net.neoforged.fml.loading.LogMarkers; -import net.neoforged.neoforgespi.language.IModFileInfo; -import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IModLocator; -import net.neoforged.neoforgespi.locating.ModFileFactory; -import org.slf4j.Logger; - -public class MinecraftLocator extends AbstractModProvider implements IModLocator { - private static final Logger LOGGER = LogUtils.getLogger(); - - @Override - public List scanMods() { - final var launchHandler = FMLLoader.getLaunchHandler(); - var baseMC = launchHandler.getMinecraftPaths(); - var mcJarContents = new JarContentsBuilder() - .paths(baseMC.minecraftPaths().toArray(Path[]::new)) - .pathFilter(baseMC.minecraftFilter()) - .build(); - var mcJarMetadata = new ModJarMetadata(mcJarContents); - var mcSecureJar = SecureJar.from(mcJarContents, mcJarMetadata); - var mcjar = ModFileFactory.FACTORY.build(mcSecureJar, this, this::buildMinecraftTOML); - mcJarMetadata.setModFile(mcjar); - var artifacts = baseMC.otherArtifacts().stream() - .map(SecureJar::from) - .map(sj -> new ModFile(sj, this, ModFileParser::modsTomlParser)) - .collect(Collectors.toList()); - var otherModsExcluded = ClasspathTransformerDiscoverer.allExcluded(); - var othermods = baseMC.otherModPaths().stream() - .filter(p -> p.stream().noneMatch(otherModsExcluded::contains)) //We cannot load MOD_CLASSES from the classpath if they are loaded on the SERVICE layer. - .map(p -> createMod(p.toArray(Path[]::new))) - .filter(Objects::nonNull); - artifacts.add(mcjar); - - return Stream.concat(artifacts.stream().map(f -> new ModFileOrException(f, null)), othermods).toList(); - } - - private IModFileInfo buildMinecraftTOML(final IModFile iModFile) { - final ModFile modFile = (ModFile) iModFile; - /* - final Path mcmodtoml = modFile.findResource("META-INF", "minecraftmod.toml"); - if (Files.notExists(mcmodtoml)) { - LOGGER.fatal(LOADING, "Mod file {} is missing minecraftmod.toml file", modFile.getFilePath()); - return null; - } - - final FileConfig mcmodstomlfile = FileConfig.builder(mcmodtoml).build(); - mcmodstomlfile.load(); - mcmodstomlfile.close(); - */ - - // We haven't changed this in years, and I can't be asked right now to special case this one file in the path. - final var conf = Config.inMemory(); - conf.set("modLoader", "minecraft"); - conf.set("loaderVersion", "1"); - conf.set("license", "Mojang Studios, All Rights Reserved"); - final var mods = Config.inMemory(); - mods.set("modId", "minecraft"); - mods.set("version", FMLLoader.versionInfo().mcVersion()); - mods.set("displayName", "Minecraft"); - mods.set("logoFile", "mcplogo.png"); - mods.set("credits", "Mojang, deobfuscated by MCP"); - mods.set("authors", "MCP: Searge,ProfMobius,IngisKahn,Fesh0r,ZeuX,R4wk,LexManos,Bspkrs"); - mods.set("description", "Minecraft, decompiled and deobfuscated with MCP technology"); - conf.set("mods", List.of(mods)); - /* - conf.putAll(mcmodstomlfile); - - final var extralangs = Stream.builder(); - final Path forgemodtoml = modFile.findResource("META-INF", "mods.toml"); - if (Files.notExists(forgemodtoml)) { - LOGGER.info("No forge mods.toml file found, not loading forge mod"); - } else { - final FileConfig forgemodstomlfile = FileConfig.builder(forgemodtoml).build(); - forgemodstomlfile.load(); - forgemodstomlfile.close(); - conf.putAll(forgemodstomlfile); - conf.>get("mods").add(0, mcmodstomlfile.>get("mods").get(0)); // Add MC as a sub-mod - extralangs.add(new IModFileInfo.LanguageSpec(mcmodstomlfile.get("modLoader"), MavenVersionAdapter.createFromVersionSpec(mcmodstomlfile.get("loaderVersion")))); - } - */ - - final NightConfigWrapper configWrapper = new NightConfigWrapper(conf); - //final ModFileInfo modFileInfo = new ModFileInfo(modFile, configWrapper, extralangs.build().toList()); - return new ModFileInfo(modFile, configWrapper, configWrapper::setFile, List.of()); - } - - @Override - public String name() { - return "minecraft"; - } - - @Override - public void scanFile(final IModFile modFile, final Consumer pathConsumer) { - LOGGER.debug(LogMarkers.SCAN, "Scan started: {}", modFile); - try (Stream files = Files.find(modFile.getSecureJar().getRootPath(), Integer.MAX_VALUE, (p, a) -> p.getNameCount() > 0 && p.getFileName().toString().endsWith(".class"))) { - files.forEach(pathConsumer); - } catch (IOException e) { - e.printStackTrace(); - } - LOGGER.debug(LogMarkers.SCAN, "Scan finished: {}", modFile); - } - - @Override - public void initArguments(final Map arguments) { - // no op - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java index a0d5733a7..96c86f8f4 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java @@ -5,101 +5,82 @@ package net.neoforged.fml.loading.moddiscovery; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Maps; import com.mojang.logging.LogUtils; -import cpw.mods.modlauncher.Launcher; -import cpw.mods.modlauncher.api.IModuleLayerManager; -import cpw.mods.modlauncher.util.ServiceLoaderUtils; +import cpw.mods.jarhandling.JarContents; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; -import java.util.Objects; -import java.util.ServiceLoader; +import java.util.Optional; import java.util.stream.Collectors; -import net.neoforged.fml.loading.EarlyLoadingException; +import java.util.zip.ZipException; +import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.loading.ImmediateWindowHandler; import net.neoforged.fml.loading.LogMarkers; import net.neoforged.fml.loading.UniqueModListBuilder; -import net.neoforged.fml.loading.progress.StartupNotificationManager; -import net.neoforged.neoforgespi.Environment; -import net.neoforged.neoforgespi.language.IModFileInfo; +import net.neoforged.fml.util.ServiceLoaderUtil; +import net.neoforged.neoforgespi.ILaunchContext; import net.neoforged.neoforgespi.locating.IDependencyLocator; +import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IModLocator; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +import net.neoforged.neoforgespi.locating.IModFileReader; +import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; public class ModDiscoverer { private static final Logger LOGGER = LogUtils.getLogger(); - private final ServiceLoader modLocators; - private final ServiceLoader dependencyLocators; - private final List modLocatorList; - private final List dependencyLocatorList; - - public ModDiscoverer(Map arguments) { - Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.MODDIRECTORYFACTORY.get(), v -> ModsFolderLocator::new); - Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.PROGRESSMESSAGE.get(), v -> StartupNotificationManager.locatorConsumer().orElseGet(() -> s -> {})); - final var moduleLayerManager = Launcher.INSTANCE.environment().findModuleLayerManager().orElseThrow(); - modLocators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IModLocator.class); - dependencyLocators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IDependencyLocator.class); - modLocatorList = ServiceLoaderUtils.streamServiceLoader(() -> modLocators, sce -> LOGGER.error("Failed to load mod locator list", sce)).collect(Collectors.toList()); - modLocatorList.forEach(l -> l.initArguments(arguments)); - dependencyLocatorList = ServiceLoaderUtils.streamServiceLoader(() -> dependencyLocators, sce -> LOGGER.error("Failed to load dependency locator list", sce)).collect(Collectors.toList()); - dependencyLocatorList.forEach(l -> l.initArguments(arguments)); - if (LOGGER.isDebugEnabled(LogMarkers.CORE)) { - LOGGER.debug(LogMarkers.CORE, "Found Mod Locators : {}", modLocatorList.stream() - .map(modLocator -> "(%s:%s)".formatted(modLocator.name(), - modLocator.getClass().getPackage().getImplementationVersion())) - .collect(Collectors.joining(","))); - } - if (LOGGER.isDebugEnabled(LogMarkers.CORE)) { - LOGGER.debug(LogMarkers.CORE, "Found Dependency Locators : {}", dependencyLocatorList.stream() - .map(dependencyLocator -> "(%s:%s)".formatted(dependencyLocator.name(), - dependencyLocator.getClass().getPackage().getImplementationVersion())) - .collect(Collectors.joining(","))); - } + private final List modFileLocators; + private final List dependencyLocators; + private final List modFileReaders; + private final ILaunchContext launchContext; + + public ModDiscoverer(ILaunchContext launchContext) { + this(launchContext, List.of()); + } + + public ModDiscoverer(ILaunchContext launchContext, + Collection additionalModFileLocators) { + this.launchContext = launchContext; + + modFileLocators = ServiceLoaderUtil.loadServices(launchContext, IModFileCandidateLocator.class, additionalModFileLocators); + modFileReaders = ServiceLoaderUtil.loadServices(launchContext, IModFileReader.class); + dependencyLocators = ServiceLoaderUtil.loadServices(launchContext, IDependencyLocator.class); } public ModValidator discoverMods() { - LOGGER.debug(LogMarkers.SCAN, "Scanning for mods and other resources to load. We know {} ways to find mods", modLocatorList.size()); + LOGGER.debug(LogMarkers.SCAN, "Scanning for mods and other resources to load. We know {} ways to find mods", modFileLocators.size()); List loadedFiles = new ArrayList<>(); - List discoveryErrorData = new ArrayList<>(); + List discoveryIssues = new ArrayList<>(); boolean successfullyLoadedMods = true; - List brokenFiles = new ArrayList<>(); ImmediateWindowHandler.updateProgress("Discovering mod files"); - //Loop all mod locators to get the prime mods to load from. - for (IModLocator locator : modLocatorList) { - try { - LOGGER.debug(LogMarkers.SCAN, "Trying locator {}", locator); - var candidates = locator.scanMods(); - LOGGER.debug(LogMarkers.SCAN, "Locator {} found {} candidates or errors", locator, candidates.size()); - var exceptions = candidates.stream().map(IModLocator.ModFileOrException::ex).filter(Objects::nonNull).toList(); - if (!exceptions.isEmpty()) { - LOGGER.debug(LogMarkers.SCAN, "Locator {} found {} invalid mod files", locator, exceptions.size()); - brokenFiles.addAll(exceptions.stream().map(e -> e instanceof InvalidModFileException ime ? ime.getBrokenFile() : null).filter(Objects::nonNull).toList()); - } - var locatedFiles = candidates.stream().map(IModLocator.ModFileOrException::file).filter(Objects::nonNull).collect(Collectors.toList()); - var badModFiles = locatedFiles.stream().filter(file -> !(file instanceof ModFile)).toList(); - if (!badModFiles.isEmpty()) { - LOGGER.error(LogMarkers.SCAN, "Locator {} returned {} files which is are not ModFile instances! They will be skipped!", locator, badModFiles.size()); - brokenFiles.addAll(badModFiles.stream().map(IModFile::getModFileInfo).toList()); - } - locatedFiles.removeAll(badModFiles); - LOGGER.debug(LogMarkers.SCAN, "Locator {} found {} valid mod files", locator, locatedFiles.size()); - handleLocatedFiles(loadedFiles, locatedFiles); - } catch (InvalidModFileException imfe) { - // We don't generally expect this exception, since it should come from the candidates stream above and be handled in the Locator, but just in case. - LOGGER.error(LogMarkers.SCAN, "Locator {} found an invalid mod file {}", locator, imfe.getBrokenFile(), imfe); - brokenFiles.add(imfe.getBrokenFile()); - } catch (EarlyLoadingException exception) { - LOGGER.error(LogMarkers.SCAN, "Failed to load mods with locator {}", locator, exception); - discoveryErrorData.addAll(exception.getAllData()); + // Loop all mod locators to get the root mods to load from. + for (var locator : modFileLocators) { + LOGGER.debug(LogMarkers.SCAN, "Trying locator {}", locator); + + var defaultAttributes = ModFileDiscoveryAttributes.DEFAULT.withLocator(locator); + var pipeline = new DiscoveryPipeline(defaultAttributes, loadedFiles, discoveryIssues); + try { + locator.findCandidates(launchContext, pipeline); + } catch (ModLoadingException e) { + discoveryIssues.addAll(e.getIssues()); + } catch (Exception e) { + discoveryIssues.add(ModLoadingIssue.error("fml.modloading.technical_error", locator.toString() + "failed").withCause(e)); } + + LOGGER.debug(LogMarkers.SCAN, "Locator {} found {} mods, {} warnings, {} errors and skipped {} candidates", locator, + pipeline.successCount, pipeline.warningCount, pipeline.errorCount, pipeline.skipCount); } //First processing run of the mod list. Any duplicates will cause resolution failure and dependency loading will be skipped. - Map> modFilesMap = Maps.newHashMap(); + Map> modFilesMap = Collections.emptyMap(); try { final UniqueModListBuilder modsUniqueListBuilder = new UniqueModListBuilder(loadedFiles); final UniqueModListBuilder.UniqueModListData uniqueModsData = modsUniqueListBuilder.buildUniqueList(); @@ -109,29 +90,23 @@ public ModValidator discoverMods() { modFilesMap = uniqueModsData.modFiles().stream() .collect(Collectors.groupingBy(IModFile::getType)); loadedFiles = uniqueModsData.modFiles(); - } catch (EarlyLoadingException exception) { + } catch (ModLoadingException exception) { LOGGER.error(LogMarkers.SCAN, "Failed to build unique mod list after mod discovery.", exception); - discoveryErrorData.addAll(exception.getAllData()); + discoveryIssues.addAll(exception.getIssues()); successfullyLoadedMods = false; } //We can continue loading if prime mods loaded successfully. if (successfullyLoadedMods) { LOGGER.debug(LogMarkers.SCAN, "Successfully Loaded {} mods. Attempting to load dependencies...", loadedFiles.size()); - for (IDependencyLocator locator : dependencyLocatorList) { + for (var locator : dependencyLocators) { try { LOGGER.debug(LogMarkers.SCAN, "Trying locator {}", locator); - final List locatedMods = ImmutableList.copyOf(loadedFiles); - - var locatedFiles = locator.scanMods(locatedMods); - if (locatedFiles.stream().anyMatch(file -> !(file instanceof ModFile))) { - LOGGER.error(LogMarkers.SCAN, "A dependency locator returned a file which is not a ModFile instance!. They will be skipped!"); - } - - handleLocatedFiles(loadedFiles, locatedFiles); - } catch (EarlyLoadingException exception) { + var pipeline = new DiscoveryPipeline(ModFileDiscoveryAttributes.DEFAULT.withDependencyLocator(locator), loadedFiles, discoveryIssues); + locator.scanMods(List.copyOf(loadedFiles), pipeline); + } catch (ModLoadingException exception) { LOGGER.error(LogMarkers.SCAN, "Failed to load dependencies with locator {}", locator, exception); - discoveryErrorData.addAll(exception.getAllData()); + discoveryIssues.addAll(exception.getIssues()); } } @@ -143,9 +118,9 @@ public ModValidator discoverMods() { //We now only need the mod files map, not the list. modFilesMap = uniqueModsAndDependenciesData.modFiles().stream() .collect(Collectors.groupingBy(IModFile::getType)); - } catch (EarlyLoadingException exception) { + } catch (ModLoadingException exception) { LOGGER.error(LogMarkers.SCAN, "Failed to build unique mod list after dependency discovery.", exception); - discoveryErrorData.addAll(exception.getAllData()); + discoveryIssues.addAll(exception.getIssues()); modFilesMap = loadedFiles.stream().collect(Collectors.groupingBy(IModFile::getType)); } } else { @@ -155,16 +130,137 @@ public ModValidator discoverMods() { //Validate the loading. With a deduplicated list, we can now successfully process the artifacts and load //transformer plugins. - var validator = new ModValidator(modFilesMap, brokenFiles, discoveryErrorData); + var validator = new ModValidator(modFilesMap, discoveryIssues); validator.stage1Validation(); return validator; } - private void handleLocatedFiles(final List loadedFiles, final List locatedFiles) { - var locatedModFiles = locatedFiles.stream().filter(ModFile.class::isInstance).map(ModFile.class::cast).toList(); - for (IModFile mf : locatedModFiles) { - LOGGER.info(LogMarkers.SCAN, "Found mod file \"{}\" of type {} with provider {}", mf.getFileName(), mf.getType(), mf.getProvider()); + private class DiscoveryPipeline implements IDiscoveryPipeline { + private final ModFileDiscoveryAttributes defaultAttributes; + private final List loadedFiles; + private final List issues; + + private int successCount; + private int errorCount; + private int warningCount; + private int skipCount; + + public DiscoveryPipeline(ModFileDiscoveryAttributes defaultAttributes, + List loadedFiles, + List issues) { + this.defaultAttributes = defaultAttributes; + this.loadedFiles = loadedFiles; + this.issues = issues; + } + + @Override + public Optional addPath(List groupedPaths, ModFileDiscoveryAttributes attributes, IncompatibleFileReporting reporting) { + var primaryPath = groupedPaths.getFirst(); + + if (!launchContext.addLocated(primaryPath)) { + LOGGER.debug("Skipping {} because it was already located earlier", primaryPath); + skipCount++; + return Optional.empty(); + } + + JarContents jarContents; + try { + jarContents = JarContents.of(groupedPaths); + } catch (Exception e) { + if (causeChainContains(e, ZipException.class)) { + addIssue(ModLoadingIssue.error("fml.modloading.brokenfile.invalidzip", primaryPath).withAffectedPath(primaryPath).withCause(e)); + } else { + addIssue(ModLoadingIssue.error("fml.modloading.brokenfile", primaryPath).withAffectedPath(primaryPath).withCause(e)); + } + return Optional.empty(); + } + + return addJarContent(jarContents, attributes, reporting); + } + + @Override + public @Nullable IModFile readModFile(JarContents jarContents, ModFileDiscoveryAttributes attributes) { + for (var reader : modFileReaders) { + var provided = reader.read(jarContents, attributes); + if (provided != null) { + return provided; + } + } + + throw new RuntimeException("No mod reader felt responsible for " + jarContents.getPrimaryPath()); + } + + @Override + public Optional addJarContent(JarContents jarContents, ModFileDiscoveryAttributes attributes, IncompatibleFileReporting reporting) { + attributes = defaultAttributes.merge(attributes); + + for (var reader : modFileReaders) { + try { + var provided = reader.read(jarContents, attributes); + if (provided != null) { + if (addModFile(provided)) { + return Optional.of(provided); + } + return Optional.empty(); + } + } catch (Exception e) { + addIssue(ModLoadingIssue.error("fml.modloading.brokenfile", jarContents.getPrimaryPath()).withAffectedPath(jarContents.getPrimaryPath()).withCause(e)); + return Optional.empty(); + } + } + + // If a jar file was found in a subdirectory of the game directory, but could not be loaded, + // it might be an incompatible mod type. We do not perform this validation for jars that we + // found on the classpath or other locations since these are usually not under user control. + if (reporting == IncompatibleFileReporting.ERROR) { + addIssue(ModLoadingIssue.error("fml.modloading.brokenfile", jarContents.getPrimaryPath())); + } else if (reporting == IncompatibleFileReporting.WARN_ON_KNOWN_INCOMPATIBILITY || reporting == IncompatibleFileReporting.WARN_ALWAYS) { + var reason = IncompatibleModReason.detect(jarContents); + if (reason.isPresent()) { + LOGGER.warn(LogMarkers.SCAN, "Found incompatible jar {} with reason {}. Skipping.", jarContents.getPrimaryPath(), reason.get()); + addIssue(ModLoadingIssue.warning(reason.get().getReason(), jarContents.getPrimaryPath()).withAffectedPath(jarContents.getPrimaryPath())); + } else if (reporting == IncompatibleFileReporting.WARN_ALWAYS) { + LOGGER.warn(LogMarkers.SCAN, "Ignoring incompatible jar {} for an unknown reason.", jarContents.getPrimaryPath()); + addIssue(ModLoadingIssue.warning("fml.modloading.brokenfile", jarContents.getPrimaryPath()).withAffectedPath(jarContents.getPrimaryPath())); + } + } + return Optional.empty(); + } + + @Override + public boolean addModFile(IModFile mf) { + if (!(mf instanceof ModFile modFile)) { + String detail = "Unexpected IModFile subclass: " + mf.getClass(); + addIssue(ModLoadingIssue.error("fml.modloading.technical_error", detail).withAffectedModFile(mf)); + return false; + } + + modFile.setDiscoveryAttributes(defaultAttributes.merge(mf.getDiscoveryAttributes())); + + var discoveryAttributes = mf.getDiscoveryAttributes(); + LOGGER.info(LogMarkers.SCAN, "Found {} file \"{}\" {}", mf.getType().name().toLowerCase(Locale.ROOT), mf.getFileName(), discoveryAttributes); + + loadedFiles.add(modFile); + successCount++; + return true; + } + + @Override + public void addIssue(ModLoadingIssue issue) { + issues.add(issue); + switch (issue.severity()) { + case WARNING -> warningCount++; + case ERROR -> errorCount++; + } + } + } + + private static boolean causeChainContains(Throwable e, Class exceptionClass) { + for (; e != null; e = e.getCause()) { + if (exceptionClass.isInstance(e)) { + return true; + } } - loadedFiles.addAll(locatedModFiles); + return false; } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java index 182eef004..1ee7d4ea5 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java @@ -8,6 +8,8 @@ import com.google.common.collect.ImmutableMap; import com.mojang.logging.LogUtils; import cpw.mods.jarhandling.SecureJar; +import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -17,35 +19,39 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.stream.Stream; import net.neoforged.fml.loading.FMLLoader; import net.neoforged.fml.loading.LogMarkers; +import net.neoforged.fml.loading.modscan.Scanner; import net.neoforged.neoforgespi.language.IModFileInfo; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.language.IModLanguageProvider; import net.neoforged.neoforgespi.language.ModFileScanData; import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IModProvider; -import net.neoforged.neoforgespi.locating.ModFileFactory; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; +import net.neoforged.neoforgespi.locating.ModFileInfoParser; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.jetbrains.annotations.ApiStatus; import org.slf4j.Logger; +@ApiStatus.Internal public class ModFile implements IModFile { private static final Logger LOGGER = LogUtils.getLogger(); private final String jarVersion; - private final ModFileFactory.ModFileInfoParser parser; + private final ModFileInfoParser parser; + private ModFileDiscoveryAttributes discoveryAttributes; private Map fileProperties; private List loaders; private Throwable scanError; private final SecureJar jar; private final Type modFileType; private final Manifest manifest; - private final IModProvider provider; private IModFileInfo modFileInfo; private ModFileScanData fileModFileScanData; private volatile CompletableFuture futureScanResult; @@ -53,20 +59,20 @@ public class ModFile implements IModFile { private List mixinConfigs; private List accessTransformers; - static final Attributes.Name TYPE = new Attributes.Name("FMLModType"); + public static final Attributes.Name TYPE = new Attributes.Name("FMLModType"); private SecureJar.Status securityStatus; - public ModFile(final SecureJar jar, final IModProvider provider, final ModFileFactory.ModFileInfoParser parser) { - this(jar, provider, parser, parseType(jar)); + public ModFile(SecureJar jar, final ModFileInfoParser parser, ModFileDiscoveryAttributes attributes) { + this(jar, parser, parseType(jar), attributes); } - public ModFile(final SecureJar jar, final IModProvider provider, final ModFileFactory.ModFileInfoParser parser, String type) { - this.provider = provider; - this.jar = jar; - this.parser = parser; + public ModFile(SecureJar jar, ModFileInfoParser parser, Type type, ModFileDiscoveryAttributes discoveryAttributes) { + this.jar = Objects.requireNonNull(jar, "jar"); + this.parser = Objects.requireNonNull(parser, "parser"); + this.discoveryAttributes = Objects.requireNonNull(discoveryAttributes, "discoveryAttributes"); manifest = this.jar.moduleDataProvider().getManifest(); - modFileType = Type.valueOf(type); + modFileType = Objects.requireNonNull(type, "type"); jarVersion = Optional.ofNullable(manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION)).orElse("0.0NONE"); this.modFileInfo = ModFileParser.readModList(this, this.parser); } @@ -138,7 +144,13 @@ public ModFileScanData compileContent() { } public void scanFile(Consumer pathConsumer) { - provider.scanFile(this, pathConsumer); + final Function status = p -> getSecureJar().verifyPath(p); + var rootPath = getSecureJar().getRootPath(); + try (Stream files = Files.find(rootPath, Integer.MAX_VALUE, (p, a) -> p.getNameCount() > 0 && p.getFileName().toString().endsWith(".class"))) { + setSecurityStatus(files.peek(pathConsumer).map(status).reduce((s1, s2) -> SecureJar.Status.values()[Math.min(s1.ordinal(), s2.ordinal())]).orElse(SecureJar.Status.INVALID)); + } catch (IOException e) { + throw new UncheckedIOException("Failed to scan " + rootPath, e); + } } public void setFutureScanResult(CompletableFuture future) { @@ -174,6 +186,9 @@ public void setFileProperties(Map fileProperties) { @Override public List getLoaders() { + if (loaders == null) { + throw new IllegalStateException("Language loaders have not yet been identified"); + } return loaders; } @@ -193,7 +208,11 @@ public void identifyLanguage() { @Override public String toString() { - return "Mod File: " + Objects.toString(this.jar.getPrimaryPath()); + if (discoveryAttributes.parent() != null) { + return "Nested Mod File " + this.jar.getPrimaryPath() + " in " + discoveryAttributes.parent(); + } else { + return "Mod File: " + this.jar.getPrimaryPath(); + } } @Override @@ -202,8 +221,12 @@ public String getFileName() { } @Override - public IModProvider getProvider() { - return provider; + public ModFileDiscoveryAttributes getDiscoveryAttributes() { + return discoveryAttributes; + } + + public void setDiscoveryAttributes(ModFileDiscoveryAttributes discoveryAttributes) { + this.discoveryAttributes = discoveryAttributes; } @Override @@ -220,9 +243,9 @@ public ArtifactVersion getJarVersion() { return new DefaultArtifactVersion(this.jarVersion); } - private static String parseType(final SecureJar jar) { + private static Type parseType(final SecureJar jar) { final Manifest m = jar.moduleDataProvider().getManifest(); final Optional value = Optional.ofNullable(m.getMainAttributes().getValue(TYPE)); - return value.orElse("MOD"); + return value.map(Type::valueOf).orElse(Type.MOD); } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileInfo.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileInfo.java index f2f062e0b..1c71e0aa9 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileInfo.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileInfo.java @@ -5,7 +5,6 @@ package net.neoforged.fml.loading.moddiscovery; -import com.google.common.base.Strings; import com.mojang.logging.LogUtils; import cpw.mods.modlauncher.api.LambdaExceptionUtils; import java.net.URL; @@ -35,6 +34,8 @@ import net.neoforged.neoforgespi.language.IModFileInfo; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.language.MavenVersionAdapter; +import net.neoforged.neoforgespi.locating.InvalidModFileException; +import org.jetbrains.annotations.ApiStatus; import org.slf4j.Logger; public class ModFileInfo implements IModFileInfo, IConfigurable { @@ -50,7 +51,8 @@ public class ModFileInfo implements IModFileInfo, IConfigurable { private final String license; private final List usesServices; - ModFileInfo(final ModFile modFile, final IConfigurable config, Consumer configFileConsumer) { + @ApiStatus.Internal + public ModFileInfo(final ModFile modFile, final IConfigurable config, Consumer configFileConsumer) { this.modFile = modFile; this.config = config; configFileConsumer.accept(this); @@ -65,6 +67,10 @@ public class ModFileInfo implements IModFileInfo, IConfigurable { // the remaining properties are optional with sensible defaults this.license = config.getConfigElement("license") .orElse(""); + // Validate the license is set. Only apply this validation to mods. + if (this.license.isBlank()) { + throw new InvalidModFileException("fml.modloading.missinglicense", this); + } this.showAsResourcePack = config.getConfigElement("showAsResourcePack") .orElse(false); this.showAsDataPack = config.getConfigElement("showAsDataPack") @@ -150,10 +156,6 @@ public URL getIssueURL() { return issueURL; } - public boolean missingLicense() { - return Strings.isNullOrEmpty(license); - } - public Optional getCodeSigningFingerprint() { var signers = this.modFile.getSecureJar().getManifestSigners(); return (signers == null ? Stream.of() : Arrays.stream(signers)) @@ -204,4 +206,9 @@ public String versionString() { public List usesServices() { return usesServices; } + + @Override + public String toString() { + return moduleName(); + } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileParser.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileParser.java index 8aeb46dbc..20cab0b5b 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileParser.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileParser.java @@ -18,24 +18,26 @@ import java.util.Map; import java.util.Optional; import net.neoforged.fml.loading.LogMarkers; +import net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader; import net.neoforged.neoforgespi.language.IModFileInfo; import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.ModFileFactory; +import net.neoforged.neoforgespi.locating.InvalidModFileException; +import net.neoforged.neoforgespi.locating.ModFileInfoParser; import org.slf4j.Logger; public class ModFileParser { private static final Logger LOGGER = LogUtils.getLogger(); - public static IModFileInfo readModList(final ModFile modFile, final ModFileFactory.ModFileInfoParser parser) { + public static IModFileInfo readModList(final ModFile modFile, final ModFileInfoParser parser) { return parser.build(modFile); } public static IModFileInfo modsTomlParser(final IModFile imodFile) { ModFile modFile = (ModFile) imodFile; LOGGER.debug(LogMarkers.LOADING, "Considering mod file candidate {}", modFile.getFilePath()); - final Path modsjson = modFile.findResource(AbstractModProvider.MODS_TOML); + final Path modsjson = modFile.findResource(JarModsDotTomlModFileReader.MODS_TOML); if (!Files.exists(modsjson)) { - LOGGER.warn(LogMarkers.LOADING, "Mod file {} is missing {} file", modFile.getFilePath(), AbstractModProvider.MODS_TOML); + LOGGER.warn(LogMarkers.LOADING, "Mod file {} is missing {} file", modFile.getFilePath(), JarModsDotTomlModFileReader.MODS_TOML); return null; } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModInfo.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModInfo.java index c23bce2ce..15bc0f26d 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModInfo.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModInfo.java @@ -13,13 +13,13 @@ import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; -import net.neoforged.fml.loading.FMLLoader; import net.neoforged.fml.loading.StringSubstitutor; import net.neoforged.fml.loading.StringUtils; import net.neoforged.neoforgespi.language.IConfigurable; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.language.MavenVersionAdapter; import net.neoforged.neoforgespi.locating.ForgeFeature; +import net.neoforged.neoforgespi.locating.InvalidModFileException; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.VersionRange; @@ -210,23 +210,7 @@ public ModVersion(final IModInfo owner, final IConfigurable config) { this.modId = config.getConfigElement("modId") .orElseThrow(() -> new InvalidModFileException("Missing required field modid in dependency", getOwningFile())); this.type = config.getConfigElement("type") - // TODO - 1.21: remove the fallback to the mandatory field - .map(str -> str.toUpperCase(Locale.ROOT)).map(DependencyType::valueOf).orElseGet(() -> { - final var mandatory = config.getConfigElement("mandatory"); - if (mandatory.isPresent()) { - if (!FMLLoader.isProduction()) { - LOGGER.error("Mod '{}' uses deprecated 'mandatory' field in the dependency declaration for '{}'. Use the 'type' field and 'required'/'optional' instead", owner.getModId(), modId); - // only error the mod being "developed" (i.e. found through the MOD_CLASSES) to prevent dependencies from causing the crash - if (owner.getOwningFile().getFile().getProvider() instanceof MinecraftLocator) { - throw new InvalidModFileException("Deprecated 'mandatory' field is used in dependency", getOwningFile()); - } - } - - return mandatory.get() ? DependencyType.REQUIRED : DependencyType.OPTIONAL; - } - - return DependencyType.REQUIRED; - }); + .map(str -> str.toUpperCase(Locale.ROOT)).map(DependencyType::valueOf).orElse(DependencyType.REQUIRED); this.reason = config.getConfigElement("reason"); this.versionRange = config.getConfigElement("versionRange") .map(MavenVersionAdapter::createFromVersionSpec) diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModJarMetadata.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModJarMetadata.java index 094994ef1..b42b8318d 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModJarMetadata.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModJarMetadata.java @@ -16,7 +16,7 @@ public final class ModJarMetadata extends LazyJarMetadata implements JarMetadata private final JarContents jarContents; private IModFile modFile; - ModJarMetadata(JarContents jarContents) { + public ModJarMetadata(JarContents jarContents) { this.jarContents = jarContents; } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModListHandler.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModListHandler.java index 7a8bc9be0..b443ff0d0 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModListHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModListHandler.java @@ -33,9 +33,9 @@ public class ModListHandler { * @return list of found mod coordinates */ public static List processModLists(final List modListPaths, final List mavenRootPaths) { - final List modCoordinates = modListPaths.stream().map(ModListHandler::transformPathToList).flatMap(Collection::stream).collect(Collectors.toList()); + final List modCoordinates = modListPaths.stream().map(ModListHandler::transformPathToList).flatMap(Collection::stream).toList(); - List> localModCoords = modCoordinates.stream().map(mc -> Pair.of(MavenCoordinateResolver.get(mc), mc)).collect(Collectors.toList()); + List> localModCoords = modCoordinates.stream().map(mc -> Pair.of(MavenCoordinateResolver.get(mc), mc)).toList(); final List> foundCoordinates = localModCoords.stream().map(mc -> mavenRootPaths.stream().map(root -> Pair.of(root.resolve(mc.getLeft()), mc.getRight())).filter(path -> Files.exists(path.getLeft())).findFirst().orElseGet(() -> { LOGGER.warn(LogMarkers.CORE, "Failed to find coordinate {}", mc); return null; diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModValidator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModValidator.java index d6eec29e9..0c9423a0a 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModValidator.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModValidator.java @@ -12,14 +12,14 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import java.util.stream.Stream; -import net.neoforged.fml.loading.EarlyLoadingException; +import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.loading.ImmediateWindowHandler; import net.neoforged.fml.loading.LoadingModList; import net.neoforged.fml.loading.LogMarkers; import net.neoforged.fml.loading.ModSorter; -import net.neoforged.neoforgespi.language.IModFileInfo; +import net.neoforged.fml.loading.modscan.BackgroundScanHandler; import net.neoforged.neoforgespi.locating.IModFile; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -29,17 +29,15 @@ public class ModValidator { private final Map> modFiles; private final List candidatePlugins; private final List candidateMods; + private final List issues; private LoadingModList loadingModList; - private List brokenFiles; - private final List discoveryErrorData; - public ModValidator(final Map> modFiles, final List brokenFiles, final List discoveryErrorData) { + public ModValidator(Map> modFiles, List issues) { this.modFiles = modFiles; this.candidateMods = lst(modFiles.get(IModFile.Type.MOD)); this.candidateMods.addAll(lst(modFiles.get(IModFile.Type.GAMELIBRARY))); this.candidatePlugins = lst(modFiles.get(IModFile.Type.LIBRARY)); - this.discoveryErrorData = discoveryErrorData; - this.brokenFiles = brokenFiles.stream().map(IModFileInfo::getFile).collect(Collectors.toList()); // mutable list + this.issues = issues; } private static List lst(@Nullable List files) { @@ -47,24 +45,21 @@ private static List lst(@Nullable List files) { } public void stage1Validation() { - brokenFiles.addAll(validateFiles(candidateMods)); + validateFiles(candidateMods); if (LOGGER.isDebugEnabled(LogMarkers.SCAN)) { LOGGER.debug(LogMarkers.SCAN, "Found {} mod files with {} mods", candidateMods.size(), candidateMods.stream().mapToInt(mf -> mf.getModInfos().size()).sum()); } ImmediateWindowHandler.updateProgress("Found " + candidateMods.size() + " mod candidates"); } - private List validateFiles(final List mods) { - final List brokenFiles = new ArrayList<>(); + private void validateFiles(final List mods) { for (Iterator iterator = mods.iterator(); iterator.hasNext();) { - ModFile modFile = iterator.next(); - if (!modFile.getProvider().isValid(modFile) || !modFile.identifyMods()) { + var modFile = iterator.next(); + if (!modFile.identifyMods()) { LOGGER.warn(LogMarkers.SCAN, "File {} has been ignored - it is invalid", modFile.getFilePath()); iterator.remove(); - brokenFiles.add(modFile); } } - return brokenFiles; } public ITransformationService.Resource getPluginResources() { @@ -80,32 +75,29 @@ public ITransformationService.Resource getModResources() { return new ITransformationService.Resource(IModuleLayerManager.Layer.GAME, modFilesToLoad.map(ModFile::getSecureJar).toList()); } - private List validateLanguages() { - List errorData = new ArrayList<>(); + private void validateLanguages() { for (Iterator iterator = this.candidateMods.iterator(); iterator.hasNext();) { - final ModFile modFile = iterator.next(); + var modFile = iterator.next(); try { modFile.identifyLanguage(); - } catch (EarlyLoadingException e) { - errorData.addAll(e.getAllData()); + } catch (ModLoadingException e) { + issues.addAll(e.getIssues()); + iterator.remove(); + } catch (Exception e) { + issues.add(ModLoadingIssue.error("").withAffectedModFile(modFile).withCause(e)); iterator.remove(); } } - return errorData; } public BackgroundScanHandler stage2Validation() { - var errors = validateLanguages(); - - var allErrors = new ArrayList<>(errors); - allErrors.addAll(this.discoveryErrorData); + validateLanguages(); - loadingModList = ModSorter.sort(candidateMods, allErrors); + loadingModList = ModSorter.sort(candidateMods, issues); loadingModList.addCoreMods(); loadingModList.addAccessTransformers(); loadingModList.addMixinConfigs(); - loadingModList.setBrokenFiles(brokenFiles); - BackgroundScanHandler backgroundScanHandler = new BackgroundScanHandler(); + var backgroundScanHandler = new BackgroundScanHandler(); loadingModList.addForScanning(backgroundScanHandler); return backgroundScanHandler; } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModsFolderLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModsFolderLocator.java deleted file mode 100644 index 1a6c73e28..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModsFolderLocator.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery; - -import com.mojang.logging.LogUtils; -import cpw.mods.modlauncher.api.LambdaExceptionUtils; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Comparator; -import java.util.Map; -import java.util.stream.Stream; -import net.neoforged.fml.loading.FMLPaths; -import net.neoforged.fml.loading.LogMarkers; -import net.neoforged.fml.loading.ModDirTransformerDiscoverer; -import net.neoforged.fml.loading.StringUtils; -import org.slf4j.Logger; - -/** - * Support loading mods located in JAR files in the mods folder - */ -public class ModsFolderLocator extends AbstractJarFileModLocator { - private static final String SUFFIX = ".jar"; - private static final Logger LOGGER = LogUtils.getLogger(); - private final Path modFolder; - private final String customName; - - public ModsFolderLocator() { - this(FMLPaths.MODSDIR.get()); - } - - ModsFolderLocator(Path modFolder) { - this(modFolder, "mods folder"); - } - - ModsFolderLocator(Path modFolder, String name) { - this.modFolder = modFolder; - this.customName = name; - } - - @Override - public Stream scanCandidates() { - LOGGER.debug(LogMarkers.SCAN, "Scanning mods dir {} for mods", this.modFolder); - var excluded = ModDirTransformerDiscoverer.allExcluded(); - - return LambdaExceptionUtils.uncheck(() -> Files.list(this.modFolder)) - .filter(p -> !excluded.contains(p) && StringUtils.toLowerCase(p.getFileName().toString()).endsWith(SUFFIX)) - .sorted(Comparator.comparing(path -> StringUtils.toLowerCase(path.getFileName().toString()))); - } - - @Override - public String name() { - return customName; - } - - @Override - public String toString() { - return "{" + customName + " locator at " + this.modFolder + "}"; - } - - @Override - public void initArguments(final Map arguments) {} -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/NightConfigWrapper.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/NightConfigWrapper.java index 9936fa6d8..458daaf88 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/NightConfigWrapper.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/NightConfigWrapper.java @@ -15,6 +15,7 @@ import java.util.stream.Collectors; import net.neoforged.neoforgespi.language.IConfigurable; import net.neoforged.neoforgespi.language.IModFileInfo; +import net.neoforged.neoforgespi.locating.InvalidModFileException; public class NightConfigWrapper implements IConfigurable { private final UnmodifiableConfig config; @@ -24,7 +25,7 @@ public NightConfigWrapper(final UnmodifiableConfig config) { this.config = config; } - NightConfigWrapper setFile(IModFileInfo file) { + public NightConfigWrapper setFile(IModFileInfo file) { this.file = file; return this; } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/JarInJarDependencyLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/JarInJarDependencyLocator.java similarity index 54% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/JarInJarDependencyLocator.java rename to loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/JarInJarDependencyLocator.java index 5894b4371..26fe294a8 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/JarInJarDependencyLocator.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/JarInJarDependencyLocator.java @@ -3,75 +3,67 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery; +package net.neoforged.fml.loading.moddiscovery.locators; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; import com.mojang.logging.LogUtils; +import cpw.mods.jarhandling.JarContents; +import java.io.InputStream; import java.net.URI; -import java.nio.file.FileSystem; import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Collection; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; -import net.neoforged.fml.loading.EarlyLoadingException; +import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.ModLoadingIssue; import net.neoforged.jarjar.selection.JarSelector; import net.neoforged.neoforgespi.language.IModInfo; +import net.neoforged.neoforgespi.locating.IDependencyLocator; +import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; import net.neoforged.neoforgespi.locating.IModFile; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; import net.neoforged.neoforgespi.locating.ModFileLoadingException; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.VersionRange; import org.slf4j.Logger; -public class JarInJarDependencyLocator extends AbstractJarFileDependencyLocator { +public class JarInJarDependencyLocator implements IDependencyLocator { private static final Logger LOGGER = LogUtils.getLogger(); @Override - public String name() { - return "JarInJar"; - } - - @Override - public List scanMods(final Iterable loadedMods) { - final List sources = Lists.newArrayList(); - loadedMods.forEach(sources::add); - - final List dependenciesToLoad = JarSelector.detectAndSelect(sources, this::loadResourceFromModFile, this::loadModFileFrom, this::identifyMod, this::exception); + public void scanMods(List loadedMods, IDiscoveryPipeline pipeline) { + List dependenciesToLoad = JarSelector.detectAndSelect( + loadedMods, + this::loadResourceFromModFile, + (file, path) -> loadModFileFrom(file, path, pipeline), + this::identifyMod, + this::exception); if (dependenciesToLoad.isEmpty()) { LOGGER.info("No dependencies to load found. Skipping!"); - return Collections.emptyList(); + } else { + LOGGER.info("Found {} dependencies adding them to mods collection", dependenciesToLoad.size()); + for (var modFile : dependenciesToLoad) { + pipeline.addModFile(modFile); + } } - - LOGGER.info("Found {} dependencies adding them to mods collection", dependenciesToLoad.size()); - return dependenciesToLoad; - } - - @Override - public void initArguments(final Map arguments) { - // NO-OP, for now - } - - @Override - protected String getDefaultJarModType() { - return IModFile.Type.LIBRARY.name(); } @SuppressWarnings("resource") - @Override - protected Optional loadModFileFrom(final IModFile file, final Path path) { + protected Optional loadModFileFrom(IModFile file, final Path path, IDiscoveryPipeline pipeline) { try { - final Path pathInModFile = file.findResource(path.toString()); - final URI filePathUri = new URI("jij:" + (pathInModFile.toAbsolutePath().toUri().getRawSchemeSpecificPart())).normalize(); - final Map outerFsArgs = ImmutableMap.of("packagePath", pathInModFile); - final FileSystem zipFS = FileSystems.newFileSystem(filePathUri, outerFsArgs); - final Path pathInFS = zipFS.getPath("/"); - return Optional.of(createMod(pathInFS).file()); + var pathInModFile = file.findResource(path.toString()); + var filePathUri = new URI("jij:" + (pathInModFile.toAbsolutePath().toUri().getRawSchemeSpecificPart())).normalize(); + var outerFsArgs = ImmutableMap.of("packagePath", pathInModFile); + var zipFS = FileSystems.newFileSystem(filePathUri, outerFsArgs); + var jar = JarContents.of(zipFS.getPath("/")); + var providerResult = pipeline.readModFile(jar, ModFileDiscoveryAttributes.DEFAULT.withParent(file)); + return Optional.ofNullable(providerResult); } catch (Exception e) { LOGGER.error("Failed to load mod file {} from {}", path, file.getFileName()); final RuntimeException exception = new ModFileLoadingException("Failed to load mod file " + file.getFileName()); @@ -81,17 +73,17 @@ protected Optional loadModFileFrom(final IModFile file, final Path pat } } - protected EarlyLoadingException exception(Collection> failedDependencies) { - final List errors = failedDependencies.stream() + protected ModLoadingException exception(Collection> failedDependencies) { + final List errors = failedDependencies.stream() .filter(entry -> !entry.sources().isEmpty()) //Should never be the case, but just to be sure .map(this::buildExceptionData) .toList(); - return new EarlyLoadingException(failedDependencies.size() + " Dependency restrictions were not met.", null, errors); + return new ModLoadingException(errors); } - private EarlyLoadingException.ExceptionData buildExceptionData(final JarSelector.ResolutionFailureInformation entry) { - return new EarlyLoadingException.ExceptionData( + private ModLoadingIssue buildExceptionData(final JarSelector.ResolutionFailureInformation entry) { + return ModLoadingIssue.error( getErrorTranslationKey(entry), entry.identifier().group() + ":" + entry.identifier().artifact(), entry.sources() @@ -117,7 +109,6 @@ private String formatError(final ModWithVersionRange modWithVersionRange) { return "\u00a7e" + modWithVersionRange.modInfo().getModId() + "\u00a7r - \u00a74" + modWithVersionRange.versionRange().toString() + "\u00a74 - \u00a72" + modWithVersionRange.artifactVersion().toString() + "\u00a72"; } - @Override protected String identifyMod(final IModFile modFile) { if (modFile.getModFileInfo() == null || modFile.getModInfos().isEmpty()) { return modFile.getFileName(); @@ -127,4 +118,21 @@ protected String identifyMod(final IModFile modFile) { } private record ModWithVersionRange(IModInfo modInfo, VersionRange versionRange, ArtifactVersion artifactVersion) {} + + protected Optional loadResourceFromModFile(final IModFile modFile, final Path path) { + try { + return Optional.of(Files.newInputStream(modFile.findResource(path.toString()))); + } catch (final NoSuchFileException e) { + LOGGER.trace("Failed to load resource {} from {}, it does not contain dependency information.", path, modFile.getFileName()); + return Optional.empty(); + } catch (final Exception e) { + LOGGER.error("Failed to load resource {} from mod {}, cause {}", path, modFile.getFileName(), e); + return Optional.empty(); + } + } + + @Override + public String toString() { + return "jarinjar"; + } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/MavenDirectoryLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/MavenDirectoryLocator.java new file mode 100644 index 000000000..f29f7bee8 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/MavenDirectoryLocator.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.locators; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.neoforged.fml.ModLoadingIssue; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.fml.loading.MavenCoordinate; +import net.neoforged.fml.loading.moddiscovery.ModListHandler; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; + +/** + * Locates mod-file candidates from Maven repository folder-structures. + * Maven coordinates for mods must be provided via the FML command line. + */ +public class MavenDirectoryLocator implements IModFileCandidateLocator { + @Override + public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { + var mavenRoots = context.mavenRoots(); + var mavenRootPaths = mavenRoots.stream().map(n -> FMLPaths.GAMEDIR.get().resolve(n)).collect(Collectors.toList()); + var mods = context.mods(); + var listedMods = ModListHandler.processModLists(context.modLists(), mavenRootPaths); + + // find the modCoords path in each supplied maven path, and turn it into a mod file + var modCoordinates = Stream.concat(mods.stream(), listedMods.stream()).toList(); + for (var modCoordinate : modCoordinates) { + Path relativePath; + try { + relativePath = MavenCoordinate.parse(modCoordinate).toRelativeRepositoryPath(); + } catch (Exception e) { + pipeline.addIssue(ModLoadingIssue.error("fml.modloading.invalid_maven_coordinate", modCoordinate).withCause(e)); + continue; + } + + var path = mavenRootPaths.stream().map(root -> root.resolve(relativePath)).filter(Files::exists).findFirst(); + if (path.isPresent()) { + pipeline.addPath(path.get(), ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.ERROR); + } else { + pipeline.addIssue(ModLoadingIssue.error("fml.modloading.maven_coordinate_not_found", modCoordinate, mavenRootPaths)); + } + } + } + + @Override + public String toString() { + return "maven libs"; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/MinecraftModInfo.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/MinecraftModInfo.java new file mode 100644 index 000000000..510adb3b5 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/MinecraftModInfo.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.locators; + +import com.electronwill.nightconfig.core.Config; +import java.util.List; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.fml.loading.moddiscovery.ModFile; +import net.neoforged.fml.loading.moddiscovery.ModFileInfo; +import net.neoforged.fml.loading.moddiscovery.NightConfigWrapper; +import net.neoforged.neoforgespi.language.IModFileInfo; +import net.neoforged.neoforgespi.locating.IModFile; + +final class MinecraftModInfo { + private MinecraftModInfo() {} + + public static IModFileInfo buildMinecraftModInfo(final IModFile iModFile) { + final ModFile modFile = (ModFile) iModFile; + + // We haven't changed this in years, and I can't be asked right now to special case this one file in the path. + final var conf = Config.inMemory(); + conf.set("modLoader", "minecraft"); + conf.set("loaderVersion", "1"); + conf.set("license", "Mojang Studios, All Rights Reserved"); + final var mods = Config.inMemory(); + mods.set("modId", "minecraft"); + mods.set("version", FMLLoader.versionInfo().mcVersion()); + mods.set("displayName", "Minecraft"); + mods.set("description", "Minecraft"); + conf.set("mods", List.of(mods)); + + final NightConfigWrapper configWrapper = new NightConfigWrapper(conf); + return new ModFileInfo(modFile, configWrapper, configWrapper::setFile, List.of()); + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ModsFolderLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ModsFolderLocator.java new file mode 100644 index 000000000..e0fc52796 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ModsFolderLocator.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.locators; + +import com.mojang.logging.LogUtils; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.ModLoadingIssue; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.fml.loading.LogMarkers; +import net.neoforged.fml.loading.StringUtils; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; +import org.slf4j.Logger; + +/** + * Support loading mods located in JAR files in the mods folder + */ +public class ModsFolderLocator implements IModFileCandidateLocator { + private static final String SUFFIX = ".jar"; + private static final Logger LOGGER = LogUtils.getLogger(); + private final Path modFolder; + private final String customName; + + public ModsFolderLocator() { + this(FMLPaths.MODSDIR.get()); + } + + ModsFolderLocator(Path modFolder) { + this(modFolder, "mods folder"); + } + + public ModsFolderLocator(Path modFolder, String name) { + this.modFolder = Objects.requireNonNull(modFolder, "modFolder"); + this.customName = Objects.requireNonNull(name, "name"); + } + + @Override + public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { + LOGGER.debug(LogMarkers.SCAN, "Scanning mods dir {} for mods", this.modFolder); + + List directoryContent; + try (var files = Files.list(this.modFolder)) { + directoryContent = files + .filter(p -> StringUtils.toLowerCase(p.getFileName().toString()).endsWith(SUFFIX)) + .sorted(Comparator.comparing(path -> StringUtils.toLowerCase(path.getFileName().toString()))) + .toList(); + } catch (UncheckedIOException | IOException e) { + throw new ModLoadingException(ModLoadingIssue.error("fml.modloading.failed_to_list_folder_content").withAffectedPath(this.modFolder).withCause(e)); + } + + for (var file : directoryContent) { + if (!Files.isRegularFile(file)) { + pipeline.addIssue(ModLoadingIssue.warning("fml.modloading.brokenfile", file).withAffectedPath(file)); + continue; + } + + pipeline.addPath(file, ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.WARN_ALWAYS); + } + } + + @Override + public String toString() { + return "{" + customName + " locator at " + this.modFolder + "}"; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/NeoForgeDevProvider.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/NeoForgeDevProvider.java new file mode 100644 index 000000000..a59fa6952 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/NeoForgeDevProvider.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.locators; + +import com.google.common.collect.Streams; +import cpw.mods.jarhandling.JarContentsBuilder; +import cpw.mods.jarhandling.SecureJar; +import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import net.neoforged.fml.loading.moddiscovery.ModJarMetadata; +import net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader; +import net.neoforged.fml.util.DevEnvUtils; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; +import net.neoforged.neoforgespi.locating.IModFile; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; + +/** + * Provides the Minecraft and NeoForge mods in a NeoForge dev environment. + */ +public class NeoForgeDevProvider implements IModFileCandidateLocator { + private final List paths; + + public NeoForgeDevProvider(List paths) { + this.paths = paths; + } + + @Override + public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { + Path minecraftResourcesRoot = null; + + // try finding client-extra jar explicitly first + var legacyClassPath = System.getProperty("legacyClassPath"); + if (legacyClassPath != null) { + minecraftResourcesRoot = Arrays.stream(legacyClassPath.split(File.pathSeparator)) + .map(Path::of) + .filter(path -> path.getFileName().toString().contains("client-extra")) + .findFirst() + .orElse(null); + } + // then fall back to finding it on the current classpath + if (minecraftResourcesRoot == null) { + minecraftResourcesRoot = DevEnvUtils.findFileSystemRootOfFileOnClasspath("assets/.mcassetsroot"); + } + + var packages = getNeoForgeSpecificPathPrefixes(); + var minecraftResourcesPrefix = minecraftResourcesRoot; + + var mcJarContents = new JarContentsBuilder() + .paths(Streams.concat(paths.stream(), Stream.of(minecraftResourcesRoot)).toArray(Path[]::new)) + .pathFilter((entry, basePath) -> { + // We serve everything, except for things in the forge packages. + if (basePath.equals(minecraftResourcesPrefix) || entry.endsWith("/")) { + return true; + } + // Any non-class file will be served from the client extra jar file mentioned above + if (!entry.endsWith(".class")) { + return false; + } + for (var pkg : packages) { + if (entry.startsWith(pkg)) { + return false; + } + } + return true; + }) + .build(); + + var mcJarMetadata = new ModJarMetadata(mcJarContents); + var mcSecureJar = SecureJar.from(mcJarContents, mcJarMetadata); + var minecraftModFile = IModFile.create(mcSecureJar, MinecraftModInfo::buildMinecraftModInfo); + mcJarMetadata.setModFile(minecraftModFile); + pipeline.addModFile(minecraftModFile); + + // We need to separate out our resources/code so that we can show up as a different data pack. + var neoforgeJarContents = new JarContentsBuilder() + .paths(paths.toArray(Path[]::new)) + .pathFilter((entry, basePath) -> { + if (!entry.endsWith(".class")) return true; + for (var pkg : packages) + if (entry.startsWith(pkg)) return true; + return false; + }) + .build(); + pipeline.addModFile(JarModsDotTomlModFileReader.createModFile(neoforgeJarContents, ModFileDiscoveryAttributes.DEFAULT)); + } + + private static String[] getNeoForgeSpecificPathPrefixes() { + return new String[] { "net/neoforged/neoforge/", "META-INF/services/", "META-INF/coremods.json", JarModsDotTomlModFileReader.MODS_TOML }; + } + + @Override + public String toString() { + return "neoforge devenv provider (" + paths + ")"; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/PathBasedLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/PathBasedLocator.java new file mode 100644 index 000000000..06a832e26 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/PathBasedLocator.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.locators; + +import java.nio.file.Path; +import java.util.List; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; + +/** + * "Locates" mods from a fixed set of paths. + */ +public record PathBasedLocator(String name, List paths) implements IModFileCandidateLocator { + public PathBasedLocator(String name, Path... paths) { + this(name, List.of(paths)); + } + + @Override + public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { + for (var path : paths) { + pipeline.addPath(path, ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.ERROR); + } + } + + @Override + public int getPriority() { + // Since this locator uses explicitly specified paths, they should not be handled by other locators first + return HIGHEST_SYSTEM_PRIORITY; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionClientProvider.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionClientProvider.java new file mode 100644 index 000000000..860c7e730 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionClientProvider.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.locators; + +import cpw.mods.jarhandling.JarContents; +import cpw.mods.jarhandling.SecureJar; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.ModLoadingIssue; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.fml.loading.LibraryFinder; +import net.neoforged.fml.loading.MavenCoordinate; +import net.neoforged.fml.loading.moddiscovery.ModJarMetadata; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; +import net.neoforged.neoforgespi.locating.IModFile; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; + +/** + * Locates the Minecraft client files in a production environment. + *

+ * It assumes that the installer produced two artifacts, one containing the Minecraft classes ("srg") which have + * been renamed to Mojangs official names using their mappings, and another containing only the Minecraft resource + * files ("extra"), and searches for these artifacts in the library directory. + */ +public class ProductionClientProvider implements IModFileCandidateLocator { + private final List additionalContent; + + public ProductionClientProvider(List additionalContent) { + this.additionalContent = additionalContent; + } + + @Override + public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { + var vers = FMLLoader.versionInfo(); + + var content = new ArrayList(); + addRequiredLibrary(new MavenCoordinate("net.minecraft", "client", "", "srg", vers.mcAndNeoFormVersion()), content); + addRequiredLibrary(new MavenCoordinate("net.minecraft", "client", "", "extra", vers.mcAndNeoFormVersion()), content); + for (var artifact : additionalContent) { + addRequiredLibrary(artifact, content); + } + + try { + var mcJarContents = JarContents.of(content); + + var mcJarMetadata = new ModJarMetadata(mcJarContents); + var mcSecureJar = SecureJar.from(mcJarContents, mcJarMetadata); + var mcjar = IModFile.create(mcSecureJar, MinecraftModInfo::buildMinecraftModInfo); + mcJarMetadata.setModFile(mcjar); + + pipeline.addModFile(mcjar); + } catch (Exception e) { + pipeline.addIssue(ModLoadingIssue.error("fml.modloading.corrupted_installation").withCause(e)); + } + } + + private static void addRequiredLibrary(MavenCoordinate coordinate, List content) { + var path = LibraryFinder.findPathForMaven(coordinate); + if (!Files.exists(path)) { + throw new ModLoadingException(ModLoadingIssue.error("fml.modloading.corrupted_installation").withAffectedPath(path)); + } else { + content.add(path); + } + } + + @Override + public String toString() { + var result = new StringBuilder("production client provider"); + for (var mavenCoordinate : additionalContent) { + result.append(" +").append(mavenCoordinate); + } + return result.toString(); + } + + @Override + public int getPriority() { + return HIGHEST_SYSTEM_PRIORITY; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionServerProvider.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionServerProvider.java new file mode 100644 index 000000000..db2f6bd7f --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionServerProvider.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.locators; + +import cpw.mods.jarhandling.JarContents; +import cpw.mods.jarhandling.JarContentsBuilder; +import cpw.mods.jarhandling.SecureJar; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import net.neoforged.fml.ModLoadingIssue; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.fml.loading.LibraryFinder; +import net.neoforged.fml.loading.MavenCoordinate; +import net.neoforged.fml.loading.moddiscovery.ModJarMetadata; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; +import net.neoforged.neoforgespi.locating.IModFile; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; + +/** + * Locates the Minecraft server files in a production environment. + */ +public class ProductionServerProvider implements IModFileCandidateLocator { + private final List additionalContent; + + public ProductionServerProvider(List additionalContent) { + this.additionalContent = additionalContent; + } + + @Override + public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { + var vers = FMLLoader.versionInfo(); + + try { + var mc = LibraryFinder.findPathForMaven("net.minecraft", "server", "", "srg", vers.mcAndNeoFormVersion()); + if (!Files.exists(mc)) { + pipeline.addIssue(ModLoadingIssue.error("fml.modloading.corrupted_installation").withAffectedPath(mc)); + return; + } + var mcextra = LibraryFinder.findPathForMaven("net.minecraft", "server", "", "extra", vers.mcAndNeoFormVersion()); + if (!Files.exists(mcextra)) { + pipeline.addIssue(ModLoadingIssue.error("fml.modloading.corrupted_installation").withAffectedPath(mc)); + return; + } + + var mcextra_filtered = SecureJar.from(new JarContentsBuilder() + // We only want it for its resources. So filter everything else out. + .pathFilter((path, base) -> { + return path.equals("META-INF/versions/") || // This is required because it bypasses our filter for the manifest, and it's a multi-release jar. + (!path.endsWith(".class") && + !path.startsWith("META-INF/")); + }) + .paths(mcextra) + .build()); + + var content = new ArrayList(); + content.add(mc); + content.add(mcextra_filtered.getRootPath()); + for (var artifact : additionalContent) { + var extraPath = LibraryFinder.findPathForMaven(artifact); + if (!Files.exists(extraPath)) { + pipeline.addIssue(ModLoadingIssue.error("fml.modloading.corrupted_installation").withAffectedPath(extraPath)); + return; + } + content.add(extraPath); + } + + var mcJarContents = JarContents.of(content); + + var mcJarMetadata = new ModJarMetadata(mcJarContents); + var mcSecureJar = SecureJar.from(mcJarContents, mcJarMetadata); + var mcjar = IModFile.create(mcSecureJar, MinecraftModInfo::buildMinecraftModInfo); + mcJarMetadata.setModFile(mcjar); + + pipeline.addModFile(mcjar); + } catch (Exception e) { + pipeline.addIssue(ModLoadingIssue.error("fml.modloading.corrupted_installation").withCause(e)); + } + } + + @Override + public String toString() { + var result = new StringBuilder("production server provider"); + for (var mavenCoordinate : additionalContent) { + result.append(" +").append(mavenCoordinate); + } + return result.toString(); + } + + @Override + public int getPriority() { + return HIGHEST_SYSTEM_PRIORITY; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/UserdevLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/UserdevLocator.java new file mode 100644 index 000000000..b91740c2d --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/UserdevLocator.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.locators; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader; +import net.neoforged.fml.util.DevEnvUtils; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; + +public class UserdevLocator implements IModFileCandidateLocator { + private final Map> modFolders; + + public UserdevLocator(Map> modFolders) { + this.modFolders = modFolders; + } + + @Override + public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { + var claimed = modFolders.values().stream().flatMap(List::stream).collect(Collectors.toCollection(HashSet::new)); + + for (var modFolderGroup : modFolders.values()) { + pipeline.addPath(modFolderGroup, ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.ERROR); + } + + var fromClasspath = new ArrayList(); + fromClasspath.addAll(DevEnvUtils.findFileSystemRootsOfFileOnClasspath(JarModsDotTomlModFileReader.MODS_TOML)); + fromClasspath.addAll(DevEnvUtils.findFileSystemRootsOfFileOnClasspath(JarModsDotTomlModFileReader.MANIFEST)); + for (var path : fromClasspath) { + if (claimed.add(path)) { + pipeline.addPath(List.of(path), ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.WARN_ON_KNOWN_INCOMPATIBILITY); + } + } + } + + @Override + public String toString() { + return "userdev mods and services"; + } + + @Override + public int getPriority() { + return LOWEST_SYSTEM_PRIORITY; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractModProvider.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/JarModsDotTomlModFileReader.java similarity index 62% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractModProvider.java rename to loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/JarModsDotTomlModFileReader.java index 37da5891f..95b166e8e 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/AbstractModProvider.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/JarModsDotTomlModFileReader.java @@ -1,61 +1,64 @@ /* - * Copyright (c) Forge Development LLC and contributors + * Copyright (c) NeoForged and contributors * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery; +package net.neoforged.fml.loading.moddiscovery.readers; import com.mojang.logging.LogUtils; -import cpw.mods.jarhandling.JarContentsBuilder; +import cpw.mods.jarhandling.JarContents; import cpw.mods.jarhandling.SecureJar; -import java.nio.file.Path; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; import net.neoforged.fml.loading.LogMarkers; +import net.neoforged.fml.loading.moddiscovery.ModFile; +import net.neoforged.fml.loading.moddiscovery.ModFileParser; +import net.neoforged.fml.loading.moddiscovery.ModJarMetadata; import net.neoforged.neoforgespi.language.IConfigurable; import net.neoforged.neoforgespi.language.IModFileInfo; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IModLocator; -import net.neoforged.neoforgespi.locating.IModProvider; -import net.neoforged.neoforgespi.locating.ModFileLoadingException; +import net.neoforged.neoforgespi.locating.IModFileReader; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; -public abstract class AbstractModProvider implements IModProvider { +/** + * Responsible for handling mod files that are explicitly marked as mods or libraries via metadata files. + */ +public class JarModsDotTomlModFileReader implements IModFileReader { private static final Logger LOGGER = LogUtils.getLogger(); public static final String MODS_TOML = "META-INF/neoforge.mods.toml"; - protected static final String MANIFEST = "META-INF/MANIFEST.MF"; - - protected IModLocator.ModFileOrException createMod(Path... path) { - var jarContents = new JarContentsBuilder() - .paths(path) - .build(); + public static final String MANIFEST = "META-INF/MANIFEST.MF"; + public static IModFile createModFile(JarContents contents, ModFileDiscoveryAttributes discoveryAttributes) { + var type = getModType(contents); IModFile mod; - var type = jarContents.getManifest().getMainAttributes().getValue(ModFile.TYPE); - if (type == null) { - type = getDefaultJarModType(); - } - if (jarContents.findFile(MODS_TOML).isPresent()) { - LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MODS_TOML, type, path); - var mjm = new ModJarMetadata(jarContents); - mod = new ModFile(SecureJar.from(jarContents, mjm), this, ModFileParser::modsTomlParser); + if (contents.findFile(MODS_TOML).isPresent()) { + LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MODS_TOML, type, contents.getPrimaryPath()); + var mjm = new ModJarMetadata(contents); + mod = new ModFile(SecureJar.from(contents, mjm), ModFileParser::modsTomlParser, discoveryAttributes); mjm.setModFile(mod); } else if (type != null) { - LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MANIFEST, type, path); - mod = new ModFile(SecureJar.from(jarContents), this, this::manifestParser, type); + LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MANIFEST, type, contents.getPrimaryPath()); + mod = new ModFile(SecureJar.from(contents), JarModsDotTomlModFileReader::manifestParser, type, discoveryAttributes); } else { - return new IModLocator.ModFileOrException(null, new ModFileLoadingException("Invalid mod file found " + Arrays.toString(path))); + return null; } - return new IModLocator.ModFileOrException(mod, null); + return mod; + } + + @Nullable + private static IModFile.Type getModType(JarContents jar) { + var typeString = jar.getManifest().getMainAttributes().getValue(ModFile.TYPE); + return typeString != null ? IModFile.Type.valueOf(typeString) : null; } - protected IModFileInfo manifestParser(final IModFile mod) { + public static IModFileInfo manifestParser(final IModFile mod) { Function> cfg = name -> Optional.ofNullable(mod.getSecureJar().moduleDataProvider().getManifest().getMainAttributes().getValue(name)); var license = cfg.apply("LICENSE").orElse(""); var dummy = new IConfigurable() { @@ -74,15 +77,12 @@ public List getConfigList(String... key) { } @Override - public boolean isValid(final IModFile modFile) { - return true; + public @Nullable IModFile read(JarContents jar, ModFileDiscoveryAttributes discoveryAttributes) { + return createModFile(jar, discoveryAttributes.withReader(this)); } - protected String getDefaultJarModType() { - return null; - } - - private record DefaultModFileInfo(IModFile mod, String license, IConfigurable configurable) implements IModFileInfo, IConfigurable { + private record DefaultModFileInfo(IModFile mod, String license, + IConfigurable configurable) implements IModFileInfo, IConfigurable { @Override public Optional getConfigElement(final String... strings) { return Optional.empty(); @@ -154,4 +154,9 @@ public String toString() { return "IModFileInfo(" + mod.getFilePath() + ")"; } } + + @Override + public String toString() { + return "mod manifest"; + } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/NestedLibraryModReader.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/NestedLibraryModReader.java new file mode 100644 index 000000000..5957edde7 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/NestedLibraryModReader.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.readers; + +import cpw.mods.jarhandling.JarContents; +import cpw.mods.jarhandling.SecureJar; +import net.neoforged.neoforgespi.locating.IModFile; +import net.neoforged.neoforgespi.locating.IModFileReader; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; +import org.jetbrains.annotations.Nullable; + +/** + * This reader will essentially handle all files as plain Java libraries, + * but will only do so for candidates that are embedded in recognized mod files. + *

+ * If a plain jar-file (that is not a mod or markes as a library) is present on the classpath + * or in the mods folder, this is usually a mistake. + * However, if such a file is embedded in a mod jar, it is usually a deliberate decision by the modder. + */ +public class NestedLibraryModReader implements IModFileReader { + @Override + public @Nullable IModFile read(JarContents jar, ModFileDiscoveryAttributes discoveryAttributes) { + // We only consider jars that are contained in the context of another mod valid library targets, + // since we assume those have been included deliberately. Loose jar files in the mods directory + // are not considered, since those are likely to have been dropped in by accident. + if (discoveryAttributes.parent() != null) { + return IModFile.create(SecureJar.from(jar), JarModsDotTomlModFileReader::manifestParser, IModFile.Type.LIBRARY, discoveryAttributes); + } + + return null; + } + + @Override + public String toString() { + return "nested library mod provider"; + } + + @Override + public int getPriority() { + // Since this will capture *any* nested jar as a library it should always run last + return LOWEST_SYSTEM_PRIORITY; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/BackgroundScanHandler.java b/loader/src/main/java/net/neoforged/fml/loading/modscan/BackgroundScanHandler.java similarity index 97% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/BackgroundScanHandler.java rename to loader/src/main/java/net/neoforged/fml/loading/modscan/BackgroundScanHandler.java index 784bc3494..ccbb2a0a2 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/BackgroundScanHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/modscan/BackgroundScanHandler.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery; +package net.neoforged.fml.loading.modscan; import com.mojang.logging.LogUtils; import java.time.Duration; @@ -19,6 +19,7 @@ import net.neoforged.fml.loading.ImmediateWindowHandler; import net.neoforged.fml.loading.LoadingModList; import net.neoforged.fml.loading.LogMarkers; +import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.neoforgespi.language.ModFileScanData; import org.slf4j.Logger; diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModAnnotation.java b/loader/src/main/java/net/neoforged/fml/loading/modscan/ModAnnotation.java similarity index 98% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModAnnotation.java rename to loader/src/main/java/net/neoforged/fml/loading/modscan/ModAnnotation.java index 8a325cded..c80c14191 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModAnnotation.java +++ b/loader/src/main/java/net/neoforged/fml/loading/modscan/ModAnnotation.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery; +package net.neoforged.fml.loading.modscan; import com.google.common.base.MoreObjects; import com.google.common.collect.Lists; diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModAnnotationVisitor.java b/loader/src/main/java/net/neoforged/fml/loading/modscan/ModAnnotationVisitor.java similarity index 97% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModAnnotationVisitor.java rename to loader/src/main/java/net/neoforged/fml/loading/modscan/ModAnnotationVisitor.java index 3fdeeeb81..149a084fd 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModAnnotationVisitor.java +++ b/loader/src/main/java/net/neoforged/fml/loading/modscan/ModAnnotationVisitor.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery; +package net.neoforged.fml.loading.modscan; import java.util.LinkedList; import org.objectweb.asm.AnnotationVisitor; diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModClassVisitor.java b/loader/src/main/java/net/neoforged/fml/loading/modscan/ModClassVisitor.java similarity index 93% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModClassVisitor.java rename to loader/src/main/java/net/neoforged/fml/loading/modscan/ModClassVisitor.java index ba04b8a96..4ab9d41aa 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModClassVisitor.java +++ b/loader/src/main/java/net/neoforged/fml/loading/modscan/ModClassVisitor.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery; +package net.neoforged.fml.loading.modscan; import java.lang.annotation.ElementType; import java.util.LinkedList; @@ -32,7 +32,7 @@ public ModClassVisitor() { @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.asmType = Type.getObjectType(name); - this.asmSuperType = superName != null && superName.length() > 0 ? Type.getObjectType(superName) : null; + this.asmSuperType = superName != null && !superName.isEmpty() ? Type.getObjectType(superName) : null; this.interfaces = Stream.of(interfaces).map(Type::getObjectType).collect(Collectors.toSet()); } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFieldVisitor.java b/loader/src/main/java/net/neoforged/fml/loading/modscan/ModFieldVisitor.java similarity index 95% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFieldVisitor.java rename to loader/src/main/java/net/neoforged/fml/loading/modscan/ModFieldVisitor.java index 8b08032a5..8c3786c11 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFieldVisitor.java +++ b/loader/src/main/java/net/neoforged/fml/loading/modscan/ModFieldVisitor.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery; +package net.neoforged.fml.loading.modscan; import java.lang.annotation.ElementType; import java.util.LinkedList; diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModMethodVisitor.java b/loader/src/main/java/net/neoforged/fml/loading/modscan/ModMethodVisitor.java similarity index 95% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModMethodVisitor.java rename to loader/src/main/java/net/neoforged/fml/loading/modscan/ModMethodVisitor.java index 135027284..0b35290cf 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModMethodVisitor.java +++ b/loader/src/main/java/net/neoforged/fml/loading/modscan/ModMethodVisitor.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery; +package net.neoforged.fml.loading.modscan; import java.lang.annotation.ElementType; import java.util.LinkedList; diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/Scanner.java b/loader/src/main/java/net/neoforged/fml/loading/modscan/Scanner.java similarity index 86% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/Scanner.java rename to loader/src/main/java/net/neoforged/fml/loading/modscan/Scanner.java index 2bb056317..e2a9e2046 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/Scanner.java +++ b/loader/src/main/java/net/neoforged/fml/loading/modscan/Scanner.java @@ -3,16 +3,15 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery; +package net.neoforged.fml.loading.modscan; import com.mojang.logging.LogUtils; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; import net.neoforged.fml.loading.LogMarkers; -import net.neoforged.neoforgespi.language.IModLanguageProvider; +import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.neoforgespi.language.ModFileScanData; import org.objectweb.asm.ClassReader; import org.slf4j.Logger; @@ -29,12 +28,12 @@ public ModFileScanData scan() { ModFileScanData result = new ModFileScanData(); result.addModFileInfo(fileToScan.getModFileInfo()); fileToScan.scanFile(p -> fileVisitor(p, result)); - final List loaders = fileToScan.getLoaders(); + var loaders = fileToScan.getLoaders(); if (loaders != null) { - loaders.forEach(loader -> { + for (var loader : loaders) { LOGGER.debug(LogMarkers.SCAN, "Scanning {} with language loader {}", fileToScan.getFilePath(), loader.name()); loader.getFileVisitor().accept(result); - }); + } } return result; } diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonClientLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonClientLaunchHandler.java index e065f4acd..5fd2f68b4 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonClientLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonClientLaunchHandler.java @@ -5,14 +5,17 @@ package net.neoforged.fml.loading.targets; -import java.nio.file.Path; import java.util.List; -import java.util.stream.Stream; +import java.util.function.Consumer; import net.neoforged.api.distmarker.Dist; -import net.neoforged.fml.loading.FMLLoader; -import net.neoforged.fml.loading.LibraryFinder; +import net.neoforged.fml.loading.MavenCoordinate; import net.neoforged.fml.loading.VersionInfo; +import net.neoforged.fml.loading.moddiscovery.locators.ProductionClientProvider; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +/** + * For production client environments (i.e. vanilla launcher). + */ public abstract class CommonClientLaunchHandler extends CommonLaunchHandler { @Override public Dist getDist() { @@ -29,18 +32,18 @@ protected void runService(String[] arguments, ModuleLayer gameLayer) throws Thro clientService(arguments, gameLayer); } - @Override - public LocatedPaths getMinecraftPaths() { - final var vers = FMLLoader.versionInfo(); - var mc = LibraryFinder.findPathForMaven("net.minecraft", "client", "", "srg", vers.mcAndNeoFormVersion()); - var mcextra = LibraryFinder.findPathForMaven("net.minecraft", "client", "", "extra", vers.mcAndNeoFormVersion()); - var mcstream = Stream.builder().add(mc).add(mcextra); - var modstream = Stream.>builder(); + /** + * @return Additional artifacts from the Games libraries folder that should be layered on top of the Minecraft jar content. + */ + protected List getAdditionalMinecraftJarContent(VersionInfo versionInfo) { + return List.of(); + } - processMCStream(vers, mcstream, modstream); + @Override + public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) { + super.collectAdditionalModFileLocators(versionInfo, output); - return new LocatedPaths(mcstream.build().toList(), null, modstream.build().toList(), this.getFmlPaths(this.getLegacyClasspath())); + var additionalContent = getAdditionalMinecraftJarContent(versionInfo); + output.accept(new ProductionClientProvider(additionalContent)); } - - protected abstract void processMCStream(VersionInfo versionInfo, Stream.Builder mc, Stream.Builder> mods); } diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonDevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonDevLaunchHandler.java index 2bca63ba4..a11d887c4 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonDevLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonDevLaunchHandler.java @@ -5,20 +5,17 @@ package net.neoforged.fml.loading.targets; -import cpw.mods.jarhandling.JarContentsBuilder; -import cpw.mods.jarhandling.SecureJar; -import cpw.mods.niofs.union.UnionPathFilter; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Stream; -import net.neoforged.fml.loading.FileUtils; -import net.neoforged.fml.loading.moddiscovery.AbstractModProvider; +import net.neoforged.fml.loading.VersionInfo; +import net.neoforged.fml.loading.moddiscovery.locators.NeoForgeDevProvider; +import net.neoforged.fml.loading.moddiscovery.locators.UserdevLocator; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +/** + * For the NeoForge development environment. + */ public abstract class CommonDevLaunchHandler extends CommonLaunchHandler { @Override public boolean isProduction() { @@ -26,26 +23,17 @@ public boolean isProduction() { } @Override - public LocatedPaths getMinecraftPaths() { - // Minecraft is extra jar {resources} + our exploded directories in dev - final var mcstream = Stream.builder(); - - // The extra jar is on the classpath, so try and pull it out of the legacy classpath - var legacyCP = this.getLegacyClasspath(); - var extra = findJarOnClasspath(legacyCP, "client-extra"); + public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) { + super.collectAdditionalModFileLocators(versionInfo, output); - // The MC code/Patcher edits are in exploded directories - final var modstream = Stream.>builder(); - final var mods = getModClasses(); - final var minecraft = mods.remove("minecraft"); - if (minecraft == null) - throw new IllegalStateException("Could not find 'minecraft' mod paths."); - minecraft.stream().distinct().forEach(mcstream::add); - mods.values().forEach(modstream::add); + var groupedModFolders = getGroupedModFolders(); + var minecraftFolders = groupedModFolders.get("minecraft"); + if (minecraftFolders == null) { + throw new IllegalStateException("Expected paths to minecraft classes to be passed via environment"); + } - mcstream.add(extra); - var mcFilter = getMcFilter(extra, minecraft, modstream); - return new LocatedPaths(mcstream.build().toList(), mcFilter, modstream.build().toList(), getFmlPaths(legacyCP)); + output.accept(new NeoForgeDevProvider(minecraftFolders)); + output.accept(new UserdevLocator(groupedModFolders)); } @Override @@ -80,51 +68,6 @@ protected String[] preLaunch(String[] arguments, ModuleLayer layer) { return args.getArguments(); } - protected static Optional searchJarOnClasspath(String[] classpath, String match) { - return Arrays.stream(classpath) - .filter(e -> FileUtils.matchFileName(e, false, match)) - .findFirst().map(Paths::get); - } - - protected static Path findJarOnClasspath(String[] classpath, String match) { - return searchJarOnClasspath(classpath, match) - .orElseThrow(() -> new IllegalStateException("Could not find " + match + " in classpath")); - } - - protected UnionPathFilter getMcFilter(Path extra, List minecraft, Stream.Builder> mods) { - final var packages = getExcludedPrefixes(); - final var extraPath = extra.toString().replace('\\', '/'); - - // We serve everything, except for things in the forge packages. - UnionPathFilter mcFilter = (path, base) -> { - if (base.equals(extraPath) || - path.endsWith("/")) - return true; - for (var pkg : packages) - if (path.startsWith(pkg)) return false; - return true; - }; - - // We need to separate out our resources/code so that we can show up as a different data pack. - var modJar = SecureJar.from(new JarContentsBuilder() - .pathFilter((path, base) -> { - if (!path.endsWith(".class")) return true; - for (var pkg : packages) - if (path.startsWith(pkg)) return true; - return false; - }) - .paths(minecraft.stream().distinct().toArray(Path[]::new)) - .build()); - //modJar.getPackages().stream().sorted().forEach(System.out::println); - mods.add(List.of(modJar.getRootPath())); - - return mcFilter; - } - - protected String[] getExcludedPrefixes() { - return new String[] { "net/neoforged/neoforge/", "META-INF/services/", "META-INF/coremods.json", AbstractModProvider.MODS_TOML }; - } - private static String getRandomNumbers(int length) { // Generate a time-based random number, to mimic how n.m.client.Main works return Long.toString(System.nanoTime() % (int) Math.pow(10, length)); diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonLaunchHandler.java index 95067cfa6..a51abf4d7 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonLaunchHandler.java @@ -8,22 +8,26 @@ import com.mojang.logging.LogUtils; import cpw.mods.modlauncher.api.ILaunchHandlerService; import cpw.mods.modlauncher.api.ServiceRunner; -import cpw.mods.niofs.union.UnionPathFilter; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URI; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; +import java.util.Properties; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.loading.FMLLoader; -import net.neoforged.fml.loading.FileUtils; import net.neoforged.fml.loading.LogMarkers; +import net.neoforged.fml.loading.VersionInfo; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.ConfigurationSource; @@ -31,21 +35,20 @@ import org.slf4j.Logger; public abstract class CommonLaunchHandler implements ILaunchHandlerService { - public record LocatedPaths(List minecraftPaths, UnionPathFilter minecraftFilter, List> otherModPaths, List otherArtifacts) {} - protected static final Logger LOGGER = LogUtils.getLogger(); public abstract Dist getDist(); - public boolean isProduction() { - return false; - } + public abstract boolean isProduction(); public boolean isData() { return false; } - public abstract LocatedPaths getMinecraftPaths(); + /** + * Return additional locators to be used for locating mods when this launch handler is used. + */ + public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) {} protected String[] preLaunch(String[] arguments, ModuleLayer layer) { // In dev, do not overwrite the logging configuration if the user explicitly set another one. @@ -71,34 +74,40 @@ private void overwriteLoggingConfiguration(final ModuleLayer layer) { Configurator.reconfigure(ConfigurationFactory.getInstance().getConfiguration(LoggerContext.getContext(), ConfigurationSource.fromUri(uri))); } - protected final String[] getLegacyClasspath() { - return Objects.requireNonNull(System.getProperty("legacyClassPath"), "Missing legacyClassPath, cannot load").split(File.pathSeparator); - } - - protected final List getFmlPaths(String[] classpath) { - String[] fmlLibraries = System.getProperty("fml.pluginLayerLibraries").split(","); - return Arrays.stream(classpath) - .filter(e -> FileUtils.matchFileName(e, true, fmlLibraries)) - .map(Paths::get) - .toList(); - } - - public static Map> getModClasses() { - final String modClasses = Optional.ofNullable(System.getenv("MOD_CLASSES")).orElse(""); - LOGGER.debug(LogMarkers.CORE, "Got mod coordinates {} from env", modClasses); - - record ExplodedModPath(String modid, Path path) {} - // "a/b/;c/d/;" -> "modid%%c:\fish\pepper;modid%%c:\fish2\pepper2\;modid2%%c:\fishy\bums;modid2%%c:\hmm" - final var modClassPaths = Arrays.stream(modClasses.split(File.pathSeparator)) - .map(inp -> inp.split("%%", 2)) - .map(splitString -> new ExplodedModPath(splitString.length == 1 ? "defaultmodid" : splitString[0], Paths.get(splitString[splitString.length - 1]))) - .collect(Collectors.groupingBy(ExplodedModPath::modid, Collectors.mapping(ExplodedModPath::path, Collectors.toList()))); - - LOGGER.debug(LogMarkers.CORE, "Found supplied mod coordinates [{}]", modClassPaths); + public static Map> getGroupedModFolders() { + Map> result; + + var modFolders = Optional.ofNullable(System.getenv("MOD_CLASSES")) + .orElse(System.getProperty("fml.modFolders", "")); + var modFoldersFile = System.getProperty("fml.modFoldersFile", ""); + if (!modFoldersFile.isEmpty()) { + LOGGER.debug(LogMarkers.CORE, "Reading additional mod folders from file {}", modFoldersFile); + var p = new Properties(); + try (var in = Files.newBufferedReader(Paths.get(modFoldersFile))) { + p.load(in); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read mod classes list from " + modFoldersFile, e); + } + + result = p.stringPropertyNames() + .stream() + .collect(Collectors.toMap( + Function.identity(), + modId -> Arrays.stream(p.getProperty(modId).split(File.pathSeparator)).map(Paths::get).toList())); + } else if (!modFolders.isEmpty()) { + LOGGER.debug(LogMarkers.CORE, "Got mod coordinates {} from env", modFolders); + record ExplodedModPath(String modId, Path path) {} + // "a/b/;c/d/;" -> "modid%%c:\fish\pepper;modid%%c:\fish2\pepper2\;modid2%%c:\fishy\bums;modid2%%c:\hmm" + result = Arrays.stream(modFolders.split(File.pathSeparator)) + .map(inp -> inp.split("%%", 2)) + .map(splitString -> new ExplodedModPath(splitString.length == 1 ? "defaultmodid" : splitString[0], Paths.get(splitString[splitString.length - 1]))) + .collect(Collectors.groupingBy(ExplodedModPath::modId, Collectors.mapping(ExplodedModPath::path, Collectors.toList()))); + } else { + result = Map.of(); + } - //final var explodedTargets = ((Map>)arguments).computeIfAbsent("explodedTargets", a -> new ArrayList<>()); - //modClassPaths.forEach((modlabel,paths) -> explodedTargets.add(new ExplodedDirectoryLocator.ExplodedMod(modlabel, paths))); - return modClassPaths; + LOGGER.debug(LogMarkers.CORE, "Found supplied mod coordinates [{}]", result); + return result; } @Override diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonServerLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonServerLaunchHandler.java index 5c9faffee..fab0faf51 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonServerLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonServerLaunchHandler.java @@ -5,16 +5,17 @@ package net.neoforged.fml.loading.targets; -import cpw.mods.jarhandling.JarContentsBuilder; -import cpw.mods.jarhandling.SecureJar; -import java.nio.file.Path; import java.util.List; -import java.util.stream.Stream; +import java.util.function.Consumer; import net.neoforged.api.distmarker.Dist; -import net.neoforged.fml.loading.FMLLoader; -import net.neoforged.fml.loading.LibraryFinder; +import net.neoforged.fml.loading.MavenCoordinate; import net.neoforged.fml.loading.VersionInfo; +import net.neoforged.fml.loading.moddiscovery.locators.ProductionServerProvider; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +/** + * For production dedicated server environments. + */ public abstract class CommonServerLaunchHandler extends CommonLaunchHandler { @Override public Dist getDist() { @@ -31,28 +32,17 @@ protected void runService(String[] arguments, ModuleLayer gameLayer) throws Thro serverService(arguments, gameLayer); } - @Override - public LocatedPaths getMinecraftPaths() { - final var vers = FMLLoader.versionInfo(); - var mc = LibraryFinder.findPathForMaven("net.minecraft", "server", "", "srg", vers.mcAndNeoFormVersion()); - var mcextra = LibraryFinder.findPathForMaven("net.minecraft", "server", "", "extra", vers.mcAndNeoFormVersion()); - var mcextra_filtered = SecureJar.from(new JarContentsBuilder() - // We only want it for its resources. So filter everything else out. - .pathFilter((path, base) -> { - return path.equals("META-INF/versions/") || // This is required because it bypasses our filter for the manifest, and it's a multi-release jar. - (!path.endsWith(".class") && - !path.startsWith("META-INF/")); - }) - .paths(mcextra) - .build()); - - var mcstream = Stream.builder().add(mc).add(mcextra_filtered.getRootPath()); - var modstream = Stream.>builder(); - - processMCStream(vers, mcstream, modstream); - - return new LocatedPaths(mcstream.build().toList(), null, modstream.build().toList(), this.getFmlPaths(this.getLegacyClasspath())); + /** + * @return Additional artifacts from the Games libraries folder that should be layered on top of the Minecraft jar content. + */ + protected List getAdditionalMinecraftJarContent(VersionInfo versionInfo) { + return List.of(); } - protected abstract void processMCStream(VersionInfo versionInfo, Stream.Builder mc, Stream.Builder> mods); + @Override + public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) { + super.collectAdditionalModFileLocators(versionInfo, output); + var additionalContent = getAdditionalMinecraftJarContent(versionInfo); + output.accept(new ProductionServerProvider(additionalContent)); + } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonUserdevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonUserdevLaunchHandler.java index da74c9bf2..b63667d18 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonUserdevLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonUserdevLaunchHandler.java @@ -5,37 +5,25 @@ package net.neoforged.fml.loading.targets; -import java.io.File; -import java.nio.file.Path; import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import net.neoforged.fml.loading.FMLLoader; +import java.util.function.Consumer; import net.neoforged.fml.loading.VersionInfo; +import net.neoforged.fml.loading.moddiscovery.locators.NeoForgeDevProvider; +import net.neoforged.fml.loading.moddiscovery.locators.UserdevLocator; +import net.neoforged.fml.util.DevEnvUtils; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; +/** + * For mod development environments. + */ public abstract class CommonUserdevLaunchHandler extends CommonDevLaunchHandler { @Override - public LocatedPaths getMinecraftPaths() { - final var vers = FMLLoader.versionInfo(); - - // Minecraft is extra jar {resources} + forge jar {patches} - final var mcstream = Stream.builder(); - // Mod code is in exploded directories - final var modstream = Stream.>builder(); - - // The MC extra and forge jars are on the classpath, so try and pull them out - var legacyCP = Objects.requireNonNull(System.getProperty("legacyClassPath"), "Missing legacyClassPath, cannot find userdev jars").split(File.pathSeparator); - var extra = findJarOnClasspath(legacyCP, "client-extra"); + public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) { + // Userdev is similar to neoforge dev with the only real difference being that the combined + // output of the neoforge and patched mincraft sources are combined into a jar file + var classesRoot = DevEnvUtils.findFileSystemRootOfFileOnClasspath("net/minecraft/client/Minecraft.class"); - processStreams(legacyCP, vers, mcstream, modstream); - getModClasses().forEach((modid, paths) -> modstream.add(paths)); - - var minecraft = mcstream.build().collect(Collectors.toList()); - var mcFilter = getMcFilter(extra, minecraft, modstream); - minecraft.add(extra); // Add extra late so the filter is made correctly - return new LocatedPaths(minecraft, mcFilter, modstream.build().toList(), getFmlPaths(legacyCP)); + output.accept(new NeoForgeDevProvider(List.of(classesRoot))); + output.accept(new UserdevLocator(getGroupedModFolders())); } - - protected abstract void processStreams(String[] classpath, VersionInfo versionInfo, Stream.Builder mc, Stream.Builder> mods); } diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLClientDevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/FMLClientDevLaunchHandler.java deleted file mode 100644 index 6e4d380c8..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLClientDevLaunchHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import net.neoforged.api.distmarker.Dist; - -public class FMLClientDevLaunchHandler extends CommonDevLaunchHandler { - @Override - public String name() { - return "fmlclientdev"; - } - - @Override - public Dist getDist() { - return Dist.CLIENT; - } - - @Override - public void runService(String[] arguments, ModuleLayer layer) throws Throwable { - clientService(arguments, layer); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLClientLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/FMLClientLaunchHandler.java deleted file mode 100644 index 01f91e7fe..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLClientLaunchHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import java.nio.file.Path; -import java.util.List; -import java.util.stream.Stream; -import net.neoforged.fml.loading.LibraryFinder; -import net.neoforged.fml.loading.VersionInfo; - -public class FMLClientLaunchHandler extends CommonClientLaunchHandler { - @Override - public String name() { - return "fmlclient"; - } - - @Override - protected void processMCStream(VersionInfo versionInfo, Stream.Builder mc, Stream.Builder> mods) { - var fmlonly = LibraryFinder.findPathForMaven("net.neoforged.fml", "fmlonly", "", "universal", versionInfo.mcAndFmlVersion()); - mods.add(List.of(fmlonly)); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLClientUserdevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/FMLClientUserdevLaunchHandler.java deleted file mode 100644 index 9ce4df395..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLClientUserdevLaunchHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import net.neoforged.api.distmarker.Dist; - -public class FMLClientUserdevLaunchHandler extends FMLUserdevLaunchHandler { - @Override - public String name() { - return "fmlclientuserdev"; - } - - @Override - public Dist getDist() { - return Dist.CLIENT; - } - - @Override - protected void runService(String[] arguments, ModuleLayer layer) throws Throwable { - clientService(arguments, layer); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLDataUserdevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/FMLDataUserdevLaunchHandler.java deleted file mode 100644 index 08719249a..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLDataUserdevLaunchHandler.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import net.neoforged.api.distmarker.Dist; - -public class FMLDataUserdevLaunchHandler extends FMLUserdevLaunchHandler { - @Override - public String name() { - return "fmldatauserdev"; - } - - @Override - public Dist getDist() { - return Dist.CLIENT; - } - - @Override - public boolean isData() { - return true; - } - - @Override - public void runService(String[] arguments, ModuleLayer layer) throws Throwable { - dataService(arguments, layer); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLServerDevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/FMLServerDevLaunchHandler.java deleted file mode 100644 index b71f105b2..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLServerDevLaunchHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import net.neoforged.api.distmarker.Dist; - -public class FMLServerDevLaunchHandler extends CommonDevLaunchHandler { - @Override - public String name() { - return "fmlserverdev"; - } - - @Override - public Dist getDist() { - return Dist.DEDICATED_SERVER; - } - - @Override - public void runService(String[] arguments, ModuleLayer layer) throws Throwable { - serverService(arguments, layer); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLServerLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/FMLServerLaunchHandler.java deleted file mode 100644 index 35f711d48..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLServerLaunchHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import java.nio.file.Path; -import java.util.List; -import java.util.stream.Stream; -import net.neoforged.fml.loading.LibraryFinder; -import net.neoforged.fml.loading.VersionInfo; - -public class FMLServerLaunchHandler extends CommonServerLaunchHandler { - @Override - public String name() { - return "fmlserver"; - } - - @Override - protected void processMCStream(VersionInfo versionInfo, Stream.Builder mc, Stream.Builder> mods) { - var fmlonly = LibraryFinder.findPathForMaven("net.neoforged.fml", "fmlonly", "", "universal", versionInfo.mcAndFmlVersion()); - mods.add(List.of(fmlonly)); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLServerUserdevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/FMLServerUserdevLaunchHandler.java deleted file mode 100644 index fef7b4c7c..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLServerUserdevLaunchHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import net.neoforged.api.distmarker.Dist; - -public class FMLServerUserdevLaunchHandler extends FMLUserdevLaunchHandler { - @Override - public String name() { - return "fmlserveruserdev"; - } - - @Override - public Dist getDist() { - return Dist.DEDICATED_SERVER; - } - - @Override - public void runService(String[] arguments, ModuleLayer layer) throws Throwable { - serverService(arguments, layer); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLUserdevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/FMLUserdevLaunchHandler.java deleted file mode 100644 index f673ea467..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/FMLUserdevLaunchHandler.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import java.nio.file.Path; -import java.util.List; -import java.util.stream.Stream; -import net.neoforged.fml.loading.VersionInfo; - -public abstract class FMLUserdevLaunchHandler extends CommonUserdevLaunchHandler { - @Override - protected void processStreams(String[] classpath, VersionInfo versionInfo, Stream.Builder mc, Stream.Builder> mods) { - var fmlonly = findJarOnClasspath(classpath, "fmlonly-" + versionInfo.mcAndFmlVersion()); - mc.add(fmlonly); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeClientLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeClientLaunchHandler.java deleted file mode 100644 index a76f42146..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeClientLaunchHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import java.nio.file.Path; -import java.util.List; -import java.util.stream.Stream; -import net.neoforged.fml.loading.LibraryFinder; -import net.neoforged.fml.loading.VersionInfo; - -public class ForgeClientLaunchHandler extends CommonClientLaunchHandler { - @Override - public String name() { - return "forgeclient"; - } - - @Override - protected void processMCStream(VersionInfo versionInfo, Stream.Builder mc, Stream.Builder> mods) { - var forgepatches = LibraryFinder.findPathForMaven("net.neoforged", "neoforge", "", "client", versionInfo.neoForgeVersion()); - var forgejar = LibraryFinder.findPathForMaven("net.neoforged", "neoforge", "", "universal", versionInfo.neoForgeVersion()); - mc.add(forgepatches); - mods.add(List.of(forgejar)); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeServerLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeServerLaunchHandler.java deleted file mode 100644 index 3b95aee36..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeServerLaunchHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import java.nio.file.Path; -import java.util.List; -import java.util.stream.Stream; -import net.neoforged.fml.loading.LibraryFinder; -import net.neoforged.fml.loading.VersionInfo; - -public class ForgeServerLaunchHandler extends CommonServerLaunchHandler { - @Override - public String name() { - return "forgeserver"; - } - - @Override - protected void processMCStream(VersionInfo versionInfo, Stream.Builder mc, Stream.Builder> mods) { - var forgepatches = LibraryFinder.findPathForMaven("net.neoforged", "neoforge", "", "server", versionInfo.neoForgeVersion()); - var forgejar = LibraryFinder.findPathForMaven("net.neoforged", "neoforge", "", "universal", versionInfo.neoForgeVersion()); - mc.add(forgepatches); - mods.add(List.of(forgejar)); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeUserdevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeUserdevLaunchHandler.java deleted file mode 100644 index f9584a351..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeUserdevLaunchHandler.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import java.nio.file.Path; -import java.util.List; -import java.util.stream.Stream; -import net.neoforged.fml.loading.VersionInfo; - -public abstract class ForgeUserdevLaunchHandler extends CommonUserdevLaunchHandler { - @Override - protected void processStreams(String[] classpath, VersionInfo versionInfo, Stream.Builder mc, Stream.Builder> mods) { - var forge = searchJarOnClasspath(classpath, "neoforge-" + versionInfo.neoForgeVersion()); - if (forge.isEmpty()) { - throw new RuntimeException("Could not find %s, nor %s jar on classpath".formatted("neoforge-" + versionInfo.neoForgeVersion(), "neoforge-" + versionInfo.neoForgeVersion())); - } - mc.add(forge.get()); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeClientDevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientDevLaunchHandler.java similarity index 86% rename from loader/src/main/java/net/neoforged/fml/loading/targets/ForgeClientDevLaunchHandler.java rename to loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientDevLaunchHandler.java index 81a28e4a5..430219e31 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeClientDevLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientDevLaunchHandler.java @@ -7,7 +7,7 @@ import net.neoforged.api.distmarker.Dist; -public class ForgeClientDevLaunchHandler extends CommonDevLaunchHandler { +public class NeoForgeClientDevLaunchHandler extends CommonDevLaunchHandler { @Override public String name() { return "forgeclientdev"; diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientLaunchHandler.java new file mode 100644 index 000000000..f9413d0a3 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientLaunchHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.targets; + +import java.util.List; +import java.util.function.Consumer; +import net.neoforged.fml.loading.LibraryFinder; +import net.neoforged.fml.loading.MavenCoordinate; +import net.neoforged.fml.loading.VersionInfo; +import net.neoforged.fml.loading.moddiscovery.locators.PathBasedLocator; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; + +public class NeoForgeClientLaunchHandler extends CommonClientLaunchHandler { + @Override + public String name() { + return "forgeclient"; + } + + /** + * Overlays the unpatched but renamed Minecraft classes with our patched versions of those classes. + */ + @Override + protected List getAdditionalMinecraftJarContent(VersionInfo versionInfo) { + return List.of(new MavenCoordinate("net.neoforged", "neoforge", "", "client", versionInfo.neoForgeVersion())); + } + + @Override + public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) { + super.collectAdditionalModFileLocators(versionInfo, output); + var nfJar = LibraryFinder.findPathForMaven("net.neoforged", "neoforge", "", "universal", versionInfo.neoForgeVersion()); + output.accept(new PathBasedLocator("neoforge", nfJar)); + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeClientUserdevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientUserdevLaunchHandler.java similarity index 85% rename from loader/src/main/java/net/neoforged/fml/loading/targets/ForgeClientUserdevLaunchHandler.java rename to loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientUserdevLaunchHandler.java index 325db7ff7..fa40f4b5d 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeClientUserdevLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientUserdevLaunchHandler.java @@ -7,7 +7,7 @@ import net.neoforged.api.distmarker.Dist; -public class ForgeClientUserdevLaunchHandler extends ForgeUserdevLaunchHandler { +public class NeoForgeClientUserdevLaunchHandler extends NeoForgeUserdevLaunchHandler { @Override public String name() { return "forgeclientuserdev"; diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeDataDevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeDataDevLaunchHandler.java similarity index 90% rename from loader/src/main/java/net/neoforged/fml/loading/targets/ForgeDataDevLaunchHandler.java rename to loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeDataDevLaunchHandler.java index f64f1c6f5..f3eb55c1e 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeDataDevLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeDataDevLaunchHandler.java @@ -7,7 +7,7 @@ import net.neoforged.api.distmarker.Dist; -public class ForgeDataDevLaunchHandler extends CommonDevLaunchHandler { +public class NeoForgeDataDevLaunchHandler extends CommonDevLaunchHandler { @Override public String name() { return "forgedatadev"; diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeDataUserdevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeDataUserdevLaunchHandler.java similarity index 87% rename from loader/src/main/java/net/neoforged/fml/loading/targets/ForgeDataUserdevLaunchHandler.java rename to loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeDataUserdevLaunchHandler.java index 5a64278d7..96ee9a4d3 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeDataUserdevLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeDataUserdevLaunchHandler.java @@ -7,7 +7,7 @@ import net.neoforged.api.distmarker.Dist; -public class ForgeDataUserdevLaunchHandler extends ForgeUserdevLaunchHandler { +public class NeoForgeDataUserdevLaunchHandler extends NeoForgeUserdevLaunchHandler { @Override public String name() { return "forgedatauserdev"; diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeServerDevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerDevLaunchHandler.java similarity index 86% rename from loader/src/main/java/net/neoforged/fml/loading/targets/ForgeServerDevLaunchHandler.java rename to loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerDevLaunchHandler.java index b8c5c0ab6..ac03c5f96 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeServerDevLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerDevLaunchHandler.java @@ -7,7 +7,7 @@ import net.neoforged.api.distmarker.Dist; -public class ForgeServerDevLaunchHandler extends CommonDevLaunchHandler { +public class NeoForgeServerDevLaunchHandler extends CommonDevLaunchHandler { @Override public String name() { return "forgeserverdev"; diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerLaunchHandler.java new file mode 100644 index 000000000..8ae1ce3c6 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerLaunchHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.targets; + +import java.util.List; +import java.util.function.Consumer; +import net.neoforged.fml.loading.LibraryFinder; +import net.neoforged.fml.loading.MavenCoordinate; +import net.neoforged.fml.loading.VersionInfo; +import net.neoforged.fml.loading.moddiscovery.locators.PathBasedLocator; +import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; + +public class NeoForgeServerLaunchHandler extends CommonServerLaunchHandler { + @Override + public String name() { + return "forgeserver"; + } + + /** + * Overlays the unpatched but renamed Minecraft classes with our patched versions of those classes. + */ + @Override + protected List getAdditionalMinecraftJarContent(VersionInfo versionInfo) { + return List.of(new MavenCoordinate("net.neoforged", "neoforge", "", "server", versionInfo.neoForgeVersion())); + } + + @Override + public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) { + super.collectAdditionalModFileLocators(versionInfo, output); + + var nfJar = LibraryFinder.findPathForMaven("net.neoforged", "neoforge", "", "universal", versionInfo.neoForgeVersion()); + output.accept(new PathBasedLocator("neoforge", nfJar)); + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeServerUserdevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerUserdevLaunchHandler.java similarity index 85% rename from loader/src/main/java/net/neoforged/fml/loading/targets/ForgeServerUserdevLaunchHandler.java rename to loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerUserdevLaunchHandler.java index f43cf86f0..52f697443 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/ForgeServerUserdevLaunchHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerUserdevLaunchHandler.java @@ -7,7 +7,7 @@ import net.neoforged.api.distmarker.Dist; -public class ForgeServerUserdevLaunchHandler extends ForgeUserdevLaunchHandler { +public class NeoForgeServerUserdevLaunchHandler extends NeoForgeUserdevLaunchHandler { @Override public String name() { return "forgeserveruserdev"; diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeUserdevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeUserdevLaunchHandler.java new file mode 100644 index 000000000..f906e3368 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeUserdevLaunchHandler.java @@ -0,0 +1,8 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.targets; + +public abstract class NeoForgeUserdevLaunchHandler extends CommonUserdevLaunchHandler {} diff --git a/loader/src/main/java/net/neoforged/fml/util/DevEnvUtils.java b/loader/src/main/java/net/neoforged/fml/util/DevEnvUtils.java new file mode 100644 index 000000000..34b06e377 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/util/DevEnvUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.util; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.ModLoadingIssue; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class DevEnvUtils { + private DevEnvUtils() {} + + public static List findFileSystemRootsOfFileOnClasspath(String relativePath) { + // If we're loaded through a module, it means the original classpath is inaccessible through the context CL + var classLoader = Thread.currentThread().getContextClassLoader(); + if (DevEnvUtils.class.getModule().isNamed()) { + classLoader = ClassLoader.getSystemClassLoader(); + } + + // Find the directory that contains the Minecraft classes via the system classpath + Iterator resourceIt; + try { + resourceIt = classLoader.getResources(relativePath).asIterator(); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to enumerate classpath locations of " + relativePath); + } + + List result = new ArrayList<>(); + while (resourceIt.hasNext()) { + var resourceUrl = resourceIt.next(); + + if ("jar".equals(resourceUrl.getProtocol())) { + var fileUri = URI.create(resourceUrl.toString().split("!")[0].substring("jar:".length())); + result.add(Paths.get(fileUri)); + } else { + Path resourcePath; + try { + resourcePath = Paths.get(resourceUrl.toURI()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Failed to convert " + resourceUrl + " to URI"); + } + + // Walk back from the resource path up to the content root that contained it + var current = Paths.get(relativePath); + while (current != null) { + current = current.getParent(); + resourcePath = resourcePath.getParent(); + if (resourcePath == null) { + throw new IllegalArgumentException("Resource " + resourceUrl + " did not have same nesting depth as " + relativePath); + } + } + + result.add(resourcePath); + } + } + + return result; + } + + public static Path findFileSystemRootOfFileOnClasspath(String relativePath) { + var paths = findFileSystemRootsOfFileOnClasspath(relativePath); + + if (paths.isEmpty()) { + throw new ModLoadingException(ModLoadingIssue.error("fml.modloading.failed_to_find_on_classpath", relativePath)); + } else if (paths.size() > 1) { + throw new ModLoadingException(ModLoadingIssue.error("fml.modloading.multiple_copies_on_classpath", relativePath, paths)); + } + + return paths.getFirst(); + } +} diff --git a/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java b/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java new file mode 100644 index 000000000..727613928 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/util/ServiceLoaderUtil.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.util; + +import com.google.common.collect.Streams; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.ServiceConfigurationError; +import net.neoforged.fml.loading.LogMarkers; +import net.neoforged.neoforgespi.ILaunchContext; +import net.neoforged.neoforgespi.locating.IOrderedProvider; +import org.jetbrains.annotations.ApiStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApiStatus.Internal +public final class ServiceLoaderUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceLoaderUtil.class); + + private ServiceLoaderUtil() {} + + public static List loadServices(ILaunchContext context, Class serviceClass) { + return loadServices(context, serviceClass, List.of()); + } + + /** + * @param serviceClass If the service class implements {@link IOrderedProvider}, the services will automatically be sorted. + */ + public static List loadServices(ILaunchContext context, Class serviceClass, Collection additionalServices) { + var serviceLoader = context.createServiceLoader(serviceClass); + + var serviceLoaderServices = serviceLoader.stream().map(p -> { + try { + return p.get(); + } catch (ServiceConfigurationError sce) { + LOGGER.error("Failed to load implementation for {}", serviceClass, sce); + return null; + } + }).filter(Objects::nonNull); + + var servicesStream = Streams.concat(additionalServices.stream(), serviceLoaderServices).distinct(); + + var applyPriority = IOrderedProvider.class.isAssignableFrom(serviceClass); + if (applyPriority) { + servicesStream = servicesStream.sorted(Comparator.comparingInt(service -> ((IOrderedProvider) service).getPriority()).reversed()); + } + + var services = servicesStream.toList(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(LogMarkers.CORE, "Found {} implementations of {}:", services.size(), serviceClass.getSimpleName()); + for (T service : services) { + String priorityPrefix = ""; + if (applyPriority) { + priorityPrefix = String.format(Locale.ROOT, "%8d - ", ((IOrderedProvider) service).getPriority()); + } + + if (additionalServices.contains(service)) { + LOGGER.debug(LogMarkers.CORE, "\t{}[built-in] {}", priorityPrefix, identifyService(service)); + } else { + LOGGER.debug(LogMarkers.CORE, "\t{}{}", priorityPrefix, identifyService(service)); + } + } + } + + return services; + } + + private static String identifyService(Object o) { + String sourceFile; + try { + var sourcePath = Paths.get(o.getClass().getProtectionDomain().getCodeSource().getLocation().toURI()); + if (Files.isDirectory(sourcePath)) { + sourceFile = sourcePath.toAbsolutePath().toString(); + } else { + sourceFile = sourcePath.getFileName().toString(); + } + } catch (URISyntaxException e) { + sourceFile = ""; + } + + return o.getClass().getName() + " from " + sourceFile; + } +} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/Environment.java b/loader/src/main/java/net/neoforged/neoforgespi/Environment.java index 4d277f396..6d3b443e5 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/Environment.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/Environment.java @@ -8,15 +8,10 @@ import cpw.mods.modlauncher.api.IEnvironment; import cpw.mods.modlauncher.api.ITransformationService; import cpw.mods.modlauncher.api.TypesafeMap; -import java.nio.file.Path; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; import net.neoforged.api.distmarker.Dist; -import net.neoforged.neoforgespi.locating.IModDirectoryLocatorFactory; -import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IModLocator; -import net.neoforged.neoforgespi.locating.ModFileFactory; +import net.neoforged.fml.loading.progress.StartupNotificationManager; /** * Global environment variables - allows discoverability with other systems without full forge @@ -29,21 +24,6 @@ public static final class Keys { * Populated by forge during {@link ITransformationService#initialize(IEnvironment)} */ public static final Supplier> DIST = IEnvironment.buildKey("FORGEDIST", Dist.class); - - /** - * Use {@link #MODDIRECTORYFACTORY} instead. - */ - @Deprecated - public static final Supplier>> MODFOLDERFACTORY = IEnvironment.buildKey("MODFOLDERFACTORY", Function.class); - /** - * Build a custom modlocator based on a supplied directory, with custom name - */ - public static final Supplier> MODDIRECTORYFACTORY = IEnvironment.buildKey("MODDIRFACTORY", IModDirectoryLocatorFactory.class); - - /** - * Factory for building {@link IModFile} instances - */ - public static final Supplier> MODFILEFACTORY = IEnvironment.buildKey("MODFILEFACTORY", ModFileFactory.class); /** * Provides a string consumer which can be used to push notification messages to the early startup GUI. */ @@ -53,30 +33,21 @@ public static final class Keys { private static Environment INSTANCE; public static void build(IEnvironment environment) { - if (INSTANCE != null) throw new RuntimeException("Environment is a singleton"); INSTANCE = new Environment(environment); + environment.computePropertyIfAbsent(Environment.Keys.PROGRESSMESSAGE.get(), v -> StartupNotificationManager.locatorConsumer().orElseGet(() -> s -> {})); } public static Environment get() { return INSTANCE; } - private final IEnvironment environment; - private final Dist dist; - private final ModFileFactory modFileFactory; private Environment(IEnvironment setup) { - this.environment = setup; this.dist = setup.getProperty(Keys.DIST.get()).orElseThrow(() -> new RuntimeException("Missing DIST in environment!")); - this.modFileFactory = setup.getProperty(Keys.MODFILEFACTORY.get()).orElseThrow(() -> new RuntimeException("Missing MODFILEFACTORY in environment!")); } public Dist getDist() { return this.dist; } - - public ModFileFactory getModFileFactory() { - return this.modFileFactory; - } } diff --git a/loader/src/main/java/net/neoforged/neoforgespi/ILaunchContext.java b/loader/src/main/java/net/neoforged/neoforgespi/ILaunchContext.java new file mode 100644 index 000000000..a65003ab7 --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/ILaunchContext.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforgespi; + +import cpw.mods.modlauncher.api.IEnvironment; +import java.nio.file.Path; +import java.util.List; +import java.util.ServiceLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides context for various FML plugins about the current launch operation. + */ +public interface ILaunchContext { + Logger LOGGER = LoggerFactory.getLogger(ILaunchContext.class); + + /** + * The Modlauncher environment. + */ + IEnvironment environment(); + + ServiceLoader createServiceLoader(Class serviceClass); + + List modLists(); + + List mods(); + + List mavenRoots(); + + /** + * Checks if a given path was already found by a previous locator, or may be already loaded. + */ + boolean isLocated(Path path); + + /** + * Marks a path as being located and returns true if it was not previously located. + */ + boolean addLocated(Path path); +} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/language/ILifecycleEvent.java b/loader/src/main/java/net/neoforged/neoforgespi/language/ILifecycleEvent.java deleted file mode 100644 index 12dcb6014..000000000 --- a/loader/src/main/java/net/neoforged/neoforgespi/language/ILifecycleEvent.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforgespi.language; - -@Deprecated(forRemoval = true, since = "2.0.8") -public interface ILifecycleEvent> { - @SuppressWarnings("unchecked") - default R concrete() { - return (R) this; - } -} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/language/IModLanguageProvider.java b/loader/src/main/java/net/neoforged/neoforgespi/language/IModLanguageProvider.java index 86482a03c..4c1205f69 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/language/IModLanguageProvider.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/language/IModLanguageProvider.java @@ -17,7 +17,6 @@ package net.neoforged.neoforgespi.language; import java.util.function.Consumer; -import java.util.function.Supplier; import net.neoforged.neoforgespi.locating.IModFile; /** @@ -32,12 +31,6 @@ public interface IModLanguageProvider { Consumer getFileVisitor(); - /** - * @deprecated Does not do anything. - */ - @Deprecated(forRemoval = true, since = "2.0.8") - default > void consumeLifecycleEvent(Supplier consumeEvent) {} - interface IModLanguageLoader { T loadMod(IModInfo info, ModFileScanData modFileScanResults, ModuleLayer layer); } diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IDependencyLocator.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IDependencyLocator.java index 81e675aed..88e653917 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/locating/IDependencyLocator.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/IDependencyLocator.java @@ -11,13 +11,11 @@ * Loaded as a ServiceLoader. Takes mechanisms for locating candidate "mod-dependencies". * and transforms them into {@link IModFile} objects. */ -public interface IDependencyLocator extends IModProvider { +public interface IDependencyLocator { /** * Invoked to find all mod dependencies that this dependency locator can find. * It is not guaranteed that all these are loaded into the runtime, * as such the result of this method should be seen as a list of candidates to load. - * - * @return All found, or discovered, mod files which function as dependencies. */ - List scanMods(final Iterable loadedMods); + void scanMods(List loadedMods, IDiscoveryPipeline pipeline); } diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IDiscoveryPipeline.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IDiscoveryPipeline.java new file mode 100644 index 000000000..46eda5ada --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/IDiscoveryPipeline.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforgespi.locating; + +import cpw.mods.jarhandling.JarContents; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import net.neoforged.fml.ModLoadingIssue; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +/** + * Offers services to {@link IModFileCandidateLocator locators} for adding mod files in various stages to the + * discovery pipeline. + */ +@ApiStatus.NonExtendable +public interface IDiscoveryPipeline { + /** + * Adds a single file or folder to the discovery pipeline, + * to be further processed by registered {@linkplain IModFileReader readers} into a {@linkplain IModFile mod file}. + * + * @param path The path + * @param attributes Additional attributes that describe the circumstance of how this path was discovered. + * @param incompatibleFileReporting The desired behavior if the given file or folder is deemed to be incompatible with NeoForge. + * @return The resulting mod file if the file or folder was successfully read. + */ + default Optional addPath(Path path, ModFileDiscoveryAttributes attributes, IncompatibleFileReporting incompatibleFileReporting) { + return addPath(List.of(path), attributes, incompatibleFileReporting); + } + + /** + * Adds a group of files or folders to the discovery pipeline, + * to be further processed by registered {@linkplain IModFileReader readers} into a {@linkplain IModFile mod file}. + * + * @param groupedPaths A set of files and folders that are combined into a single virtual Jar file for mod loading. + * @param attributes Additional attributes that describe the circumstance of how this path was discovered. + * @param incompatibleFileReporting The desired behavior if the given file or folder is deemed to be incompatible with NeoForge. + * @return The resulting mod file if the file or folder was successfully read. + */ + Optional addPath(List groupedPaths, ModFileDiscoveryAttributes attributes, IncompatibleFileReporting incompatibleFileReporting); + + /** + * Adds a pre-created {@link JarContents jar} to the discovery pipeline + * to be further processed by registered {@linkplain IModFileReader readers} into a {@linkplain IModFile mod file}. + * + * @param contents The contents of the mod file. + * @param attributes Additional attributes that describe the circumstance of how this path was discovered. + * @param incompatibleFileReporting The desired behavior if the given file or folder is deemed to be incompatible with NeoForge. + * @return The resulting mod file if the file or folder was successfully read. + */ + Optional addJarContent(JarContents contents, ModFileDiscoveryAttributes attributes, IncompatibleFileReporting incompatibleFileReporting); + + /** + * Add a pre-created mod file to the discovery pipeline. + * + * @param modFile The mod file. This must not be a custom implementation of {@link IModFile}. Use the static factory methods on {@link IModFile} instead. + * @return True if the file was successfully added. + */ + boolean addModFile(IModFile modFile); + + /** + * Add an issue to the pipeline that arose during the discovery of mod files (i.e. broken files). + */ + void addIssue(ModLoadingIssue issue); + + /** + * Use the registered {@linkplain IModFileReader readers} to attempt to create a mod-file from the given jar + * contents. + * + * @param attributes Additional attributes that describe the circumstance of how this path was discovered. + * @return The created mod file or null if no reader was able to handle the jar contents. + */ + @Nullable + IModFile readModFile(JarContents jarContents, ModFileDiscoveryAttributes attributes); +} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModDirectoryLocatorFactory.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModDirectoryLocatorFactory.java deleted file mode 100644 index 7c1557860..000000000 --- a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModDirectoryLocatorFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforgespi.locating; - -import java.nio.file.Path; -import net.neoforged.neoforgespi.Environment; - -/** - * Functional interface for generating a custom {@link IModLocator} from a directory, with a specific name. - * - * FML provides this factory at {@link Environment.Keys#MODDIRECTORYFACTORY} during - * locator construction. - */ -public interface IModDirectoryLocatorFactory { - IModLocator build(Path directory, String name); -} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFile.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFile.java index 22afeb41d..492242075 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFile.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFile.java @@ -10,27 +10,66 @@ import java.util.List; import java.util.Map; import java.util.function.Supplier; +import net.neoforged.fml.loading.moddiscovery.ModFile; import net.neoforged.neoforgespi.language.IModFileInfo; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.language.IModLanguageProvider; import net.neoforged.neoforgespi.language.ModFileScanData; +import org.jetbrains.annotations.ApiStatus; /** * Represents a single "mod" file in the runtime. - * + *

* Although these are known as "Mod"-Files, they do not always represent mods. * However, they should be more treated as an extension or modification of minecraft. * And as such can contain any number of things, like language loaders, dependencies of other mods * or code which does not interact with minecraft at all and is just a utility for any of the other mod * files. */ +@ApiStatus.NonExtendable public interface IModFile { /** - * The language loaders which are included in this mod file. + * Builds a new mod file instance depending on the current runtime. + * + * @param jar The secure jar to load the mod file from. + * @param parser The parser which is responsible for parsing the metadata of the file itself. + * @return The mod file. + */ + static IModFile create(SecureJar jar, ModFileInfoParser parser) throws InvalidModFileException { + return new ModFile(jar, parser, ModFileDiscoveryAttributes.DEFAULT); + } + + /** + * Builds a new mod file instance depending on the current runtime. * + * @param jar The secure jar to load the mod file from. + * @param parser The parser which is responsible for parsing the metadata of the file itself. + * @param attributes Additional attributes of the modfile. + * @return The mod file. + */ + static IModFile create(SecureJar jar, ModFileInfoParser parser, ModFileDiscoveryAttributes attributes) throws InvalidModFileException { + return new ModFile(jar, parser, attributes); + } + + /** + * Builds a new mod file instance depending on the current runtime. + * + * @param jar The secure jar to load the mod file from. + * @param parser The parser which is responsible for parsing the metadata of the file itself. + * @param type the type of the mod + * @param attributes Additional attributes of the modfile. + * @return The mod file. + */ + static IModFile create(SecureJar jar, ModFileInfoParser parser, IModFile.Type type, ModFileDiscoveryAttributes attributes) throws InvalidModFileException { + return new ModFile(jar, parser, type, attributes); + } + + /** + * The language loaders which are included in this mod file. + *

* If this method returns any entries then {@link #getType()} has to return {@link Type#LIBRARY}, * else this mod file will not be loaded in the proper module layer in 1.17 and above. - * + *

* As such, returning entries from this method is mutually exclusive with returning entries from {@link #getModInfos()}. * * @return The mod language providers provided by this mod file. (Also known as the loaders). @@ -65,7 +104,7 @@ public interface IModFile { /** * The path to the underlying mod file. - * + * * @return The path to the mod file. */ Path getFilePath(); @@ -87,10 +126,10 @@ public interface IModFile { /** * Returns a list of all mods located inside this jar. - * + *

* If this method returns any entries then {@link #getType()} has to return {@link Type#MOD}, * else this mod file will not be loaded in the proper module layer in 1.17 and above. - * + *

* As such returning entries from this method is mutually exclusive with {@link #getLoaders()}. * * @return The mods in this mod file. @@ -106,22 +145,19 @@ public interface IModFile { /** * The raw file name of this file. - * + * * @return The raw file name. */ String getFileName(); /** - * The provider who provided the runtime with this jar. - * Implicitly indicates what caused the load of the PR. (Mod in mods directory, mod in dev environment, etc) - * - * @return The provider of this file. + * Get attributes about how this mod file was discovered. */ - IModProvider getProvider(); + ModFileDiscoveryAttributes getDiscoveryAttributes(); /** * The metadata info related to this particular file. - * + * * @return The info for this file. */ IModFileInfo getModFileInfo(); diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFileCandidateLocator.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFileCandidateLocator.java new file mode 100644 index 000000000..da822d39c --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFileCandidateLocator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforgespi.locating; + +import java.io.File; +import net.neoforged.fml.loading.moddiscovery.locators.ModsFolderLocator; +import net.neoforged.neoforgespi.ILaunchContext; + +/** + * Loaded as a ServiceLoader. Takes mechanisms for locating candidate "mod" JARs. + */ +public interface IModFileCandidateLocator extends IOrderedProvider { + /** + * Creates an IModFileCandidateLocator that searches for mod jar-files in the given filesystem location. + */ + static IModFileCandidateLocator forFolder(File folder, String identifier) { + return new ModsFolderLocator(folder.toPath(), identifier); + } + + /** + * Discovers potential mods to be loaded by FML. + * + * @param pipeline Adds discovered mods and issues to this pipeline. + */ + void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline); +} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFileReader.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFileReader.java new file mode 100644 index 000000000..8ee5504dd --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFileReader.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforgespi.locating; + +import cpw.mods.jarhandling.JarContents; +import org.jetbrains.annotations.Nullable; + +/** + * Inspects {@link JarContents} found by {@link IModFileCandidateLocator} and tries to turn them into {@link IModFile}. + *

+ * Picked up via ServiceLoader. + */ +public interface IModFileReader extends IOrderedProvider { + /** + * Provides a mod from the given {@code jar}. + * Any thrown exception will be reported in relationship to the given jar contents. + * + * @param jar the mod jar contents + * @param attributes The attributes relating to this mod files discovery. + * @return {@code null} if this provider can't handle the given jar, + * otherwise the mod-file created from the given contents and attributes. + */ + @Nullable + IModFile read(JarContents jar, ModFileDiscoveryAttributes attributes); +} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModLocator.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModLocator.java deleted file mode 100644 index 7cc4708a1..000000000 --- a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModLocator.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Minecraft Forge - * Copyright (c) 2016-2019. - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation version 2.1 - * of the License. - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -package net.neoforged.neoforgespi.locating; - -import java.util.List; - -/** - * Loaded as a ServiceLoader. Takes mechanisms for locating candidate "mods" - * and transforms them into {@link IModFile} objects. - */ -public interface IModLocator extends IModProvider { - /** - * A simple record which contains either a valid modfile or a reason one failed to be constructed by {@link #scanMods()} - * - * @param file the file - * @param ex an exception that occurred during the attempt to load the mod - */ - record ModFileOrException(IModFile file, ModFileLoadingException ex) {} - - /** - * Invoked to find all mods that this mod locator can find. - * It is not guaranteed that all these are loaded into the runtime, - * as such the result of this method should be seen as a list of candidates to load. - * - * @return All found, or discovered, mod files. - */ - List scanMods(); -} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModProvider.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModProvider.java deleted file mode 100644 index 187a958fa..000000000 --- a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModProvider.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforgespi.locating; - -import java.nio.file.Path; -import java.util.Map; -import java.util.function.Consumer; - -/** - * Describes objects which can provide mods (or related jars) to the loading runtime. - */ -public interface IModProvider { - /** - * The name of the provider. - * Has to be unique between all providers loaded into the runtime. - * - * @return The name. - */ - String name(); - - /** - * Invoked to scan a particular {@link IModFile} for metadata. - * - * @param modFile The mod file to scan. - * @param pathConsumer A consumer which extracts metadata from the path given. - */ - void scanFile(IModFile modFile, Consumer pathConsumer); - - /** - * Invoked with the game startup arguments to allow for configuration of the provider. - * - * @param arguments The arguments. - */ - void initArguments(Map arguments); - - /** - * Indicates if the given mod file is valid. - * - * @param modFile The mod file in question. - * @return True to mark as valid, false otherwise. - */ - boolean isValid(IModFile modFile); -} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IOrderedProvider.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IOrderedProvider.java new file mode 100644 index 000000000..0f7052191 --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/IOrderedProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforgespi.locating; + +/** + * Base interface for providers that support ordering. + */ +public interface IOrderedProvider { + /** + * The default priority of providers that otherwise do not specify a priority. + */ + int DEFAULT_PRIORITY = 0; + + /** + * The highest priority a providers built into NeoForge will use. + */ + int HIGHEST_SYSTEM_PRIORITY = 1000; + + /** + * The lowest priority a providers built into NeoForge will use. + */ + int LOWEST_SYSTEM_PRIORITY = -1000; + + /** + * Gets the priority in which this provider will be called. + * A higher value means the provider will be called earlier. + */ + default int getPriority() { + return DEFAULT_PRIORITY; + } +} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IncompatibleFileReporting.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IncompatibleFileReporting.java new file mode 100644 index 000000000..53e8b3be6 --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/IncompatibleFileReporting.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforgespi.locating; + +/** + * Defines how files that are not handled are reported. + */ +public enum IncompatibleFileReporting { + /** + * Add a {@link net.neoforged.fml.ModLoadingIssue} of severity {@link net.neoforged.fml.ModLoadingIssue.Severity#ERROR} if the + * file is not determined to be a compatible mod or explicit library. + */ + ERROR, + /** + * Add a {@link net.neoforged.fml.ModLoadingIssue} of severity {@link net.neoforged.fml.ModLoadingIssue.Severity#WARNING} if the + * file is not determined to be a compatible mod or explicit library. + */ + WARN_ALWAYS, + /** + * Add a {@link net.neoforged.fml.ModLoadingIssue} of severity {@link net.neoforged.fml.ModLoadingIssue.Severity#WARNING} if the + * file is not determined to be a compatible mod or explicit library, and the file triggers the built-in detection for + * incompatible modding systems ({@link net.neoforged.fml.loading.moddiscovery.IncompatibleModReason}). + */ + WARN_ON_KNOWN_INCOMPATIBILITY, + /** + * Do nothing if the file is not detected as compatible. + */ + IGNORE +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/InvalidModFileException.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/InvalidModFileException.java similarity index 85% rename from loader/src/main/java/net/neoforged/fml/loading/moddiscovery/InvalidModFileException.java rename to loader/src/main/java/net/neoforged/neoforgespi/locating/InvalidModFileException.java index 32a591713..39a9f37a8 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/InvalidModFileException.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/InvalidModFileException.java @@ -3,12 +3,11 @@ * SPDX-License-Identifier: LGPL-2.1-only */ -package net.neoforged.fml.loading.moddiscovery; +package net.neoforged.neoforgespi.locating; import java.util.Locale; import java.util.Optional; import net.neoforged.neoforgespi.language.IModFileInfo; -import net.neoforged.neoforgespi.locating.ModFileLoadingException; public class InvalidModFileException extends ModFileLoadingException { private final IModFileInfo modFileInfo; diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileDiscoveryAttributes.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileDiscoveryAttributes.java new file mode 100644 index 000000000..3cb56cbfd --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileDiscoveryAttributes.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforgespi.locating; + +import org.jetbrains.annotations.Nullable; + +/** + * Attributes of a modfile relating to how it was discovered. + * + * @param parent The mod file that logically contains this mod file. + * @param reader The reader that was used to get a mod-file from jar contents. May be null if the mod file was directly created by a locator. + */ +public record ModFileDiscoveryAttributes(@Nullable IModFile parent, + @Nullable IModFileReader reader, + @Nullable IModFileCandidateLocator locator, + @Nullable IDependencyLocator dependencyLocator) { + + public static final ModFileDiscoveryAttributes DEFAULT = new ModFileDiscoveryAttributes(null, null, null, null); + public ModFileDiscoveryAttributes withParent(IModFile parent) { + return new ModFileDiscoveryAttributes(parent, reader, locator, dependencyLocator); + } + + public ModFileDiscoveryAttributes withReader(IModFileReader reader) { + return new ModFileDiscoveryAttributes(parent, reader, locator, dependencyLocator); + } + + public ModFileDiscoveryAttributes withLocator(IModFileCandidateLocator locator) { + return new ModFileDiscoveryAttributes(parent, reader, locator, dependencyLocator); + } + + public ModFileDiscoveryAttributes withDependencyLocator(IDependencyLocator dependencyLocator) { + return new ModFileDiscoveryAttributes(parent, reader, locator, dependencyLocator); + } + + public ModFileDiscoveryAttributes merge(ModFileDiscoveryAttributes attributes) { + return new ModFileDiscoveryAttributes( + attributes.parent != null ? attributes.parent : parent, + attributes.reader != null ? attributes.reader : reader, + attributes.locator != null ? attributes.locator : locator, + attributes.dependencyLocator != null ? attributes.dependencyLocator : dependencyLocator); + } + + @Override + public String toString() { + var result = new StringBuilder(); + result.append("["); + if (parent != null) { + result.append("parent: "); + result.append(parent.getFilePath().getFileName()); + } + if (locator != null) { + if (result.length() > 1) { + result.append(", "); + } + result.append("locator: "); + result.append(locator); + } + if (dependencyLocator != null) { + if (result.length() > 1) { + result.append(", "); + } + result.append("locator: "); + result.append(dependencyLocator); + } + if (reader != null) { + if (result.length() > 1) { + result.append(", "); + } + result.append("reader: "); + result.append(reader); + } + result.append("]"); + return result.toString(); + } +} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileFactory.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileFactory.java deleted file mode 100644 index 7a6f1d480..000000000 --- a/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforgespi.locating; - -import cpw.mods.jarhandling.SecureJar; -import net.neoforged.neoforgespi.Environment; -import net.neoforged.neoforgespi.language.IModFileInfo; - -/** - * A factory to build new mod file instances. - */ -public interface ModFileFactory { - /** - * The current instance. Equals to {@link Environment#getModFileFactory()}, of the current environment instance. - */ - ModFileFactory FACTORY = Environment.get().getModFileFactory(); - - /** - * Builds a new mod file instance depending on the current runtime. - * - * @param jar The secure jar to load the mod file from. - * @param provider The provider which is offering the mod file for loading- - * @param parser The parser which is responsible for parsing the metadata of the file itself. - * @return The mod file. - */ - IModFile build(final SecureJar jar, final IModProvider provider, ModFileInfoParser parser); - - /** - * A parser specification for building a particular mod files metadata. - */ - interface ModFileInfoParser { - /** - * Invoked to get the freshly build mod files metadata. - * - * @param file The file to parse the metadata for. - * @return The mod file metadata info. - */ - IModFileInfo build(IModFile file); - } -} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileInfoParser.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileInfoParser.java new file mode 100644 index 000000000..be1463f36 --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileInfoParser.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforgespi.locating; + +import net.neoforged.neoforgespi.language.IModFileInfo; + +/** + * A parser specification for building a particular mod files metadata. + */ +@FunctionalInterface +public interface ModFileInfoParser { + /** + * Invoked to get the freshly build mod files metadata. + * + * @param file The file to parse the metadata for. + * @return The mod file metadata info. + */ + IModFileInfo build(IModFile file) throws InvalidModFileException; +} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileLoadingException.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileLoadingException.java index 03b95c4a9..b8a45f735 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileLoadingException.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/ModFileLoadingException.java @@ -6,7 +6,7 @@ package net.neoforged.neoforgespi.locating; /** - * An exception that can be thrown/caught by {@link IModLocator} code, indicating a bad mod file or something similar. + * An exception that can be thrown/caught by {@link IModFileCandidateLocator} code, indicating a bad mod file or something similar. */ public class ModFileLoadingException extends RuntimeException { public ModFileLoadingException(String message) { diff --git a/loader/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ILaunchHandlerService b/loader/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ILaunchHandlerService index a263947e3..6b96d9a95 100644 --- a/loader/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ILaunchHandlerService +++ b/loader/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ILaunchHandlerService @@ -1,8 +1,8 @@ -net.neoforged.fml.loading.targets.ForgeClientLaunchHandler -net.neoforged.fml.loading.targets.ForgeClientDevLaunchHandler -net.neoforged.fml.loading.targets.ForgeClientUserdevLaunchHandler -net.neoforged.fml.loading.targets.ForgeServerLaunchHandler -net.neoforged.fml.loading.targets.ForgeServerDevLaunchHandler -net.neoforged.fml.loading.targets.ForgeServerUserdevLaunchHandler -net.neoforged.fml.loading.targets.ForgeDataDevLaunchHandler -net.neoforged.fml.loading.targets.ForgeDataUserdevLaunchHandler +net.neoforged.fml.loading.targets.NeoForgeClientLaunchHandler +net.neoforged.fml.loading.targets.NeoForgeClientDevLaunchHandler +net.neoforged.fml.loading.targets.NeoForgeClientUserdevLaunchHandler +net.neoforged.fml.loading.targets.NeoForgeServerLaunchHandler +net.neoforged.fml.loading.targets.NeoForgeServerDevLaunchHandler +net.neoforged.fml.loading.targets.NeoForgeServerUserdevLaunchHandler +net.neoforged.fml.loading.targets.NeoForgeDataDevLaunchHandler +net.neoforged.fml.loading.targets.NeoForgeDataUserdevLaunchHandler diff --git a/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IDependencyLocator b/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IDependencyLocator index 8dc3d96d6..bf5b64fc2 100644 --- a/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IDependencyLocator +++ b/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IDependencyLocator @@ -1 +1 @@ -net.neoforged.fml.loading.moddiscovery.JarInJarDependencyLocator \ No newline at end of file +net.neoforged.fml.loading.moddiscovery.locators.JarInJarDependencyLocator diff --git a/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileCandidateLocator b/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileCandidateLocator new file mode 100644 index 000000000..0db32accb --- /dev/null +++ b/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileCandidateLocator @@ -0,0 +1,2 @@ +net.neoforged.fml.loading.moddiscovery.locators.ModsFolderLocator +net.neoforged.fml.loading.moddiscovery.locators.MavenDirectoryLocator diff --git a/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileReader b/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileReader new file mode 100644 index 000000000..d8f8f99fc --- /dev/null +++ b/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileReader @@ -0,0 +1,2 @@ +net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader +net.neoforged.fml.loading.moddiscovery.readers.NestedLibraryModReader \ No newline at end of file diff --git a/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModLocator b/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModLocator deleted file mode 100644 index 920303df9..000000000 --- a/loader/src/main/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModLocator +++ /dev/null @@ -1,6 +0,0 @@ -net.neoforged.fml.loading.moddiscovery.ModsFolderLocator -net.neoforged.fml.loading.moddiscovery.MavenDirectoryLocator -net.neoforged.fml.loading.moddiscovery.ExplodedDirectoryLocator -net.neoforged.fml.loading.moddiscovery.MinecraftLocator -net.neoforged.fml.loading.moddiscovery.ClasspathLocator -net.neoforged.fml.loading.moddiscovery.BuiltinGameLibraryLocator diff --git a/loader/src/test/java/net/neoforged/fml/loading/ClasspathTransformerDiscovererTest.java b/loader/src/test/java/net/neoforged/fml/loading/ClasspathTransformerDiscovererTest.java new file mode 100644 index 000000000..8acdb90e0 --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/ClasspathTransformerDiscovererTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import static org.assertj.core.api.Assertions.assertThat; + +import cpw.mods.modlauncher.api.NamedPath; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ClasspathTransformerDiscovererTest { + // A service locator file that should be picked up by the locator + private static final IdentifiableContent ML_SERVICE_FILE = new IdentifiableContent("ML_SERVICE", "META-INF/services/cpw.mods.modlauncher.api.ITransformationService", "some.Class".getBytes()); + + SimulatedInstallation simulatedInstallation; + private Path mlServicesJar; + private List gradleModule; + + @BeforeEach + void setUp() throws IOException { + simulatedInstallation = new SimulatedInstallation(); + + // Add a ML service candidate jar + mlServicesJar = simulatedInstallation.getProjectRoot().resolve("ml-services.jar"); + + // Add an exploded ML service ONLY in the modFolders property, but not on the classpath + gradleModule = simulatedInstallation.setupGradleModule(ML_SERVICE_FILE); + } + + @Test + void testLocateTransformerServiceInDev() throws Exception { + var candidates = runLocator("forgeclientdev"); + + assertThat(candidates) + .anySatisfy(candidate -> { + assertThat(candidate.name()).isEqualTo(mlServicesJar.toUri().toString()); + assertThat(candidate.paths()).containsOnly(mlServicesJar); + }); + assertThat(candidates) + .anySatisfy(candidate -> { + assertThat(candidate.paths()).containsOnly(gradleModule.toArray(Path[]::new)); + }); + } + + @Test + void testLocateTransformerServiceNotInDev() throws Exception { + var candidates = runLocator("forgeclient"); + assertThat(candidates).isEmpty(); + } + + private List runLocator(String launchTarget) throws IOException { + var locator = new ClasspathTransformerDiscoverer(); + + SimulatedInstallation.writeJarFile(mlServicesJar, ML_SERVICE_FILE); + SimulatedInstallation.setModFoldersProperty(Map.of("ML_SERVICES_FROM_DIR", gradleModule)); + + List candidates; + var cl = new URLClassLoader(new URL[] { + mlServicesJar.toUri().toURL() + }); + var previousCl = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(cl); + try { + return locator.candidates(simulatedInstallation.getGameDir(), launchTarget); + } finally { + Thread.currentThread().setContextClassLoader(previousCl); + } + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/CustomSubclassModFileReader.java b/loader/src/test/java/net/neoforged/fml/loading/CustomSubclassModFileReader.java new file mode 100644 index 000000000..5fc64d717 --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/CustomSubclassModFileReader.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import static org.mockito.Mockito.mock; + +import cpw.mods.jarhandling.JarContents; +import net.neoforged.neoforgespi.locating.IModFile; +import net.neoforged.neoforgespi.locating.IModFileReader; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; +import org.jetbrains.annotations.Nullable; + +public class CustomSubclassModFileReader implements IModFileReader { + public static final IdentifiableContent TRIGGER = new IdentifiableContent("CUSTOM_MODFILE_SUBCLASS_TRIGGER", "custom_modfile_subclass_trigger"); + + @Override + public @Nullable IModFile read(JarContents jar, ModFileDiscoveryAttributes attributes) { + if (jar.findFile(TRIGGER.relativePath()).isPresent()) { + return mock(IModFile.class); + } + return null; + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java new file mode 100644 index 000000000..c5f633592 --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java @@ -0,0 +1,529 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import cpw.mods.jarhandling.SecureJar; +import cpw.mods.modlauncher.api.IEnvironment; +import cpw.mods.modlauncher.api.IModuleLayerManager; +import cpw.mods.modlauncher.api.ITransformationService; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import net.neoforged.fml.ModLoadingIssue; +import net.neoforged.jarjar.metadata.ContainedJarIdentifier; +import net.neoforged.jarjar.metadata.ContainedJarMetadata; +import net.neoforged.jarjar.metadata.ContainedVersion; +import net.neoforged.neoforgespi.Environment; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.artifact.versioning.VersionRange; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoSettings; + +@MockitoSettings +class FMLLoaderTest { + private static final ContainedVersion JIJ_V1 = new ContainedVersion(VersionRange.createFromVersion("1.0"), new DefaultArtifactVersion("1.0")); + @Mock + MockedStatic immediateWindowHandlerMock; + + TestModuleLayerManager moduleLayerManager = new TestModuleLayerManager(); + + TestEnvironment environment = new TestEnvironment(moduleLayerManager); + + SimulatedInstallation installation; + + // can be used to mark paths as already being located before, i.e. if they were loaded + // by the two early ModLoader discovery interfaces ClasspathTransformerDiscoverer + // and ModDirTransformerDiscoverer, which pick up files like mixin. + Set locatedPaths = new HashSet<>(); + + @BeforeEach + void setUp() throws IOException { + installation = new SimulatedInstallation(); + } + + @AfterEach + void clearSystemProperties() throws Exception { + installation.close(); + } + + @Nested + class WithoutMods { + @Test + void testProductionClientDiscovery() throws Exception { + installation.setupProductionClient(); + + var result = launch("forgeclient"); + assertThat(result.issues()).isEmpty(); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.pluginLayerModules()).isEmpty(); + + installation.assertMinecraftClientJar(result); + installation.assertNeoForgeJar(result); + } + + @Test + void testProductionServerDiscovery() throws Exception { + installation.setupProductionServer(); + + var result = launch("forgeserver"); + assertThat(result.issues()).isEmpty(); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.pluginLayerModules()).isEmpty(); + + installation.assertMinecraftServerJar(result); + installation.assertNeoForgeJar(result); + } + + @Test + void testNeoForgeDevServerDiscovery() throws Exception { + var result = launchInNeoForgeDevEnvironment("forgeserverdev"); + assertThat(result.issues()).isEmpty(); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.pluginLayerModules()).isEmpty(); + + installation.assertMinecraftClientJar(result); + installation.assertNeoForgeJar(result); + } + + @Test + void testNeoForgeDevDataDiscovery() throws Exception { + var result = launchInNeoForgeDevEnvironment("forgedatadev"); + assertThat(result.issues()).isEmpty(); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.pluginLayerModules()).isEmpty(); + + installation.assertMinecraftClientJar(result); + installation.assertNeoForgeJar(result); + } + + @Test + void testNeoForgeDevClientDiscovery() throws Exception { + var result = launchInNeoForgeDevEnvironment("forgeclientdev"); + assertThat(result.issues()).isEmpty(); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.pluginLayerModules()).isEmpty(); + + installation.assertMinecraftClientJar(result); + installation.assertNeoForgeJar(result); + } + + @Test + void testUserDevServerDiscovery() throws Exception { + var classpath = installation.setupUserdevProject(); + + var result = launchWithAdditionalClasspath("forgeserveruserdev", classpath); + assertThat(result.issues()).isEmpty(); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.pluginLayerModules()).isEmpty(); + + installation.assertMinecraftClientJar(result); + installation.assertNeoForgeJar(result); + } + + @Test + void testUserDevDataDiscovery() throws Exception { + var classpath = installation.setupUserdevProject(); + + var result = launchWithAdditionalClasspath("forgedatauserdev", classpath); + assertThat(result.issues()).isEmpty(); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.pluginLayerModules()).isEmpty(); + + installation.assertMinecraftClientJar(result); + installation.assertNeoForgeJar(result); + } + + @Test + void testUserDevClientDiscovery() throws Exception { + var classpath = installation.setupUserdevProject(); + + var result = launchWithAdditionalClasspath("forgeclientuserdev", classpath); + assertThat(result.issues()).isEmpty(); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge"); + assertThat(result.pluginLayerModules()).isEmpty(); + + installation.assertMinecraftClientJar(result); + installation.assertNeoForgeJar(result); + } + } + + @Nested + class WithMods { + @Test + void testProductionClientDiscovery() throws Exception { + installation.setupProductionClient(); + installation.setupModInModsFolder("testmod1", "1.0"); + installation.setupModInModsFolder("testmod1", "1.0"); + + var result = launch("forgeclient"); + assertThat(result.issues()).isEmpty(); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge", "testmod1"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "neoforge", "testmod1"); + assertThat(result.pluginLayerModules()).isEmpty(); + + installation.assertMinecraftClientJar(result); + installation.assertNeoForgeJar(result); + } + + /** + * A jar without a manifest and without a neoforge.mods.toml is not loaded, but leads + * to a warning. + */ + @Test + void testPlainJarInModsFolderIsNotLoadedButWarnedAbout() throws Exception { + installation.setupProductionClient(); + installation.setupPlainJarInModsFolder("plainmod.jar"); + + var result = launch("forgeclient"); + + assertThat(result.gameLayerModules()).doesNotContainKey("plainmod"); + assertThat(result.pluginLayerModules()).doesNotContainKey("plainmod"); + var plainJar = installation.getModsFolder().resolve("plainmod.jar"); + assertThat(result.issues()).containsOnly(ModLoadingIssue.warning( + "fml.modloading.brokenfile", plainJar).withAffectedPath(plainJar)); + } + + /** + * A mod-jar that contains another mod and a plugin jar. + */ + @Test + void testJarInJar() throws Exception { + installation.setupProductionClient(); + installation.writeModJar("jijmod.jar", + SimulatedInstallation.createModsToml("jijmod", "1.0"), + SimulatedInstallation.createJarFile( + "EMBEDDED_MOD", "META-INF/jarjar/embedded_mod-1.0.jar", SimulatedInstallation.createModsToml("embeddedmod", "1.0")), + SimulatedInstallation.createJarFile( + "EMBEDDED_SERVICE", "META-INF/jarjar/embedded_service-1.0.jar", SimulatedInstallation.createManifest("SERVICE_MANIFEST", Map.of("FMLModType", "LIBRARY"))), + SimulatedInstallation.createJarFile( + "EMBEDDED_GAMELIB", "META-INF/jarjar/embedded_gamelib-1.0.jar", SimulatedInstallation.createManifest("GAMELIB_MANIFEST", Map.of("FMLModType", "GAMELIBRARY"))), + SimulatedInstallation.createJarFile( + "EMBEDDED_LIB", "META-INF/jarjar/embedded_lib-1.0.jar"), + SimulatedInstallation.createJijMetadata( + new ContainedJarMetadata(new ContainedJarIdentifier("modgroup", "embedded-mod"), JIJ_V1, "META-INF/jarjar/embedded_mod-1.0.jar", false), + new ContainedJarMetadata(new ContainedJarIdentifier("modgroup", "embedded-service"), JIJ_V1, "META-INF/jarjar/embedded_service-1.0.jar", false), + new ContainedJarMetadata(new ContainedJarIdentifier("modgroup", "embedded-gamelib"), JIJ_V1, "META-INF/jarjar/embedded_gamelib-1.0.jar", false), + new ContainedJarMetadata(new ContainedJarIdentifier("modgroup", "embedded-lib"), JIJ_V1, "META-INF/jarjar/embedded_lib-1.0.jar", false))); + + var result = launch("forgeclient"); + assertThat(result.gameLayerModules()).containsOnlyKeys("minecraft", "embeddedmod", "embedded.gamelib", "jijmod", "neoforge"); + assertThat(result.pluginLayerModules()).containsOnlyKeys("embedded.lib", "embedded.service"); + assertThat(result.loadedMods()).containsOnlyKeys("minecraft", "neoforge", "embeddedmod", "jijmod"); + assertThat(result.issues()).isEmpty(); + } + + /** + * If a mod file is present in multiple versions, the latest one is used. + */ + @Test + void testHighestVersionWins() throws Exception { + installation.setupProductionClient(); + installation.setupModInModsFolder("testmod1", "1.0"); + installation.setupModInModsFolder("testmod1", "12.0"); + installation.setupModInModsFolder("testmod1", "3.0"); + + var result = launch("forgeclient"); + + var loadedMod = result.loadedMods().get("testmod1"); + assertNotNull(loadedMod); + assertEquals("12.0", loadedMod.versionString()); + } + + @Test + void testUserdevWithModProject() throws Exception { + var additionalClasspath = installation.setupUserdevProject(); + + var entrypointClass = SimulatedInstallation.generateClass("MOD_ENTRYPOINT", "mod/Entrypoint.class"); + var modManifest = SimulatedInstallation.createModsToml("mod", "1.0.0"); + + var mainModule = installation.setupGradleModule(entrypointClass, modManifest); + additionalClasspath.addAll(mainModule); + + // Tell FML that the classes and resources directory belong together + SimulatedInstallation.setModFoldersProperty(Map.of("mod", mainModule)); + + var result = launchWithAdditionalClasspath("forgeclientuserdev", additionalClasspath); + assertThat(result.pluginLayerModules()).doesNotContainKey("mod"); + assertThat(result.gameLayerModules()).containsKey("mod"); + installation.assertModContent(result, "mod", List.of(entrypointClass, modManifest)); + } + + /** + * Special test-case that checks we can add additional candidates via the modFolders system property, + * even if they are not on the classpath. + */ + @Test + void testUserdevWithModProjectNotOnClasspath() throws Exception { + var additionalClasspath = installation.setupUserdevProject(); + + var entrypointClass = SimulatedInstallation.generateClass("MOD_ENTRYPOINT", "mod/Entrypoint.class"); + var modManifest = SimulatedInstallation.createModsToml("mod", "1.0.0"); + + var mainModule = installation.setupGradleModule(entrypointClass, modManifest); + // NOTE: mainModule is not added to the classpath here + + // Tell FML that the classes and resources directory belong together + SimulatedInstallation.setModFoldersProperty(Map.of("mod", mainModule)); + + var result = launchWithAdditionalClasspath("forgeclientuserdev", additionalClasspath); + assertThat(result.pluginLayerModules()).doesNotContainKey("mod"); + assertThat(result.gameLayerModules()).containsKey("mod"); + installation.assertModContent(result, "mod", List.of(entrypointClass, modManifest)); + } + + @Test + void testUserdevWithServiceProject() throws Exception { + var additionalClasspath = installation.setupUserdevProject(); + + var entrypointClass = SimulatedInstallation.generateClass("MOD_SERVICE", "mod/SomeService.class"); + var modManifest = SimulatedInstallation.createManifest("mod", Map.of("Automatic-Module-Name", "mod", "FMLModType", "LIBRARY")); + + var mainModule = installation.setupGradleModule(entrypointClass, modManifest); + additionalClasspath.addAll(mainModule); + + // Tell FML that the classes and resources directory belong together + SimulatedInstallation.setModFoldersProperty(Map.of("mod", mainModule)); + + var result = launchWithAdditionalClasspath("forgeclientuserdev", additionalClasspath); + assertThat(result.pluginLayerModules()).containsKey("mod"); + assertThat(result.gameLayerModules()).doesNotContainKey("mod"); + assertThat(result.loadedMods()).doesNotContainKey("mod"); + installation.assertSecureJarContent(result.pluginLayerModules().get("mod"), List.of(entrypointClass, modManifest)); + } + + /** + * Check that a ModLauncher service does not end up being loaded twice. + */ + @Test + void testUserdevWithModLauncherServiceProject() throws Exception { + var additionalClasspath = installation.setupUserdevProject(); + + var entrypointClass = SimulatedInstallation.generateClass("MOD_SERVICE", "mod/SomeService.class"); + var modManifest = SimulatedInstallation.createManifest("mod", Map.of("Automatic-Module-Name", "mod", "FMLModType", "LIBRARY")); + + var mainModule = installation.setupGradleModule(entrypointClass, modManifest); + additionalClasspath.addAll(mainModule); + + // Tell FML that the classes and resources directory belong together, this would also be read + // by the Classpath ML locator + SimulatedInstallation.setModFoldersProperty(Map.of("mod", mainModule)); + locatedPaths.add(mainModule.getFirst()); // Mark the primary path as located by ML so it gets skipped by FML + + var result = launchWithAdditionalClasspath("forgeclientuserdev", additionalClasspath); + assertThat(result.pluginLayerModules()).doesNotContainKey("mod"); + assertThat(result.gameLayerModules()).doesNotContainKey("mod"); + assertThat(result.loadedMods()).doesNotContainKey("mod"); + } + } + + @Nested + class Errors { + @Test + void testCorruptedServerInstallation() throws Exception { + installation.setupProductionServer(); + + var serverPath = installation.getLibrariesDir().resolve("net/minecraft/server/1.20.4-202401020304/server-1.20.4-202401020304-srg.jar"); + Files.delete(serverPath); + + var result = launch("forgeserver"); + assertThat(result.issues()).containsOnly( + ModLoadingIssue.error("fml.modloading.corrupted_installation").withAffectedPath(serverPath)); + } + + @Test + void testCorruptedClientInstallation() throws Exception { + installation.setupProductionClient(); + + var clientPath = installation.getLibrariesDir().resolve("net/minecraft/client/1.20.4-202401020304/client-1.20.4-202401020304-srg.jar"); + Files.delete(clientPath); + + var result = launch("forgeclient"); + assertThat(result.issues()).containsOnly( + ModLoadingIssue.error("fml.modloading.corrupted_installation").withAffectedPath(clientPath)); + } + + @Test + void testInvalidJarFile() throws Exception { + installation.setupProductionClient(); + + var path = installation.getModsFolder().resolve("mod.jar"); + Files.write(path, new byte[] { 1, 2, 3 }); + + var result = launch("forgeclient"); + // Clear the cause, otherwise equality will fail + assertThat(result.issues()).extracting(issue -> issue.withCause(null)).containsOnly( + ModLoadingIssue.error("fml.modloading.brokenfile.invalidzip", path).withAffectedPath(path)); + } + + /** + * Tests that an unknown FMLModType is recorded as an error for that file. + */ + @Test + void testJarFileWithInvalidFmlModType() throws Exception { + installation.setupProductionClient(); + + var path = installation.writeModJar("test.jar", new IdentifiableContent("INVALID_MANIFEST", "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\nFMLModType: XXX\n".getBytes())); + + var result = launch("forgeclient"); + // Clear the cause, otherwise equality will fail + assertThat(result.issues()).extracting(issue -> issue.withCause(null)).containsOnly( + ModLoadingIssue.error("fml.modloading.brokenfile", path).withAffectedPath(path)); + } + + /** + * Test that a locator or reader returning a custom subclass of IModFile is reported. + */ + @Test + void testInvalidSubclassOfModFile() throws Exception { + installation.setupProductionClient(); + + installation.writeModJar("test.jar", CustomSubclassModFileReader.TRIGGER); + + var result = launch("forgeclient"); + assertThat(result.issues()).extracting(ModLoadingIssue::toString).allMatch( + msg -> msg.startsWith("ERROR: fml.modloading.technical_error, Unexpected IModFile subclass:")); + } + } + + @Nested + class Warnings { + @Test + void testIncompatibleModsToml() throws Exception { + installation.setupProductionClient(); + var path = installation.writeModJar("mod.jar", new IdentifiableContent("MOD_TOML", "META-INF/mods.toml")); + + var result = launch("forgeclient"); + assertThat(result.issues()).containsOnly( + ModLoadingIssue.warning("fml.modloading.brokenfile.minecraft_forge", path).withAffectedPath(path)); + } + + @Test + void testFabricMod() throws Exception { + installation.setupProductionClient(); + var path = installation.writeModJar("mod.jar", new IdentifiableContent("FABRIC_MOD_JSON", "fabric.mod.json")); + + var result = launch("forgeclient"); + assertThat(result.issues()).containsOnly( + ModLoadingIssue.warning("fml.modloading.brokenfile.fabric", path).withAffectedPath(path)); + } + + @Test + void testFileIsDirectory() throws Exception { + installation.setupProductionClient(); + + var path = installation.getModsFolder().resolve("mod.jar"); + Files.createDirectories(path); + + var result = launch("forgeclient"); + assertThat(result.issues()).containsOnly( + ModLoadingIssue.warning("fml.modloading.brokenfile", path).withAffectedPath(path)); + } + } + + private LaunchResult launchInNeoForgeDevEnvironment(String launchTarget) throws Exception { + var additionalClasspath = installation.setupNeoForgeDevProject(); + + return launchWithAdditionalClasspath(launchTarget, additionalClasspath); + } + + LaunchResult launchWithAdditionalClasspath(String launchTarget, List additionalClassPath) throws Exception { + var urls = additionalClassPath.stream().map(path -> { + try { + return path.toUri().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }).toArray(URL[]::new); + + var previousCl = Thread.currentThread().getContextClassLoader(); + try (var cl = new URLClassLoader(urls, getClass().getClassLoader())) { + Thread.currentThread().setContextClassLoader(cl); + return launch(launchTarget); + } finally { + Thread.currentThread().setContextClassLoader(previousCl); + } + } + + LaunchResult launch(String launchTarget) throws Exception { + environment.computePropertyIfAbsent(IEnvironment.Keys.GAMEDIR.get(), ignored -> installation.getGameDir()); + environment.computePropertyIfAbsent(IEnvironment.Keys.LAUNCHTARGET.get(), ignored -> launchTarget); + + FMLPaths.loadAbsolutePaths(installation.getGameDir()); + + FMLLoader.onInitialLoad(environment); + FMLPaths.setup(environment); + FMLConfig.load(); + FMLLoader.setupLaunchHandler(environment, new VersionInfo( + SimulatedInstallation.NEOFORGE_VERSION, + SimulatedInstallation.FML_VERSION, + SimulatedInstallation.MC_VERSION, + SimulatedInstallation.NEOFORM_VERSION)); + FMLEnvironment.setupInteropEnvironment(environment); + Environment.build(environment); + + var launchContext = new TestLaunchContext(environment, locatedPaths); + var pluginResources = FMLLoader.beginModScan(launchContext); + // In this phase, FML should only return plugin libraries + assertThat(pluginResources).extracting(ITransformationService.Resource::target).containsOnly(IModuleLayerManager.Layer.PLUGIN); + + var gameLayerResources = FMLLoader.completeScan(launchContext, List.of()); + // In this phase, FML should only return game layer content + assertThat(gameLayerResources).extracting(ITransformationService.Resource::target).containsOnly(IModuleLayerManager.Layer.GAME); + + var loadingModList = LoadingModList.get(); + var loadedMods = loadingModList.getModFiles(); + + var pluginSecureJars = pluginResources.stream() + .flatMap(r -> r.resources().stream()) + .collect(Collectors.toMap( + SecureJar::name, + Function.identity())); + var gameSecureJars = gameLayerResources.stream() + .flatMap(r -> r.resources().stream()) + .collect(Collectors.toMap( + SecureJar::name, + Function.identity())); + + // Wait for background scans of all mods to complete + for (var modFile : loadingModList.getModFiles()) { + modFile.getFile().getScanResult(); + } + + return new LaunchResult( + pluginSecureJars, + gameSecureJars, + loadingModList.getModLoadingIssues(), + loadedMods.stream().collect(Collectors.toMap( + o -> o.getMods().getFirst().getModId(), + o -> o))); + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/IdentifiableContent.java b/loader/src/test/java/net/neoforged/fml/loading/IdentifiableContent.java new file mode 100644 index 000000000..bfccc8ff1 --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/IdentifiableContent.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +/** + * Content that is added to folders or jars inside a {@link SimulatedInstallation}. It can later be identified + * again, based purely on the content of a virtual SecureJar. + */ +public record IdentifiableContent(String name, String relativePath, byte[] content) { + IdentifiableContent(String name, String relativePath) { + this(name, relativePath, name.getBytes()); + } + + public IdentifiableContent(String name, String relativePath, byte[] content) { + this.name = name; + this.relativePath = relativePath; + this.content = content; + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/LaunchContextTest.java b/loader/src/test/java/net/neoforged/fml/loading/LaunchContextTest.java new file mode 100644 index 000000000..019ea01ed --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/LaunchContextTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +import cpw.mods.modlauncher.api.IEnvironment; +import cpw.mods.modlauncher.api.IModuleLayerManager; +import java.io.IOException; +import java.lang.module.Configuration; +import java.lang.module.ModuleFinder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class LaunchContextTest { + @TempDir + Path tempDir; + Path jarPath; + Path otherJarPath; + LaunchContext context; + + @BeforeEach + void setUp() throws IOException { + // Create a fake module layer with a jar on it. + jarPath = tempDir.resolve("test.jar"); + try (var out = new JarOutputStream(Files.newOutputStream(jarPath))) { + var je = new JarEntry("META-INF/MANIFEST.MF"); + out.putNextEntry(je); + out.write("Manifest-Version: 1.0\nAutomatic-Module-Name: fancymodule\n".getBytes()); + } + otherJarPath = tempDir.resolve("test-other-jar.jar"); + + var cf = Configuration.resolveAndBind( + ModuleFinder.of(), + List.of(ModuleLayer.boot().configuration()), + ModuleFinder.of(jarPath), + List.of("fancymodule")); + var moduleLayer = ModuleLayer.defineModulesWithOneLoader( + cf, + List.of(ModuleLayer.boot()), + ClassLoader.getPlatformClassLoader()).layer(); + + var environment = mock(IEnvironment.class); + var moduleLayerManager = new IModuleLayerManager() { + @Override + public Optional getLayer(Layer layer) { + return layer == Layer.SERVICE ? Optional.of(moduleLayer) : Optional.empty(); + } + }; + context = new LaunchContext(environment, moduleLayerManager, List.of(), List.of(), List.of()); + } + + @Test + void testIsLocatedForJarOnLayer() { + assertTrue(context.isLocated(jarPath)); + } + + @Test + void testIsLocatedForJarNotOnLayer() { + assertFalse(context.isLocated(otherJarPath)); + } + + @Test + void testAddLocatedForJarOnLayer() { + assertFalse(context.addLocated(jarPath)); + } + + @Test + void testAddLocatedForJarNotOnLayer() { + assertFalse(context.isLocated(otherJarPath)); + assertTrue(context.addLocated(otherJarPath)); + assertFalse(context.addLocated(otherJarPath)); + assertTrue(context.isLocated(otherJarPath)); + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/LaunchResult.java b/loader/src/test/java/net/neoforged/fml/loading/LaunchResult.java new file mode 100644 index 000000000..06b77746d --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/LaunchResult.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import cpw.mods.jarhandling.SecureJar; +import java.util.List; +import java.util.Map; +import net.neoforged.fml.ModLoadingIssue; +import net.neoforged.fml.loading.moddiscovery.ModFileInfo; + +record LaunchResult(Map pluginLayerModules, + Map gameLayerModules, + List issues, + Map loadedMods) {} diff --git a/loader/src/test/java/net/neoforged/fml/loading/MavenCoordinateTest.java b/loader/src/test/java/net/neoforged/fml/loading/MavenCoordinateTest.java new file mode 100644 index 000000000..0a8143813 --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/MavenCoordinateTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MavenCoordinateTest { + @ParameterizedTest + @ValueSource(strings = { "g:a:v", + "g:a:v:classifier", + "g:a:v@zip", + "g:a:v:classifier@zip" }) + void testParseToStringRoundtrip(String compactForm) { + assertEquals(compactForm, MavenCoordinate.parse(compactForm).toString()); + } + + @Test + void testNullExtensionCoercion() { + var coordinate = new MavenCoordinate("g", "a", null, "", "v"); + assertEquals("", coordinate.extension()); + } + + @Test + void testNullClassifierCoercion() { + var coordinate = new MavenCoordinate("g", "a", "", null, "v"); + assertEquals("", coordinate.classifier()); + } + + @ParameterizedTest + @ValueSource(strings = { "g:a:c:v:extra", "g:a", "g:a:v@t@t" }) + void testParseInvalidForms(String value) { + assertThrows(IllegalArgumentException.class, () -> MavenCoordinate.parse(value)); + } + + @Test + void testParseGAV() { + assertEquals(new MavenCoordinate("g", "a", "", "", "v"), MavenCoordinate.parse("g:a:v")); + } + + @Test + void testParseGAVWithClassifier() { + assertEquals(new MavenCoordinate("g", "a", "", "classifier", "v"), MavenCoordinate.parse("g:a:v:classifier")); + } + + @Test + void testParseGAVWithExtension() { + assertEquals(new MavenCoordinate("g", "a", "zip", "", "v"), MavenCoordinate.parse("g:a:v@zip")); + } + + @Test + void testParseGAVWithClassifierAndExtension() { + assertEquals(new MavenCoordinate("g", "a", "zip", "classifier", "v"), MavenCoordinate.parse("g:a:v:classifier@zip")); + } + + @ParameterizedTest + @CsvSource(textBlock = """ + g:a:v, g/a/v/a-v.jar + g.h.j:a:v, g/h/j/a/v/a-v.jar + g:a:v:c, g/a/v/a-v-c.jar + g.h.j:a:v:c, g/h/j/a/v/a-v-c.jar + g:a:v@zip, g/a/v/a-v.zip + g:a:v:c@zip, g/a/v/a-v-c.zip + """) + void testToRelativeRepositoryPath(String compactForm, String expectedPath) { + assertEquals(Paths.get(expectedPath), MavenCoordinate.parse(compactForm).toRelativeRepositoryPath()); + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/SimulatedInstallation.java b/loader/src/test/java/net/neoforged/fml/loading/SimulatedInstallation.java new file mode 100644 index 000000000..fc79c1e57 --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/SimulatedInstallation.java @@ -0,0 +1,457 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import cpw.mods.jarhandling.SecureJar; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.neoforged.jarjar.metadata.ContainedJarMetadata; +import net.neoforged.jarjar.metadata.Metadata; +import net.neoforged.jarjar.metadata.MetadataIOHandler; +import net.neoforged.jarjar.selection.util.Constants; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; + +/** + * Simulates various installation types for NeoForge + */ +class SimulatedInstallation implements AutoCloseable { + private static final IdentifiableContent CLIENT_ASSETS = new IdentifiableContent("CLIENT_ASSETS", "assets/.mcassetsroot"); + private static final IdentifiableContent SHARED_ASSETS = new IdentifiableContent("SHARED_ASSETS", "data/.mcassetsroot"); + /** + * A class that is contained in both client and dedicated server distribution, renamed to official mappings. + */ + private static final IdentifiableContent RENAMED_SHARED = generateClass("RENAMED_SHARED", "net/minecraft/server/MinecraftServer.class"); + /** + * A class that is contained in both client and dedicated server distribution, renamed to official mappings, + * and containing NeoForge patches. + */ + private static final IdentifiableContent PATCHED_SHARED = generateClass("PATCHED_SHARED", "net/minecraft/server/MinecraftServer.class"); + /** + * A class that is only in the client distribution, renamed to official mappings. + */ + private static final IdentifiableContent RENAMED_CLIENT = generateClass("RENAMED_CLIENT", "net/minecraft/client/Minecraft.class"); + /** + * A class that is contained in both client and dedicated server distribution, renamed to official mappings, + * and containing NeoForge patches. + */ + private static final IdentifiableContent PATCHED_CLIENT = generateClass("PATCHED_CLIENT", "net/minecraft/client/Minecraft.class"); + private static final IdentifiableContent NEOFORGE_CLASSES = generateClass("NEOFORGE_CLASSES", "net/neoforged/neoforge/common/NeoForgeMod.class"); + private static final IdentifiableContent NEOFORGE_MODS_TOML = new IdentifiableContent("NEOFORGE_MODS_TOML", "META-INF/neoforge.mods.toml", writeNeoForgeModsToml()); + private static final IdentifiableContent NEOFORGE_MANIFEST = new IdentifiableContent("NEOFORGE_MANIFEST", JarFile.MANIFEST_NAME, writeNeoForgeManifest()); + private static final IdentifiableContent NEOFORGE_ASSETS = new IdentifiableContent("NEOFORGE_ASSETS", "neoforged_logo.png"); + + public static final String LIBRARIES_DIRECTORY_PROPERTY = "libraryDirectory"; + public static final String MOD_FOLDERS_PROPERTIES = "fml.modFolders"; + public static final String NEOFORGE_VERSION = "20.4.9999"; + public static final String FML_VERSION = "3.0.9999"; + public static final String MC_VERSION = "1.20.4"; + public static final String NEOFORM_VERSION = "202401020304"; + // Simulates the runtime directory passed to the game (present in every directory) + private final Path gameDir; + // Simulates the libraries directory found in production installations (both client & server) + private final Path librariesDir; + // Used for testing running out of a Gradle project. Is the simulated Gradle project root directory. + private final Path projectRoot; + + private static final IdentifiableContent[] SERVER_EXTRA_JAR_CONTENT = { SHARED_ASSETS }; + private static final IdentifiableContent[] CLIENT_EXTRA_JAR_CONTENT = { CLIENT_ASSETS, SHARED_ASSETS }; + private static final IdentifiableContent[] NEOFORGE_UNIVERSAL_JAR_CONTENT = { NEOFORGE_ASSETS, NEOFORGE_CLASSES, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST }; + private static final IdentifiableContent[] USERDEV_CLIENT_JAR_CONTENT = { PATCHED_CLIENT, PATCHED_SHARED }; + + // For a production client: Simulates the "libraries" directory found in the Vanilla Minecraft installation directory (".minecraft") + // For a production server: The NF installer creates a "libraries" directory in the server root + // In both cases, the location of this directory is passed via a System property "libraryDirectory" + public SimulatedInstallation() throws IOException { + gameDir = Files.createTempDirectory("gameDir"); + librariesDir = Files.createTempDirectory("librariesDir"); + projectRoot = Files.createTempDirectory("projectRoot"); + } + + public Path getModsFolder() throws IOException { + var modsFolder = gameDir.resolve("mods"); + Files.createDirectories(modsFolder); + return modsFolder; + } + + @Override + public void close() throws Exception { + MoreFiles.deleteRecursively(gameDir, RecursiveDeleteOption.ALLOW_INSECURE); + MoreFiles.deleteRecursively(librariesDir, RecursiveDeleteOption.ALLOW_INSECURE); + MoreFiles.deleteRecursively(projectRoot, RecursiveDeleteOption.ALLOW_INSECURE); + System.clearProperty(LIBRARIES_DIRECTORY_PROPERTY); + System.clearProperty(MOD_FOLDERS_PROPERTIES); + } + + public void setupPlainJarInModsFolder(String filename) throws IOException { + var modsDir = getModsFolder(); + writeJarFile(modsDir.resolve(filename), generateClass("test-class", "pkg/TestClass.class")); + } + + public void setupModInModsFolder(String modId, String version) throws IOException { + var modsDir = getModsFolder(); + var filename = modId + "-" + version + ".jar"; + var modEntrypointClass = generateClass(modId + "_ENTRYPOINT", modId + "/ModEntrypoint.class"); + var modsToml = createModsToml(modId, version); + writeJarFile(modsDir.resolve(filename), modEntrypointClass, modsToml); + } + + public void setup(String modId, String version) throws IOException { + var modsDir = getModsFolder(); + var filename = modId + "-" + version + ".jar"; + var modEntrypointClass = generateClass(modId + "_ENTRYPOINT", modId + "/ModEntrypoint.class"); + var modsToml = createModsToml(modId, version); + writeJarFile(modsDir.resolve(filename), modEntrypointClass, modsToml); + } + + public void setupProductionClient() throws IOException { + System.setProperty(LIBRARIES_DIRECTORY_PROPERTY, librariesDir.toString()); + + writeLibrary("net.minecraft", "client", MC_VERSION + "-" + NEOFORM_VERSION, "srg", RENAMED_CLIENT, RENAMED_SHARED); + writeLibrary("net.minecraft", "client", MC_VERSION + "-" + NEOFORM_VERSION, "extra", CLIENT_ASSETS, SHARED_ASSETS); + writeLibrary("net.neoforged", "neoforge", NEOFORGE_VERSION, "client", PATCHED_CLIENT); + writeLibrary("net.neoforged", "neoforge", NEOFORGE_VERSION, "universal", NEOFORGE_UNIVERSAL_JAR_CONTENT); + } + + public void setupProductionServer() throws IOException { + System.setProperty(LIBRARIES_DIRECTORY_PROPERTY, librariesDir.toString()); + + writeLibrary("net.minecraft", "server", MC_VERSION + "-" + NEOFORM_VERSION, "srg", RENAMED_SHARED); + writeLibrary("net.minecraft", "server", MC_VERSION + "-" + NEOFORM_VERSION, "extra", SERVER_EXTRA_JAR_CONTENT); + writeLibrary("net.neoforged", "neoforge", NEOFORGE_VERSION, "server", PATCHED_SHARED); + writeLibrary("net.neoforged", "neoforge", NEOFORGE_VERSION, "universal", NEOFORGE_UNIVERSAL_JAR_CONTENT); + } + + // The classes directory in a NeoForge development environment will contain both the Minecraft + // and the NeoForge classes. This is due to both calling each other and having to be compiled in + // the same javac compilation as a result. + public ArrayList setupNeoForgeDevProject() throws IOException { + var additionalClasspath = new ArrayList(); + + // Emulate the layout of a NeoForge development environment + // In dev, we have a joined distribution containing both dedicated server and client + var classesDir = projectRoot.resolve("projects/neoforge/build/classes/java/main"); + additionalClasspath.add(classesDir); + writeFiles(classesDir, PATCHED_CLIENT, PATCHED_SHARED, NEOFORGE_CLASSES); + + var resourcesDir = projectRoot.resolve("projects/neoforge/build/resources/main"); + additionalClasspath.add(resourcesDir); + writeFiles(resourcesDir, NEOFORGE_ASSETS, NEOFORGE_MODS_TOML, NEOFORGE_MANIFEST); + + var clientExtraJar = projectRoot.resolve("client-extra.jar"); + additionalClasspath.add(clientExtraJar); + writeJarFile(clientExtraJar, CLIENT_EXTRA_JAR_CONTENT); + + setModFoldersProperty(Map.of("minecraft", List.of(classesDir, resourcesDir))); + + return additionalClasspath; + } + + /** + * In userdev, the Gradle tooling recompiles a joined Minecraft jar and injects the NeoForge classes and resources. + * Original Minecraft assets are split off into a client-extra.jar similar to neodev. + */ + public List setupUserdevProject() throws IOException { + var additionalClasspath = new ArrayList(); + + var neoforgeJar = projectRoot.resolve("neoforge-joined.jar"); + additionalClasspath.add(neoforgeJar); + writeJarFile(neoforgeJar, Stream.concat(Stream.of(USERDEV_CLIENT_JAR_CONTENT), Stream.of(NEOFORGE_UNIVERSAL_JAR_CONTENT)).toArray(IdentifiableContent[]::new)); + + var clientExtraJar = projectRoot.resolve("client-extra.jar"); + additionalClasspath.add(clientExtraJar); + writeJarFile(clientExtraJar, CLIENT_EXTRA_JAR_CONTENT); + + return additionalClasspath; + } + + public static void setModFoldersProperty(Map> modFolders) { + var modFolderList = modFolders.entrySet() + .stream() + .flatMap(entry -> entry.getValue().stream().map(path -> entry.getKey() + "%%" + path)) + .collect(Collectors.joining(File.pathSeparator)); + + System.setProperty(MOD_FOLDERS_PROPERTIES, modFolderList); + } + + public Path getLibrariesDir() { + return librariesDir; + } + + public Path getGameDir() { + return gameDir; + } + + public Path getProjectRoot() { + return projectRoot; + } + + /** + * Dynamically generates a class. This is not 100% correct, but should be sufficient for the + * background scanner to read it. + */ + public static IdentifiableContent generateClass(String id, String relativePath) { + var className = relativePath.replace(".class", ""); + var classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + classWriter.visitAnnotation("Lfake/ClassAnnotation;", true); + classWriter.visit(Opcodes.V21, 0, className, null, null, null); + return new IdentifiableContent(id, relativePath, classWriter.toByteArray()); + } + + private static byte[] writeNeoForgeManifest() { + return "FML-System-Mods: neoforge\n".getBytes(); + } + + private static byte[] writeNeoForgeModsToml() { + return """ + modLoader = "javafml" + loaderVersion = "[3,]" + license = "LICENSE" + + [[mods]] + modId="neoforge" + """.getBytes(); + } + + public static IdentifiableContent createManifest(String name, Map attributes) throws IOException { + Manifest manifest = new Manifest(); + // If no manifest version is written, nothing is written. + manifest.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0"); + for (var entry : attributes.entrySet()) { + manifest.getMainAttributes().putValue(entry.getKey(), entry.getValue()); + } + var bout = new ByteArrayOutputStream(); + manifest.write(bout); + var content = bout.toByteArray(); + return new IdentifiableContent(name, JarFile.MANIFEST_NAME, content); + } + + public static IdentifiableContent createModsToml(String modId, String version) { + var content = """ + modLoader = "javafml" + loaderVersion = "[3,]" + license = "LICENSE" + + [[mods]] + modId="%s" + version="%s" + """.formatted(modId, version).getBytes(); + return new IdentifiableContent(modId + "_MODS_TOML", "META-INF/neoforge.mods.toml", content); + } + + public Path writeLibrary(String group, String artifact, String version, IdentifiableContent... content) throws IOException { + return writeLibrary(group, artifact, version, null, content); + } + + private Path writeLibrary(String group, String artifact, String version, @Nullable String classifier, IdentifiableContent... content) throws IOException { + var folder = librariesDir.resolve(group.replace('.', '/')) + .resolve(artifact) + .resolve(version); + Files.createDirectories(folder); + + var filename = artifact + "-" + version; + if (classifier != null) { + filename += "-" + classifier; + } + filename += ".jar"; + + var file = folder.resolve(filename); + writeJarFile(file, content); + return file; + } + + public Path writeModJar(String filename, IdentifiableContent... content) throws IOException { + var path = getModsFolder().resolve(filename); + writeJarFile(path, content); + return path; + } + + public static void writeJarFile(Path file, IdentifiableContent... content) throws IOException { + try (var fout = Files.newOutputStream(file)) { + writeJarFile(fout, content); + } + } + + public static IdentifiableContent createJarFile(String name, String relativePath, IdentifiableContent... content) throws IOException { + var bout = new ByteArrayOutputStream(); + writeJarFile(bout, content); + return new IdentifiableContent(name, relativePath, bout.toByteArray()); + } + + public static void writeJarFile(OutputStream out, IdentifiableContent... content) throws IOException { + try (var jout = new JarOutputStream(out)) { + // Make sure the Manifest is written first + for (var identifiableContent : content) { + if (JarFile.MANIFEST_NAME.equals(identifiableContent.relativePath())) { + var ze = new JarEntry(JarFile.MANIFEST_NAME); + jout.putNextEntry(ze); + jout.write(identifiableContent.content()); + jout.closeEntry(); + } + } + + for (var identifiableContent : content) { + if (JarFile.MANIFEST_NAME.equals(identifiableContent.relativePath())) { + continue; // Written earlier + } + + var ze = new JarEntry(identifiableContent.relativePath()); + jout.putNextEntry(ze); + jout.write(identifiableContent.content()); + jout.closeEntry(); + } + } + } + + public void writeFiles(Path folder, IdentifiableContent... content) throws IOException { + for (var identifiableContent : content) { + var path = folder.resolve(identifiableContent.relativePath()); + Files.createDirectories(path.getParent()); + try (var out = Files.newOutputStream(path)) { + out.write(identifiableContent.content()); + } + } + } + + public void assertMinecraftServerJar(LaunchResult launchResult) throws IOException { + var expectedContent = new ArrayList(); + Collections.addAll(expectedContent, SERVER_EXTRA_JAR_CONTENT); + expectedContent.add(PATCHED_SHARED); + + assertModContent(launchResult, "minecraft", expectedContent); + } + + public void assertMinecraftClientJar(LaunchResult launchResult) throws IOException { + var expectedContent = new ArrayList(); + Collections.addAll(expectedContent, CLIENT_EXTRA_JAR_CONTENT); + expectedContent.add(PATCHED_CLIENT); + expectedContent.add(PATCHED_SHARED); + + assertModContent(launchResult, "minecraft", expectedContent); + } + + public void assertNeoForgeJar(LaunchResult launchResult) throws IOException { + var expectedContent = List.of( + NEOFORGE_ASSETS, + NEOFORGE_CLASSES, + NEOFORGE_MODS_TOML, + NEOFORGE_MANIFEST); + + assertModContent(launchResult, "neoforge", expectedContent); + } + + public void assertModContent(LaunchResult launchResult, String modId, Collection content) throws IOException { + assertThat(launchResult.loadedMods()).containsKey(modId); + + var modFileInfo = launchResult.loadedMods().get(modId); + assertNotNull(modFileInfo, "mod " + modId + " is missing"); + + assertSecureJarContent(modFileInfo.getFile().getSecureJar(), content); + } + + public void assertSecureJarContent(SecureJar jar, Collection content) throws IOException { + var paths = listFilesRecursively(jar); + + assertThat(paths.keySet()).containsOnly(content.stream().map(IdentifiableContent::relativePath).toArray(String[]::new)); + + for (var identifiableContent : content) { + var expectedContent = identifiableContent.content(); + var actualContent = Files.readAllBytes(paths.get(identifiableContent.relativePath())); + if (isPrintableAscii(expectedContent) && isPrintableAscii(actualContent)) { + assertThat(new String(actualContent)).isEqualTo(new String(expectedContent)); + } else { + assertThat(actualContent).isEqualTo(expectedContent); + } + } + } + + private boolean isPrintableAscii(byte[] potentialText) { + for (byte b : potentialText) { + if (b < 0x20 || b == 0x7f) { + return false; + } + } + return true; + } + + private static Map listFilesRecursively(SecureJar jar) throws IOException { + Map paths; + var rootPath = jar.getRootPath(); + try (var stream = Files.walk(rootPath)) { + paths = stream + .filter(Files::isRegularFile) + .map(rootPath::relativize) + .collect(Collectors.toMap( + path -> path.toString().replace('\\', '/'), + Function.identity())); + } + return paths; + } + + public static IdentifiableContent createJijMetadata(ContainedJarMetadata... containedJars) { + Metadata metadata = new Metadata(Arrays.asList(containedJars)); + + byte[] content; + try (var in = MetadataIOHandler.toInputStream(metadata)) { + content = in.readAllBytes(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + return new IdentifiableContent("JIJ_METADATA", Constants.CONTAINED_JARS_METADATA_PATH, content); + } + + public List setupGradleModule(IdentifiableContent... buildOutput) throws IOException { + return setupGradleModule(null, buildOutput); + } + + public List setupGradleModule(@Nullable String subfolder, IdentifiableContent... buildOutput) throws IOException { + Path moduleRoot = projectRoot; + if (subfolder != null) { + moduleRoot = moduleRoot.resolve(subfolder); + } + + // Build typical single-module gradle output directories + var classesDir = moduleRoot.resolve("build/classes/java/main"); + Files.createDirectories(classesDir); + var resourcesDir = moduleRoot.resolve("build/resources/main"); + Files.createDirectories(resourcesDir); + for (IdentifiableContent identifiableContent : buildOutput) { + if (identifiableContent.relativePath().endsWith(".class")) { + writeFiles(classesDir, identifiableContent); + } else { + writeFiles(resourcesDir, identifiableContent); + } + } + + return List.of(classesDir, resourcesDir); + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/TestEnvironment.java b/loader/src/test/java/net/neoforged/fml/loading/TestEnvironment.java new file mode 100644 index 000000000..6f3c5350c --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/TestEnvironment.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import cpw.mods.modlauncher.api.IEnvironment; +import cpw.mods.modlauncher.api.ILaunchHandlerService; +import cpw.mods.modlauncher.api.IModuleLayerManager; +import cpw.mods.modlauncher.api.TypesafeMap; +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.function.Function; +import net.neoforged.accesstransformer.ml.AccessTransformerService; +import net.neoforged.fml.common.asm.RuntimeDistCleaner; +import org.jetbrains.annotations.Nullable; + +class TestEnvironment implements IEnvironment { + private final TypesafeMap map = new TypesafeMap(IEnvironment.class); + private final TestModuleLayerManager moduleLayerManager; + @Nullable + public AccessTransformerService accessTransformerService = new AccessTransformerService(); + @Nullable + public RuntimeDistCleaner runtimeDistCleaner = new RuntimeDistCleaner(); + + public TestEnvironment(TestModuleLayerManager moduleLayerManager) { + this.moduleLayerManager = moduleLayerManager; + } + + @Override + public Optional getProperty(TypesafeMap.Key key) { + return map.get(key); + } + + @Override + public T computePropertyIfAbsent(TypesafeMap.Key key, Function, ? extends T> valueFunction) { + return map.computeIfAbsent(key, valueFunction); + } + + @Override + public Optional findLaunchPlugin(String name) { + return switch (name) { + case "accesstransformer" -> Optional.ofNullable(accessTransformerService); + case "runtimedistcleaner" -> Optional.ofNullable(runtimeDistCleaner); + default -> throw new IllegalStateException("Unexpected value: " + name); + }; + } + + @Override + public Optional findLaunchHandler(String name) { + return ServiceLoader.load(ILaunchHandlerService.class).stream() + .map(ServiceLoader.Provider::get) + .filter(service -> service.name().equals(name)) + .findFirst(); + } + + @Override + public Optional findModuleLayerManager() { + return Optional.of(moduleLayerManager); + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/TestLaunchContext.java b/loader/src/test/java/net/neoforged/fml/loading/TestLaunchContext.java new file mode 100644 index 000000000..3ad6c7183 --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/TestLaunchContext.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import cpw.mods.modlauncher.api.IEnvironment; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import net.neoforged.neoforgespi.ILaunchContext; + +class TestLaunchContext implements ILaunchContext { + private final IEnvironment environment; + private final Set locatedPaths; + + public TestLaunchContext(IEnvironment environment, Set locatedPaths) { + this.environment = environment; + this.locatedPaths = new HashSet<>(locatedPaths); + } + + @Override + public IEnvironment environment() { + return environment; + } + + @Override + public ServiceLoader createServiceLoader(Class serviceClass) { + return ServiceLoader.load(serviceClass); + } + + @Override + public List modLists() { + return List.of(); + } + + @Override + public List mods() { + return List.of(); + } + + @Override + public List mavenRoots() { + return List.of(); + } + + @Override + public boolean isLocated(Path path) { + return locatedPaths.contains(path); + } + + @Override + public boolean addLocated(Path path) { + return locatedPaths.add(path); + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/TestModuleLayerManager.java b/loader/src/test/java/net/neoforged/fml/loading/TestModuleLayerManager.java new file mode 100644 index 000000000..653ba4c2e --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/TestModuleLayerManager.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading; + +import cpw.mods.modlauncher.api.IModuleLayerManager; +import java.lang.module.Configuration; +import java.util.EnumMap; +import java.util.List; +import java.util.Optional; + +public class TestModuleLayerManager implements IModuleLayerManager { + public static final ModuleLayer SERVICE_LAYER = createEmptyLayer(); + public static final ModuleLayer PLUGIN_LAYER = createEmptyLayer(); + + private final EnumMap layers = new EnumMap<>(Layer.class); + + public void setLayer(Layer layer, ModuleLayer moduleLayer) { + this.layers.put(layer, moduleLayer); + } + + @Override + public Optional getLayer(Layer layer) { + return Optional.ofNullable(layers.get(layer)); + } + + private static ModuleLayer createEmptyLayer() { + return ModuleLayer.defineModulesWithOneLoader( + Configuration.empty(), + List.of(), + ClassLoader.getSystemClassLoader()).layer(); + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/moddiscovery/providers/ProductionClientProviderTest.java b/loader/src/test/java/net/neoforged/fml/loading/moddiscovery/providers/ProductionClientProviderTest.java new file mode 100644 index 000000000..6e87d2ecc --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/loading/moddiscovery/providers/ProductionClientProviderTest.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.providers; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import net.neoforged.fml.loading.MavenCoordinate; +import net.neoforged.fml.loading.moddiscovery.locators.ProductionClientProvider; +import org.junit.jupiter.api.Test; + +class ProductionClientProviderTest { + @Test + void testToString() { + var provider = new ProductionClientProvider(List.of()); + assertEquals("production client provider", provider.toString()); + + var providerPlus1 = new ProductionClientProvider(List.of(MavenCoordinate.parse("g:a:v"))); + assertEquals("production client provider +g:a:v", providerPlus1.toString()); + + var providerPlus2 = new ProductionClientProvider(List.of(MavenCoordinate.parse("g:a:v"), MavenCoordinate.parse("g:a:c:v"))); + assertEquals("production client provider +g:a:v +g:a:c:v", providerPlus2.toString()); + } +} diff --git a/loader/src/test/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileReader b/loader/src/test/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileReader new file mode 100644 index 000000000..da0785e9a --- /dev/null +++ b/loader/src/test/resources/META-INF/services/net.neoforged.neoforgespi.locating.IModFileReader @@ -0,0 +1 @@ +net.neoforged.fml.loading.CustomSubclassModFileReader \ No newline at end of file diff --git a/loader/src/test/resources/log4j2-test.xml b/loader/src/test/resources/log4j2-test.xml new file mode 100644 index 000000000..b8d02518d --- /dev/null +++ b/loader/src/test/resources/log4j2-test.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + +