/* * 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.loading.moddiscovery; import cpw.mods.gross.Java9ClassLoaderUtil; import cpw.mods.modlauncher.Launcher; import cpw.mods.modlauncher.ServiceLoaderStreamUtils; import cpw.mods.modlauncher.api.LamdbaExceptionUtils; import net.minecraftforge.fml.loading.FMLEnvironment; import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.loading.LoadingModList; import net.minecraftforge.fml.loading.ModDirTransformerDiscoverer; import net.minecraftforge.fml.loading.ModSorter; import net.minecraftforge.fml.loading.progress.StartupMessageManager; import net.minecraftforge.forgespi.Environment; import net.minecraftforge.forgespi.locating.IModFile; import net.minecraftforge.forgespi.locating.IModLocator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; import java.util.function.Consumer; import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipError; import static net.minecraftforge.fml.loading.LogMarkers.CORE; import static net.minecraftforge.fml.loading.LogMarkers.SCAN; public class ModDiscoverer { private static final Logger LOGGER = LogManager.getLogger(); private final ServiceLoader locators; private final List locatorList; private final LocatorClassLoader locatorClassLoader; public ModDiscoverer(Map arguments) { Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.MODFOLDERFACTORY.get(), v->ModsFolderLocator::new); Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.MODDIRECTORYFACTORY.get(), v->ModsFolderLocator::new); Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.PROGRESSMESSAGE.get(), v->StartupMessageManager.locatorConsumer().orElseGet(()->s->{})); locatorClassLoader = new LocatorClassLoader(); Launcher.INSTANCE.environment().computePropertyIfAbsent(FMLEnvironment.Keys.LOCATORCLASSLOADER.get(), v->locatorClassLoader); ModDirTransformerDiscoverer.getExtraLocators() .stream() .map(LamdbaExceptionUtils.rethrowFunction(p->p.toUri().toURL())) .forEach(locatorClassLoader::addURL); locators = ServiceLoader.load(IModLocator.class, locatorClassLoader); locatorList = ServiceLoaderStreamUtils.toList(this.locators); locatorList.forEach(l->l.initArguments(arguments)); locatorList.add(new MinecraftLocator()); LOGGER.debug(CORE,"Found Mod Locators : {}", ()->locatorList.stream().map(iModLocator -> "("+iModLocator.name() + ":" + iModLocator.getClass().getPackage().getImplementationVersion()+")").collect(Collectors.joining(","))); } ModDiscoverer(List locatorList) { this.locatorList = locatorList; this.locatorClassLoader = null; this.locators = null; } public BackgroundScanHandler discoverMods() { LOGGER.debug(SCAN,"Scanning for mods and other resources to load. We know {} ways to find mods", locatorList.size()); final Map> modFiles = locatorList.stream() .peek(loc -> LOGGER.debug(SCAN,"Trying locator {}", loc)) .map(IModLocator::scanMods) .flatMap(Collection::stream) .peek(mf -> LOGGER.debug(SCAN,"Found mod file {} of type {} with locator {}", mf.getFileName(), mf.getType(), mf.getLocator())) .peek(mf -> StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Found mod file "+mf.getFileName()+" of type "+mf.getType()))) .map(ModFile.class::cast) .collect(Collectors.groupingBy(IModFile::getType)); FMLLoader.getLanguageLoadingProvider().addAdditionalLanguages(modFiles.get(IModFile.Type.LANGPROVIDER)); BackgroundScanHandler backgroundScanHandler = new BackgroundScanHandler(modFiles); final List mods = modFiles.getOrDefault(IModFile.Type.MOD, Collections.emptyList()); final List brokenFiles = new ArrayList<>(); for (Iterator iterator = mods.iterator(); iterator.hasNext(); ) { ModFile mod = iterator.next(); if (!mod.getLocator().isValid(mod) || !mod.identifyMods()) { LOGGER.warn(SCAN, "File {} has been ignored - it is invalid", mod.getFilePath()); iterator.remove(); brokenFiles.add(mod); } } LOGGER.debug(SCAN,"Found {} mod files with {} mods", mods::size, ()->mods.stream().mapToInt(mf -> mf.getModInfos().size()).sum()); StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Found "+mods.size()+" modfiles to load")); final LoadingModList loadingModList = ModSorter.sort(mods); loadingModList.addCoreMods(); loadingModList.addAccessTransformers(); loadingModList.addForScanning(backgroundScanHandler); loadingModList.setBrokenFiles(brokenFiles); return backgroundScanHandler; } private static class LocatorClassLoader extends URLClassLoader { LocatorClassLoader() { super(Java9ClassLoaderUtil.getSystemClassPathURLs(), getSystemClassLoader()); } @Override protected void addURL(final URL url) { super.addURL(url); } } private static class MinecraftLocator implements IModLocator { private final Path mcJar = FMLLoader.getMCPaths()[0]; private final FileSystem fileSystem; MinecraftLocator() { if (!Files.isDirectory(mcJar)) { try { fileSystem = FileSystems.newFileSystem(mcJar, getClass().getClassLoader()); } catch (ZipError | IOException e) { LOGGER.fatal(SCAN,"Invalid Minecraft JAR file - no filesystem created"); throw new RuntimeException(e); } } else { fileSystem = null; } } @Override public List scanMods() { return Collections.singletonList(ModFile.newFMLInstance(mcJar, this)); } @Override public String name() { return "minecraft"; } @Override public Path findPath(final IModFile modFile, final String... path) { String[] newPath = Arrays.copyOf(path, path.length); if (path.length == 2 && Objects.equals(path[1], "mods.toml")) { final URI jarFileURI; try { jarFileURI = getClass().getClassLoader().getResource("minecraftmod.toml").toURI(); if (Objects.equals(jarFileURI.getScheme(), "jar")) { // Initialize the filesystem for the forge jar, because otherwise this barfs? FileSystems.newFileSystem(jarFileURI, new HashMap<>()); } } catch (URISyntaxException | IOException e) { LOGGER.fatal(SCAN, "Unable to read minecraft default mod"); throw new RuntimeException(e); } return Paths.get(jarFileURI); } if (Files.isDirectory(mcJar)) return findPathDirectory(modFile, path); return findPathJar(modFile, path); } private Path findPathDirectory(final IModFile modFile, final String... path) { if (path.length < 1) { throw new IllegalArgumentException("Missing path"); } final Path target = Paths.get(path[0], Arrays.copyOfRange(path, 1, path.length)); // try right path first (resources) return mcJar.resolve(target); } private Path findPathJar(final IModFile modFile, final String... path) { return fileSystem.getPath(path[0], Arrays.copyOfRange(path, 1, path.length)); } @Override public void scanFile(final IModFile modFile, final Consumer pathConsumer) { LOGGER.debug(SCAN,"Scan started: {}", modFile); Path path; if (Files.isDirectory(mcJar)) path = mcJar; else path = fileSystem.getPath("/"); try (Stream files = Files.find(path, Integer.MAX_VALUE, (p, a) -> p.getNameCount() > 0 && p.getFileName().toString().endsWith(".class"))) { files.forEach(pathConsumer); } catch (IOException e) { e.printStackTrace(); } LOGGER.debug(SCAN,"Scan finished: {}", modFile); } @Override public Optional findManifest(final Path file) { return Optional.empty(); } @Override public void initArguments(final Map arguments) { // no op } @Override public boolean isValid(final IModFile modFile) { return true; } } }