diff --git a/core/src/main/java/net/neoforged/fml/ModLoader.java b/core/src/main/java/net/neoforged/fml/ModLoader.java index 07e20ef15..176280e07 100644 --- a/core/src/main/java/net/neoforged/fml/ModLoader.java +++ b/core/src/main/java/net/neoforged/fml/ModLoader.java @@ -100,6 +100,11 @@ private ModLoader() 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()); + + FMLLoader.getLoadingModList().getWarnings().stream() + .flatMap(ModLoadingWarning::fromEarlyException) + .forEach(this.loadingWarnings::add); + FMLLoader.getLoadingModList().getModFiles().stream() .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 diff --git a/core/src/main/java/net/neoforged/fml/ModLoadingWarning.java b/core/src/main/java/net/neoforged/fml/ModLoadingWarning.java index 78aba2969..2d2d7d22b 100644 --- a/core/src/main/java/net/neoforged/fml/ModLoadingWarning.java +++ b/core/src/main/java/net/neoforged/fml/ModLoadingWarning.java @@ -6,6 +6,7 @@ package net.neoforged.fml; import com.google.common.collect.Streams; +import net.neoforged.fml.loading.EarlyLoadingException; import net.neoforged.neoforgespi.language.IModInfo; import java.util.Arrays; @@ -43,4 +44,8 @@ public ModLoadingWarning(final IModInfo modInfo, final ModLoadingStage warningSt public String formatToString() { return Bindings.getMessageParser().get().parseMessage(i18nMessage, Streams.concat(Stream.of(modInfo, warningStage), context.stream()).toArray()); } + + static Stream fromEarlyException(final EarlyLoadingException e) { + return e.getAllData().stream().map(ed->new ModLoadingWarning(ed.getModInfo(), ModLoadingStage.VALIDATE, ed.getI18message(), ed.getArgs())); + } } diff --git a/gradle.properties b/gradle.properties index 28f6f294a..5094022d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -spi_version=8.0.0 +spi_version=8.0.6-breaks-clause mergetool_version=2.0.0 accesstransformers_version=10.0.1 coremods_version=6.0.0 diff --git a/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java b/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java index 1b2e021b3..b790639c5 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java +++ b/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java @@ -36,6 +36,7 @@ public class LoadingModList private final List sortedList; private final Map fileById; private final List preLoadErrors; + private final List preLoadWarnings; private List brokenFiles; private LoadingModList(final List modFiles, final List sortedList) @@ -53,6 +54,7 @@ private LoadingModList(final List modFiles, final List sortedL .map(ModInfo.class::cast) .collect(Collectors.toMap(ModInfo::getModId, ModInfo::getOwningFile)); this.preLoadErrors = new ArrayList<>(); + this.preLoadWarnings = new ArrayList<>(); } public static LoadingModList of(List modFiles, List sortedList, final EarlyLoadingException earlyLoadingException) @@ -170,6 +172,10 @@ public List getErrors() { return preLoadErrors; } + public List getWarnings() { + return preLoadWarnings; + } + public void setBrokenFiles(final List brokenFiles) { this.brokenFiles = brokenFiles; } diff --git a/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java b/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java index 153bfab86..fa7adb620 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java +++ b/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java @@ -52,11 +52,13 @@ public static LoadingModList sort(List mods, final List(ModInfo)mf.getModInfos().get(0)).collect(toList()), e); } + final List warnings = new ArrayList<>(); // try and validate dependencies - final List failedList = Stream.concat(ms.verifyDependencyVersions().stream(), errors.stream()).toList(); + final List failedList = Stream.concat(ms.verifyDependencyVersions(warnings).stream(), errors.stream()).toList(); // if we miss one or the other, we abort now + final LoadingModList list; if (!failedList.isEmpty()) { - return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf->(ModInfo)mf.getModInfos().get(0)).collect(toList()), new EarlyLoadingException("failure to validate mod list", null, failedList)); + list = LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf->(ModInfo)mf.getModInfos().get(0)).collect(toList()), new EarlyLoadingException("failure to validate mod list", null, failedList)); } else { // Otherwise, lets try and sort the modlist and proceed EarlyLoadingException earlyLoadingException = null; @@ -65,8 +67,17 @@ public static LoadingModList sort(List mods, final List> modFilesByFirstId } } - private List verifyDependencyVersions() + private List verifyDependencyVersions(final List warnings) { final var modVersions = modFiles.stream() .map(ModFile::getModInfos) @@ -197,21 +208,45 @@ private List verifyDependencyVersions() .filter(mv -> mv.getSide().isCorrectSide()) .collect(toSet()); - final long mandatoryRequired = modRequirements.stream().filter(IModInfo.ModVersion::isMandatory).count(); + final long mandatoryRequired = modRequirements.stream().filter(ver -> ver.getType() == IModInfo.DependencyType.REQUIRED).count(); LOGGER.debug(LogMarkers.LOADING, "Found {} mod requirements ({} mandatory, {} optional)", modRequirements.size(), mandatoryRequired, modRequirements.size() - mandatoryRequired); final var missingVersions = modRequirements.stream() - .filter(mv -> (mv.isMandatory() || modVersions.containsKey(mv.getModId())) && this.modVersionNotContained(mv, modVersions)) + .filter(mv -> (mv.getType() == IModInfo.DependencyType.REQUIRED || (modVersions.containsKey(mv.getModId()) && mv.getType() == IModInfo.DependencyType.OPTIONAL)) && this.modVersionNotContained(mv, modVersions)) .collect(toSet()); - final long mandatoryMissing = missingVersions.stream().filter(IModInfo.ModVersion::isMandatory).count(); + final long mandatoryMissing = missingVersions.stream().filter(mv -> mv.getType() == IModInfo.DependencyType.REQUIRED).count(); LOGGER.debug(LogMarkers.LOADING, "Found {} mod requirements missing ({} mandatory, {} optional)", missingVersions.size(), mandatoryMissing, missingVersions.size() - mandatoryMissing); - if (!missingVersions.isEmpty()) { + final var incompatibleVersions = modRequirements.stream().filter(ver -> ver.getType() == IModInfo.DependencyType.INCOMPATIBLE) + .filter(ver -> modVersions.containsKey(ver.getModId()) && !this.modVersionNotContained(ver, modVersions)) + .toList(); + + final var conflictingVersions = modRequirements.stream().filter(ver -> ver.getType() == IModInfo.DependencyType.CONFLICTING) + .filter(ver -> modVersions.containsKey(ver.getModId()) && !this.modVersionNotContained(ver, modVersions)) + .toList(); + + if (!conflictingVersions.isEmpty()) { + LOGGER.error( + LogMarkers.LOADING, + "Conflicts between mods:\n{}\nIssues may arise. Continue at your own risk.", + conflictingVersions.stream() + .map(ver -> formatIncompatibleDependencyError(ver, "Conflicting", modVersions)) + .collect(Collectors.joining("\n")) + ); + + conflictingVersions.stream() + .map(mv -> new EarlyLoadingException.ExceptionData("fml.modloading.conflictingmod", + mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), + modVersions.get(mv.getModId()))) + .forEach(warnings::add); + } + + if (!missingVersions.isEmpty() || !incompatibleVersions.isEmpty()) { if (mandatoryMissing > 0) { LOGGER.error( LogMarkers.LOADING, "Missing or unsupported mandatory dependencies:\n{}", missingVersions.stream() - .filter(IModInfo.ModVersion::isMandatory) + .filter(mv -> mv.getType() == IModInfo.DependencyType.REQUIRED) .map(ver -> formatDependencyError(ver, modVersions)) .collect(Collectors.joining("\n")) ); @@ -221,16 +256,32 @@ private List verifyDependencyVersions() LogMarkers.LOADING, "Unsupported installed optional dependencies:\n{}", missingVersions.stream() - .filter(ver -> !ver.isMandatory()) + .filter(ver -> ver.getType() == IModInfo.DependencyType.OPTIONAL) .map(ver -> formatDependencyError(ver, modVersions)) .collect(Collectors.joining("\n")) ); } - return missingVersions.stream() - .map(mv -> new EarlyLoadingException.ExceptionData(mv.isMandatory() ? "fml.modloading.missingdependency" : "fml.modloading.missingdependency.optional", - mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), - modVersions.getOrDefault(mv.getModId(), new DefaultArtifactVersion("null")))) + if (!incompatibleVersions.isEmpty()) { + LOGGER.error( + LogMarkers.LOADING, + "Incompatibilities between mods:\n{}", + incompatibleVersions.stream() + .map(ver -> formatIncompatibleDependencyError(ver, "Incompatible", modVersions)) + .collect(Collectors.joining("\n")) + ); + } + + return Stream.concat( + missingVersions.stream() + .map(mv -> new EarlyLoadingException.ExceptionData(mv.getType() == IModInfo.DependencyType.REQUIRED ? "fml.modloading.missingdependency" : "fml.modloading.missingdependency.optional", + mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), + modVersions.getOrDefault(mv.getModId(), new DefaultArtifactVersion("null")))), + incompatibleVersions.stream() + .map(mv -> new EarlyLoadingException.ExceptionData("fml.modloading.incompatiblemod", + mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), + modVersions.get(mv.getModId()))) + ) .toList(); } return Collections.emptyList(); @@ -248,6 +299,18 @@ private static String formatDependencyError(IModInfo.ModVersion dependency, Map< ); } + private static String formatIncompatibleDependencyError(IModInfo.ModVersion dependency, String type, Map modVersions) + { + return String.format( + "\tMod ID: '%s', %s with: '%s', versions: '%s', Actual version: '%s'", + dependency.getModId(), + type, + dependency.getOwner().getModId(), + dependency.getVersionRange(), + modVersions.get(dependency.getModId()).toString() + ); + } + private boolean modVersionNotContained(final IModInfo.ModVersion mv, final Map modVersions) { return !(VersionSupportMatrix.testVersionSupportMatrix(mv.getVersionRange(), mv.getModId(), "mod", (modId, range) -> modVersions.containsKey(modId) && diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModInfo.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModInfo.java index 0ef9acadc..c2d1e1c46 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModInfo.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModInfo.java @@ -20,6 +20,7 @@ import java.net.URL; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; @@ -204,7 +205,7 @@ class ModVersion implements net.neoforged.neoforgespi.language.IModInfo.ModVersi private IModInfo owner; private final String modId; private final VersionRange versionRange; - private final boolean mandatory; + private final DependencyType type; private final Ordering ordering; private final DependencySide side; private Optional referralUrl; @@ -213,8 +214,8 @@ public ModVersion(final IModInfo owner, final IConfigurable config) { this.owner = owner; this.modId = config.getConfigElement("modId") .orElseThrow(()->new InvalidModFileException("Missing required field modid in dependency", getOwningFile())); - this.mandatory = config.getConfigElement("mandatory") - .orElseThrow(()->new InvalidModFileException("Missing required field mandatory in dependency", getOwningFile())); + this.type = config.getConfigElement("type") + .map(str -> str.toUpperCase(Locale.ROOT)).map(DependencyType::valueOf).orElse(DependencyType.REQUIRED); this.versionRange = config.getConfigElement("versionRange") .map(MavenVersionAdapter::createFromVersionSpec) .orElse(UNBOUNDED); @@ -242,9 +243,8 @@ public VersionRange getVersionRange() } @Override - public boolean isMandatory() - { - return mandatory; + public DependencyType getType() { + return type; } @Override