/* * Minecraft Forge * Copyright (c) 2016-2020. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation version 2.1 * of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package net.minecraftforge.fml; import com.google.common.collect.ImmutableList; import cpw.mods.modlauncher.TransformingClassLoader; import net.minecraft.util.registry.Bootstrap; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.model.generators.ExistingFileHelper; import net.minecraftforge.common.capabilities.CapabilityManager; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.config.ConfigTracker; import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.event.lifecycle.GatherDataEvent; import net.minecraftforge.fml.event.lifecycle.IModBusEvent; import net.minecraftforge.fml.event.lifecycle.ModLifecycleEvent; import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.loading.FMLPaths; import net.minecraftforge.fml.loading.LoadingModList; import net.minecraftforge.fml.loading.moddiscovery.InvalidModIdentifier; import net.minecraftforge.fml.loading.moddiscovery.ModFile; import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; import net.minecraftforge.fml.loading.progress.StartupMessageManager; import net.minecraftforge.fml.network.FMLNetworkConstants; import net.minecraftforge.fml.network.NetworkRegistry; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.forgespi.language.IModLanguageProvider; import net.minecraftforge.registries.GameData; import net.minecraftforge.registries.ObjectHolderRegistry; import net.minecraftforge.versions.forge.ForgeVersion; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import static net.minecraftforge.fml.Logging.CORE; import static net.minecraftforge.fml.Logging.LOADING; /** * Loads mods. * * Dispatch cycle is seen in {@link #loadMods()} and {@link #finishMods()} * * Overall sequence for loadMods is: *
*
CONSTRUCT
*
Constructs the mod instance. Mods can typically setup basic environment such as Event listeners * and Configuration specifications here.
*
Automated dispatches
*
Dispatches automated elements : {@link net.minecraftforge.fml.common.Mod.EventBusSubscriber}, * {@link net.minecraftforge.event.RegistryEvent}, {@link net.minecraftforge.common.capabilities.CapabilityInject} * and others
*
CONFIG_LOAD
*
Dispatches ConfigLoadEvent to mods
*
COMMON_SETUP
*
Dispatches {@link net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent} to mods
*
SIDED_SETUP
*
Dispatches {@link net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent} or * {@link net.minecraftforge.fml.event.lifecycle.FMLDedicatedServerSetupEvent} to mods
*
* * Overall sequence for finishMods is: *
*
ENQUEUE_IMC
*
Dispatches {@link net.minecraftforge.fml.event.lifecycle.InterModEnqueueEvent} to mods, * for enqueuing {@link InterModComms} messages for other mods to receive subsequently
*
PROCESS_IMC
*
Dispatches {@link net.minecraftforge.fml.event.lifecycle.InterModProcessEvent} to mods, * for processing {@link InterModComms} messages received from other mods prior to this event
*
COMPLETE
*
Dispatches {@link net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent} to mods, * and completes the mod loading sequence.
*
*/ public class ModLoader { private static final Logger LOGGER = LogManager.getLogger(); private static ModLoader INSTANCE; private final TransformingClassLoader launchClassLoader; private final LoadingModList loadingModList; private final List loadingExceptions; private final List loadingWarnings; private GatherDataEvent.DataGeneratorConfig dataGeneratorConfig; private ExistingFileHelper existingFileHelper; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private final Optional> statusConsumer = StartupMessageManager.modLoaderConsumer(); private ModLoader() { INSTANCE = this; this.launchClassLoader = FMLLoader.getLaunchClassLoader(); this.loadingModList = FMLLoader.getLoadingModList(); this.loadingExceptions = FMLLoader.getLoadingModList(). getErrors().stream().flatMap(ModLoadingException::fromEarlyException).collect(Collectors.toList()); this.loadingWarnings = FMLLoader.getLoadingModList(). getBrokenFiles().stream().map(file -> new ModLoadingWarning(null, ModLoadingStage.VALIDATE, InvalidModIdentifier.identifyJarProblem(file.getFilePath()).orElse("fml.modloading.brokenfile"), file.getFileName())).collect(Collectors.toList()); LOGGER.debug(CORE, "Loading Network data for FML net version: {}", FMLNetworkConstants.init()); CrashReportExtender.registerCrashCallable("ModLauncher", FMLLoader::getLauncherInfo); CrashReportExtender.registerCrashCallable("ModLauncher launch target", FMLLoader::launcherHandlerName); CrashReportExtender.registerCrashCallable("ModLauncher naming", FMLLoader::getNaming); CrashReportExtender.registerCrashCallable("ModLauncher services", this::computeModLauncherServiceList); CrashReportExtender.registerCrashCallable("FML", ForgeVersion::getSpec); CrashReportExtender.registerCrashCallable("Forge", ()->ForgeVersion.getGroup()+":"+ForgeVersion.getVersion()); CrashReportExtender.registerCrashCallable("FML Language Providers", this::computeLanguageList); } private String computeLanguageList() { return "\n"+FMLLoader.getLanguageLoadingProvider().applyForEach(lp->lp.name() +"@"+ lp.getClass().getPackage().getImplementationVersion()).collect(Collectors.joining("\n\t\t", "\t\t", "")); } private String computeModLauncherServiceList() { final List> mods = FMLLoader.modLauncherModList(); return "\n"+mods.stream().map(mod->mod.getOrDefault("file","nofile")+ " "+mod.getOrDefault("name", "missing")+ " "+mod.getOrDefault("type","NOTYPE")+ " "+mod.getOrDefault("description", "")). collect(Collectors.joining("\n\t\t","\t\t","")); } public static ModLoader get() { return INSTANCE == null ? INSTANCE = new ModLoader() : INSTANCE; } /** * Run on the primary starting thread by ClientModLoader and ServerModLoader * @param syncExecutor An executor to run tasks on the main thread * @param parallelExecutor An executor to run tasks on a parallel loading thread pool * @param periodicTask Optional periodic task to perform on the main thread while other activities run */ public void gatherAndInitializeMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) { statusConsumer.ifPresent(c->c.accept("Waiting for scan to complete")); FMLLoader.backgroundScanHandler.waitForScanToComplete(periodicTask); statusConsumer.ifPresent(c->c.accept("Loading mods")); final ModList modList = ModList.of(loadingModList .getModFiles() .stream() .map(ModFileInfo::getFile) .collect(Collectors.toList()), loadingModList.getMods()); if (!this.loadingExceptions.isEmpty()) { LOGGER.fatal(CORE, "Error during pre-loading phase", loadingExceptions.get(0)); modList.setLoadedMods(Collections.emptyList()); throw new LoadingFailedException(loadingExceptions); } statusConsumer.ifPresent(c->c.accept("Building Mod List")); final List modContainers = loadingModList.getModFiles().stream(). map(ModFileInfo::getFile). map(mf -> buildMods(mf, launchClassLoader)). flatMap(Collection::stream). collect(Collectors.toList()); if (!loadingExceptions.isEmpty()) { LOGGER.fatal(CORE, "Failed to initialize mod containers", loadingExceptions.get(0)); modList.setLoadedMods(Collections.emptyList()); throw new LoadingFailedException(loadingExceptions); } modList.setLoadedMods(modContainers); statusConsumer.ifPresent(c->c.accept(String.format("Constructing %d mods", modList.size()))); dispatchAndHandleError(ModLoadingStage.CONSTRUCT, syncExecutor, parallelExecutor, periodicTask); statusConsumer.ifPresent(c->c.accept("Creating registries")); dispatchAndHandleError(ModLoadingStage.CREATE_REGISTRIES, syncExecutor, parallelExecutor, periodicTask); ObjectHolderRegistry.findObjectHolders(); CapabilityManager.INSTANCE.injectCapabilities(modList.getAllScanData()); statusConsumer.ifPresent(c->c.accept("Populating registries")); dispatchAndHandleError(ModLoadingStage.LOAD_REGISTRIES, syncExecutor, parallelExecutor, periodicTask); statusConsumer.ifPresent(c->c.accept("Early mod loading complete")); } public void loadMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Function> beforeSidedEvent, final Function> afterSidedEvent, final Runnable periodicTask) { statusConsumer.ifPresent(c->c.accept("Loading mod config")); DistExecutor.unsafeRunWhenOn(Dist.CLIENT, ()->()-> ConfigTracker.INSTANCE.loadConfigs(ModConfig.Type.CLIENT, FMLPaths.CONFIGDIR.get())); ConfigTracker.INSTANCE.loadConfigs(ModConfig.Type.COMMON, FMLPaths.CONFIGDIR.get()); statusConsumer.ifPresent(c->c.accept("Mod setup: SETUP")); dispatchAndHandleError(ModLoadingStage.COMMON_SETUP, syncExecutor, parallelExecutor, periodicTask); statusConsumer.ifPresent(c->c.accept("Mod setup: SIDED SETUP")); dispatchAndHandleError(ModLoadingStage.SIDED_SETUP, syncExecutor, parallelExecutor, periodicTask, beforeSidedEvent, afterSidedEvent); statusConsumer.ifPresent(c->c.accept("Mod setup complete")); } public void finishMods(final ModWorkManager.DrivenExecutor syncExecutor, final Executor parallelExecutor, final Runnable periodicTask) { statusConsumer.ifPresent(c->c.accept("Mod setup: ENQUEUE IMC")); dispatchAndHandleError(ModLoadingStage.ENQUEUE_IMC, syncExecutor, parallelExecutor, periodicTask); statusConsumer.ifPresent(c->c.accept("Mod setup: PROCESS IMC")); dispatchAndHandleError(ModLoadingStage.PROCESS_IMC, syncExecutor, parallelExecutor, periodicTask); statusConsumer.ifPresent(c->c.accept("Mod setup: Final completion")); dispatchAndHandleError(ModLoadingStage.COMPLETE, syncExecutor, parallelExecutor, periodicTask); statusConsumer.ifPresent(c->c.accept("Freezing data")); GameData.freezeData(); NetworkRegistry.lock(); statusConsumer.ifPresent(c->c.accept(String.format("Mod loading complete - %d mods loaded", ModList.get().size()))); } private void dispatchAndHandleError(ModLoadingStage state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker) { waitForTransition(state, syncExecutor, ticker, state.buildTransition(syncExecutor, parallelExecutor)); } private void dispatchAndHandleError(ModLoadingStage state, ModWorkManager.DrivenExecutor syncExecutor, Executor parallelExecutor, final Runnable ticker, Function> preSyncTask, Function> postSyncTask) { waitForTransition(state, syncExecutor, ticker, state.buildTransition(syncExecutor, parallelExecutor, preSyncTask, postSyncTask)); } private void waitForTransition(final ModLoadingStage state, final ModWorkManager.DrivenExecutor syncExecutor, final Runnable ticker, final CompletableFuture> transition) { while (!transition.isDone()) { ticker.run(); syncExecutor.drive(); } try { transition.join(); } catch (CompletionException e) { Throwable t = e.getCause(); final List notModLoading = Arrays.stream(t.getSuppressed()) .filter(obj -> !(obj instanceof ModLoadingException)) .collect(Collectors.toList()); if (!notModLoading.isEmpty()) { LOGGER.fatal("Encountered non-modloading exceptions!", e); throw e; } final List modLoadingExceptions = Arrays.stream(t.getSuppressed()) .filter(ModLoadingException.class::isInstance) .map(ModLoadingException.class::cast) .collect(Collectors.toList()); LOGGER.fatal(LOADING,"Failed to complete lifecycle event {}, {} errors found", state, modLoadingExceptions.size()); throw new LoadingFailedException(modLoadingExceptions); } } private List buildMods(final ModFile modFile, final TransformingClassLoader modClassLoader) { final Map modInfoMap = modFile.getModFileInfo().getMods().stream().collect(Collectors.toMap(IModInfo::getModId, Function.identity())); LOGGER.debug(LOADING, "ModContainer is {}", ModContainer.class.getClassLoader()); final List containers = modFile.getScanResult().getTargets().entrySet().stream(). map(e -> buildModContainerFromTOML(modFile, modClassLoader, modInfoMap, e)) .filter(Objects::nonNull) .collect(Collectors.toList()); if (containers.size() != modInfoMap.size()) { LOGGER.fatal(LOADING,"File {} constructed {} mods: {}, but had {} mods specified: {}", modFile.getFilePath(), containers.size(), containers.stream().map(c -> c != null ? c.getModId() : "(null)").sorted().collect(Collectors.toList()), modInfoMap.size(), modInfoMap.values().stream().map(IModInfo::getModId).sorted().collect(Collectors.toList())); loadingExceptions.add(new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingclasses", null, modFile.getFilePath())); } return containers; } private ModContainer buildModContainerFromTOML(final ModFile modFile, final TransformingClassLoader modClassLoader, final Map modInfoMap, final Map.Entry idToProviderEntry) { try { final String modId = idToProviderEntry.getKey(); final IModLanguageProvider.IModLanguageLoader languageLoader = idToProviderEntry.getValue(); IModInfo info = Optional.ofNullable(modInfoMap.get(modId)). // throw a missing metadata error if there is no matching modid in the modInfoMap from the mods.toml file orElseThrow(()->new ModLoadingException(null, ModLoadingStage.CONSTRUCT, "fml.modloading.missingmetadata", null, modId)); return languageLoader.loadMod(info, modClassLoader, modFile.getScanResult()); } catch (ModLoadingException mle) { // exceptions are caught and added to the error list for later handling. Null is returned here. loadingExceptions.add(mle); return null; } } public void postEvent(T e) { ModList.get().forEachModContainer((id, mc) -> mc.acceptEvent(e)); } public List getWarnings() { return ImmutableList.copyOf(this.loadingWarnings); } public void addWarning(ModLoadingWarning warning) { this.loadingWarnings.add(warning); } private static boolean runningDataGen = false; public static boolean isDataGenRunning () { return runningDataGen; } public void runDataGenerator(final Set mods, final Path path, final Collection inputs, Collection existingPacks, final boolean serverGenerators, final boolean clientGenerators, final boolean devToolGenerators, final boolean reportsGenerator, final boolean structureValidator, final boolean flat) { // if (mods.contains("minecraft") && mods.size() == 1) return; // LOGGER.info("Initializing Data Gatherer for mods {}", mods); // runningDataGen = true; // Bootstrap.register(); // dataGeneratorConfig = new GatherDataEvent.DataGeneratorConfig(mods, path, inputs, serverGenerators, clientGenerators, devToolGenerators, reportsGenerator, structureValidator, flat); // existingFileHelper = new ExistingFileHelper(existingPacks, structureValidator); // gatherAndInitializeMods(() -> {}); // dispatchAndHandleError(LifecycleEventProvider.GATHERDATA, Runnable::run, () -> {}); // dataGeneratorConfig.runAll(); } public Function getDataGeneratorEvent() { return mc -> new GatherDataEvent(mc, dataGeneratorConfig.makeGenerator(p->dataGeneratorConfig.isFlat() ? p : p.resolve(mc.getModId()), dataGeneratorConfig.getMods().contains(mc.getModId())), dataGeneratorConfig, existingFileHelper); } }