diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/IModLoadingState.java b/fmlcore/src/main/java/net/minecraftforge/fml/IModLoadingState.java index 1b259ec743..3574755d73 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/IModLoadingState.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/IModLoadingState.java @@ -68,13 +68,15 @@ public interface IModLoadingState { * @return a transition task for this state * @see #buildTransition(Executor, Executor, ProgressMeter, Function, Function) */ - default - Optional> buildTransition(final Executor syncExecutor, - final Executor parallelExecutor, - final ProgressMeter progressBar) { + default Optional> buildTransition( + final Executor syncExecutor, + final Executor parallelExecutor, + final ProgressMeter progressBar + ) { return buildTransition(syncExecutor, parallelExecutor, progressBar, - e -> CompletableFuture.runAsync(() -> {}, e), - e -> CompletableFuture.runAsync(() -> {}, e)); + e -> CompletableFuture.completedFuture(null), + e -> CompletableFuture.completedFuture(null) + ); } /** diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/IModStateTransition.java b/fmlcore/src/main/java/net/minecraftforge/fml/IModStateTransition.java index f64dc6950c..73b79af5df 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/IModStateTransition.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/IModStateTransition.java @@ -9,8 +9,6 @@ import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.event.IModBusEvent; import net.minecraftforge.fml.loading.progress.ProgressMeter; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.BiFunction; @@ -18,103 +16,67 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; -@SuppressWarnings("unchecked") +import org.jetbrains.annotations.Nullable; + public interface IModStateTransition { + /** Magic value to allow me to optimize the futures list by ignoring the default value without making old methods nullable. */ + public static final BiFunction, CompletableFuture> NULL_HOOK = (e, g) -> CompletableFuture.completedFuture(null); + static IModStateTransition buildNoopTransition() { - return new NoopTransition(); + return ModStateTransitionHelper.NOOP; } - default - CompletableFuture build(final String name, - final Executor syncExecutor, - final Executor parallelExecutor, - final ProgressMeter progressBar, - final Function> preSyncTask, - final Function> postSyncTask) { - List> futures = new ArrayList<>(); - - this.eventFunctionStream().get() - .map(f -> (EventGenerator) f) - .reduce((head, tail) -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, progressBar, head, ModLoadingStage::currentState, tail)) - .ifPresent(last -> addCompletableFutureTaskForModDispatch(syncExecutor, parallelExecutor, futures, progressBar, last, nextModLoadingStage(), null)); - - final CompletableFuture preSyncTaskCF = preSyncTask.apply(syncExecutor); - final CompletableFuture eventDispatchCF = ModList.gather(futures).thenCompose(ModList::completableFutureFromExceptionList); - final CompletableFuture postEventDispatchCF = preSyncTaskCF - .thenApplyAsync(n -> { - progressBar.label(progressBar.name() + ": dispatching "+name); - return null; - }, parallelExecutor) - .thenComposeAsync(n -> eventDispatchCF, parallelExecutor) - .thenApply(r -> { - postSyncTask.apply(syncExecutor); - return null; - }); - return this.finalActivityGenerator().apply(syncExecutor, postEventDispatchCF); + default CompletableFuture build( + final String name, + final Executor syncExecutor, + final Executor parallelExecutor, + final ProgressMeter progressBar, + final Function> preSyncTask, + final Function> postSyncTask + ) { + return ModStateTransitionHelper.build(this, name, syncExecutor, parallelExecutor, progressBar, preSyncTask, postSyncTask); } default BiFunction nextModLoadingStage() { return ModLoadingStage::nextState; } - private - EventGenerator addCompletableFutureTaskForModDispatch(final Executor syncExecutor, - final Executor parallelExecutor, - final List> completableFutures, - final ProgressMeter progressBar, - final EventGenerator eventGenerator, - final BiFunction nextState, - final EventGenerator nextGenerator) { - final Executor selectedExecutor = threadSelector().apply(syncExecutor, parallelExecutor); - - var preDispatchHook = (BiFunction, CompletableFuture>) preDispatchHook(); - completableFutures.add(preDispatchHook.apply(selectedExecutor, eventGenerator)); - - completableFutures.add(ModList.get().futureVisitor(eventGenerator, progressBar, nextState).apply(threadSelector().apply(syncExecutor, parallelExecutor))); - - var postDispatchHook = (BiFunction, CompletableFuture>) postDispatchHook(); - completableFutures.add(postDispatchHook.apply(selectedExecutor, eventGenerator)); - - return nextGenerator; + /** + * This used to allow you to fire multiple events during the transition. However, in doing so it would cause issues with the default + * ModContainer's event handlers causing issues such as mod classes being constructed multiple times. + */ + @Deprecated(since = "1.21.3", forRemoval = true) + default Supplier>> eventFunctionStream() { + return () -> Stream.ofNullable(eventFunction()); + } + + @Nullable + default EventGenerator eventFunction() { + return null; } - Supplier>> eventFunctionStream(); ThreadSelector threadSelector(); BiFunction, CompletableFuture> finalActivityGenerator(); - BiFunction, CompletableFuture> preDispatchHook(); - BiFunction, CompletableFuture> postDispatchHook(); + + /** + * I think this was meant as a way to do some things for each mod container beforge/after the transition had been sent to the container. + * However, the Future returned by this was never linked to the main transition future in any way. Which means that it was run + * in parallel and couldn't guarantee the state of the ModContainer. + *

+ * Plus all existing code that I could find returned a completedFuture, so I don't think anyone ever used this. + *

+ * If I were to add this back it would be a CompletableFuture wrap(ModContainer, CompletableFuture) + *

+ * Added magic value NULL_HOOK to allow me to optimize the futures list by ignoring the default value without making this method nullable. + */ + @Deprecated(since = "1.21.3", forRemoval = true) + default BiFunction, CompletableFuture> preDispatchHook() { return NULL_HOOK; } + @Deprecated(since = "1.21.3", forRemoval = true) + default BiFunction, CompletableFuture> postDispatchHook() { return NULL_HOOK; } interface EventGenerator extends Function { static EventGenerator fromFunction(Function fn) { return fn::apply; } } -} - -record NoopTransition() implements IModStateTransition { - - @Override - public Supplier>> eventFunctionStream() { - return Stream::of; - } - - @Override - public ThreadSelector threadSelector() { - return ThreadSelector.SYNC; - } - - @Override - public BiFunction, CompletableFuture> finalActivityGenerator() { - return (e, t) -> t.thenApplyAsync(Function.identity(), e); - } - - @Override - public BiFunction, CompletableFuture> preDispatchHook() { - return (t, f)-> CompletableFuture.completedFuture(null); - } - - @Override - public BiFunction, CompletableFuture> postDispatchHook() { - return (t, f)-> CompletableFuture.completedFuture(null); - } } \ No newline at end of file diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java index 0b07d18455..8bc24f8ab3 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModContainer.java @@ -9,18 +9,15 @@ import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.config.IConfigEvent; import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.event.IModBusEvent; -import net.minecraftforge.fml.loading.progress.ProgressMeter; import net.minecraftforge.forgespi.language.IModInfo; import java.util.EnumMap; -import java.util.HashMap; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.function.BiFunction; +import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Supplier; @@ -33,13 +30,8 @@ import java.util.function.Supplier; * a mechanism by which we can wrap actual mod code so that the loader and other * facilities can treat mods at arms length. *

- * - * @author cpw - * */ - -public abstract class ModContainer -{ +public abstract class ModContainer { protected final String modId; protected final String namespace; protected final IModInfo modInfo; @@ -48,11 +40,14 @@ public abstract class ModContainer protected final Map activityMap = new EnumMap<>(ModLoadingStage.class); protected final Map>, Supplier> extensionPoints = new IdentityHashMap<>(); protected final EnumMap configs = new EnumMap<>(ModConfig.Type.class); - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + final Set dependencies = new HashSet<>(); + /** + * If you want to handle the event, override {@link #dispatchConfigEvent(IConfigEvent)} + */ + @Deprecated(since = "1.21.3", forRemoval = true) protected Optional> configHandler = Optional.empty(); - public ModContainer(IModInfo info) - { + public ModContainer(IModInfo info) { this.modId = info.getModId(); // TODO: Currently not reading namespace from configuration.. this.namespace = this.modId; @@ -82,58 +77,35 @@ public abstract class ModContainer /** * Errored container state, used for filtering. Does nothing. */ - ModContainer() - { + ModContainer() { this.modLoadingStage = ModLoadingStage.ERROR; modId = "BROKEN"; namespace = "BROKEN"; modInfo = null; } + /** * @return the modid for this mod */ - public final String getModId() - { + public final String getModId() { return modId; } /** * @return the resource prefix for the mod */ - public final String getNamespace() - { + public final String getNamespace() { return namespace; } /** * @return The current loading stage for this mod */ - public ModLoadingStage getCurrentState() - { + public ModLoadingStage getCurrentState() { return modLoadingStage; } - public static CompletableFuture buildTransitionHandler( - final ModContainer target, - final IModStateTransition.EventGenerator eventGenerator, - final ProgressMeter progressBar, - final BiFunction stateChangeHandler, - final Executor executor) { - return CompletableFuture - .runAsync(() -> { - ModLoadingContext.get().setActiveContainer(target); - target.activityMap.getOrDefault(target.modLoadingStage, ()->{}).run(); - target.acceptEvent(eventGenerator.apply(target)); - }, executor) - .whenComplete((mc, exception) -> { - target.modLoadingStage = stateChangeHandler.apply(target.modLoadingStage, exception); - progressBar.increment(); - ModLoadingContext.get().setActiveContainer(null); - }); - } - - public IModInfo getModInfo() - { + public IModInfo getModInfo() { return modInfo; } @@ -142,8 +114,7 @@ public abstract class ModContainer return Optional.ofNullable((T)extensionPoints.getOrDefault(point,()-> null).get()); } - public > void registerExtensionPoint(Class> point, Supplier extension) - { + public > void registerExtensionPoint(Class> point, Supplier extension) { extensionPoints.put(point, extension); } diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModList.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModList.java index c091d78a9d..aeca9b5161 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModList.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModList.java @@ -5,9 +5,6 @@ package net.minecraftforge.fml; -import net.minecraftforge.eventbus.api.Event; -import net.minecraftforge.fml.event.IModBusEvent; -import net.minecraftforge.fml.loading.progress.ProgressMeter; import net.minecraftforge.forgespi.language.IModFileInfo; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.forgespi.language.ModFileScanData; @@ -15,21 +12,15 @@ import net.minecraftforge.fml.loading.moddiscovery.ModFile; import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; import net.minecraftforge.fml.loading.moddiscovery.ModInfo; import net.minecraftforge.forgespi.locating.IModFile; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Collection; +import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Executor; import java.util.function.BiConsumer; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -43,20 +34,21 @@ public class ModList { private static ModList INSTANCE; private final List modFiles; private final List sortedList; - private final Map fileById; + private final Map fileById; private List mods; private Map indexedMods; private List modFileScanData; private List sortedContainers; private ModList(final List modFiles, final List sortedList) { - this.modFiles = modFiles.stream().map(ModFile::getModFileInfo).map(ModFileInfo.class::cast).collect(Collectors.toList()); - this.sortedList = sortedList.stream(). - map(ModInfo.class::cast). - collect(Collectors.toList()); - this.fileById = this.modFiles.stream().map(IModFileInfo::getMods).flatMap(Collection::stream). - map(ModInfo.class::cast). - collect(Collectors.toMap(ModInfo::getModId, ModInfo::getOwningFile)); + this.modFiles = modFiles.stream().map(ModFile::getModFileInfo).toList(); + this.sortedList = sortedList.stream().map(IModInfo.class::cast).toList(); + var byId = new HashMap(); + for (var file : this.modFiles) { + for (var mod : file.getMods()) + byId.put(mod.getModId(), mod.getOwningFile()); + } + this.fileById = Collections.unmodifiableMap(byId); CrashReportCallables.registerCrashCallable("Mod List", this::crashReport); } @@ -95,54 +87,9 @@ public class ModList { return this.fileById.get(modid); } - Function> futureVisitor( - final IModStateTransition.EventGenerator eventGenerator, - final ProgressMeter progressBar, - final BiFunction stateChange) { - return executor -> gather( - this.mods.stream() - .map(mod -> ModContainer.buildTransitionHandler(mod, eventGenerator, progressBar, stateChange, executor)) - .toList() - ).thenComposeAsync(ModList::completableFutureFromExceptionList, executor); - } - static CompletionStage completableFutureFromExceptionList(List> t) { - if (t.stream().noneMatch(e->e.getValue()!=null)) { - return CompletableFuture.completedFuture(null); - } else { - final List throwables = t.stream().filter(e -> e.getValue() != null).map(Map.Entry::getValue).toList(); - CompletableFuture cf = new CompletableFuture<>(); - final RuntimeException accumulator = new RuntimeException(); - cf.completeExceptionally(accumulator); - for (Throwable exception : throwables) { - if (exception instanceof CompletionException) { - exception = exception.getCause(); - } - if (exception.getSuppressed().length != 0) { - for (Throwable throwable : exception.getSuppressed()) { - accumulator.addSuppressed(throwable); - } - } else { - accumulator.addSuppressed(exception); - } - } - return cf; - } - } - - static CompletableFuture>> gather(List> futures) { - List> list = new ArrayList<>(futures.size()); - CompletableFuture[] results = new CompletableFuture[futures.size()]; - for (var future : futures) { - int i = list.size(); - list.add(null); - results[i] = future.whenComplete((result, exception) -> list.set(i, new AbstractMap.SimpleImmutableEntry<>(result, exception))); - } - return CompletableFuture.allOf(results).handle((r, th)->null).thenApply(res -> list); - } - void setLoadedMods(final List modContainers) { this.mods = modContainers; - this.sortedContainers = modContainers.stream().sorted(Comparator.comparingInt(c->sortedList.indexOf(c.getModInfo()))).toList(); + this.sortedContainers = modContainers.stream().sorted(Comparator.comparingInt(c -> sortedList.indexOf(c.getModInfo()))).toList(); this.indexedMods = modContainers.stream().collect(Collectors.toMap(ModContainer::getModId, Function.identity())); } @@ -201,6 +148,10 @@ public class ModList { this.sortedContainers.forEach(containerConsumer); } + public List getLoadedMods() { + return this.sortedContainers; + } + public Stream applyForEachModContainer(Function function) { return indexedMods.values().stream().map(function); } diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java index 5d61beb0ad..e9851c44ea 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java @@ -6,7 +6,6 @@ package net.minecraftforge.fml; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Streams; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.event.IModBusEvent; import net.minecraftforge.fml.loading.FMLEnvironment; @@ -33,8 +32,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; - import static net.minecraftforge.fml.Logging.CORE; import static net.minecraftforge.fml.Logging.LOADING; @@ -74,8 +71,7 @@ import static net.minecraftforge.fml.Logging.LOADING; * and completes the mod loading sequence. * */ -public class ModLoader -{ +public class ModLoader { private static final Logger LOGGER = LogManager.getLogger(); private final LoadingModList loadingModList; @@ -84,17 +80,15 @@ public class ModLoader private final List loadingWarnings; private final ModStateManager stateManager; private static boolean loadingStateValid; - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private final Optional> statusConsumer = StartupNotificationManager.modLoaderConsumer(); + private final Consumer statusConsumer = StartupNotificationManager.modLoaderConsumer().orElse(msg -> {}); private final Set completedStates = new HashSet<>(); private ModList modList; - private ModLoader() - { + private ModLoader() { this.loadingModList = FMLLoader.getLoadingModList(); this.loadingExceptions = this.loadingModList.getErrors().stream() .flatMap(ModLoadingException::fromEarlyException) - .collect(Collectors.toList()); + .toList(); this.loadingWarnings = this.loadingModList.getBrokenFiles().stream() .map(file -> new ModLoadingWarning(null, ModLoadingStage.VALIDATE, InvalidModIdentifier.identifyJarProblem(file.getFilePath()).orElse("fml.modloading.brokenfile"), file.getFileName())) .collect(Collectors.toList()); @@ -156,7 +150,7 @@ public class ModLoader loadingModList.getMods()); if (!this.loadingExceptions.isEmpty()) { LOGGER.fatal(CORE, "Error during pre-loading phase", loadingExceptions.getFirst()); - statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); + statusConsumer.accept("ERROR DURING MOD LOADING"); modList.setLoadedMods(Collections.emptyList()); loadingStateValid = false; throw new LoadingFailedException(loadingExceptions); @@ -169,7 +163,7 @@ public class ModLoader if (!failedBounds.isEmpty()) { LOGGER.fatal(CORE, "Failed to validate feature bounds for mods: {}", failedBounds); - statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); + statusConsumer.accept("ERROR DURING MOD LOADING"); modList.setLoadedMods(Collections.emptyList()); loadingStateValid = false; throw new LoadingFailedException(failedBounds.stream() @@ -177,19 +171,42 @@ public class ModLoader .toList()); } - final List modContainers = loadingModList.getModFiles().stream() - .map(ModFileInfo::getFile) - .map(this::buildMods) - .flatMap(List::stream) - .toList(); + final var modContainers = new HashMap(); + for (var file : loadingModList.getModFiles()) { + var containers = this.buildMods(file.getFile()); + for (var container : containers) + modContainers.put(container.getModId(), container); + } + if (!loadingExceptions.isEmpty()) { LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.getFirst()); - statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); + statusConsumer.accept("ERROR DURING MOD LOADING"); modList.setLoadedMods(Collections.emptyList()); loadingStateValid = false; throw new LoadingFailedException(loadingExceptions); } - modList.setLoadedMods(modContainers); + + // Gather all dependencies, so we can make sure they have finished their transition events before we fire ours + for (var mod : modContainers.values()) { + for (var dep : mod.getModInfo().getDependencies()) { + var target = modContainers.get(dep.getModId()); + if (target == null) + continue; + + switch (dep.getOrdering()) { + case AFTER: + mod.dependencies.add(target); + break; + case BEFORE: + target.dependencies.add(mod); + break; + default: + break; + } + } + } + + modList.setLoadedMods(modContainers.values().stream().toList()); this.modList = modList; var stateList = stateManager.getStates(ModLoadingPhase.GATHER); var progress = StartupMessageManager.addProgressBar("Mod Gather", stateList.stream().mapToInt(mls -> mls.size().applyAsInt(this.modList)).sum()); @@ -214,7 +231,7 @@ public class ModLoader for (IModLoadingState mls : stateList) { dispatchAndHandleError(mls, syncExecutor, parallelExecutor, periodicTask, progress); } - statusConsumer.ifPresent(c->c.accept(String.format("Mod loading complete - %d mods loaded", this.modList.size()))); + statusConsumer.accept(String.format("Mod loading complete - %d mods loaded", this.modList.size())); progress.complete(); } @@ -243,10 +260,12 @@ public class ModLoader pb.complete(); syncExecutor.drive(ticker); } + private void waitForTransition(final IModLoadingState state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker, final CompletableFuture transition) { while (!transition.isDone()) { syncExecutor.drive(ticker); } + try { transition.join(); } catch (CompletionException e) { @@ -256,36 +275,38 @@ public class ModLoader .anyMatch(obj -> !(obj instanceof ModLoadingException)); if (hasNotModLoadingEx) { LOGGER.fatal("Encountered non-modloading exceptions!", e); - statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); + statusConsumer.accept("ERROR DURING MOD LOADING"); throw e; } final List modLoadingExceptions = Arrays.stream(t.getSuppressed()) .filter(ModLoadingException.class::isInstance) .map(ModLoadingException.class::cast) - .collect(Collectors.toList()); + .toList(); LOGGER.fatal(LOADING,"Failed to complete lifecycle event {}, {} errors found", state.name(), modLoadingExceptions.size()); - statusConsumer.ifPresent(c->c.accept("ERROR DURING MOD LOADING")); + statusConsumer.accept("ERROR DURING MOD LOADING"); throw new LoadingFailedException(modLoadingExceptions); } } - private List buildMods(final IModFile modFile) - { - final Map modInfoMap = modFile.getModFileInfo().getMods().stream().collect(Collectors.toMap(IModInfo::getModId, Function.identity())); + private List buildMods(final IModFile modFile) { + final Map modInfoMap = new HashMap<>(); + for (var mod : modFile.getModFileInfo().getMods()) + modInfoMap.put(mod.getModId(), mod); LOGGER.trace(LOADING, "ModContainer is {}", ModContainer.class.getClassLoader()); - final List containers = modFile.getScanResult().getTargets() - .entrySet() - .stream() - .map(e -> buildModContainerFromTOML(modFile, modInfoMap, e)) - .filter(Objects::nonNull) - .toList(); + final List containers = new ArrayList<>(); + for (var entry : modFile.getScanResult().getTargets().entrySet()) { + var container = buildModContainerFromTOML(modFile, modInfoMap, entry.getKey(), entry.getValue()); + if (container != null) + containers.add(container); + } + if (containers.size() != modInfoMap.size()) { var modIds = modInfoMap.values().stream().map(IModInfo::getModId).sorted().toList(); var containerIds = containers.stream().map(c -> c != null ? c.getModId() : "(null)").sorted().toList(); - LOGGER.fatal(LOADING,"File {} constructed {} mods: {}, but had {} mods specified: {}", + LOGGER.fatal(LOADING, "File {} constructed {} mods: {}, but had {} mods specified: {}", modFile.getFilePath(), containers.size(), containerIds, modInfoMap.size(), modIds); @@ -300,15 +321,15 @@ public class ModLoader loadingExceptions.add(new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingclasses", null, modFile.getFilePath())); } + // remove errored mod containers return containers.stream().filter(mc -> mc.modLoadingStage != ModLoadingStage.ERROR).toList(); } - private ModContainer buildModContainerFromTOML(final IModFile modFile, final Map modInfoMap, final Map.Entry idToProviderEntry) { + private ModContainer buildModContainerFromTOML(final IModFile modFile, final Map modInfoMap, + final String modId, final IModLanguageProvider.IModLanguageLoader languageLoader + ) { try { - final String modId = idToProviderEntry.getKey(); - final IModLanguageProvider.IModLanguageLoader languageLoader = idToProviderEntry.getValue(); - IModInfo info = modInfoMap.get(modId); // throw a missing metadata error if there is no matching modid in the modInfoMap from the mods.toml file if (info == null) @@ -351,6 +372,7 @@ public class ModLoader } ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e)); } + public T postEventWithReturn(T e) { if (!loadingStateValid) { LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName()); @@ -359,9 +381,15 @@ public class ModLoader ModList.get().forEachModInOrder(mc -> mc.acceptEvent(e)); return e; } + + @SuppressWarnings("removal") public void postEventWrapContainerInModOrder(T event) { - postEventWithWrapInModOrder(event, (mc, e) -> ModLoadingContext.get().setActiveContainer(mc), (mc, e) -> ModLoadingContext.get().setActiveContainer(null)); + postEventWithWrapInModOrder(event, + (mc, e) -> ModLoadingContext.get().setActiveContainer(mc), + (mc, e) -> ModLoadingContext.get().setActiveContainer(null) + ); } + public void postEventWithWrapInModOrder(T e, BiConsumer pre, BiConsumer post) { if (!loadingStateValid) { LOGGER.error("Cowardly refusing to send event {} to a broken mod state", e.getClass().getName()); @@ -374,13 +402,11 @@ public class ModLoader }); } - public List getWarnings() - { + public List getWarnings() { return ImmutableList.copyOf(this.loadingWarnings); } - public void addWarning(ModLoadingWarning warning) - { + public void addWarning(ModLoadingWarning warning) { this.loadingWarnings.add(warning); } diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoadingState.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoadingState.java index ddb7d94fab..96917e5992 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoadingState.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoadingState.java @@ -26,19 +26,23 @@ import java.util.function.ToIntFunction; * @param inlineRunnable an optional runnable, which runs before starting the transition from this state to the next * @param transition optional state transition information */ -public record ModLoadingState(String name, String previous, - Function message, - ToIntFunction size, - ModLoadingPhase phase, - Optional> inlineRunnable, - Optional transition) implements IModLoadingState { +public record ModLoadingState( + String name, + String previous, + Function message, + ToIntFunction size, + ModLoadingPhase phase, + Optional> inlineRunnable, + Optional transition +) implements IModLoadingState { @Override - public - Optional> buildTransition(final Executor syncExecutor, - final Executor parallelExecutor, - final ProgressMeter progressBar, - final Function> preSyncTask, - final Function> postSyncTask) { + public Optional> buildTransition( + final Executor syncExecutor, + final Executor parallelExecutor, + final ProgressMeter progressBar, + final Function> preSyncTask, + final Function> postSyncTask + ) { var transition = this.transition.orElse(null); return transition == null ? Optional.empty() @@ -52,9 +56,12 @@ public record ModLoadingState(String name, String previous, * @param name the name of the state * @param previous the name of the immediately previous state to this state * @param phase the mod loading phase the state belongs to + * + * @deprecated Use the builder */ + @Deprecated(since = "1.21.3", forRemoval = true) public static ModLoadingState empty(final String name, final String previous, final ModLoadingPhase phase) { - return new ModLoadingState(name, previous, ml -> "", f->0, phase, Optional.empty(), Optional.empty()); + return of(name, phase).after(previous).empty(); } /** @@ -65,11 +72,13 @@ public record ModLoadingState(String name, String previous, * @param previous the name of the immediately previous state to this state * @param phase the mod loading phase the state belongs to * @param transition the state transition information - * @return a mod loading state with state transition information and a default message + * + * @deprecated Use the builder */ + @Deprecated(since = "1.21.3", forRemoval = true) public static ModLoadingState withTransition(final String name, final String previous, final ModLoadingPhase phase, final IModStateTransition transition) { - return new ModLoadingState(name, previous, ml -> "Processing transition " + name, ModList::size, phase, Optional.empty(), Optional.of(transition)); + return of(name, phase).after(previous).withTransition(transition); } /** @@ -81,11 +90,14 @@ public record ModLoadingState(String name, String previous, * @param phase the mod loading phase the state belongs to * @param transition the state transition information * @return a mod loading state with state transition information and a custom message + * + * @deprecated Use the builder */ + @Deprecated(since = "1.21.3", forRemoval = true) public static ModLoadingState withTransition(final String name, final String previous, final Function message, final ModLoadingPhase phase, final IModStateTransition transition) { - return new ModLoadingState(name, previous, message, ModList::size, phase, Optional.empty(), Optional.of(transition)); + return of(name, phase).after(previous).message(message).withTransition(transition); } /** @@ -97,9 +109,59 @@ public record ModLoadingState(String name, String previous, * @param phase the mod loading phase the state belongs to * @param inline an optional runnable, which runs before starting the transition from this state to the next * @return a mod loading state with an inline runnable and default message + * + * @deprecated Use the builder */ + @Deprecated(since = "1.21.3", forRemoval = true) public static ModLoadingState withInline(final String name, final String previous, final ModLoadingPhase phase, final Consumer inline) { - return new ModLoadingState(name, previous, ml -> "Processing work " + name, ml->0, phase, Optional.of(inline), Optional.empty()); + return of(name, phase).after(previous).withInline(inline); + } + + public static Builder of(final String name, final ModLoadingPhase phase) { + return new Builder(name, phase); + } + + public static class Builder { + private final String name; + private final ModLoadingPhase phase; + private String after; + private Function message = null; + private ToIntFunction size = null; + + private Builder(final String name, final ModLoadingPhase phase) { + this.name = name; + this.phase = phase; + } + + public Builder after(final ModLoadingState value) { return after(value.name()); } + public Builder after(final String value) { + this.after = value; + return this; + } + + public Builder message(final String value) { return message(ml -> value); } + public Builder message(final Function value) { + this.message = value; + return this; + } + + public Builder size(final int value) { return size(ml -> value); } + public Builder size(final ToIntFunction value) { + this.size = value; + return this; + } + + public ModLoadingState empty() { + return new ModLoadingState(name, after, message != null ? message : ml -> "", size != null ? size : ml -> 0, phase, Optional.empty(), Optional.empty()); + } + + public ModLoadingState withTransition(final IModStateTransition transition) { + return new ModLoadingState(name, after, message != null ? message : ml -> "Processing transition " + name, size != null ? size : ModList::size, phase, Optional.empty(), Optional.of(transition)); + } + + public ModLoadingState withInline(final Consumer inline) { + return new ModLoadingState(name, after, message != null ? message : ml -> "Processing work " + name, size != null ? size : ml -> 0, phase, Optional.of(inline), Optional.empty()); + } } } \ No newline at end of file diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModStateTransitionHelper.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModStateTransitionHelper.java new file mode 100644 index 0000000000..4be44759fc --- /dev/null +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModStateTransitionHelper.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.jetbrains.annotations.ApiStatus; + +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.fml.IModStateTransition.EventGenerator; +import net.minecraftforge.fml.event.IModBusEvent; +import net.minecraftforge.fml.loading.progress.ProgressMeter; + +@ApiStatus.Internal +class ModStateTransitionHelper { + static final IModStateTransition NOOP = new NoopTransition(); + record NoopTransition() implements IModStateTransition { + @Override + public ThreadSelector threadSelector() { + return ThreadSelector.SYNC; + } + + @Override + public BiFunction, CompletableFuture> finalActivityGenerator() { + return (e, t) -> t.thenApplyAsync(Function.identity(), e); + } + } + + static CompletionStage completableFutureFromExceptionList(List> t) { + if (t.stream().noneMatch(e -> e.exception() != null)) { + return CompletableFuture.completedFuture(null); + } else { + final var throwables = t.stream().map(FutureResult::exception).filter(e -> e != null).toList(); + CompletableFuture cf = new CompletableFuture<>(); + final RuntimeException accumulator = new RuntimeException(); + cf.completeExceptionally(accumulator); + for (Throwable exception : throwables) { + if (exception instanceof CompletionException) { + exception = exception.getCause(); + } + if (exception.getSuppressed().length != 0) { + for (Throwable throwable : exception.getSuppressed()) { + accumulator.addSuppressed(throwable); + } + } else { + accumulator.addSuppressed(exception); + } + } + return cf; + } + } + + record FutureResult(V value, Throwable exception){} + + static CompletableFuture>> gather(Collection> futures) { + var list = new ArrayList>(futures.size()); + var results = new CompletableFuture[futures.size()]; + + for (var future : futures) { + int i = list.size(); + list.add(null); + results[i] = future.whenComplete((result, exception) -> list.set(i, new FutureResult<>(result, exception))); + } + + return CompletableFuture.allOf(results).handle((r, th)->null).thenApply(res -> list); + } + + private static void addCompletableFutureTaskForModDispatch( + final IModStateTransition transition, + final Executor executor, + final List> completableFutures, + final ProgressMeter progressBar, + final EventGenerator eventGenerator, + final BiFunction nextState + ) { + + @SuppressWarnings("removal") + var preDispatchHook = getHook(transition.preDispatchHook(), executor, eventGenerator); + if (preDispatchHook != null) + completableFutures.add(preDispatchHook); + + var modFutures = new LinkedHashMap>(); + for (var mod : ModList.get().getLoadedMods()) { + + CompletableFuture parent = null; + if (mod.dependencies.isEmpty()) { + parent = CompletableFuture.allOf(); + } else { + var deps = new CompletableFuture[mod.dependencies.size()]; + int idx = 0; + for (var depContainer : mod.dependencies) { + var future = modFutures.get(depContainer.getModId()); + if (future == null) + throw new IllegalStateException("Could not find dependency future " + depContainer.getModId() + " for " + mod.getModId()); + deps[idx++] = future; + } + parent = CompletableFuture.allOf(deps); + } + + @SuppressWarnings("removal") + var dispatch = parent + .thenRunAsync(() -> { + ModLoadingContext.get().setActiveContainer(mod); + var handler = mod.activityMap.get(mod.modLoadingStage); + if (handler != null) + handler.run(); + mod.acceptEvent(eventGenerator.apply(mod)); + }, executor) + .whenComplete((mc, exception) -> { + mod.modLoadingStage = nextState.apply(mod.modLoadingStage, exception); + progressBar.increment(); + ModLoadingContext.get().setActiveContainer(null); + }); + + modFutures.put(mod.getModId(), dispatch); + } + + var dispatch = gather(modFutures.values()).thenComposeAsync(ModStateTransitionHelper::completableFutureFromExceptionList, executor); + completableFutures.add(dispatch); + + @SuppressWarnings("removal") + var postDispatchHook = getHook(transition.preDispatchHook(), executor, eventGenerator); + if (postDispatchHook != null) + completableFutures.add(postDispatchHook); + } + + private static CompletableFuture getHook(BiFunction, CompletableFuture> hook, Executor executor, EventGenerator eventGenerator) { + if (hook == null || hook == IModStateTransition.NULL_HOOK) return null; + @SuppressWarnings("unchecked") + var hookTyped = (BiFunction, CompletableFuture>)hook; + return hookTyped.apply(executor, eventGenerator); + } + + static CompletableFuture build( + final IModStateTransition transition, + final String name, + final Executor syncExecutor, + final Executor parallelExecutor, + final ProgressMeter progressBar, + final Function> preSyncTask, + final Function> postSyncTask + ) { + List> futures = new ArrayList<>(); + final var executor = transition.threadSelector().apply(syncExecutor, parallelExecutor); + + @SuppressWarnings({ "removal", "unchecked" }) + var events = transition.eventFunctionStream().get().map(f -> (EventGenerator)f).toList(); + for (int x = 0; x < events.size(); x++) { + var gen = events.get(x); + BiFunction state = x == events.size() - 1 + ? transition.nextModLoadingStage() + : ModLoadingStage::currentState; + addCompletableFutureTaskForModDispatch(transition, executor, futures, progressBar, gen, state); + } + + final CompletableFuture preSyncTaskCF = preSyncTask.apply(syncExecutor); + final CompletableFuture eventDispatchCF = gather(futures).thenCompose(ModStateTransitionHelper::completableFutureFromExceptionList); + final CompletableFuture postEventDispatchCF = preSyncTaskCF + .thenApplyAsync(n -> { + progressBar.label(progressBar.name() + ": dispatching " + name); + return null; + }, parallelExecutor) + .thenComposeAsync(n -> eventDispatchCF, parallelExecutor) + .thenApply(r -> { + postSyncTask.apply(syncExecutor); + return null; + }); + return transition.finalActivityGenerator().apply(syncExecutor, postEventDispatchCF); + } +} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/LoadingModList.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/LoadingModList.java index 65b34a6cd4..498b0c0db4 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/LoadingModList.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/LoadingModList.java @@ -5,31 +5,25 @@ package net.minecraftforge.fml.loading; -import cpw.mods.modlauncher.api.LamdbaExceptionUtils; import net.minecraftforge.fml.loading.moddiscovery.BackgroundScanHandler; import net.minecraftforge.fml.loading.moddiscovery.ModFile; import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; import net.minecraftforge.fml.loading.moddiscovery.ModInfo; import net.minecraftforge.forgespi.locating.IModFile; -import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; -import java.util.Enumeration; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.stream.Collectors; /** * Master list of all mods in the loading context. This class cannot refer outside the * loading package */ -public class LoadingModList -{ +public class LoadingModList { private static LoadingModList INSTANCE; private final List modFiles; private final List sortedList; @@ -37,15 +31,14 @@ public class LoadingModList private final List preLoadErrors; private List brokenFiles; - private LoadingModList(final List modFiles, final List sortedList) - { + private LoadingModList(final List modFiles, final List sortedList) { this.modFiles = modFiles.stream() .map(ModFile::getModFileInfo) .map(ModFileInfo.class::cast) - .collect(Collectors.toList()); + .toList(); this.sortedList = sortedList.stream() .map(ModInfo.class::cast) - .collect(Collectors.toList()); + .toList(); this.fileById = this.modFiles.stream() .map(ModFileInfo::getMods) .flatMap(Collection::stream) @@ -54,21 +47,18 @@ public class LoadingModList this.preLoadErrors = new ArrayList<>(); } - public static LoadingModList of(List modFiles, List sortedList, final EarlyLoadingException earlyLoadingException) - { + public static LoadingModList of(List modFiles, List sortedList, final EarlyLoadingException earlyLoadingException) { INSTANCE = new LoadingModList(modFiles, sortedList); if (earlyLoadingException != null) - { INSTANCE.preLoadErrors.add(earlyLoadingException); - } return INSTANCE; } public static LoadingModList get() { return INSTANCE; } - public void addCoreMods() - { + + public void addCoreMods() { modFiles.stream() .map(ModFileInfo::getFile) .map(ModFile::getCoreMods) @@ -76,8 +66,7 @@ public class LoadingModList .forEach(FMLLoader.getCoreModProvider()::addCoreMod); } - public void addAccessTransformers() - { + public void addAccessTransformers() { for (ModFileInfo modFile : modFiles) { ModFile mod = modFile.getFile(); var at = mod.getAccessTransformer().orElse(null); @@ -87,21 +76,18 @@ public class LoadingModList } } - public void addForScanning(BackgroundScanHandler backgroundScanHandler) - { + public void addForScanning(BackgroundScanHandler backgroundScanHandler) { backgroundScanHandler.setLoadingModList(this); modFiles.stream() .map(ModFileInfo::getFile) .forEach(backgroundScanHandler::submitForScanning); } - public List getModFiles() - { + public List getModFiles() { return modFiles; } - public Path findResource(final String className) - { + public Path findResource(final String className) { for (ModFileInfo mf : modFiles) { final Path resource = mf.getFile().findResource(className); if (Files.exists(resource)) return resource; @@ -109,55 +95,11 @@ public class LoadingModList return null; } - public Enumeration findAllURLsForResource(final String resName) { - final String resourceName; - // strip a leading slash - if (resName.startsWith("/")) { - resourceName = resName.substring(1); - } else { - resourceName = resName; - } - return new Enumeration() { - private final Iterator modFileIterator = modFiles.iterator(); - private URL next; - @Override - public boolean hasMoreElements() { - if (next!=null) return true; - next = findNextURL(); - return next != null; - } - - @Override - public URL nextElement() { - if (next == null) { - next = findNextURL(); - if (next == null) throw new NoSuchElementException(); - } - URL result = next; - next = null; - return result; - } - - private URL findNextURL() { - while (modFileIterator.hasNext()) { - final ModFileInfo next = modFileIterator.next(); - final Path resource = next.getFile().findResource(resourceName); - if (Files.exists(resource)) { - return LamdbaExceptionUtils.uncheck(()->new URL("modjar://" + next.getMods().get(0).getModId() + "/" + resourceName)); - } - } - return null; - } - }; - } - - public ModFileInfo getModFileById(String modid) - { + public ModFileInfo getModFileById(String modid) { return this.fileById.get(modid); } - public List getMods() - { + public List getMods() { return this.sortedList; } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/ModSorter.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/ModSorter.java index 5ae65c60f6..9054865acc 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/ModSorter.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/ModSorter.java @@ -8,7 +8,7 @@ package net.minecraftforge.fml.loading; import com.google.common.graph.GraphBuilder; import com.google.common.graph.MutableGraph; import com.mojang.logging.LogUtils; -import net.minecraftforge.forgespi.language.IModInfo; +import net.minecraftforge.forgespi.language.IModInfo.ModVersion; import net.minecraftforge.fml.loading.EarlyLoadingException.ExceptionData; import net.minecraftforge.fml.loading.moddiscovery.ModFile; import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; @@ -20,235 +20,250 @@ import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.slf4j.Logger; import java.util.ArrayList; -import java.util.Collections; +import java.util.Collection; import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; -import static java.util.stream.Collectors.*; import static net.minecraftforge.fml.loading.LogMarkers.LOADING; -public class ModSorter -{ +public class ModSorter { private static final Logger LOGGER = LogUtils.getLogger(); - private final UniqueModListBuilder uniqueModListBuilder; - private List modFiles; - private List sortedList; - private Map modIdNameLookup; - private List systemMods; - private ModSorter(final List modFiles) - { - this.uniqueModListBuilder = new UniqueModListBuilder(modFiles); - } + private record State(List files, List mods) {} + + private ModSorter() {} + + @SuppressWarnings("removal") + public static LoadingModList sort(List mods, final List errors) { + State systemMods = detectSystemMods(mods); + List modFiles; - public static LoadingModList sort(List mods, final List errors) - { - final ModSorter ms = new ModSorter(mods); try { - ms.buildUniqueList(); + modFiles = new UniqueModListBuilder(mods).buildUniqueList().modFiles(); } catch (EarlyLoadingException e) { // We cannot build any list with duped mods. We have to abort immediately and report it // Note this will never actually throw an error because the duplicate checks are done in ModDiscovererer before we get to this phase // So all this is really doing is wasting time. // But i'm leaving it here until I rewrite all of cpw's mod loading code because its such a clusterfuck. - return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf->(ModInfo)mf.getModInfos().get(0)).toList(), e); + return LoadingModList.of(systemMods.files(), systemMods.mods(), e); } + + var named = new HashMap(); + for (var file : modFiles) { + for (var info : file.getModInfos()) + named.put(info.getModId(), (ModInfo)info); + } + // try and validate dependencies - final List failedList = Stream.concat(ms.verifyDependencyVersions().stream(), errors.stream()).toList(); + final List failedList = Stream.concat(verifyDependencyVersions(modFiles).stream(), errors.stream()).toList(); + // if we miss one or the other, we abort now if (!failedList.isEmpty()) { - return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf->(ModInfo)mf.getModInfos().get(0)).toList(), new EarlyLoadingException("failure to validate mod list", null, failedList)); + return LoadingModList.of(systemMods.files(), systemMods.mods(), new EarlyLoadingException("failure to validate mod list", null, failedList)); } else { // Otherwise, lets try and sort the modlist and proceed - EarlyLoadingException earlyLoadingException = null; try { - ms.sort(); + var sorted = sort(modFiles, named); + return LoadingModList.of(sorted.files(), sorted.mods(), null); } catch (EarlyLoadingException e) { - earlyLoadingException = e; + // The only exception that can happen here is a cyclic exception, but fall back to system mods so we can display the nice screen. + return LoadingModList.of(systemMods.files(), systemMods.mods(), e); } - return LoadingModList.of(ms.modFiles, ms.sortedList, earlyLoadingException); } } - private void sort() - { - // lambdas are identity based, so sorting them is impossible unless you hold reference to them - final MutableGraph graph = GraphBuilder.directed().build(); - AtomicInteger counter = new AtomicInteger(); - Map infos = modFiles.stream() - .map(ModFile::getModFileInfo) - .filter(ModFileInfo.class::isInstance) - .map(ModFileInfo.class::cast) - .collect(toMap(Function.identity(), e -> counter.incrementAndGet())); - infos.keySet().forEach(graph::addNode); - modFiles.stream() - .map(ModFile::getModInfos) - .flatMap(List::stream) - .map(IModInfo::getDependencies) - .flatMap(List::stream) - .forEach(dep -> addDependency(graph, dep)); + private static State sort(final List modFiles, final Map named) { + final MutableGraph graph = GraphBuilder.directed().build(); - final List sorted; - try - { - sorted = TopologicalSort.topologicalSort(graph, Comparator.comparing(infos::get)); - } - catch (CyclePresentException e) - { - Set> cycles = e.getCycles(); - if (LOGGER.isErrorEnabled(LOADING)) - { - LOGGER.error(LOADING, "Mod Sorting failed.\nDetected Cycles: {}\n", cycles); + int counter = 0; + var infos = new HashMap(); + for (var file : modFiles) { + if (file.getModFileInfo() instanceof ModFileInfo info) { + for (var imod : info.getMods()) { + var mod = (ModInfo)imod; + infos.put(mod, counter++); + graph.addNode(mod); + } } - var dataList = cycles.stream() - .flatMap(Set::stream) - .mapMulti((mf,c)->mf.getMods().forEach(c)) - .map(IModInfo::getModId) - .map(list -> new ExceptionData("fml.modloading.cycle", list)) - .toList(); + } + + for (var file : modFiles) { + for (var info : file.getModInfos()) { + for (var dep : info.getDependencies()) { + // Ordering isn't effected by sides, should it be? + //if (!dep.getSide().isCorrectSide()) + // continue; + + var target = named.get(dep.getModId()); + + // soft dep that doesn't exist. No edge required. + if (target == null) + continue; + + var self = (ModInfo)dep.getOwner(); + + switch (dep.getOrdering()) { + case BEFORE -> graph.putEdge(self, target); + case AFTER -> graph.putEdge(target, self); + default -> {} + } + } + } + } + + final List sorted; + try { + sorted = TopologicalSort.topologicalSort(graph, Comparator.comparing(infos::get)); + } catch (CyclePresentException e) { + Set> cycles = e.getCycles(); + var buf = new StringBuilder(); + buf.append("Mod Sorting failed - Detected Cycles: \n"); + + var dataList = new ArrayList(); + for (var cycle : cycles) { + buf.append("\tCycle:\n"); + for (var mod : cycle) { + var modDeps = new StringBuilder() + .append(mod.getModId()) + .append(' ') + .append(mod.getDependencies().stream() + .filter(v -> cycle.stream().anyMatch(m -> m.getModId().equals(v.getModId()))) + .map(dep -> dep.getOrdering().name() + " " + dep.getModId()) + .collect(Collectors.joining(", ")) + ); + dataList.add(new ExceptionData("fml.modloading.cycle", modDeps.toString())); + buf.append("\t\tMod: ").append(modDeps.toString()).append('\n'); + } + } + + LOGGER.error(LOADING, buf.toString()); + throw new EarlyLoadingException("Sorting error", e, dataList); } - this.sortedList = sorted.stream() - .map(ModFileInfo::getMods) - .flatMap(List::stream) - .map(ModInfo.class::cast) - .collect(toList()); - this.modFiles = sorted.stream() - .map(ModFileInfo::getFile) - .collect(toList()); - } - private void addDependency(MutableGraph topoGraph, IModInfo.ModVersion dep) - { - final IModInfo targetModInfo = modIdNameLookup.get(dep.getModId()); - // soft dep that doesn't exist. Just return. No edge required. - if (targetModInfo == null || !(targetModInfo.getOwningFile() instanceof final ModFileInfo target)) return; + var files = new LinkedHashSet(); + var list = new ArrayList(); - final ModFileInfo self = (ModFileInfo)dep.getOwner().getOwningFile(); - if (self == target) - return; // in case a jar has two mods that have dependencies between - - switch (dep.getOrdering()) { - case BEFORE -> topoGraph.putEdge(self, target); - case AFTER -> topoGraph.putEdge(target, self); - default -> {} + for (var mod : sorted) { + files.add(mod.getOwningFile().getFile()); + list.add(mod); } + + return new State(files.stream().toList(), list); } - private void buildUniqueList() - { - final UniqueModListBuilder.UniqueModListData uniqueModListData = uniqueModListBuilder.buildUniqueList(); - - this.modFiles = uniqueModListData.modFiles(); - - detectSystemMods(uniqueModListData.modFilesByFirstId()); - - modIdNameLookup = uniqueModListData.modFilesByFirstId().entrySet().stream() - .filter(e -> !e.getValue().get(0).getModInfos().isEmpty()) - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> e.getValue().get(0).getModInfos().get(0) - )); - } - - private void detectSystemMods(final Map> modFilesByFirstId) { + private static State detectSystemMods(final List modFiles) { // Capture system mods (ex. MC, Forge) here, so we can keep them for later var systemMods = List.of("minecraft", "forge"); LOGGER.debug("Configured system mods: {}", systemMods); - this.systemMods = new ArrayList<>(); + var mods = new ArrayList(); + var files = new ArrayList(); for (var systemMod : systemMods) { - var container = modFilesByFirstId.get(systemMod); - if (container != null && !container.isEmpty()) { - LOGGER.debug("Found system mod: {}", systemMod); - this.systemMods.add(container.getFirst()); - } else { + var mod = findMod(modFiles, systemMod); + if (mod == null) throw new IllegalStateException("Failed to find system mod: " + systemMod); - } + + LOGGER.debug("Found system mod: {}", systemMod); + mods.add(mod.info()); + files.add(mod.file()); } + + return new State(files, mods); } - private List verifyDependencyVersions() - { - final var modVersions = modFiles.stream() - .map(ModFile::getModInfos) - .flatMap(List::stream) - .collect(toMap(IModInfo::getModId, IModInfo::getVersion)); + private record ModPair(ModFile file, ModInfo info) {} + private static ModPair findMod(final List modFiles, String name) { + for (var file : modFiles) { + for (var mod : file.getModFileInfo().getMods()) { + if (name.equals(mod.getModId())) + return new ModPair(file, (ModInfo)mod); + } + } + return null; + } - final var modVersionDependencies = modFiles.stream() - .map(ModFile::getModInfos) - .flatMap(List::stream) - .collect(groupingBy(Function.identity(), flatMapping(e -> e.getDependencies().stream(), toList()))); + private static List verifyDependencyVersions(final List files) { + final var modVersions = new HashMap(); + final var modRequirements = new HashSet(); + int mandatoryRequired = 0; - final var modRequirements = modVersionDependencies.values().stream() - .flatMap(List::stream) - .filter(mv -> mv.getSide().isCorrectSide()) - .collect(toSet()); + for (var file : files) { + for (var info : file.getModInfos()) { + modVersions.put(info.getModId(), info.getVersion()); + for (var dep : info.getDependencies()) { + if (dep.getSide().isCorrectSide()) { + if (modRequirements.add(dep) && dep.isMandatory()) + mandatoryRequired++; + } + } + } + } - final long mandatoryRequired = modRequirements.stream().filter(IModInfo.ModVersion::isMandatory).count(); LOGGER.debug(LOADING, "Found {} mod requirements ({} mandatory, {} optional)", modRequirements.size(), mandatoryRequired, modRequirements.size() - mandatoryRequired); - final var missingVersions = modRequirements.stream() - .filter(mv -> (mv.isMandatory() || modVersions.containsKey(mv.getModId())) && !modVersionContained(mv, modVersions)) - .collect(toSet()); - final long mandatoryMissing = missingVersions.stream().filter(IModInfo.ModVersion::isMandatory).count(); - LOGGER.debug(LOADING, "Found {} mod requirements missing ({} mandatory, {} optional)", missingVersions.size(), mandatoryMissing, missingVersions.size() - mandatoryMissing); - if (!missingVersions.isEmpty()) { - if (mandatoryMissing > 0) { - LOGGER.error( - LOADING, - "Missing or unsupported mandatory dependencies:\n{}", - missingVersions.stream() - .filter(IModInfo.ModVersion::isMandatory) - .map(ver -> formatDependencyError(ver, modVersions)) - .collect(Collectors.joining("\n")) - ); - } - if (missingVersions.size() - mandatoryMissing > 0) { - LOGGER.error( - LOADING, - "Unsupported installed optional dependencies:\n{}", - missingVersions.stream() - .filter(ver -> !ver.isMandatory()) - .map(ver -> formatDependencyError(ver, modVersions)) - .collect(Collectors.joining("\n")) - ); - } + final var missingMandatory = new HashSet(); + final var missingOptional = new HashSet(); - return missingVersions.stream() - .map(mv -> new ExceptionData(mv.isMandatory() ? "fml.modloading.missingdependency" : "fml.modloading.missingdependency.optional", - mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), - modVersions.getOrDefault(mv.getModId(), new DefaultArtifactVersion("null")))) - .toList(); + for (var dep : modRequirements) { + var modId = dep.getModId(); + var existing = modVersions.get(modId); + + if (!dep.isMandatory() && existing == null) + continue; + + var range = dep.getVersionRange(); + if (existing != null && (range.containsVersion(existing) || "0.0NONE".equals(existing.toString()))) + continue; + + if (!VersionSupportMatrix.testVersionSupportMatrix(range, modId, "mod")) + (dep.isMandatory() ? missingMandatory : missingOptional).add(dep); } - return Collections.emptyList(); + + + LOGGER.debug(LOADING, "Found {} mod requirements missing ({} mandatory, {} optional)", missingMandatory.size() + missingOptional.size(), missingMandatory.size(), missingOptional.size()); + + var ret = new ArrayList(); + + if (!missingMandatory.isEmpty()) { + LOGGER.error(LOADING, "Missing or unsupported mandatory dependencies:\n{}", formatDependencyError(missingMandatory, modVersions)); + for (var mv : missingMandatory) + ret.add(data(mv, modVersions, "fml.modloading.missingdependency")); + } + + if (!missingOptional.isEmpty()) { + LOGGER.error(LOADING, "Unsupported installed optional dependencies:\n{}", formatDependencyError(missingMandatory, modVersions)); + for (var mv : missingMandatory) + ret.add(data(mv, modVersions, "fml.modloading.missingdependency.optional")); + } + + return ret; } - private static String formatDependencyError(IModInfo.ModVersion dependency, Map modVersions) - { - ArtifactVersion installed = modVersions.get(dependency.getModId()); - return String.format( + private static String formatDependencyError(Collection missing, Map modVersions) { + var ret = new ArrayList(); + for (var dep : missing) { + var installed = modVersions.get(dep.getModId()); + ret.add(String.format( "\tMod ID: '%s', Requested by: '%s', Expected range: '%s', Actual version: '%s'", - dependency.getModId(), - dependency.getOwner().getModId(), - dependency.getVersionRange(), + dep.getModId(), + dep.getOwner().getModId(), + dep.getVersionRange(), installed != null ? installed.toString() : "[MISSING]" - ); + )); + } + return String.join("\n", ret); } - private static boolean modVersionContained(IModInfo.ModVersion mv, Map modVersions) { - var modId = mv.getModId(); - var range = mv.getVersionRange(); - if (modVersions.containsKey(modId) - && (range.containsVersion(modVersions.get(modId)) || modVersions.get(modId).toString().equals("0.0NONE"))) - return true; - - return VersionSupportMatrix.testVersionSupportMatrix(mv.getVersionRange(), mv.getModId(), "mod"); + private static final ArtifactVersion NULL_VERSION = new DefaultArtifactVersion("null"); + private static ExceptionData data(ModVersion mv, Map modVersions, String key) { + return new ExceptionData(key, mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), modVersions.getOrDefault(mv.getModId(), NULL_VERSION)); } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/UniqueModListBuilder.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/UniqueModListBuilder.java index b2486c1bee..c5a75899ea 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/UniqueModListBuilder.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/UniqueModListBuilder.java @@ -9,6 +9,7 @@ import com.mojang.logging.LogUtils; import net.minecraftforge.fml.loading.moddiscovery.ModFile; import net.minecraftforge.forgespi.language.IModInfo; import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.jetbrains.annotations.ApiStatus; import org.slf4j.Logger; import java.util.ArrayList; @@ -23,16 +24,18 @@ import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.joining; import static net.minecraftforge.fml.loading.LogMarkers.LOADING; -public class UniqueModListBuilder -{ +@ApiStatus.Internal +@Deprecated(since = "1.21.3", forRemoval = true) // TODO: [FML][Loading] Convert to package private in 1.22 +public class UniqueModListBuilder { private final static Logger LOGGER = LogUtils.getLogger(); private final List modFiles; - public UniqueModListBuilder(final List modFiles) {this.modFiles = modFiles;} + public UniqueModListBuilder(final List modFiles) { + this.modFiles = modFiles; + } - public UniqueModListData buildUniqueList() - { + public UniqueModListData buildUniqueList() { List uniqueModList; List uniqueLibListWithVersion; @@ -139,5 +142,4 @@ public class UniqueModListBuilder } public record UniqueModListData(List modFiles, Map> modFilesByFirstId) {} - } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java index 0f805b284c..f098966a2c 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java @@ -68,6 +68,7 @@ public class ModDiscoverer { } } + @SuppressWarnings("removal") public ModValidator discoverMods() { LOGGER.debug(LogMarkers.SCAN,"Scanning for mods and other resources to load. We know {} ways to find mods", modLocatorList.size()); List loadedFiles = new ArrayList<>(); diff --git a/javafmllanguage/src/main/java/net/minecraftforge/fml/javafmlmod/FMLModContainer.java b/javafmllanguage/src/main/java/net/minecraftforge/fml/javafmlmod/FMLModContainer.java index a83bdb531b..2df30f64ad 100644 --- a/javafmllanguage/src/main/java/net/minecraftforge/fml/javafmlmod/FMLModContainer.java +++ b/javafmllanguage/src/main/java/net/minecraftforge/fml/javafmlmod/FMLModContainer.java @@ -13,6 +13,7 @@ import net.minecraftforge.eventbus.api.IEventListener; import net.minecraftforge.fml.ModContainer; import net.minecraftforge.fml.ModLoadingException; import net.minecraftforge.fml.ModLoadingStage; +import net.minecraftforge.fml.config.IConfigEvent; import net.minecraftforge.fml.event.IModBusEvent; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.forgespi.language.ModFileScanData; @@ -29,7 +30,6 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; -import java.util.Optional; import java.util.jar.Attributes; public class FMLModContainer extends ModContainer { @@ -47,7 +47,6 @@ public class FMLModContainer extends ModContainer { this.scanResults = modFileScanResults; activityMap.put(ModLoadingStage.CONSTRUCT, this::constructMod); this.eventBus = BusBuilder.builder().setExceptionHandler(FMLModContainer::onEventFailed).setTrackPhases(false).markerType(IModBusEvent.class).useModLauncher().build(); - this.configHandler = Optional.of(ce->this.eventBus.post(ce.self())); this.contextExtension = () -> context; try { var moduleName = info.getOwningFile().moduleName(); @@ -189,4 +188,14 @@ public class FMLModContainer extends ModContainer { throw new ModLoadingException(modInfo, modLoadingStage, "fml.modloading.errorduringevent", t); } } + + @Override + public void dispatchConfigEvent(IConfigEvent event) { + this.eventBus.post(event.self()); + } + + @Override + public String toString() { + return "FMLModContainer[" + this.getModInfo().getModId() + ", " + this.getClass().getName() + "]"; + } } diff --git a/src/main/java/net/minecraftforge/common/ForgeStatesProvider.java b/src/main/java/net/minecraftforge/common/ForgeStatesProvider.java index 9f1b1f6af4..3cd2196c4e 100644 --- a/src/main/java/net/minecraftforge/common/ForgeStatesProvider.java +++ b/src/main/java/net/minecraftforge/common/ForgeStatesProvider.java @@ -11,22 +11,39 @@ import net.minecraftforge.fml.IModLoadingState; import net.minecraftforge.fml.IModStateProvider; import net.minecraftforge.fml.ModLoadingPhase; import net.minecraftforge.fml.ModLoadingState; +import net.minecraftforge.fml.core.ModStateProvider; import net.minecraftforge.network.NetworkRegistry; import net.minecraftforge.registries.GameData; import net.minecraftforge.registries.ObjectHolderRegistry; import net.minecraftforge.registries.RegistryManager; public class ForgeStatesProvider implements IModStateProvider { - final ModLoadingState CREATE_REGISTRIES = ModLoadingState.withInline("CREATE_REGISTRIES", "CONSTRUCT", ModLoadingPhase.GATHER, ml -> RegistryManager.postNewRegistryEvent()); - final ModLoadingState OBJECT_HOLDERS = ModLoadingState.withInline("OBJECT_HOLDERS", "CREATE_REGISTRIES", ModLoadingPhase.GATHER, ml -> ObjectHolderRegistry.findObjectHolders()); - final ModLoadingState INJECT_CAPABILITIES = ModLoadingState.withInline("INJECT_CAPABILITIES", "OBJECT_HOLDERS", ModLoadingPhase.GATHER, CapabilityManager::injectCapabilities); - final ModLoadingState UNFREEZE = ModLoadingState.withInline("UNFREEZE_DATA", "INJECT_CAPABILITIES", ModLoadingPhase.GATHER, ml -> GameData.unfreezeData()); - final ModLoadingState LOAD_REGISTRIES = ModLoadingState.withInline("LOAD_REGISTRIES", "UNFREEZE_DATA", ModLoadingPhase.GATHER, ml -> GameData.postRegisterEvents()); - final ModLoadingState FREEZE = ModLoadingState.withInline("FREEZE_DATA", "COMPLETE", ModLoadingPhase.COMPLETE, ml -> GameData.freezeData()); - final ModLoadingState NETLOCK = ModLoadingState.withInline("NETWORK_LOCK", "FREEZE_DATA", ModLoadingPhase.COMPLETE, ml -> NetworkRegistry.lock()); + public static final ModLoadingState CREATE_REGISTRIES = gather("CREATE_REGISTRIES", ModStateProvider.CONSTRUCT, RegistryManager::postNewRegistryEvent); + public static final ModLoadingState OBJECT_HOLDERS = gather("OBJECT_HOLDERS", CREATE_REGISTRIES, ObjectHolderRegistry::findObjectHolders); + public static final ModLoadingState INJECT_CAPABILITIES = gather("INJECT_CAPABILITIES", OBJECT_HOLDERS).withInline(CapabilityManager::injectCapabilities); + public static final ModLoadingState UNFREEZE_DATA = gather("UNFREEZE_DATA", INJECT_CAPABILITIES, GameData::unfreezeData); + public static final ModLoadingState LOAD_REGISTRIES = gather("LOAD_REGISTRIES", UNFREEZE_DATA, GameData::postRegisterEvents); + public static final ModLoadingState FREEZE_DATA = complete("FREEZE_DATA", ModStateProvider.COMPLETE, GameData::freezeData); + public static final ModLoadingState NETWORK_LOCK = complete("NETWORK_LOCK", FREEZE_DATA, NetworkRegistry::lock); + + private static ModLoadingState.Builder gather(String name, ModLoadingState after) { + return ModLoadingState.of(name, ModLoadingPhase.GATHER).after(after); + } + + private static ModLoadingState gather(String name, ModLoadingState after, Runnable inline) { + return gather(name, after).withInline(ml -> inline.run()); + } + + private static ModLoadingState.Builder complete(String name, ModLoadingState after) { + return ModLoadingState.of(name, ModLoadingPhase.COMPLETE).after(after); + } + + private static ModLoadingState complete(String name, ModLoadingState after, Runnable inline) { + return complete(name, after).withInline(ml -> inline.run()); + } @Override public List getAllStates() { - return List.of(CREATE_REGISTRIES, OBJECT_HOLDERS, INJECT_CAPABILITIES, UNFREEZE, LOAD_REGISTRIES, FREEZE, NETLOCK); + return List.of(CREATE_REGISTRIES, OBJECT_HOLDERS, INJECT_CAPABILITIES, UNFREEZE_DATA, LOAD_REGISTRIES, FREEZE_DATA, NETWORK_LOCK); } } diff --git a/src/main/java/net/minecraftforge/fml/core/ModStateProvider.java b/src/main/java/net/minecraftforge/fml/core/ModStateProvider.java index 1be49f1fba..651fee7b50 100644 --- a/src/main/java/net/minecraftforge/fml/core/ModStateProvider.java +++ b/src/main/java/net/minecraftforge/fml/core/ModStateProvider.java @@ -34,15 +34,13 @@ public class ModStateProvider implements IModStateProvider { * * @see ModLoadingPhase#ERROR */ - final ModLoadingState ERROR = ModLoadingState.empty("ERROR", "", - ModLoadingPhase.ERROR); + public static final ModLoadingState ERROR = ModLoadingState.of("ERROR", ModLoadingPhase.ERROR).empty(); /** * First {@linkplain ModLoadingPhase#GATHER gathering state}, for the validation of the mod list. * TODO: figure out where this is used and why this exists instead of CONSTRUCT being the first state */ - private final ModLoadingState VALIDATE = ModLoadingState.empty("VALIDATE", "", - ModLoadingPhase.GATHER); + public static final ModLoadingState VALIDATE = ModLoadingState.of("VALIDATE", ModLoadingPhase.GATHER).empty(); /** * {@linkplain ModLoadingPhase#GATHER Gathering state} after {@linkplain #VALIDATE validation}, for the construction @@ -51,22 +49,20 @@ public class ModStateProvider implements IModStateProvider { * @see FMLConstructModEvent * @see ModLoadingStage#CONSTRUCT */ - final ModLoadingState CONSTRUCT = ModLoadingState.withTransition("CONSTRUCT", "VALIDATE", - ml -> "Constructing %d mods".formatted(ml.size()), - ModLoadingPhase.GATHER, - new ParallelTransition(ModLoadingStage.CONSTRUCT, FMLConstructModEvent.class)); + public static final ModLoadingState CONSTRUCT = ModLoadingState.of("CONSTRUCT", ModLoadingPhase.GATHER) + .after(VALIDATE) + .message(ml -> "Constructing %d mods".formatted(ml.size())) + .withTransition(new ParallelTransition(ModLoadingStage.CONSTRUCT, FMLConstructModEvent::new)); /** * First {@linkplain ModLoadingPhase#LOAD loading state}, for loading of the common and (if applicable) * {@linkplain Dist#CLIENT client-side} mod configurations. */ - private final ModLoadingState CONFIG_LOAD = ModLoadingState.withInline("CONFIG_LOAD", "", - ModLoadingPhase.LOAD, - ml -> { - DistExecutor.unsafeRunWhenOn(Dist.CLIENT, - () -> () -> ConfigTracker.INSTANCE.loadConfigs(ModConfig.Type.CLIENT, FMLPaths.CONFIGDIR.get())); - ConfigTracker.INSTANCE.loadConfigs(ModConfig.Type.COMMON, FMLPaths.CONFIGDIR.get()); - }); + public static final ModLoadingState CONFIG_LOAD = ModLoadingState.of("CONFIG_LOAD", ModLoadingPhase.LOAD) + .withInline(ml -> { + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> ConfigTracker.INSTANCE.loadConfigs(ModConfig.Type.CLIENT, FMLPaths.CONFIGDIR.get())); + ConfigTracker.INSTANCE.loadConfigs(ModConfig.Type.COMMON, FMLPaths.CONFIGDIR.get()); + }); /** * {@linkplain ModLoadingPhase#LOAD Loading state} after {@linkplain #CONFIG_LOAD configuration loading}, for @@ -75,9 +71,9 @@ public class ModStateProvider implements IModStateProvider { * @see FMLCommonSetupEvent * @see ModLoadingStage#COMMON_SETUP */ - private final ModLoadingState COMMON_SETUP = ModLoadingState.withTransition("COMMON_SETUP", "CONFIG_LOAD", - ModLoadingPhase.LOAD, - new ParallelTransition(ModLoadingStage.COMMON_SETUP, FMLCommonSetupEvent.class)); + public static final ModLoadingState COMMON_SETUP = ModLoadingState.of("COMMON_SETUP", ModLoadingPhase.LOAD) + .after(CONFIG_LOAD) + .withTransition(new ParallelTransition(ModLoadingStage.COMMON_SETUP, FMLCommonSetupEvent::new)); /** * {@linkplain ModLoadingPhase#LOAD Loading state} after {@linkplain #COMMON_SETUP common setup}, for side-specific @@ -87,10 +83,14 @@ public class ModStateProvider implements IModStateProvider { * @see FMLDedicatedServerSetupEvent * @see ModLoadingStage#SIDED_SETUP */ - private final ModLoadingState SIDED_SETUP = ModLoadingState.withTransition("SIDED_SETUP", "COMMON_SETUP", - ModLoadingPhase.LOAD, - new ParallelTransition(ModLoadingStage.SIDED_SETUP, - DistExecutor.unsafeRunForDist(()->()-> FMLClientSetupEvent.class, ()->()-> FMLDedicatedServerSetupEvent.class))); + public static final ModLoadingState SIDED_SETUP = ModLoadingState.of("SIDED_SETUP", ModLoadingPhase.LOAD) + .after(COMMON_SETUP) + .withTransition(new ParallelTransition(ModLoadingStage.SIDED_SETUP, + DistExecutor.unsafeRunForDist( + () -> () -> FMLClientSetupEvent::new, + () -> () -> FMLDedicatedServerSetupEvent::new + ) + )); /** * First {@linkplain ModLoadingPhase#COMPLETE completion state}, for enqueuing {@link net.minecraftforge.fml.InterModComms} @@ -99,9 +99,8 @@ public class ModStateProvider implements IModStateProvider { * @see InterModEnqueueEvent * @see ModLoadingStage#ENQUEUE_IMC */ - private final ModLoadingState ENQUEUE_IMC = ModLoadingState.withTransition("ENQUEUE_IMC", "", - ModLoadingPhase.COMPLETE, - new ParallelTransition(ModLoadingStage.ENQUEUE_IMC, InterModEnqueueEvent.class)); + public static final ModLoadingState ENQUEUE_IMC = ModLoadingState.of("ENQUEUE_IMC", ModLoadingPhase.COMPLETE) + .withTransition(new ParallelTransition(ModLoadingStage.ENQUEUE_IMC, InterModEnqueueEvent::new)); /** * {@linkplain ModLoadingPhase#COMPLETE Completion state} after {@linkplain #ENQUEUE_IMC}, for processing of messages @@ -110,9 +109,9 @@ public class ModStateProvider implements IModStateProvider { * @see InterModProcessEvent * @see ModLoadingStage#PROCESS_IMC */ - private final ModLoadingState PROCESS_IMC = ModLoadingState.withTransition("PROCESS_IMC", "ENQUEUE_IMC", - ModLoadingPhase.COMPLETE, - new ParallelTransition(ModLoadingStage.PROCESS_IMC, InterModProcessEvent.class)); + public static final ModLoadingState PROCESS_IMC = ModLoadingState.of("PROCESS_IMC", ModLoadingPhase.COMPLETE) + .after(ENQUEUE_IMC) + .withTransition(new ParallelTransition(ModLoadingStage.PROCESS_IMC, InterModProcessEvent::new)); /** * {@linkplain ModLoadingPhase#COMPLETE Completion state} after {@linkplain #PROCESS_IMC}, marking the completion @@ -121,18 +120,17 @@ public class ModStateProvider implements IModStateProvider { * @see FMLLoadCompleteEvent * @see ModLoadingStage#COMPLETE */ - private final ModLoadingState COMPLETE = ModLoadingState.withTransition("COMPLETE", "PROCESS_IMC", - ml -> "completing load of %d mods".formatted(ml.size()), - ModLoadingPhase.COMPLETE, - new ParallelTransition(ModLoadingStage.COMPLETE, FMLLoadCompleteEvent.class)); + public static final ModLoadingState COMPLETE = ModLoadingState.of("COMPLETE", ModLoadingPhase.COMPLETE) + .after(PROCESS_IMC) + .message(ml -> "completing load of %d mods".formatted(ml.size())) + .withTransition(new ParallelTransition(ModLoadingStage.COMPLETE, FMLLoadCompleteEvent::new)); /** * The marker state for the completion of the full mod loading process. * * @see ModLoadingStage#DONE */ - private final ModLoadingState DONE = ModLoadingState.empty("DONE", "", - ModLoadingPhase.DONE); + public static final ModLoadingState DONE = ModLoadingState.of("DONE", ModLoadingPhase.DONE).empty(); @Override public List getAllStates() { diff --git a/src/main/java/net/minecraftforge/fml/core/ParallelTransition.java b/src/main/java/net/minecraftforge/fml/core/ParallelTransition.java index 25b63e05cd..ecfc718c84 100644 --- a/src/main/java/net/minecraftforge/fml/core/ParallelTransition.java +++ b/src/main/java/net/minecraftforge/fml/core/ParallelTransition.java @@ -5,23 +5,23 @@ package net.minecraftforge.fml.core; -import cpw.mods.modlauncher.api.LamdbaExceptionUtils; +import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.IModStateTransition; import net.minecraftforge.fml.ModContainer; import net.minecraftforge.fml.ModLoadingStage; import net.minecraftforge.fml.ThreadSelector; +import net.minecraftforge.fml.event.IModBusEvent; import net.minecraftforge.fml.event.lifecycle.ParallelDispatchEvent; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.BiFunction; -import java.util.function.Supplier; -import java.util.stream.Stream; -record ParallelTransition(ModLoadingStage stage, Class event) implements IModStateTransition { +record ParallelTransition(ModLoadingStage stage, BiFunction event) implements IModStateTransition { + @SuppressWarnings("unchecked") @Override - public Supplier>> eventFunctionStream() { - return () -> Stream.of(IModStateTransition.EventGenerator.fromFunction(LamdbaExceptionUtils.rethrowFunction((ModContainer mc) -> event.getConstructor(ModContainer.class, ModLoadingStage.class).newInstance(mc, stage)))); + public EventGenerator eventFunction() { + return EventGenerator.fromFunction(mod -> (T)event.apply(mod, stage)); } @Override @@ -36,14 +36,4 @@ record ParallelTransition(ModLoadingStage stage, Class, CompletableFuture> preDispatchHook() { - return (t, f) -> CompletableFuture.completedFuture(null); - } - - @Override - public BiFunction, CompletableFuture> postDispatchHook() { - return (t, f) -> CompletableFuture.completedFuture(null); - } } diff --git a/src/main/java/net/minecraftforge/fml/event/lifecycle/FMLClientSetupEvent.java b/src/main/java/net/minecraftforge/fml/event/lifecycle/FMLClientSetupEvent.java index 8fe148d814..1376bd17c8 100644 --- a/src/main/java/net/minecraftforge/fml/event/lifecycle/FMLClientSetupEvent.java +++ b/src/main/java/net/minecraftforge/fml/event/lifecycle/FMLClientSetupEvent.java @@ -23,10 +23,8 @@ import net.minecraftforge.fml.ModLoadingStage; * * This is a parallel dispatch event. */ -public class FMLClientSetupEvent extends ParallelDispatchEvent -{ - public FMLClientSetupEvent(ModContainer container, ModLoadingStage stage) - { +public class FMLClientSetupEvent extends ParallelDispatchEvent { + public FMLClientSetupEvent(ModContainer container, ModLoadingStage stage) { super(container, stage); } } diff --git a/src/main/resources/assets/forge/lang/en_us.json b/src/main/resources/assets/forge/lang/en_us.json index f49962e808..2f52ab36f8 100644 --- a/src/main/resources/assets/forge/lang/en_us.json +++ b/src/main/resources/assets/forge/lang/en_us.json @@ -69,7 +69,7 @@ "fml.modloading.missingdependency.optional": "Mod \u00a7e{4}\u00a7r only supports \u00a73{3}\u00a7r \u00a7o{5,vr}\u00a7r\n\u00a77Currently, \u00a73{3}\u00a7r\u00a77 is \u00a7o{6}", "fml.dependencyloading.conflictingdependencies": "Some mods have requested conflicting versions of: \u00a76{3}\u00a7r. Requested by: \u00a7e{4}\u00a7r.", "fml.dependencyloading.mismatchedcontaineddependencies": "Some mods have agreed upon an acceptable version range for : \u00a76{3}\u00a7r, but no jar was provided which matched the range. Requested by: \u00a7e{4}\u00a7r.", - "fml.modloading.cycle": "Detected a mod dependency cycle: {0}", + "fml.modloading.cycle": "Detected a mod dependency cycle: {3}", "fml.modloading.failedtoprocesswork":"{0,modinfo,name} ({0,modinfo,id}) encountered an error processing deferred work\n\u00a77{2,exc,msg}", "fml.modloading.brokenfile": "File {2} is not a valid mod file", "fml.modloading.brokenfile.oldforge": "File {2} is for an older version of Forge and cannot be loaded", diff --git a/src/test/java/net/minecraftforge/debug/loading/sort/ModLoadSortingAfter.java b/src/test/java/net/minecraftforge/debug/loading/sort/ModLoadSortingAfter.java new file mode 100644 index 0000000000..9e9ffd1f82 --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/loading/sort/ModLoadSortingAfter.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.debug.loading.sort; + +import java.time.Duration; + +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.test.BaseTestMod; + +@Mod(ModLoadSortingAfter.MODID) +public class ModLoadSortingAfter extends BaseTestMod { + static final String MODID = "load_sort_after"; + protected static final Logger LOGGER = LogUtils.getLogger(); + static boolean hasInit = false; + + public ModLoadSortingAfter(FMLJavaModLoadingContext context) { + super(context); + try { + Thread.sleep(Duration.ofSeconds(1)); + } catch (InterruptedException e) { + sneak(e); + } + hasInit = true; + } +} diff --git a/src/test/java/net/minecraftforge/debug/loading/sort/ModLoadSortingBefore.java b/src/test/java/net/minecraftforge/debug/loading/sort/ModLoadSortingBefore.java new file mode 100644 index 0000000000..ad6492ea9e --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/loading/sort/ModLoadSortingBefore.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.debug.loading.sort; + +import java.time.Duration; + +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.test.BaseTestMod; + +@Mod(ModLoadSortingBefore.MODID) +public class ModLoadSortingBefore extends BaseTestMod { + static final String MODID = "load_sort_before"; + protected static final Logger LOGGER = LogUtils.getLogger(); + static boolean hasInit = false; + + public ModLoadSortingBefore(FMLJavaModLoadingContext context) { + super(context); + try { + Thread.sleep(Duration.ofSeconds(1)); + } catch (InterruptedException e) { + sneak(e); + } + hasInit = true; + } +} diff --git a/src/test/java/net/minecraftforge/debug/loading/sort/ModLoadSortingTest.java b/src/test/java/net/minecraftforge/debug/loading/sort/ModLoadSortingTest.java new file mode 100644 index 0000000000..c5ad22ceca --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/loading/sort/ModLoadSortingTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.debug.loading.sort; + +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.gametest.GameTestHolder; +import net.minecraftforge.test.BaseTestMod; + +@GameTestHolder("forge." + ModLoadSortingTest.MODID) +@Mod(ModLoadSortingTest.MODID) +public class ModLoadSortingTest extends BaseTestMod { + static final String MODID = "load_sort_test"; + + protected static final Logger LOGGER = LogUtils.getLogger(); + + private static boolean beforeHasInit; + private static boolean afterHasInit; + + public ModLoadSortingTest(FMLJavaModLoadingContext context) { + super(context); + afterHasInit = ModLoadSortingAfter.hasInit; + beforeHasInit = ModLoadSortingBefore.hasInit; + } + + @GameTest(template = "forge:empty3x3x3") + public static void ran_after_parent(GameTestHelper helper) { + helper.assertTrue(afterHasInit, "Mod constructor was fired before dependency finished"); + helper.succeed(); + } + + @GameTest(template = "forge:empty3x3x3") + public static void ran_before_child(GameTestHelper helper) { + helper.assertFalse(beforeHasInit, "Mod constructor was fired before dependency finished"); + helper.succeed(); + } +} diff --git a/src/test/java/net/minecraftforge/test/BaseTestMod.java b/src/test/java/net/minecraftforge/test/BaseTestMod.java index d8edb76296..89d936fdb9 100644 --- a/src/test/java/net/minecraftforge/test/BaseTestMod.java +++ b/src/test/java/net/minecraftforge/test/BaseTestMod.java @@ -85,7 +85,7 @@ public abstract class BaseTestMod { } @SuppressWarnings("unchecked") - private static R sneak(Throwable e) throws E { + protected static R sneak(Throwable e) throws E { throw (E)e; } } diff --git a/src/test/resources/load_sort_test/META-INF/mods.toml b/src/test/resources/load_sort_test/META-INF/mods.toml new file mode 100644 index 0000000000..408b2cc727 --- /dev/null +++ b/src/test/resources/load_sort_test/META-INF/mods.toml @@ -0,0 +1,20 @@ +[[mods]] +modId="load_sort_test" +[[mods]] +modId="load_sort_before" +[[mods]] +modId="load_sort_after" + +[[dependencies.load_sort_test]] +modId="load_sort_after" +mandatory=true +versionRange="[1,)" +ordering="AFTER" +side="BOTH" + +[[dependencies.load_sort_test]] +modId="load_sort_before" +mandatory=true +versionRange="[1,)" +ordering="BEFORE" +side="BOTH" \ No newline at end of file