diff --git a/gradle.properties b/gradle.properties index 5094022d9..f94f971f5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -spi_version=8.0.6-breaks-clause +spi_version=8.0.7-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/ModSorter.java b/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java index fa7adb620..8461e790b 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java +++ b/loader/src/main/java/net/neoforged/fml/loading/ModSorter.java @@ -52,13 +52,17 @@ 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(warnings).stream(), errors.stream()).toList(); - // if we miss one or the other, we abort now + final DependencyResolutionResult resolutionResult = ms.verifyDependencyVersions(); + + final List warnings = new ArrayList<>(); + final LoadingModList list; - if (!failedList.isEmpty()) { - 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)); + + // if we miss a dependency or detect an incompatibility, we abort now + if (!resolutionResult.versionResolution.isEmpty() || !resolutionResult.incompatibilities.isEmpty()) { + 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, resolutionResult.buildErrorMessages())); } else { // Otherwise, lets try and sort the modlist and proceed EarlyLoadingException earlyLoadingException = null; @@ -70,11 +74,12 @@ public static LoadingModList sort(List mods, final List> modFilesByFirstId } } - private List verifyDependencyVersions(final List warnings) + public record DependencyResolutionResult( + Collection incompatibilities, + Collection conflicts, + Collection versionResolution, + Map modVersions + ) { + public List buildWarningMessages() { + return conflicts.stream() + .map(mv -> new EarlyLoadingException.ExceptionData("fml.modloading.conflictingmod", + mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), + modVersions.get(mv.getModId()), mv.getReason().orElse("fml.modloading.conflictingmod.noreason"))) + .toList(); + } + + public List buildErrorMessages() { + return Stream.concat( + versionResolution.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")))), + incompatibilities.stream() + .map(mv -> new EarlyLoadingException.ExceptionData("fml.modloading.incompatiblemod", + mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), + modVersions.get(mv.getModId()), mv.getReason().orElse("fml.modloading.incompatiblemod.noreason"))) + ) + .toList(); + } + } + + private DependencyResolutionResult verifyDependencyVersions() { final var modVersions = modFiles.stream() .map(ModFile::getModInfos) @@ -218,73 +252,54 @@ private List verifyDependencyVersions(final final var incompatibleVersions = modRequirements.stream().filter(ver -> ver.getType() == IModInfo.DependencyType.INCOMPATIBLE) .filter(ver -> modVersions.containsKey(ver.getModId()) && !this.modVersionNotContained(ver, modVersions)) - .toList(); + .collect(toSet()); final var conflictingVersions = modRequirements.stream().filter(ver -> ver.getType() == IModInfo.DependencyType.CONFLICTING) .filter(ver -> modVersions.containsKey(ver.getModId()) && !this.modVersionNotContained(ver, modVersions)) - .toList(); + .collect(toSet()); if (!conflictingVersions.isEmpty()) { LOGGER.error( LogMarkers.LOADING, - "Conflicts between mods:\n{}\nIssues may arise. Continue at your own risk.", + "Conflicts between mods:\n{}\n\tIssues may arise. Continue at your own risk.", conflictingVersions.stream() - .map(ver -> formatIncompatibleDependencyError(ver, "Conflicting", modVersions)) + .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(mv -> mv.getType() == IModInfo.DependencyType.REQUIRED) - .map(ver -> formatDependencyError(ver, modVersions)) - .collect(Collectors.joining("\n")) - ); - } - if (missingVersions.size() - mandatoryMissing > 0) { - LOGGER.error( - LogMarkers.LOADING, - "Unsupported installed optional dependencies:\n{}", - missingVersions.stream() - .filter(ver -> ver.getType() == IModInfo.DependencyType.OPTIONAL) - .map(ver -> formatDependencyError(ver, modVersions)) - .collect(Collectors.joining("\n")) - ); - } - - if (!incompatibleVersions.isEmpty()) { - LOGGER.error( - LogMarkers.LOADING, - "Incompatibilities between mods:\n{}", - incompatibleVersions.stream() - .map(ver -> formatIncompatibleDependencyError(ver, "Incompatible", modVersions)) - .collect(Collectors.joining("\n")) - ); - } + if (mandatoryMissing > 0) { + LOGGER.error( + LogMarkers.LOADING, + "Missing or unsupported mandatory dependencies:\n{}", + missingVersions.stream() + .filter(mv -> mv.getType() == IModInfo.DependencyType.REQUIRED) + .map(ver -> formatDependencyError(ver, modVersions)) + .collect(Collectors.joining("\n")) + ); + } + if (missingVersions.size() - mandatoryMissing > 0) { + LOGGER.error( + LogMarkers.LOADING, + "Unsupported installed optional dependencies:\n{}", + missingVersions.stream() + .filter(ver -> ver.getType() == IModInfo.DependencyType.OPTIONAL) + .map(ver -> formatDependencyError(ver, 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(); + if (!incompatibleVersions.isEmpty()) { + LOGGER.error( + LogMarkers.LOADING, + "Incompatibilities between mods:\n{}", + incompatibleVersions.stream() + .map(ver -> formatIncompatibleDependencyError(ver, "incompatible", modVersions)) + .collect(Collectors.joining("\n")) + ); } - return Collections.emptyList(); + + return new DependencyResolutionResult(incompatibleVersions, conflictingVersions, missingVersions, modVersions); } private static String formatDependencyError(IModInfo.ModVersion dependency, Map modVersions) @@ -302,10 +317,10 @@ 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, + "\tMod '%s' is %s with: '%s', versions: '%s'; Version found: '%s'", dependency.getOwner().getModId(), + type, + dependency.getModId(), dependency.getVersionRange(), modVersions.get(dependency.getModId()).toString() ); 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 c2d1e1c46..a369eec3c 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 @@ -6,6 +6,7 @@ package net.neoforged.fml.loading.moddiscovery; import com.mojang.logging.LogUtils; +import net.neoforged.fml.loading.FMLLoader; import net.neoforged.fml.loading.StringSubstitutor; import net.neoforged.fml.loading.StringUtils; import net.neoforged.neoforgespi.language.IConfigurable; @@ -206,6 +207,7 @@ class ModVersion implements net.neoforged.neoforgespi.language.IModInfo.ModVersi private final String modId; private final VersionRange versionRange; private final DependencyType type; + private final Optional reason; private final Ordering ordering; private final DependencySide side; private Optional referralUrl; @@ -215,7 +217,20 @@ public ModVersion(final IModInfo owner, final IConfigurable config) { this.modId = config.getConfigElement("modId") .orElseThrow(()->new InvalidModFileException("Missing required field modid in dependency", getOwningFile())); this.type = config.getConfigElement("type") - .map(str -> str.toUpperCase(Locale.ROOT)).map(DependencyType::valueOf).orElse(DependencyType.REQUIRED); + .map(str -> str.toUpperCase(Locale.ROOT)).map(DependencyType::valueOf).orElseGet(() -> { + final var mandatory = config.getConfigElement("mandatory"); + if (mandatory.isPresent()) { + if (!FMLLoader.isProduction()) { + LOGGER.error("Mod '{}' uses deprecated 'mandatory' field in the dependency declaration for '{}'. Use the 'type' field and 'required'/'optional' instead", owner.getModId(), modId); + throw new InvalidModFileException("Deprecated 'mandatory' field is used in dependency", getOwningFile()); + } + + return mandatory.get() ? DependencyType.REQUIRED : DependencyType.OPTIONAL; + } + + return DependencyType.REQUIRED; + }); + this.reason = config.getConfigElement("reason"); this.versionRange = config.getConfigElement("versionRange") .map(MavenVersionAdapter::createFromVersionSpec) .orElse(UNBOUNDED); @@ -247,6 +262,11 @@ public DependencyType getType() { return type; } + @Override + public Optional getReason() { + return reason; + } + @Override public Ordering getOrdering() {