From 4b8ac82980807e856de596f560a8a3b03209ae5d Mon Sep 17 00:00:00 2001 From: Matyrobbrt <65940752+Matyrobbrt@users.noreply.github.com> Date: Sat, 16 Dec 2023 20:41:56 +0200 Subject: [PATCH] Deprecate the `mandatory` dependency field and replace it with a `type` field (#51) --- .../java/net/neoforged/fml/ModLoader.java | 5 + .../net/neoforged/fml/ModLoadingWarning.java | 5 + gradle.properties | 2 +- .../neoforged/fml/loading/LoadingModList.java | 6 + .../net/neoforged/fml/loading/ModSorter.java | 150 +++++++++++++----- .../fml/loading/moddiscovery/ModInfo.java | 32 +++- 6 files changed, 157 insertions(+), 43 deletions(-) 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 ac531cfc9..bcc4b069f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -spi_version=9.0.1 +spi_version=9.0.2 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..fa7ded0f7 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,15 @@ public static LoadingModList sort(List mods, final List(ModInfo)mf.getModInfos().get(0)).collect(toList()), e); } + // try and validate dependencies - final List failedList = Stream.concat(ms.verifyDependencyVersions().stream(), errors.stream()).toList(); - // if we miss one or the other, we abort now - 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)); + final DependencyResolutionResult resolutionResult = ms.verifyDependencyVersions(); + + final LoadingModList list; + + // 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; @@ -65,8 +69,18 @@ public static LoadingModList sort(List mods, final List> modFilesByFirstId } } - private List verifyDependencyVersions() + public record DependencyResolutionResult( + Collection incompatibilities, + Collection discouraged, + Collection versionResolution, + Map modVersions + ) { + public List buildWarningMessages() { + return Stream.concat(discouraged.stream() + .map(mv -> new EarlyLoadingException.ExceptionData("fml.modloading.discouragedmod", + mv.getOwner(), mv.getModId(), mv.getOwner().getModId(), mv.getVersionRange(), + modVersions.get(mv.getModId()), mv.getReason().orElse("fml.modloading.discouragedmod.noreason"))), + + Stream.of(new EarlyLoadingException.ExceptionData("fml.modloading.discouragedmod.proceed"))) + .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) @@ -197,43 +242,64 @@ 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()) { - if (mandatoryMissing > 0) { - LOGGER.error( - LogMarkers.LOADING, - "Missing or unsupported mandatory dependencies:\n{}", - missingVersions.stream() - .filter(IModInfo.ModVersion::isMandatory) - .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.isMandatory()) - .map(ver -> formatDependencyError(ver, modVersions)) - .collect(Collectors.joining("\n")) - ); - } + final var incompatibleVersions = modRequirements.stream().filter(ver -> ver.getType() == IModInfo.DependencyType.INCOMPATIBLE) + .filter(ver -> modVersions.containsKey(ver.getModId()) && !this.modVersionNotContained(ver, modVersions)) + .collect(toSet()); - 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")))) - .toList(); + final var discouragedVersions = modRequirements.stream().filter(ver -> ver.getType() == IModInfo.DependencyType.DISCOURAGED) + .filter(ver -> modVersions.containsKey(ver.getModId()) && !this.modVersionNotContained(ver, modVersions)) + .collect(toSet()); + + if (!discouragedVersions.isEmpty()) { + LOGGER.error( + LogMarkers.LOADING, + "Conflicts between mods:\n{}\n\tIssues may arise. Continue at your own risk.", + discouragedVersions.stream() + .map(ver -> formatIncompatibleDependencyError(ver, "discourages", 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")) + ); } - return Collections.emptyList(); + 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, "is incompatible with", modVersions)) + .collect(Collectors.joining("\n")) + ); + } + + return new DependencyResolutionResult(incompatibleVersions, discouragedVersions, missingVersions, modVersions); } private static String formatDependencyError(IModInfo.ModVersion dependency, Map modVersions) @@ -248,6 +314,18 @@ private static String formatDependencyError(IModInfo.ModVersion dependency, Map< ); } + private static String formatIncompatibleDependencyError(IModInfo.ModVersion dependency, String type, Map modVersions) + { + return String.format( + "\tMod '%s' %s '%s', versions: '%s'; Version found: '%s'", + dependency.getOwner().getModId(), + type, + dependency.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..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; @@ -20,6 +21,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 +206,8 @@ 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 Optional reason; private final Ordering ordering; private final DependencySide side; private Optional referralUrl; @@ -213,8 +216,21 @@ 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).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); @@ -242,9 +258,13 @@ public VersionRange getVersionRange() } @Override - public boolean isMandatory() - { - return mandatory; + public DependencyType getType() { + return type; + } + + @Override + public Optional getReason() { + return reason; } @Override