Skip to content

Commit

Permalink
Deprecate the mandatory dependency field and replace it with a `typ…
Browse files Browse the repository at this point in the history
…e` field (#51)
  • Loading branch information
Matyrobbrt authored Dec 16, 2023
1 parent edb6959 commit 4b8ac82
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 43 deletions.
5 changes: 5 additions & 0 deletions core/src/main/java/net/neoforged/fml/ModLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/net/neoforged/fml/ModLoadingWarning.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ModLoadingWarning> fromEarlyException(final EarlyLoadingException e) {
return e.getAllData().stream().map(ed->new ModLoadingWarning(ed.getModInfo(), ModLoadingStage.VALIDATE, ed.getI18message(), ed.getArgs()));
}
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class LoadingModList
private final List<ModInfo> sortedList;
private final Map<String, ModFileInfo> fileById;
private final List<EarlyLoadingException> preLoadErrors;
private final List<EarlyLoadingException> preLoadWarnings;
private List<IModFile> brokenFiles;

private LoadingModList(final List<ModFile> modFiles, final List<ModInfo> sortedList)
Expand All @@ -53,6 +54,7 @@ private LoadingModList(final List<ModFile> modFiles, final List<ModInfo> 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<ModFile> modFiles, List<ModInfo> sortedList, final EarlyLoadingException earlyLoadingException)
Expand Down Expand Up @@ -170,6 +172,10 @@ public List<EarlyLoadingException> getErrors() {
return preLoadErrors;
}

public List<EarlyLoadingException> getWarnings() {
return preLoadWarnings;
}

public void setBrokenFiles(final List<IModFile> brokenFiles) {
this.brokenFiles = brokenFiles;
}
Expand Down
150 changes: 114 additions & 36 deletions loader/src/main/java/net/neoforged/fml/loading/ModSorter.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,15 @@ public static LoadingModList sort(List<ModFile> mods, final List<EarlyLoadingExc
// We cannot build any list with duped mods. We have to abort immediately and report it
return LoadingModList.of(ms.systemMods, ms.systemMods.stream().map(mf->(ModInfo)mf.getModInfos().get(0)).collect(toList()), e);
}

// try and validate dependencies
final List<EarlyLoadingException.ExceptionData> 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;
Expand All @@ -65,8 +69,18 @@ public static LoadingModList sort(List<ModFile> mods, final List<EarlyLoadingExc
} catch (EarlyLoadingException e) {
earlyLoadingException = e;
}
return LoadingModList.of(ms.modFiles, ms.sortedList, earlyLoadingException);
list = LoadingModList.of(ms.modFiles, ms.sortedList, earlyLoadingException);
}

// If we have conflicts those are considered warnings
if (!resolutionResult.discouraged.isEmpty()) {
list.getWarnings().add(new EarlyLoadingException(
"found mod conflicts",
null,
resolutionResult.buildWarningMessages()
));
}
return list;
}

@SuppressWarnings("UnstableApiUsage")
Expand Down Expand Up @@ -180,7 +194,38 @@ private void detectSystemMods(final Map<String, List<ModFile>> modFilesByFirstId
}
}

private List<EarlyLoadingException.ExceptionData> verifyDependencyVersions()
public record DependencyResolutionResult(
Collection<IModInfo.ModVersion> incompatibilities,
Collection<IModInfo.ModVersion> discouraged,
Collection<IModInfo.ModVersion> versionResolution,
Map<String, ArtifactVersion> modVersions
) {
public List<EarlyLoadingException.ExceptionData> 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<EarlyLoadingException.ExceptionData> 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)
Expand All @@ -197,43 +242,64 @@ private List<EarlyLoadingException.ExceptionData> 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<String, ArtifactVersion> modVersions)
Expand All @@ -248,6 +314,18 @@ private static String formatDependencyError(IModInfo.ModVersion dependency, Map<
);
}

private static String formatIncompatibleDependencyError(IModInfo.ModVersion dependency, String type, Map<String, ArtifactVersion> 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<String, ArtifactVersion> modVersions)
{
return !(VersionSupportMatrix.testVersionSupportMatrix(mv.getVersionRange(), mv.getModId(), "mod", (modId, range) -> modVersions.containsKey(modId) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String> reason;
private final Ordering ordering;
private final DependencySide side;
private Optional<URL> referralUrl;
Expand All @@ -213,8 +216,21 @@ public ModVersion(final IModInfo owner, final IConfigurable config) {
this.owner = owner;
this.modId = config.<String>getConfigElement("modId")
.orElseThrow(()->new InvalidModFileException("Missing required field modid in dependency", getOwningFile()));
this.mandatory = config.<Boolean>getConfigElement("mandatory")
.orElseThrow(()->new InvalidModFileException("Missing required field mandatory in dependency", getOwningFile()));
this.type = config.<String>getConfigElement("type")
.map(str -> str.toUpperCase(Locale.ROOT)).map(DependencyType::valueOf).orElseGet(() -> {
final var mandatory = config.<Boolean>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.<String>getConfigElement("reason");
this.versionRange = config.<String>getConfigElement("versionRange")
.map(MavenVersionAdapter::createFromVersionSpec)
.orElse(UNBOUNDED);
Expand Down Expand Up @@ -242,9 +258,13 @@ public VersionRange getVersionRange()
}

@Override
public boolean isMandatory()
{
return mandatory;
public DependencyType getType() {
return type;
}

@Override
public Optional<String> getReason() {
return reason;
}

@Override
Expand Down

0 comments on commit 4b8ac82

Please sign in to comment.