From 3bf6c17bb8ae924d0bfbcd896624dc59480ed8dd Mon Sep 17 00:00:00 2001 From: cpw Date: Wed, 23 Oct 2019 21:30:17 -0400 Subject: [PATCH] Allow pack locators to load from the mods folder. Regular mods are now automatically de-duped based on the version number - the highest version file of a "root mod id" is now selected automatically. Signed-off-by: cpw --- .../fml/loading/FMLEnvironment.java | 7 ++ .../fml/loading/LoadingModList.java | 33 +++-- .../loading/ModDirTransformerDiscoverer.java | 54 ++++++--- .../minecraftforge/fml/loading/ModSorter.java | 114 +++++++++++++----- .../loading/moddiscovery/ModDiscoverer.java | 26 +++- .../moddiscovery/ModsFolderLocator.java | 16 +-- 6 files changed, 188 insertions(+), 62 deletions(-) diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLEnvironment.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLEnvironment.java index f1471b31c..cfcb0d6e0 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLEnvironment.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/FMLEnvironment.java @@ -20,9 +20,12 @@ package net.minecraftforge.fml.loading; import cpw.mods.modlauncher.api.IEnvironment; +import cpw.mods.modlauncher.api.TypesafeMap; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.forgespi.Environment; +import java.util.function.Supplier; + public class FMLEnvironment { public static final Dist dist = FMLLoader.getDist(); @@ -32,4 +35,8 @@ public class FMLEnvironment environment.computePropertyIfAbsent(IEnvironment.Keys.NAMING.get(), v->naming); environment.computePropertyIfAbsent(Environment.Keys.DIST.get(), v->dist); } + + public static class Keys { + public static final Supplier> LOCATORCLASSLOADER = IEnvironment.buildKey("LOCATORCLASSLOADER",ClassLoader.class); + } } diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/LoadingModList.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/LoadingModList.java index a1dbd34f2..8e928f08d 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/LoadingModList.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/LoadingModList.java @@ -51,13 +51,18 @@ public class LoadingModList private LoadingModList(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(ModFileInfo::getMods).flatMap(Collection::stream). - map(ModInfo.class::cast). - collect(Collectors.toMap(ModInfo::getModId, ModInfo::getOwningFile)); + 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(ModFileInfo::getMods) + .flatMap(Collection::stream) + .map(ModInfo.class::cast) + .collect(Collectors.toMap(ModInfo::getModId, ModInfo::getOwningFile)); this.preLoadErrors = new ArrayList<>(); } @@ -76,19 +81,27 @@ public class LoadingModList } public void addCoreMods() { - modFiles.stream().map(ModFileInfo::getFile).map(ModFile::getCoreMods).flatMap(List::stream).forEach(FMLLoader.getCoreModProvider()::addCoreMod); + modFiles.stream() + .map(ModFileInfo::getFile) + .map(ModFile::getCoreMods) + .flatMap(List::stream) + .forEach(FMLLoader.getCoreModProvider()::addCoreMod); } public void addAccessTransformers() { - modFiles.stream().map(ModFileInfo::getFile).forEach(mod -> mod.getAccessTransformer().ifPresent(path -> FMLLoader.addAccessTransformer(path, mod))); + modFiles.stream() + .map(ModFileInfo::getFile) + .forEach(mod -> mod.getAccessTransformer().ifPresent(path -> FMLLoader.addAccessTransformer(path, mod))); } public void addForScanning(BackgroundScanHandler backgroundScanHandler) { this.scanner = backgroundScanHandler; backgroundScanHandler.setLoadingModList(this); - modFiles.stream().map(ModFileInfo::getFile).forEach(backgroundScanHandler::submitForScanning); + modFiles.stream() + .map(ModFileInfo::getFile) + .forEach(backgroundScanHandler::submitForScanning); } public List getModFiles() diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/ModDirTransformerDiscoverer.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/ModDirTransformerDiscoverer.java index 0df64f48c..7d192d9f0 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/ModDirTransformerDiscoverer.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/ModDirTransformerDiscoverer.java @@ -28,36 +28,58 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.zip.ZipFile; public class ModDirTransformerDiscoverer implements ITransformerDiscoveryService { @Override public List candidates(final Path gameDirectory) { + ModDirTransformerDiscoverer.scan(gameDirectory); + return ModDirTransformerDiscoverer.transformers; + } + + private static List transformers; + + private static List locators; + public static List allExcluded() { + ArrayList paths = new ArrayList<>(); + paths.addAll(transformers); + paths.addAll(locators); + return paths; + } + + public static List getExtraLocators() { + return locators; + } + + private static void scan(final Path gameDirectory) { final Path modsDir = gameDirectory.resolve(FMLPaths.MODSDIR.relative()); if (!Files.exists(modsDir)) { // Skip if the mods dir doesn't exist yet. - return Collections.emptyList(); + return; } - List paths = new ArrayList<>(); + transformers = new ArrayList<>(); + locators = new ArrayList<>(); try { Files.createDirectories(modsDir); - Files.walk(modsDir, 1).forEach(p -> { - if (!Files.isRegularFile(p)) return; - if (!p.toString().endsWith(".jar")) return; - if (LamdbaExceptionUtils.uncheck(()->Files.size(p)) == 0) return; - try (ZipFile zf = new ZipFile(new File(p.toUri()))) { - if (zf.getEntry("META-INF/services/cpw.mods.modlauncher.api.ITransformationService") != null) { - paths.add(p); - } - } catch (IOException ioe) { - LogManager.getLogger().error("Zip Error when loading jar file {}", p, ioe); - } - }); + Files.walk(modsDir, 1).forEach(ModDirTransformerDiscoverer::visitFile); } catch (IOException | IllegalStateException ioe) { LogManager.getLogger().error("Error during early discovery", ioe); } - return paths; + } + + private static void visitFile(Path path) { + if (!Files.isRegularFile(path)) return; + if (!path.toString().endsWith(".jar")) return; + if (LamdbaExceptionUtils.uncheck(() -> Files.size(path)) == 0) return; + try (ZipFile zf = new ZipFile(new File(path.toUri()))) { + if (zf.getEntry("META-INF/services/cpw.mods.modlauncher.api.ITransformationService") != null) { + transformers.add(path.toRealPath()); + } else if (zf.getEntry("META-INF/services/net.minecraftforge.forgespi.locating.IModLocator") != null) { + locators.add(path.toRealPath()); + } + } catch (IOException ioe) { + LogManager.getLogger().error("Zip Error when loading jar file {}", path, ioe); + } } } diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java index 6cea6a004..5b58f41be 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java @@ -30,12 +30,14 @@ import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; import net.minecraftforge.fml.loading.moddiscovery.ModInfo; import net.minecraftforge.fml.loading.toposort.CyclePresentException; import net.minecraftforge.fml.loading.toposort.TopologicalSort; +import net.minecraftforge.forgespi.locating.IModFile; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.util.StringBuilderFormattable; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import java.util.AbstractMap; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -45,7 +47,6 @@ 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 net.minecraftforge.fml.loading.LogMarkers.LOADING; @@ -87,11 +88,19 @@ public class ModSorter // 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).collect(Collectors.toMap(Function.identity(), (e) -> counter.incrementAndGet())); + Map infos = modFiles + .stream() + .map(ModFile::getModFileInfo) + .collect(Collectors.toMap(Function.identity(), e -> counter.incrementAndGet())); infos.keySet().forEach(i -> graph.addNode((ModFileInfo) i)); - modFiles.stream().map(ModFile::getModInfos).flatMap(Collection::stream). - map(IModInfo::getDependencies).flatMap(Collection::stream). - forEach(dep -> addDependency(graph, dep)); + modFiles + .stream() + .map(ModFile::getModInfos) + .flatMap(Collection::stream) + .map(IModInfo::getDependencies) + .flatMap(Collection::stream) + .forEach(dep -> addDependency(graph, dep)); + final List sorted; try { @@ -109,15 +118,24 @@ public class ModSorter List dataList = cycles.stream() .map(Set::stream) .map(stream -> stream - .flatMap(modFileInfo -> modFileInfo.getMods().stream() - .map(IModInfo::getModId)).collect(Collectors.toList())) + .flatMap(modFileInfo -> modFileInfo.getMods() + .stream() + .map(IModInfo::getModId)) + .collect(Collectors.toList())) .map(list -> new ExceptionData("fml.modloading.cycle", list)) .collect(Collectors.toList()); throw new EarlyLoadingException("Sorting error", e, dataList); } - this.sortedList = sorted.stream().map(ModFileInfo::getMods). - flatMap(Collection::stream).map(ModInfo.class::cast).collect(Collectors.toList()); - this.modFiles = sorted.stream().map(ModFileInfo::getFile).collect(Collectors.toList()); + this.sortedList = sorted + .stream() + .map(ModFileInfo::getMods) + .flatMap(Collection::stream) + .map(ModInfo.class::cast) + .collect(Collectors.toList()); + this.modFiles = sorted + .stream() + .map(ModFileInfo::getFile) + .collect(Collectors.toList()); } @SuppressWarnings("UnstableApiUsage") @@ -144,13 +162,27 @@ public class ModSorter private void buildUniqueList() { - final Stream modInfos = modFiles.stream() + // Collect mod files by first modid in the file. This will be used for deduping purposes + final Map> modFilesByFirstId = modFiles + .stream() + .collect(Collectors.groupingBy(mf -> mf.getModInfos().get(0).getModId())); + + // Select the newest by artifact version sorting of non-unique files thus identified + this.modFiles = modFilesByFirstId.entrySet().stream() + .map(this::selectNewestModInfo) + .map(Map.Entry::getValue) + .map(ModFile.class::cast) + .collect(Collectors.toList()); + + // Transform to the full mod id list + final Map> modIds = modFiles.stream() .map(ModFile::getModInfos) .flatMap(Collection::stream) - .map(ModInfo.class::cast); - final Map> modIds = modInfos.collect(Collectors.groupingBy(IModInfo::getModId)); + .map(ModInfo.class::cast) + .collect(Collectors.groupingBy(IModInfo::getModId)); - // TODO: make this figure out dupe handling better + // Its theoretically possible that some mod has somehow moved an id to a secondary place, thus causing a dupe. + // We can't handle this final List>> dupedMods = modIds .entrySet() .stream() @@ -171,30 +203,58 @@ public class ModSorter .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0))); } + private Map.Entry selectNewestModInfo(Map.Entry> fullList) { + List modInfoList = fullList.getValue(); + if (modInfoList.size() > 1) { + LOGGER.debug("Found {} mods for first modid {}, selecting most recent based on version data", modInfoList.size(), fullList.getKey()); + modInfoList.sort(Comparator.comparing(mf -> mf.getModInfos().get(0).getVersion()).reversed()); + LOGGER.debug("Selected file {} for modid {} with version {}", modInfoList.get(0).getFileName(), fullList.getKey(), modInfoList.get(0).getModInfos().get(0).getVersion()); + } + return new AbstractMap.SimpleImmutableEntry<>(fullList.getKey(), modInfoList.get(0)); + } + private void verifyDependencyVersions() { - final Map modVersions = modFiles.stream().map(ModFile::getModInfos). - flatMap(Collection::stream).collect(Collectors.toMap(IModInfo::getModId, IModInfo::getVersion)); - final Map> modVersionDependencies = modFiles.stream(). - map(ModFile::getModInfos).flatMap(Collection::stream). - collect(Collectors.groupingBy(Function.identity(), Java9BackportUtils.flatMapping(e -> e.getDependencies().stream(), Collectors.toList()))); - final Set mandatoryModVersions = modVersionDependencies.values().stream().flatMap(Collection::stream). - filter(mv -> mv.isMandatory() && mv.getSide().isCorrectSide()).collect(Collectors.toSet()); + final Map modVersions = modFiles + .stream() + .map(ModFile::getModInfos) + .flatMap(Collection::stream) + .collect(Collectors.toMap(IModInfo::getModId, IModInfo::getVersion)); + + final Map> modVersionDependencies = modFiles + .stream() + .map(ModFile::getModInfos) + .flatMap(Collection::stream) + .collect(Collectors.groupingBy(Function.identity(), Java9BackportUtils.flatMapping(e -> e.getDependencies().stream(), Collectors.toList()))); + + final Set mandatoryModVersions = modVersionDependencies + .values() + .stream() + .flatMap(Collection::stream) + .filter(mv -> mv.isMandatory() && mv.getSide().isCorrectSide()) + .collect(Collectors.toSet()); + LOGGER.debug(LOADING, "Found {} mandatory requirements", mandatoryModVersions.size()); - final Set missingVersions = mandatoryModVersions.stream().filter(mv->this.modVersionMatches(mv, modVersions)).collect(Collectors.toSet()); + final Set missingVersions = mandatoryModVersions + .stream() + .filter(mv->this.modVersionMatches(mv, modVersions)) + .collect(Collectors.toSet()); LOGGER.debug(LOADING, "Found {} mandatory mod requirements missing", missingVersions.size()); if (!missingVersions.isEmpty()) { - final List exceptionData = missingVersions.stream().map(mv -> - new EarlyLoadingException.ExceptionData("fml.modloading.missingdependency", mv.getModId(), - mv.getOwner().getModId(), mv.getVersionRange(), modVersions.getOrDefault(mv.getModId(), new DefaultArtifactVersion("null")))). - collect(Collectors.toList()); + final List exceptionData = missingVersions + .stream() + .map(mv -> new EarlyLoadingException.ExceptionData("fml.modloading.missingdependency", + mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), + modVersions.getOrDefault(mv.getModId(), new DefaultArtifactVersion("null")))) + .collect(Collectors.toList()); throw new EarlyLoadingException("Missing mods", null, exceptionData); } } private boolean modVersionMatches(final IModInfo.ModVersion mv, final Map modVersions) { - return !modVersions.containsKey(mv.getModId()) || !mv.getVersionRange().containsVersion(modVersions.get(mv.getModId())); + return !(modVersions.containsKey(mv.getModId()) && + (mv.getVersionRange().containsVersion(modVersions.get(mv.getModId())) || modVersions.get(mv.getModId()).toString().equals("NONE"))); } } diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java index 92a18b7ce..2011c6185 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/moddiscovery/ModDiscoverer.java @@ -19,10 +19,14 @@ 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; @@ -34,6 +38,8 @@ 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; @@ -64,11 +70,18 @@ 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.PROGRESSMESSAGE.get(), v->StartupMessageManager.locatorConsumer().orElseGet(()->s->{})); - locators = ServiceLoader.load(IModLocator.class); + 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()); @@ -77,6 +90,7 @@ public class ModDiscoverer { ModDiscoverer(List locatorList) { this.locatorList = locatorList; + this.locatorClassLoader = null; this.locators = null; } @@ -114,6 +128,16 @@ public class ModDiscoverer { 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; diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java index 4b4d679ca..d2f37ac6a 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java @@ -56,14 +56,14 @@ public class ModsFolderLocator extends AbstractJarFileLocator { @Override public List scanMods() { LOGGER.debug(SCAN,"Scanning mods dir {} for mods", this.modFolder); - List excluded = new ModDirTransformerDiscoverer().candidates(FMLPaths.GAMEDIR.get()); - return uncheck(()-> Files.list(this.modFolder)). - filter(p->!excluded.contains(p)). - sorted(Comparator.comparing(path-> StringUtils.toLowerCase(path.getFileName().toString()))). - filter(p->StringUtils.toLowerCase(p.getFileName().toString()).endsWith(SUFFIX)). - map(p->new ModFile(p, this)). - peek(f->modJars.compute(f, (mf, fs)->createFileSystem(mf))). - collect(Collectors.toList()); + List excluded = ModDirTransformerDiscoverer.allExcluded(); + return uncheck(()-> Files.list(this.modFolder)) + .filter(p->!excluded.contains(p)) + .sorted(Comparator.comparing(path-> StringUtils.toLowerCase(path.getFileName().toString()))) + .filter(p->StringUtils.toLowerCase(p.getFileName().toString()).endsWith(SUFFIX)) + .map(p->new ModFile(p, this)) + .peek(f->modJars.compute(f, (mf, fs)->createFileSystem(mf))) + .collect(Collectors.toList()); } @Override