diff --git a/loader/src/main/java/net/neoforged/fml/ModLoader.java b/loader/src/main/java/net/neoforged/fml/ModLoader.java index 75efe1804..3fe8f9a15 100644 --- a/loader/src/main/java/net/neoforged/fml/ModLoader.java +++ b/loader/src/main/java/net/neoforged/fml/ModLoader.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -190,6 +191,12 @@ public static void waitForTask(String name, Runnable periodicTask, CompletableFu } } + /** + * Exception that is fired when a mod loading future cannot be executed because a dependent future failed. + * It is only used for control flow and easy filtering out, but never logged or propagated further. + */ + private static class DependentFutureFailedException extends RuntimeException {} + /** * Dispatches a task across all mod containers in parallel, with progress displayed on the loading screen. */ @@ -197,15 +204,45 @@ public static void dispatchParallelTask(String name, Executor parallelExecutor, var progress = StartupNotificationManager.addProgressBar(name, modList.size()); try { periodicTask.run(); + Map> modFutures = new IdentityHashMap<>(); var futureList = modList.getSortedMods().stream() .map(modContainer -> { - return CompletableFuture.runAsync(() -> { - ModLoadingContext.get().setActiveContainer(modContainer); - task.accept(modContainer); - }, parallelExecutor).whenComplete((result, exception) -> { - progress.increment(); - ModLoadingContext.get().setActiveContainer(null); - }); + // Build future for all dependencies first + var deps = LoadingModList.get().getDependencies(modContainer.getModInfo()); + @SuppressWarnings("unchecked") + CompletableFuture[] depFutures = new CompletableFuture[deps.size()]; + for (int i = 0; i < deps.size(); ++i) { + depFutures[i] = modFutures.get(deps.get(i)); + if (depFutures[i] == null) { + throw new IllegalStateException("Dependency future for mod %s which is a dependency of %s not found!".formatted( + deps.get(i).getModId(), modContainer.getModId())); + } + } + + var combinedFuture = depFutures.length == 0 ? CompletableFuture.completedFuture(null) : CompletableFuture.allOf(depFutures); + + // Build the future for this container + var future = combinedFuture.handleAsync((void_, exception) -> { + if (exception != null) { + // If there was any exception, short circuit. + // The exception will already be handled by `waitForFuture` since it comes from another mod. + LOGGER.error("Skipping {} future for mod {} because a dependency threw an exception.", name, modContainer.getModId()); + progress.increment(); + // Throw a marker exception to make sure that dependencies of *this* task don't get executed. + throw new DependentFutureFailedException(); + } + + try { + ModLoadingContext.get().setActiveContainer(modContainer); + task.accept(modContainer); + } finally { + progress.increment(); + ModLoadingContext.get().setActiveContainer(null); + } + return null; + }, parallelExecutor); + modFutures.put(modContainer.getModInfo(), future); + return future; }) .toList(); var singleFuture = ModList.gather(futureList) @@ -226,7 +263,9 @@ private static void waitForFuture(String name, Runnable periodicTask, Completabl // Merge all potential modloading issues var errorCount = 0; for (var error : e.getCause().getSuppressed()) { - if (error instanceof ModLoadingException modLoadingException) { + if (error instanceof DependentFutureFailedException) { + continue; + } else if (error instanceof ModLoadingException modLoadingException) { loadingIssues.addAll(modLoadingException.getIssues()); } else { loadingIssues.add(ModLoadingIssue.error("fml.modloading.uncaughterror", name).withCause(e));