Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dependency overrides #214

Merged
merged 8 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions loader/src/main/java/net/neoforged/fml/loading/FMLConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package net.neoforged.fml.loading;

import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.ConfigSpec;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
Expand All @@ -14,9 +15,14 @@
import com.mojang.logging.LogUtils;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.slf4j.Logger;

public class FMLConfig {
Expand Down Expand Up @@ -81,12 +87,14 @@ private static Object maxThreads(final Object value) {

private static final Logger LOGGER = LogUtils.getLogger();
private static final FMLConfig INSTANCE = new FMLConfig();
private static final Map<String, List<DependencyOverride>> DEPENDENCY_OVERRIDES = new HashMap<>();
Matyrobbrt marked this conversation as resolved.
Show resolved Hide resolved
private static final ConfigSpec configSpec = new ConfigSpec();
private static final CommentedConfig configComments = CommentedConfig.inMemory();
static {
for (ConfigValue cv : ConfigValue.values()) {
cv.buildConfigEntry(configSpec, configComments);
}
configSpec.define("dependencyOverrides", () -> null, object -> true);
}

private CommentedFileConfig configData;
Expand Down Expand Up @@ -119,6 +127,37 @@ public static void load() {
}
}
FMLPaths.getOrCreateGameRelativePath(Paths.get(FMLConfig.getConfigValue(ConfigValue.DEFAULT_CONFIG_PATH)));

DEPENDENCY_OVERRIDES.clear();
var overridesObject = INSTANCE.configData.get("dependencyOverrides");
if (overridesObject != null) {
if (!(overridesObject instanceof Config cfg)) {
LOGGER.error("Invalid dependency overrides declaration in config. Expected object but found {}", overridesObject);
return;
}

cfg.valueMap().forEach((modId, object) -> {
var asList = object instanceof List<?> ls ? ls : List.of(object);
var overrides = DEPENDENCY_OVERRIDES.computeIfAbsent(modId, k -> new ArrayList<>());
for (Object o : asList) {
var str = (String) o;
var start = str.charAt(0);
if (start != '+' && start != '-') {
LOGGER.error("Found invalid dependency override for mod '{}'. Expected +/- in override '{}'. Did you forget to specify the override type?", modId, str);
} else {
var removal = start == '-';
var depMod = str.substring(1);
Matyrobbrt marked this conversation as resolved.
Show resolved Hide resolved
overrides.add(new DependencyOverride(depMod, removal));
}
}
});
}

if (!DEPENDENCY_OVERRIDES.isEmpty()) {
LOGGER.warn("*".repeat(30) + " Found dependency overrides " + "*".repeat(30));
DEPENDENCY_OVERRIDES.forEach((modId, ov) -> LOGGER.warn("Dependency overrides for mod '{}': {}", modId, ov.stream().map(DependencyOverride::getMessage).collect(Collectors.joining(", "))));
LOGGER.warn("*".repeat(88));
}
}

public static String getConfigValue(ConfigValue v) {
Expand Down Expand Up @@ -147,4 +186,20 @@ public static <T> void updateConfig(ConfigValue v, T value) {
public static String defaultConfigPath() {
return getConfigValue(ConfigValue.DEFAULT_CONFIG_PATH);
}

public static List<DependencyOverride> getOverrides(String modId) {
var ov = DEPENDENCY_OVERRIDES.get(modId);
if (ov == null) return List.of();
return ov;
}

public static Map<String, List<DependencyOverride>> getDependencyOverrides() {
return Collections.unmodifiableMap(DEPENDENCY_OVERRIDES);
}

public record DependencyOverride(String modId, boolean remove) {
public String getMessage() {
return (remove ? "softening dependency constraints against" : "adding explicit AFTER ordering against") + " '" + modId + "'";
}
}
}
21 changes: 20 additions & 1 deletion loader/src/main/java/net/neoforged/fml/loading/ModSorter.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ private void sort() {
.map(IModInfo::getDependencies).<IModInfo.ModVersion>mapMulti(Iterable::forEach)
.forEach(dep -> addDependency(graph, dep));

FMLConfig.getDependencyOverrides().forEach((id, overrides) -> {
for (FMLConfig.DependencyOverride override : overrides) {
if (!override.remove()) {
graph.putEdge((ModInfo) modIdNameLookup.get(override.modId()), (ModInfo) modIdNameLookup.get(id));
Matyrobbrt marked this conversation as resolved.
Show resolved Hide resolved
}
}
});

final List<ModInfo> sorted;
try {
sorted = TopologicalSort.topologicalSort(graph, Comparator.comparing(infos::get));
Expand Down Expand Up @@ -237,7 +245,18 @@ private DependencyResolutionResult verifyDependencyVersions() {
final var modVersionDependencies = modFiles.stream()
.map(ModFile::getModInfos)
.<IModInfo>mapMulti(Iterable::forEach)
.collect(groupingBy(Function.identity(), flatMapping(e -> e.getDependencies().stream(), toList())));
.collect(groupingBy(Function.identity(), flatMapping(e -> {
var overrides = FMLConfig.getOverrides(e.getModId());
if (!overrides.isEmpty()) {
var ids = overrides.stream()
.filter(FMLConfig.DependencyOverride::remove)
.map(FMLConfig.DependencyOverride::modId)
.collect(toSet());
return e.getDependencies().stream()
.filter(v -> !ids.contains(v.getModId()));
}
return e.getDependencies().stream();
}, toList())));

final var modRequirements = modVersionDependencies.values().stream().<IModInfo.ModVersion>mapMulti(Iterable::forEach)
.filter(mv -> mv.getSide().isCorrectSide())
Expand Down
5 changes: 5 additions & 0 deletions loader/src/main/resources/META-INF/defaultfmlconfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ earlyWindowControl = true
#Max threads for early initialization parallelism, -1 is based on processor count
maxThreads = -1

# Define dependency overrides below
# Dependency overrides can be used to forcibly remove a dependency constraint from a mod or to force a mod to load AFTER another mod
# Using dependency overrides can cause issues. Use at your own risk.
# Example dependency override for the mod with the id 'targetMod': dependency constraints (incompatibility clauses or restrictive version ranges) against mod 'dep1' are removed, and the mod will now load after the mod 'dep2'
# dependencyOverrides.targetMod = ["-dep1", "+dep2"]
Technici4n marked this conversation as resolved.
Show resolved Hide resolved
Matyrobbrt marked this conversation as resolved.
Show resolved Hide resolved
28 changes: 28 additions & 0 deletions loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.electronwill.nightconfig.core.Config;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import net.neoforged.fml.ModLoader;
Expand Down Expand Up @@ -392,6 +395,31 @@ void testUnsatisfiedNeoForgeRange() throws Exception {
assertThat(getTranslatedIssues(e.getIssues())).containsOnly("ERROR: Mod testproject requires neoforge 999.6 or above\nCurrently, neoforge is 1\n");
}

@Test
void testDependencyOverride() throws Exception {
installation.setupProductionClient();
installation.appendToConfig("dependencyOverrides.targetmod = [\"-depmod\", \"-incompatiblemod\"]");
installation.buildModJar("depmod.jar").withMod("depmod", "1.0");
installation.buildModJar("incompatiblemod.jar").withMod("incompatiblemod", "1.0");
installation.buildModJar("targetmod.jar")
.withModsToml(builder -> {
builder.unlicensedJavaMod();
builder.addMod("targetmod", "1.0", c -> {
var sub = Config.inMemory();
sub.set("modId", "depmod");
sub.set("versionRange", "[2,)");
sub.set("type", "required");

var sub2 = Config.inMemory();
sub2.set("modId", "incompatiblemod");
sub2.set("versionRange", "[1,");
sub2.set("type", "incompatible");
c.set("dependencies.targetmod", new ArrayList<>(Arrays.asList(sub, sub2)));
});
});
assertThat(launchAndLoad("forgeclient").issues()).isEmpty();
}

@Test
void testDuplicateMods() throws Exception {
installation.setupProductionClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public ModFileBuilder withTestmodModsToml(Consumer<ModsTomlBuilder> customizer)
});
}

public ModFileBuilder withMod(String id, String version) {
return withModsToml(builder -> builder.unlicensedJavaMod().addMod(id, version));
}

public ModFileBuilder withModTypeManifest(IModFile.Type type) {
return withManifest(Map.of(
"FMLModType", type.name()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
Expand Down Expand Up @@ -316,6 +317,15 @@ public ModFileBuilder buildModJar(String filename) throws IOException {
return new ModFileBuilder(path);
}

public void appendToConfig(String text) throws IOException {
var in = Objects.requireNonNull(FMLConfig.class.getResourceAsStream("/META-INF/defaultfmlconfig.toml"));
text = new String(in.readAllBytes()) + '\n' + text;
in.close();
var file = getGameDir().resolve("config/fml.toml");
Files.createDirectories(file.getParent());
Files.writeString(file, text);
}

public static void writeJarFile(Path file, IdentifiableContent... content) throws IOException {
try (var fout = Files.newOutputStream(file)) {
writeJarFile(fout, content);
Expand Down