From 5979636ee59cec593f9f8ee0d4c561e4afe319a2 Mon Sep 17 00:00:00 2001 From: Marc Hermans Date: Wed, 15 Jun 2022 21:27:47 +0200 Subject: [PATCH] Jar-In-Jar (#8715) --- build.gradle | 8 +- .../net/minecraftforge/fml/ModLoader.java | 9 +- fmlloader/build.gradle | 1 + .../fml/loading/EarlyLoadingException.java | 2 +- .../minecraftforge/fml/loading/ModSorter.java | 75 ++---- .../fml/loading/UniqueModListBuilder.java | 132 ++++++++++ .../AbstractJarFileDependencyLocator.java | 51 ++++ .../AbstractJarFileModLocator.java | 30 +++ ...r.java => AbstractJarFileModProvider.java} | 30 +-- ...dLocator.java => AbstractModProvider.java} | 70 ++++-- .../moddiscovery/ClasspathLocator.java | 3 +- .../JarInJarDependencyLocator.java | 111 +++++++++ .../moddiscovery/MavenDirectoryLocator.java | 3 +- .../moddiscovery/MinecraftLocator.java | 4 +- .../loading/moddiscovery/ModDiscoverer.java | 142 +++++++++-- .../fml/loading/moddiscovery/ModFile.java | 27 +- .../loading/moddiscovery/ModValidator.java | 22 +- .../moddiscovery/ModsFolderLocator.java | 3 +- ...forge.forgespi.locating.IDependencyLocator | 1 + .../client/gui/LoadingErrorScreen.java | 15 +- .../client/gui/ModListScreen.java | 4 +- .../resources/assets/forge/lang/en_us.json | 2 + .../META-INF/jarjar/ForgeJiJTestMod-1.2.jar | Bin 0 -> 26146 bytes .../resources/META-INF/jarjar/metadata.json | 15 ++ src/test/resources/META-INF/mods.toml | 232 ++++++++++++++++++ 25 files changed, 837 insertions(+), 155 deletions(-) create mode 100644 fmlloader/src/main/java/net/minecraftforge/fml/loading/UniqueModListBuilder.java create mode 100644 fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileDependencyLocator.java create mode 100644 fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileModLocator.java rename fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/{AbstractJarFileLocator.java => AbstractJarFileModProvider.java} (74%) rename fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/{AbstractModLocator.java => AbstractModProvider.java} (56%) create mode 100644 fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/JarInJarDependencyLocator.java create mode 100644 fmlloader/src/main/resources/META-INF/services/net.minecraftforge.forgespi.locating.IDependencyLocator create mode 100644 src/test/resources/META-INF/jarjar/ForgeJiJTestMod-1.2.jar create mode 100644 src/test/resources/META-INF/jarjar/metadata.json diff --git a/build.gradle b/build.gradle index 09496c4a7c..008a4b2438 100644 --- a/build.gradle +++ b/build.gradle @@ -20,8 +20,8 @@ plugins { id 'net.minecraftforge.gradleutils' version '2.+' id 'eclipse' id 'de.undercouch.download' version '4.1.2' - id 'net.minecraftforge.gradle.patcher' version '[5.1.39,)' apply false - id 'net.minecraftforge.gradle.mcp' version '[5.1.39,)' apply false + id 'net.minecraftforge.gradle.patcher' version '[5.1.43,)' apply false + id 'net.minecraftforge.gradle.mcp' version '[5.1.43,)' apply false } Util.init() //Init all our extension methods! @@ -65,6 +65,7 @@ ext { APACHE_COMMONS_LANG3_VERSION = '3.12.0' JOPT_SIMPLE_VERSION = '5.0.4' COMMONS_IO_VERSION = '2.11.0' + JARJAR_VERSION = '0.2.24' GIT_INFO = gradleutils.gitInfo VERSION = gradleutils.getFilteredMCTagOffsetBranchVersion(true, '[0-9]', MC_VERSION) @@ -75,7 +76,7 @@ ext { INSTALLER_TOOLS = 'net.minecraftforge:installertools:1.3.0' JAR_SPLITTER = 'net.minecraftforge:jarsplitter:1.1.4' FART = 'net.minecraftforge:ForgeAutoRenamingTool:0.1.22:all' - MIN_TAG_FOR_CHANGELOG = "39.0" + MIN_TAG_FOR_CHANGELOG = "41.0" } println('Version: ' + VERSION + @@ -283,6 +284,7 @@ def sharedDeps = { installer "org.jline:jline-terminal:${JLINE_VERSION}" // Dep of TerminalConsoleAppender installer "org.spongepowered:mixin:${MIXIN_VERSION}" installer 'org.openjdk.nashorn:nashorn-core:15.3' + installer "net.minecraftforge:JarJar:${JARJAR_VERSION}" /* installer 'org.lwjgl:lwjgl:3.2.2' diff --git a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java index 21be77c0a1..5dc6eca83e 100644 --- a/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java +++ b/fmlcore/src/main/java/net/minecraftforge/fml/ModLoader.java @@ -12,13 +12,13 @@ import net.minecraftforge.fml.loading.FMLEnvironment; import net.minecraftforge.fml.loading.FMLLoader; 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.moddiscovery.ModInfo; import net.minecraftforge.fml.loading.progress.StartupMessageManager; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.forgespi.language.IModLanguageProvider; import net.minecraftforge.forgespi.locating.ForgeFeature; +import net.minecraftforge.forgespi.locating.IModFile; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -27,7 +27,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; 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; @@ -96,7 +95,7 @@ public class ModLoader .map(file -> new ModLoadingWarning(null, ModLoadingStage.VALIDATE, InvalidModIdentifier.identifyJarProblem(file.getFilePath()).orElse("fml.modloading.brokenfile"), file.getFileName())) .collect(Collectors.toList()); FMLLoader.getLoadingModList().getModFiles().stream() - .filter(ModFileInfo::missingLicense) //Search for files with missing licenses + .filter(ModFileInfo::missingLicense) .filter(modFileInfo -> modFileInfo.getMods().stream().noneMatch(thisModInfo -> this.loadingExceptions.stream().map(ModLoadingException::getModInfo).anyMatch(otherInfo -> otherInfo == thisModInfo))) //Ignore files where any other mod already encountered an error .map(modFileInfo -> new ModLoadingException(null, ModLoadingStage.VALIDATE, "fml.modloading.missinglicense", null, modFileInfo.getFile())) .forEach(this.loadingExceptions::add); @@ -239,7 +238,7 @@ public class ModLoader } } - private List buildMods(final ModFile modFile) + private List buildMods(final IModFile modFile) { final Map modInfoMap = modFile.getModFileInfo().getMods().stream().collect(Collectors.toMap(IModInfo::getModId, Function.identity())); @@ -261,7 +260,7 @@ public class ModLoader return containers.stream().filter(mc -> mc.modLoadingStage != ModLoadingStage.ERROR).collect(Collectors.toList()); } - private ModContainer buildModContainerFromTOML(final ModFile modFile, final Map modInfoMap, final Map.Entry idToProviderEntry) { + private ModContainer buildModContainerFromTOML(final IModFile modFile, final Map modInfoMap, final Map.Entry idToProviderEntry) { try { final String modId = idToProviderEntry.getKey(); final IModLanguageProvider.IModLanguageLoader languageLoader = idToProviderEntry.getValue(); diff --git a/fmlloader/build.gradle b/fmlloader/build.gradle index 2b13f4e2f5..d7a21c0200 100644 --- a/fmlloader/build.gradle +++ b/fmlloader/build.gradle @@ -28,6 +28,7 @@ dependencies { api("cpw.mods:modlauncher:${MODLAUNCHER_VERSION}") api("net.minecraftforge:coremods:${COREMODS_VERSION}") api("com.mojang:logging:${MOJANG_LOGGING_VERSION}") + api "net.minecraftforge:JarJar:${JARJAR_VERSION}" implementation("net.sf.jopt-simple:jopt-simple:${JOPT_SIMPLE_VERSION}") implementation("cpw.mods:securejarhandler:${SECUREJARHANDLER_VERSION}") diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/EarlyLoadingException.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/EarlyLoadingException.java index e151f2b146..371fe9e44b 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/EarlyLoadingException.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/EarlyLoadingException.java @@ -47,7 +47,7 @@ public class EarlyLoadingException extends RuntimeException { return errorMessages; } - EarlyLoadingException(final String message, final Throwable originalException, List errorMessages) { + public EarlyLoadingException(final String message, final Throwable originalException, List errorMessages) { super(message, originalException); this.errorMessages = errorMessages; } 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 1e705ba693..4529342547 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/ModSorter.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/ModSorter.java @@ -18,7 +18,6 @@ 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.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.slf4j.Logger; @@ -36,14 +35,15 @@ import static net.minecraftforge.fml.loading.LogMarkers.LOADING; public class ModSorter { private static final Logger LOGGER = LogUtils.getLogger(); + private final UniqueModListBuilder uniqueModListBuilder; private List modFiles; private List sortedList; - private Map modIdNameLookup; + private Map modIdNameLookup; private List systemMods; private ModSorter(final List modFiles) { - this.modFiles = modFiles; + this.uniqueModListBuilder = new UniqueModListBuilder(modFiles); } public static LoadingModList sort(List mods, final List errors) @@ -78,11 +78,12 @@ 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 infos = modFiles.stream() .map(ModFile::getModFileInfo) .filter(ModFileInfo.class::isInstance) + .map(ModFileInfo.class::cast) .collect(toMap(Function.identity(), e -> counter.incrementAndGet())); - infos.keySet().forEach(i -> graph.addNode((ModFileInfo) i)); + infos.keySet().forEach(graph::addNode); modFiles.stream() .map(ModFile::getModInfos) .mapMulti(Iterable::forEach) @@ -124,10 +125,9 @@ public class ModSorter private void addDependency(MutableGraph topoGraph, IModInfo.ModVersion dep) { final ModFileInfo self = (ModFileInfo)dep.getOwner().getOwningFile(); - final ModInfo targetModInfo = modIdNameLookup.get(dep.getModId()); + final IModInfo targetModInfo = modIdNameLookup.get(dep.getModId()); // soft dep that doesn't exist. Just return. No edge required. - if (targetModInfo == null) return; - final ModFileInfo target = targetModInfo.getOwningFile(); + if (targetModInfo == null || !(targetModInfo.getOwningFile() instanceof final ModFileInfo target)) return; if (self == target) return; // in case a jar has two mods that have dependencies between switch (dep.getOrdering()) { @@ -138,10 +138,22 @@ public class ModSorter private void buildUniqueList() { - // Collect mod files by module name. This will be used for deduping purposes - final Map> modFilesByFirstId = modFiles.stream() - .collect(groupingBy(mf -> mf.getModFileInfo().moduleName())); + 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) + { // Capture system mods (ex. MC, Forge) here, so we can keep them for later final Set systemMods = new HashSet<>(); // The minecraft mod is always a system mod @@ -168,47 +180,6 @@ public class ModSorter throw new IllegalStateException("Failed to find system mod: " + systemMod); } } - - // 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(toList()); - - // Transform to the full mod id list - final Map> modIds = modFiles.stream() - .map(ModFile::getModInfos) - .flatMap(Collection::stream) - .map(ModInfo.class::cast) - .collect(groupingBy(IModInfo::getModId)); - - // 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.values().stream() - .filter(modInfos -> modInfos.size() > 1) - .map(modInfos -> modInfos.get(0)) - .toList(); - - if (!dupedMods.isEmpty()) { - final List duplicateModErrors = dupedMods.stream() - .map(dm -> new EarlyLoadingException.ExceptionData("fml.modloading.dupedmod", - dm, Objects.toString(dm))).toList(); - throw new EarlyLoadingException("Duplicate mods found", null, duplicateModErrors); - } - - modIdNameLookup = modIds.entrySet().stream() - .collect(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 Map.entry(fullList.getKey(), modInfoList.get(0)); } private List verifyDependencyVersions() diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/UniqueModListBuilder.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/UniqueModListBuilder.java new file mode 100644 index 0000000000..bf8431c247 --- /dev/null +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/UniqueModListBuilder.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.loading; + +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.slf4j.Logger; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.*; + +public class UniqueModListBuilder +{ + private final static Logger LOGGER = LogUtils.getLogger(); + + private final List modFiles; + + public UniqueModListBuilder(final List modFiles) {this.modFiles = modFiles;} + + public UniqueModListData buildUniqueList() + { + List uniqueModList; + List uniqueLibListWithVersion; + + // Collect mod files by module name. This will be used for deduping purposes + final Map> modFilesByFirstId = modFiles.stream() + .filter(mf -> mf.getModFileInfo() != null) + .collect(groupingBy(UniqueModListBuilder::getModId)); + + final Map> libFilesWithVersionByModuleName = modFiles.stream() + .filter(mf -> mf.getModFileInfo() == null) + .collect(groupingBy(UniqueModListBuilder::getModId)); + + // Select the newest by artifact version sorting of non-unique files thus identified + uniqueModList = modFilesByFirstId.entrySet().stream() + .map(this::selectNewestModInfo) + .toList(); + + // Select the newest by artifact version sorting of non-unique files thus identified + uniqueLibListWithVersion = libFilesWithVersionByModuleName.entrySet().stream() + .map(this::selectNewestModInfo) + .toList(); + + // Transform to the full mod id list + final Map> modIds = uniqueModList.stream() + .filter(mf -> mf.getModFileInfo() != null) //Filter out non-mod files, we don't care about those for now..... + .map(ModFile::getModInfos) + .flatMap(Collection::stream) + .collect(groupingBy(IModInfo::getModId)); + + // Transform to the full lib id list + final Map> versionedLibIds = uniqueLibListWithVersion.stream() + .map(UniqueModListBuilder::getModId) + .collect(Collectors.toMap( + Function.identity(), + libFilesWithVersionByModuleName::get + )); + + // 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.values().stream() + .filter(modInfos -> modInfos.size() > 1) + .map(modInfos -> modInfos.get(0)) + .toList(); + + if (!dupedMods.isEmpty()) { + final List duplicateModErrors = dupedMods.stream() + .map(dm -> new EarlyLoadingException.ExceptionData("fml.modloading.dupedmod", dm, Objects.toString(dm))) + .toList(); + throw new EarlyLoadingException("Duplicate mods found", null, duplicateModErrors); + } + + final List dupedLibs = versionedLibIds.values().stream() + .filter(modFiles -> modFiles.size() > 1) + .map(modFiles -> modFiles.get(0)) + .toList(); + + if (!dupedLibs.isEmpty()) { + final List duplicateLibErrors = dupedLibs.stream() + .map(dm -> new EarlyLoadingException.ExceptionData("fml.modloading.dupedlib.versioned", dm, Objects.toString(dm))) + .toList(); + throw new EarlyLoadingException("Duplicate plugins or libraries found", null, duplicateLibErrors); + } + + // Collect unique mod files by module name. This will be used for deduping purposes + final Map> uniqueModFilesByFirstId = uniqueModList.stream() + .collect(groupingBy(UniqueModListBuilder::getModId)); + + final List loadedList = new ArrayList<>(); + loadedList.addAll(uniqueModList); + loadedList.addAll(uniqueLibListWithVersion); + + return new UniqueModListData(loadedList, uniqueModFilesByFirstId); + } + + private ModFile 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(this::getVersion).reversed()); + LOGGER.debug("Selected file {} for modid {} with version {}", modInfoList.get(0).getFileName(), fullList.getKey(), modInfoList.get(0).getModInfos().get(0).getVersion()); + } + return modInfoList.get(0); + } + + private ArtifactVersion getVersion(final ModFile mf) + { + if (mf.getModFileInfo() == null) { + return mf.getJarVersion(); + } + return mf.getModInfos().get(0).getVersion(); + } + + private static String getModId(ModFile modFile) { + if (modFile.getModFileInfo() == null || modFile.getModFileInfo().getMods().isEmpty()) { + return modFile.getSecureJar().name(); + } + + return modFile.getModFileInfo().moduleName(); + } + + public record UniqueModListData(List modFiles, Map> modFilesByFirstId) {} + +} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileDependencyLocator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileDependencyLocator.java new file mode 100644 index 0000000000..8e9c652cd0 --- /dev/null +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileDependencyLocator.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.loading.moddiscovery; + +import com.mojang.logging.LogUtils; +import net.minecraftforge.forgespi.locating.IDependencyLocator; +import net.minecraftforge.forgespi.locating.IModFile; +import org.slf4j.Logger; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +public abstract class AbstractJarFileDependencyLocator extends AbstractJarFileModProvider implements IDependencyLocator +{ + private static final Logger LOGGER = LogUtils.getLogger(); + + protected Optional loadResourceFromModFile(final IModFile modFile, final Path path) { + try { + return Optional.of(Files.newInputStream(modFile.findResource(path.toString()))); + } + catch (final FileNotFoundException e) { + LOGGER.debug("Failed to load resource {} from {}, it does not contain dependency information.", path, modFile.getFileName()); + return Optional.empty(); + } + catch (final Exception e) { + LOGGER.error("Failed to load resource {} from mod {}, cause {}", path, modFile.getFileName(), e); + return Optional.empty(); + } + } + + protected Optional loadModFileFrom(final IModFile file, final Path path) { + try { + final Path pathInModFile = file.findResource(path.toString()); + return createMod(pathInModFile); + } + catch (Exception e) { + LOGGER.error("Failed to load mod file {} from {}", path, file.getFileName()); + return Optional.empty(); + } + } + + protected String identifyMod(final IModFile modFile) { + return modFile.getFileName(); + } +} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileModLocator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileModLocator.java new file mode 100644 index 0000000000..43700f3199 --- /dev/null +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileModLocator.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.loading.moddiscovery; + +import net.minecraftforge.forgespi.locating.IModFile; +import net.minecraftforge.forgespi.locating.IModLocator; + +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public abstract class AbstractJarFileModLocator extends AbstractJarFileModProvider implements IModLocator +{ + @Override + public List scanMods() + { + return scanCandidates() + .map(this::createMod) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + + public abstract Stream scanCandidates(); +} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileLocator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileModProvider.java similarity index 74% rename from fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileLocator.java rename to fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileModProvider.java index 14b0c88591..3a337fd5ff 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileLocator.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractJarFileModProvider.java @@ -7,20 +7,19 @@ package net.minecraftforge.fml.loading.moddiscovery; import com.mojang.logging.LogUtils; import cpw.mods.jarhandling.SecureJar; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; import net.minecraftforge.fml.loading.LogMarkers; import net.minecraftforge.forgespi.locating.IModFile; import org.slf4j.Logger; -public abstract class AbstractJarFileLocator extends AbstractModLocator { +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; + +public abstract class AbstractJarFileModProvider extends AbstractModProvider +{ private static final Logger LOGGER = LogUtils.getLogger(); @Override @@ -34,15 +33,4 @@ public abstract class AbstractJarFileLocator extends AbstractModLocator { } LOGGER.debug(LogMarkers.SCAN,"Scan finished: {}", file); } - - @Override - public List scanMods() { - return scanCandidates() - .map(this::createMod) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); - } - - public abstract Stream scanCandidates(); } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractModLocator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractModProvider.java similarity index 56% rename from fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractModLocator.java rename to fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractModProvider.java index 97b9a3ddc2..c2212627e2 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractModLocator.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/AbstractModProvider.java @@ -13,7 +13,7 @@ import net.minecraftforge.forgespi.language.IConfigurable; import net.minecraftforge.forgespi.language.IModFileInfo; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.forgespi.locating.IModFile; -import net.minecraftforge.forgespi.locating.IModLocator; +import net.minecraftforge.forgespi.locating.IModProvider; import org.slf4j.Logger; import java.nio.file.Path; @@ -24,8 +24,9 @@ import java.util.Optional; import java.util.function.Function; import java.util.jar.Manifest; -public abstract class AbstractModLocator implements IModLocator { - private static final Logger LOGGER = LogUtils.getLogger(); +public abstract class AbstractModProvider implements IModProvider +{ + private static final Logger LOGGER = LogUtils.getLogger(); protected static final String MODS_TOML = "META-INF/mods.toml"; protected static final String MANIFEST = "META-INF/MANIFEST.MF"; @@ -40,12 +41,15 @@ public abstract class AbstractModLocator implements IModLocator { IModFile mod; var type = sj.getManifest().getMainAttributes().getValue(ModFile.TYPE); + if (type == null) { + type = getDefaultJarModType(); + } if (sj.findFile(MODS_TOML).isPresent()) { LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MODS_TOML, type, path); mod = new ModFile(sj, this, ModFileParser::modsTomlParser); } else if (type != null) { LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MANIFEST, type, path); - mod = new ModFile(sj, this, this::manifestParser); + mod = new ModFile(sj, this, this::manifestParser, type); } else { return Optional.empty(); } @@ -68,29 +72,49 @@ public abstract class AbstractModLocator implements IModLocator { } }; - return new IModFileInfo() { - @Override public List getMods() { return Collections.emptyList(); } - @Override public List requiredLanguageLoaders() { return Collections.emptyList(); } - @Override public boolean showAsResourcePack() { return false; } - @Override public Map getFileProperties() { return Collections.emptyMap(); } - @Override public String getLicense() { return license; } - @Override public IModFile getFile() { return mod; } - @Override public IConfigurable getConfig() { return dummy; } - - // These Should never be called as it's only called from ModJarMetadata.version and we bypass that - @Override public String moduleName() { return mod.getSecureJar().name(); } - @Override public String versionString() { return null; } - @Override public List usesServices() { return null; } - - @Override - public String toString() { - return "IModFileInfo(" + mod.getFilePath() + ")"; - } - }; + return new DefaultModFileInfo(mod, license, dummy); } @Override public boolean isValid(final IModFile modFile) { return true; } + + protected String getDefaultJarModType() { + return null; + } + + private record DefaultModFileInfo(IModFile mod, String license, IConfigurable configurable) implements IModFileInfo, IConfigurable { + @Override + public Optional getConfigElement(final String... strings) + { + return Optional.empty(); + } + + @Override + public List getConfigList(final String... strings) + { + return null; + } + + @Override public List getMods() { return Collections.emptyList(); } + @Override public List requiredLanguageLoaders() { return Collections.emptyList(); } + @Override public boolean showAsResourcePack() { return false; } + @Override public Map getFileProperties() { return Collections.emptyMap(); } + @Override + public String getLicense() { return license; } + + @Override public IModFile getFile() { return mod; } + @Override public IConfigurable getConfig() { return configurable; } + + // These Should never be called as it's only called from ModJarMetadata.version and we bypass that + @Override public String moduleName() { return mod.getSecureJar().name(); } + @Override public String versionString() { return null; } + @Override public List usesServices() { return null; } + + @Override + public String toString() { + return "IModFileInfo(" + mod.getFilePath() + ")"; + } + } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ClasspathLocator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ClasspathLocator.java index 0283bf3b01..bf5242cbbe 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ClasspathLocator.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ClasspathLocator.java @@ -24,7 +24,8 @@ import java.util.List; import java.util.Map; import java.util.stream.Stream; -public class ClasspathLocator extends AbstractJarFileLocator { +public class ClasspathLocator extends AbstractJarFileModLocator +{ private static final Logger LOGGER = LogUtils.getLogger(); private final List legacyClasspath = Arrays.stream(System.getProperty("legacyClassPath", "").split(File.pathSeparator)).map(Path::of).toList(); private boolean enabled = false; diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/JarInJarDependencyLocator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/JarInJarDependencyLocator.java new file mode 100644 index 0000000000..4c0c4e6569 --- /dev/null +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/JarInJarDependencyLocator.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.fml.loading.moddiscovery; + +import com.google.common.collect.Lists; +import com.mojang.logging.LogUtils; +import net.minecraftforge.fml.loading.EarlyLoadingException; +import net.minecraftforge.forgespi.language.IModInfo; +import net.minecraftforge.forgespi.locating.IModFile; +import net.minecraftforge.jarjar.selection.JarSelector; +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.artifact.versioning.VersionRange; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class JarInJarDependencyLocator extends AbstractJarFileDependencyLocator +{ + private static final Logger LOGGER = LogUtils.getLogger(); + + @Override + public String name() + { + return "JarInJar"; + } + + @Override + public List scanMods(final Iterable loadedMods) { + final List sources = Lists.newArrayList(); + loadedMods.forEach(sources::add); + + + final List dependenciesToLoad = JarSelector.detectAndSelect( + sources, + this::loadResourceFromModFile, + this::loadModFileFrom, + this::identifyMod, + this::exception + ); + + if (dependenciesToLoad.isEmpty()) { + LOGGER.info("No dependencies to load found. Skipping!"); + return Collections.emptyList(); + } + + LOGGER.info("Found {} dependencies adding them to mods collection", dependenciesToLoad.size()); + return dependenciesToLoad; + } + + @Override + public void initArguments(final Map arguments) + { + // NO-OP, for now + } + + @Override + protected String getDefaultJarModType() + { + return IModFile.Type.GAMELIBRARY.name(); + } + + protected EarlyLoadingException exception(Collection> failedDependencies) + { + + final List errors = failedDependencies.stream() + .filter(entry -> !entry.sources().isEmpty()) //Should never be the case, but just to be sure + .map(entry -> new EarlyLoadingException.ExceptionData( + getErrorTranslationKey(entry), + entry.identifier().group() + ":" + entry.identifier().artifact(), + entry.sources() + .stream() + .flatMap(this::getModWithVersionRangeStream) + .map(this::formatError) + .collect(Collectors.joining(", ")))).toList(); + + return new EarlyLoadingException(failedDependencies.size() + " Dependency restrictions were not met.", null, errors); + } + + @NotNull + private String getErrorTranslationKey(final JarSelector.ResolutionFailureInformation entry) + { + return entry.failureReason() == JarSelector.FailureReason.VERSION_RESOLUTION_FAILED + ? "fml.dependencyloading.conflictingdependencies" + : "fml.dependencyloading.mismatchedcontaineddependencies"; + } + + @NotNull + private Stream getModWithVersionRangeStream(final JarSelector.SourceWithRequestedVersionRange file) + { + return file.source().getModFileInfo().getMods().stream().map(modInfo -> new ModWithVersionRange(modInfo, file.requestedVersionRange(), file.includedVersion())); + } + + @NotNull + private String formatError(final ModWithVersionRange modWithVersionRange) + { + return "\u00a7e" + modWithVersionRange.modInfo().getModId() + "\u00a7r - \u00a74" + + modWithVersionRange.versionRange().toString() + "\u00a74 - \u00a72" + + modWithVersionRange.artifactVersion().toString() + "\u00a72"; + } + + private record ModWithVersionRange(IModInfo modInfo, VersionRange versionRange, ArtifactVersion artifactVersion) {} +} diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MavenDirectoryLocator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MavenDirectoryLocator.java index 94ebc4d07d..e8afa04a53 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MavenDirectoryLocator.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MavenDirectoryLocator.java @@ -15,7 +15,8 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; -public class MavenDirectoryLocator extends AbstractJarFileLocator { +public class MavenDirectoryLocator extends AbstractJarFileModLocator +{ private List modCoords; @Override diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MinecraftLocator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MinecraftLocator.java index ea527c050b..0fe7a4386b 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MinecraftLocator.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/MinecraftLocator.java @@ -12,6 +12,7 @@ import net.minecraftforge.fml.loading.FMLLoader; import net.minecraftforge.fml.loading.LogMarkers; import net.minecraftforge.forgespi.language.IModFileInfo; import net.minecraftforge.forgespi.locating.IModFile; +import net.minecraftforge.forgespi.locating.IModLocator; import net.minecraftforge.forgespi.locating.ModFileFactory; import org.slf4j.Logger; @@ -22,7 +23,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; -public class MinecraftLocator extends AbstractModLocator { +public class MinecraftLocator extends AbstractModProvider implements IModLocator +{ private static final Logger LOGGER = LogUtils.getLogger(); @Override 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 db90d1881c..71e3bf301d 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 @@ -5,13 +5,18 @@ package net.minecraftforge.fml.loading.moddiscovery; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; import com.mojang.logging.LogUtils; import cpw.mods.modlauncher.Launcher; import cpw.mods.modlauncher.api.IModuleLayerManager; import cpw.mods.modlauncher.util.ServiceLoaderUtils; +import net.minecraftforge.fml.loading.EarlyLoadingException; import net.minecraftforge.fml.loading.LogMarkers; +import net.minecraftforge.fml.loading.UniqueModListBuilder; import net.minecraftforge.fml.loading.progress.StartupMessageManager; import net.minecraftforge.forgespi.Environment; +import net.minecraftforge.forgespi.locating.IDependencyLocator; import net.minecraftforge.forgespi.locating.IModFile; import net.minecraftforge.forgespi.locating.IModLocator; import org.slf4j.Logger; @@ -24,47 +29,130 @@ import java.util.stream.Collectors; public class ModDiscoverer { private static final Logger LOGGER = LogUtils.getLogger(); - private final ServiceLoader locators; - private final List locatorList; + private final ServiceLoader modLocators; + private final ServiceLoader dependencyLocators; + private final List modLocatorList; + private final List dependencyLocatorList; public ModDiscoverer(Map arguments) { Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.MODDIRECTORYFACTORY.get(), v->ModsFolderLocator::new); Launcher.INSTANCE.environment().computePropertyIfAbsent(Environment.Keys.PROGRESSMESSAGE.get(), v-> StartupMessageManager.locatorConsumer().orElseGet(()-> s->{})); final var moduleLayerManager = Launcher.INSTANCE.environment().findModuleLayerManager().orElseThrow(); - locators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IModLocator.class); - locatorList = ServiceLoaderUtils.streamServiceLoader(()->locators, sce->LOGGER.error("Failed to load locator list", sce)).collect(Collectors.toList()); - locatorList.forEach(l->l.initArguments(arguments)); + modLocators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IModLocator.class); + dependencyLocators = ServiceLoader.load(moduleLayerManager.getLayer(IModuleLayerManager.Layer.SERVICE).orElseThrow(), IDependencyLocator.class); + modLocatorList = ServiceLoaderUtils.streamServiceLoader(()-> modLocators, sce->LOGGER.error("Failed to load mod locator list", sce)).collect(Collectors.toList()); + modLocatorList.forEach(l->l.initArguments(arguments)); + dependencyLocatorList = ServiceLoaderUtils.streamServiceLoader(()-> dependencyLocators, sce->LOGGER.error("Failed to load dependency locator list", sce)).collect(Collectors.toList()); + dependencyLocatorList.forEach(l->l.initArguments(arguments)); if (LOGGER.isDebugEnabled(LogMarkers.CORE)) { - LOGGER.debug(LogMarkers.CORE, "Found Mod Locators : {}", locatorList.stream() - .map(iModLocator -> "(" + iModLocator.name() + ":" + iModLocator.getClass().getPackage().getImplementationVersion() + ")").collect(Collectors.joining(","))); + LOGGER.debug(LogMarkers.CORE, "Found Mod Locators : {}", modLocatorList.stream() + .map(modLocator -> "(%s:%s)".formatted(modLocator.name(), + modLocator.getClass().getPackage().getImplementationVersion())).collect(Collectors.joining(","))); + } + if (LOGGER.isDebugEnabled(LogMarkers.CORE)) + { + LOGGER.debug(LogMarkers.CORE, "Found Dependency Locators : {}", dependencyLocatorList.stream() + .map(dependencyLocator -> "(%s:%s)".formatted(dependencyLocator.name(), + dependencyLocator.getClass().getPackage().getImplementationVersion())).collect(Collectors.joining(","))); } - } - - ModDiscoverer(List locatorList) { - this.locatorList = locatorList; - this.locators = null; } public ModValidator discoverMods() { - LOGGER.debug(LogMarkers.SCAN,"Scanning for mods and other resources to load. We know {} ways to find mods", locatorList.size()); - var loadedFiles = new ArrayList<>(); - for (IModLocator locator : locatorList) { - LOGGER.debug(LogMarkers.SCAN,"Trying locator {}", locator); - var modFiles = locator.scanMods(); - for (IModFile mf : modFiles) { - LOGGER.info(LogMarkers.SCAN, "Found mod file {} of type {} with provider {}", mf.getFileName(), mf.getType(), mf.getProvider()); - StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Found mod file "+mf.getFileName()+" of type "+mf.getType())); - } - loadedFiles.addAll(modFiles); - } - final var modFilesMap = loadedFiles.stream() - .map(ModFile.class::cast) - .collect(Collectors.groupingBy(IModFile::getType)); + LOGGER.debug(LogMarkers.SCAN,"Scanning for mods and other resources to load. We know {} ways to find mods", modLocatorList.size()); + List loadedFiles = new ArrayList<>(); + List discoveryErrorData = new ArrayList<>(); + boolean successfullyLoadedMods = true; - var validator = new ModValidator(modFilesMap); + //Loop all mod locators to get the prime mods to load from. + for (IModLocator locator : modLocatorList) { + try { + LOGGER.debug(LogMarkers.SCAN,"Trying locator {}", locator); + var locatedFiles = locator.scanMods(); + + if (locatedFiles.stream().anyMatch(file -> !(file instanceof ModFile))) { + LOGGER.error(LogMarkers.SCAN, "A mod locator returned a file which is not a ModFile instance!. They will be skipped!"); + } + + handleLocatedFiles(loadedFiles, locatedFiles); + } catch (EarlyLoadingException exception) { + LOGGER.error(LogMarkers.SCAN, "Failed to load mods with locator {}", locator, exception); + discoveryErrorData.addAll(exception.getAllData()); + } + } + + //First processing run of the mod list. Any duplicates will cause resolution failure and dependency loading will be skipped. + Map> modFilesMap = Maps.newHashMap(); + try { + final UniqueModListBuilder modsUniqueListBuilder = new UniqueModListBuilder(loadedFiles); + final UniqueModListBuilder.UniqueModListData uniqueModsData = modsUniqueListBuilder.buildUniqueList(); + + //Grab the temporary results. + //This allows loading to continue to a base state, in case dependency loading fails. + modFilesMap = uniqueModsData.modFiles().stream() + .collect(Collectors.groupingBy(IModFile::getType)); + loadedFiles = uniqueModsData.modFiles(); + } + catch (EarlyLoadingException exception) { + LOGGER.error(LogMarkers.SCAN, "Failed to build unique mod list after mod discovery.", exception); + discoveryErrorData.addAll(exception.getAllData()); + successfullyLoadedMods = false; + } + + //We can continue loading if prime mods loaded successfully. + if (successfullyLoadedMods) { + LOGGER.debug(LogMarkers.SCAN, "Successfully Loaded {} mods. Attempting to load dependencies...", loadedFiles.size()); + for (IDependencyLocator locator : dependencyLocatorList) { + try { + LOGGER.debug(LogMarkers.SCAN,"Trying locator {}", locator); + final List locatedMods = ImmutableList.copyOf(loadedFiles); + + var locatedFiles = locator.scanMods(locatedMods); + if (locatedFiles.stream().anyMatch(file -> !(file instanceof ModFile))) { + LOGGER.error(LogMarkers.SCAN, "A dependency locator returned a file which is not a ModFile instance!. They will be skipped!"); + } + + handleLocatedFiles(loadedFiles, locatedFiles); + } + catch (EarlyLoadingException exception) { + LOGGER.error(LogMarkers.SCAN, "Failed to load dependencies with locator {}", locator, exception); + discoveryErrorData.addAll(exception.getAllData()); + } + } + + //Second processing run of the mod list. Any duplicates will cause resolution failure and only the mods list will be loaded. + try { + final UniqueModListBuilder modsAndDependenciesUniqueListBuilder = new UniqueModListBuilder(loadedFiles); + final UniqueModListBuilder.UniqueModListData uniqueModsAndDependenciesData = modsAndDependenciesUniqueListBuilder.buildUniqueList(); + + //We now only need the mod files map, not the list. + modFilesMap = uniqueModsAndDependenciesData.modFiles().stream() + .collect(Collectors.groupingBy(IModFile::getType)); + } catch (EarlyLoadingException exception) { + LOGGER.error(LogMarkers.SCAN, "Failed to build unique mod list after dependency discovery.", exception); + discoveryErrorData.addAll(exception.getAllData()); + modFilesMap = loadedFiles.stream().collect(Collectors.groupingBy(IModFile::getType)); + } + } + else { + //Failure notify the listeners. + LOGGER.error(LogMarkers.SCAN, "Mod Discovery failed. Skipping dependency discovery."); + } + + //Validate the loading. With a deduplicated list, we can now successfully process the artifacts and load + //transformer plugins. + var validator = new ModValidator(modFilesMap, discoveryErrorData); validator.stage1Validation(); return validator; } + private void handleLocatedFiles(final List loadedFiles, final List locatedFiles) + { + var locatedModFiles = locatedFiles.stream().filter(ModFile.class::isInstance).map(ModFile.class::cast).toList(); + for (IModFile mf : locatedModFiles) { + LOGGER.info(LogMarkers.SCAN, "Found mod file {} of type {} with provider {}", mf.getFileName(), mf.getType(), mf.getProvider()); + StartupMessageManager.modLoaderConsumer().ifPresent(c->c.accept("Found mod file "+mf.getFileName()+" of type "+mf.getType())); + } + loadedFiles.addAll(locatedModFiles); + } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFile.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFile.java index d7fef5beff..888f046038 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFile.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFile.java @@ -18,6 +18,8 @@ import net.minecraftforge.forgespi.language.ModFileScanData; import net.minecraftforge.forgespi.locating.IModFile; import net.minecraftforge.forgespi.locating.IModProvider; import net.minecraftforge.forgespi.locating.ModFileFactory; +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.slf4j.Logger; import java.nio.file.Files; @@ -51,9 +53,9 @@ public class ModFile implements IModFile { private Throwable scanError; private final SecureJar jar; private final Type modFileType; - private final Manifest manifest; + private final Manifest manifest; private final IModProvider provider; - private IModFileInfo modFileInfo; + private IModFileInfo modFileInfo; private ModFileScanData fileModFileScanData; private CompletableFuture futureScanResult; private List coreMods; @@ -63,14 +65,18 @@ public class ModFile implements IModFile { private SecureJar.Status securityStatus; public ModFile(final SecureJar jar, final IModProvider provider, final ModFileFactory.ModFileInfoParser parser) { + this(jar, provider, parser, parseType(jar)); + } + + public ModFile(final SecureJar jar, final IModProvider provider, final ModFileFactory.ModFileInfoParser parser, String type) { this.provider = provider; this.jar = jar; this.parser = parser; manifest = this.jar.getManifest(); - final Optional value = Optional.ofNullable(manifest.getMainAttributes().getValue(TYPE)); - modFileType = Type.valueOf(value.orElse("MOD")); + modFileType = Type.valueOf(type); jarVersion = Optional.ofNullable(manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION)).orElse("0.0NONE"); + this.modFileInfo = ModFileParser.readModList(this, this.parser); } @Override @@ -189,7 +195,7 @@ public class ModFile implements IModFile { @Override public IModProvider getProvider() { - return this.provider; + return provider; } @Override @@ -201,4 +207,15 @@ public class ModFile implements IModFile { public void setSecurityStatus(final SecureJar.Status status) { this.securityStatus = status; } + + public ArtifactVersion getJarVersion() + { + return new DefaultArtifactVersion(this.jarVersion); + } + + private static String parseType(final SecureJar jar) { + final Manifest m = jar.getManifest(); + final Optional value = Optional.ofNullable(m.getMainAttributes().getValue(TYPE)); + return value.orElse("MOD"); + } } diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModValidator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModValidator.java index a830ce0f85..b7417fb6fe 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModValidator.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModValidator.java @@ -26,13 +26,15 @@ public class ModValidator { private final List candidateMods; private LoadingModList loadingModList; private List brokenFiles; + private final List discoveryErrorData; - public ModValidator(final Map> modFiles) { + public ModValidator(final Map> modFiles, final List discoveryErrorData) { this.modFiles = modFiles; this.candidateMods = lst(modFiles.get(IModFile.Type.MOD)); this.candidateMods.addAll(lst(modFiles.get(IModFile.Type.GAMELIBRARY))); this.candidatePlugins = lst(modFiles.get(IModFile.Type.LANGPROVIDER)); this.candidatePlugins.addAll(lst(modFiles.get(IModFile.Type.LIBRARY))); + this.discoveryErrorData = discoveryErrorData; } private static List lst(List files) { @@ -52,22 +54,22 @@ public class ModValidator { final List brokenFiles = new ArrayList<>(); for (Iterator iterator = mods.iterator(); iterator.hasNext(); ) { - ModFile mod = iterator.next(); - if (!mod.getProvider().isValid(mod) || !mod.identifyMods()) { - LOGGER.warn(LogMarkers.SCAN, "File {} has been ignored - it is invalid", mod.getFilePath()); + ModFile modFile = iterator.next(); + if (!modFile.getProvider().isValid(modFile) || !modFile.identifyMods()) { + LOGGER.warn(LogMarkers.SCAN, "File {} has been ignored - it is invalid", modFile.getFilePath()); iterator.remove(); - brokenFiles.add(mod); + brokenFiles.add(modFile); } } return brokenFiles; } public ITransformationService.Resource getPluginResources() { - return new ITransformationService.Resource(IModuleLayerManager.Layer.PLUGIN, this.candidatePlugins.stream().map(ModFile::getSecureJar).toList()); + return new ITransformationService.Resource(IModuleLayerManager.Layer.PLUGIN, this.candidatePlugins.stream().map(IModFile::getSecureJar).toList()); } public ITransformationService.Resource getModResources() { - return new ITransformationService.Resource(IModuleLayerManager.Layer.GAME, this.candidateMods.stream().map(ModFile::getSecureJar).toList()); + return new ITransformationService.Resource(IModuleLayerManager.Layer.GAME, this.candidateMods.stream().map(IModFile::getSecureJar).toList()); } private List validateLanguages() { @@ -86,7 +88,11 @@ public class ModValidator { public BackgroundScanHandler stage2Validation() { var errors = validateLanguages(); - loadingModList = ModSorter.sort(candidateMods, errors); + + var allErrors = new ArrayList<>(errors); + allErrors.addAll(this.discoveryErrorData); + + loadingModList = ModSorter.sort(candidateMods, allErrors); loadingModList.addCoreMods(); loadingModList.addAccessTransformers(); loadingModList.setBrokenFiles(brokenFiles); diff --git a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java index 7d77055411..a5999a2115 100644 --- a/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java +++ b/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModsFolderLocator.java @@ -23,7 +23,8 @@ import static cpw.mods.modlauncher.api.LamdbaExceptionUtils.uncheck; /** * Support loading mods located in JAR files in the mods folder */ -public class ModsFolderLocator extends AbstractJarFileLocator { +public class ModsFolderLocator extends AbstractJarFileModLocator +{ private static final String SUFFIX = ".jar"; private static final Logger LOGGER = LogUtils.getLogger(); private final Path modFolder; diff --git a/fmlloader/src/main/resources/META-INF/services/net.minecraftforge.forgespi.locating.IDependencyLocator b/fmlloader/src/main/resources/META-INF/services/net.minecraftforge.forgespi.locating.IDependencyLocator new file mode 100644 index 0000000000..feaf7ec746 --- /dev/null +++ b/fmlloader/src/main/resources/META-INF/services/net.minecraftforge.forgespi.locating.IDependencyLocator @@ -0,0 +1 @@ +net.minecraftforge.fml.loading.moddiscovery.JarInJarDependencyLocator \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/client/gui/LoadingErrorScreen.java b/src/main/java/net/minecraftforge/client/gui/LoadingErrorScreen.java index a4d8e4b124..543dce0466 100644 --- a/src/main/java/net/minecraftforge/client/gui/LoadingErrorScreen.java +++ b/src/main/java/net/minecraftforge/client/gui/LoadingErrorScreen.java @@ -94,7 +94,11 @@ public class LoadingErrorScreen extends ErrorScreen { } public static class LoadingEntryList extends ObjectSelectionList { LoadingEntryList(final LoadingErrorScreen parent, final List errors, final List warnings) { - super(parent.minecraft, parent.width, parent.height, 35, parent.height - 50, 2 * parent.minecraft.font.lineHeight + 8); + super(Objects.requireNonNull(parent.minecraft), parent.width, parent.height, 35, parent.height - 50, + Math.max( + errors.stream().mapToInt(error -> parent.font.split(Component.literal(error.getMessage() != null ? error.getMessage() : ""), parent.width - 20).size()).max().orElse(0), + warnings.stream().mapToInt(warning -> parent.font.split(Component.literal(warning.formatToString() != null ? warning.formatToString() : ""), parent.width - 20).size()).max().orElse(0) + ) * parent.minecraft.font.lineHeight + 8); boolean both = !errors.isEmpty() && !warnings.isEmpty(); if (both) addEntry(new LoadingMessageEntry(parent.errorHeader, true)); @@ -140,13 +144,14 @@ public class LoadingErrorScreen extends ErrorScreen { @Override public void render(PoseStack poseStack, int entryIdx, int top, int left, final int entryWidth, final int entryHeight, final int mouseX, final int mouseY, final boolean p_194999_5_, final float partialTick) { Font font = Minecraft.getInstance().font; - final List strings = font.split(message, LoadingEntryList.this.width); + final List strings = font.split(message, LoadingEntryList.this.width - 20); int y = top + 2; - for (int i = 0; i < Math.min(strings.size(), 2); i++) { + for (FormattedCharSequence string : strings) + { if (center) - font.draw(poseStack, strings.get(i), left + (width) - font.width(strings.get(i)) / 2F, y, 0xFFFFFF); + font.draw(poseStack, string, left + (width) - font.width(string) / 2F, y, 0xFFFFFF); else - font.draw(poseStack, strings.get(i), left + 5, y, 0xFFFFFF); + font.draw(poseStack, string, left + 5, y, 0xFFFFFF); y += font.lineHeight; } } diff --git a/src/main/java/net/minecraftforge/client/gui/ModListScreen.java b/src/main/java/net/minecraftforge/client/gui/ModListScreen.java index d6651575d6..6169648056 100644 --- a/src/main/java/net/minecraftforge/client/gui/ModListScreen.java +++ b/src/main/java/net/minecraftforge/client/gui/ModListScreen.java @@ -20,6 +20,8 @@ import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.renderer.GameRenderer; import net.minecraftforge.client.gui.widget.ModListWidget; +import net.minecraftforge.fml.loading.moddiscovery.ModFile; +import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; import net.minecraftforge.resource.PathResourcePack; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; @@ -450,7 +452,7 @@ public class ModListScreen extends Screen if (vercheck.status() == VersionChecker.Status.OUTDATED || vercheck.status() == VersionChecker.Status.BETA_OUTDATED) lines.add(ForgeI18n.parseMessage("fml.menu.mods.info.updateavailable", vercheck.url() == null ? "" : vercheck.url())); - lines.add(ForgeI18n.parseMessage("fml.menu.mods.info.license", selectedMod.getOwningFile().getLicense())); + lines.add(ForgeI18n.parseMessage("fml.menu.mods.info.license", ((ModFileInfo) selectedMod.getOwningFile()).getLicense())); lines.add(null); lines.add(selectedMod.getDescription()); diff --git a/src/main/resources/assets/forge/lang/en_us.json b/src/main/resources/assets/forge/lang/en_us.json index b9ce92ec8c..8437ad38c1 100644 --- a/src/main/resources/assets/forge/lang/en_us.json +++ b/src/main/resources/assets/forge/lang/en_us.json @@ -67,6 +67,8 @@ "fml.modloading.failedtoloadforge": "Failed to load Forge", "fml.modloading.missingdependency": "Mod \u00a7e{4}\u00a7r requires \u00a76{3}\u00a7r \u00a7o{5,vr}\u00a7r\n\u00a77Currently, \u00a76{3}\u00a7r\u00a77 is \u00a7o{6,i18n,fml.messages.artifactversion.ornotinstalled}", "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.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", diff --git a/src/test/resources/META-INF/jarjar/ForgeJiJTestMod-1.2.jar b/src/test/resources/META-INF/jarjar/ForgeJiJTestMod-1.2.jar new file mode 100644 index 0000000000000000000000000000000000000000..df2812e60d0ac49c0ccd45b8ced68e7efbe8ed69 GIT binary patch literal 26146 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