diff --git a/loader/src/main/java/net/neoforged/fml/ModLoader.java b/loader/src/main/java/net/neoforged/fml/ModLoader.java index 1ec94fd69..f8a60c475 100644 --- a/loader/src/main/java/net/neoforged/fml/ModLoader.java +++ b/loader/src/main/java/net/neoforged/fml/ModLoader.java @@ -11,10 +11,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; @@ -37,6 +39,7 @@ import net.neoforged.fml.loading.moddiscovery.ModInfo; import net.neoforged.fml.loading.progress.StartupNotificationManager; import net.neoforged.neoforgespi.language.IModInfo; +import net.neoforged.neoforgespi.language.IModLanguageLoader; import net.neoforged.neoforgespi.language.ModFileScanData; import net.neoforged.neoforgespi.locating.ForgeFeature; import net.neoforged.neoforgespi.locating.IModFile; @@ -275,12 +278,20 @@ private static void waitForFuture(String name, Runnable periodicTask, Completabl } private static List buildMods(final IModFile modFile) { - return modFile.getModFileInfo() + final Map> byLoader = new IdentityHashMap<>(); + var containers = modFile.getModFileInfo() .getMods() .stream() - .map(info -> buildModContainerFromTOML(info, modFile.getScanResult())) + .map(info -> { + var container = buildModContainerFromTOML(info, modFile.getScanResult()); + var cont = byLoader.computeIfAbsent(info.getLoader(), k -> new HashSet<>()); + if (container != null) cont.add(container); + return container; + }) .filter(Objects::nonNull) .toList(); + byLoader.forEach((loader, loaded) -> loader.validate(modFile, loaded, ModLoader::addLoadingIssue)); + return containers; } private static ModContainer buildModContainerFromTOML(final IModInfo modInfo, final ModFileScanData scanData) { diff --git a/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLJavaModLanguageProvider.java b/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLJavaModLanguageProvider.java index 237796bbb..7b1f93bb9 100644 --- a/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLJavaModLanguageProvider.java +++ b/loader/src/main/java/net/neoforged/fml/javafmlmod/FMLJavaModLanguageProvider.java @@ -6,16 +6,21 @@ package net.neoforged.fml.javafmlmod; import java.lang.annotation.ElementType; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; import net.neoforged.fml.ModContainer; -import net.neoforged.fml.ModLoadingException; import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.neoforgespi.IIssueReporting; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.language.IModLanguageLoader; import net.neoforged.neoforgespi.language.ModFileScanData; +import net.neoforged.neoforgespi.locating.IModFile; public class FMLJavaModLanguageProvider implements IModLanguageLoader { + @Override public String name() { return "javafml"; } @@ -24,13 +29,28 @@ public String name() { public ModContainer loadMod(IModInfo info, ModFileScanData modFileScanResults, ModuleLayer layer) { final var modClasses = modFileScanResults.getAnnotatedBy(Mod.class, ElementType.TYPE) .filter(data -> data.annotationData().get("value").equals(info.getModId())) + .filter(ad -> AutomaticEventSubscriber.getSides(ad.annotationData().get("dist")).contains(FMLLoader.getDist())) + .map(ad -> ad.clazz().getClassName()) .toList(); - if (modClasses.isEmpty()) { - throw new ModLoadingException(ModLoadingIssue.error("fml.modloading.javafml.missing_entrypoint").withAffectedMod(info)); + return new FMLModContainer(info, modClasses, modFileScanResults, layer); + } + + @Override + public void validate(IModFile file, Collection loadedContainers, IIssueReporting reporter) { + final Set modIds = new HashSet<>(); + for (IModInfo modInfo : file.getModInfos()) { + if (modInfo.getLoader() == this) { + modIds.add(modInfo.getModId()); + } } - return new FMLModContainer(info, modClasses - .stream().filter(ad -> AutomaticEventSubscriber.getSides(ad.annotationData().get("dist")).contains(FMLLoader.getDist())) - .map(ad -> ad.clazz().getClassName()) - .toList(), modFileScanResults, layer); + + file.getScanResult().getAnnotatedBy(Mod.class, ElementType.TYPE) + .filter(data -> !modIds.contains((String) data.annotationData().get("value"))) + .forEach(data -> { + var modId = data.annotationData().get("value"); + var entrypointClass = data.clazz().getClassName(); + var issue = ModLoadingIssue.error("fml.modloading.javafml.dangling_entrypoint", modId, entrypointClass, file.getFilePath()).withAffectedModFile(file); + reporter.addIssue(issue); + }); } } diff --git a/loader/src/main/java/net/neoforged/neoforgespi/IIssueReporting.java b/loader/src/main/java/net/neoforged/neoforgespi/IIssueReporting.java new file mode 100644 index 000000000..ed0b763bb --- /dev/null +++ b/loader/src/main/java/net/neoforged/neoforgespi/IIssueReporting.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforgespi; + +import net.neoforged.fml.ModLoadingIssue; + +/** + * Interface used to report issues to the game. + */ +public interface IIssueReporting { + /** + * Report an issue to the loader. + */ + void addIssue(ModLoadingIssue issue); +} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/language/IModLanguageLoader.java b/loader/src/main/java/net/neoforged/neoforgespi/language/IModLanguageLoader.java index e6d4ae5bf..27ed24132 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/language/IModLanguageLoader.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/language/IModLanguageLoader.java @@ -16,8 +16,10 @@ package net.neoforged.neoforgespi.language; +import java.util.Collection; import net.neoforged.fml.ModContainer; import net.neoforged.fml.ModLoadingException; +import net.neoforged.neoforgespi.IIssueReporting; import net.neoforged.neoforgespi.locating.IModFile; /** @@ -43,4 +45,13 @@ public interface IModLanguageLoader { * @throws ModLoadingException if loading encountered an exception */ ModContainer loadMod(IModInfo info, ModFileScanData modFileScanResults, ModuleLayer layer) throws ModLoadingException; + + /** + * Validate mod files using this loader, and report any issues (such as entrpoints without medatata). + * + * @param file the file to validate + * @param loadedContainers the containers of mods in the file, that have been created using this loader. This list does not contain errored containers + * @param reporter the interface used to report issues to the game + */ + default void validate(IModFile file, Collection loadedContainers, IIssueReporting reporter) {} } diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IDiscoveryPipeline.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IDiscoveryPipeline.java index 46eda5ada..b5bb88c99 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/locating/IDiscoveryPipeline.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/IDiscoveryPipeline.java @@ -9,7 +9,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Optional; -import net.neoforged.fml.ModLoadingIssue; +import net.neoforged.neoforgespi.IIssueReporting; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -18,7 +18,7 @@ * discovery pipeline. */ @ApiStatus.NonExtendable -public interface IDiscoveryPipeline { +public interface IDiscoveryPipeline extends IIssueReporting { /** * 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}. @@ -62,11 +62,6 @@ default Optional addPath(Path path, ModFileDiscoveryAttributes attribu */ 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.