Skip to content

Commit

Permalink
Add dependency overrides
Browse files Browse the repository at this point in the history
  • Loading branch information
Matyrobbrt committed Nov 14, 2024
1 parent ee3c2b9 commit b3d6c05
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 1 deletion.
45 changes: 45 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,7 +15,11 @@
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 org.slf4j.Logger;
Expand Down Expand Up @@ -81,12 +86,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<>();
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 +126,32 @@ 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);
LOGGER.warn("Found dependency override for mod '{}': {} '{}'", modId, removal ? "softening dependency constraints against" : "adding explicit AFTER ordering against", depMod);
overrides.add(new DependencyOverride(depMod, removal));
}
}
});
}
}

public static String getConfigValue(ConfigValue v) {
Expand Down Expand Up @@ -147,4 +180,16 @@ 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) {}
}
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));
}
}
});

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"]
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 @@ -6,13 +6,18 @@
package net.neoforged.fml.loading;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.in;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import com.electronwill.nightconfig.core.Config;
import net.neoforged.fml.ModLoader;
import net.neoforged.fml.ModLoadingException;
import net.neoforged.fml.ModWorkManager;
Expand Down Expand Up @@ -392,6 +397,29 @@ 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\"]");
installation.buildModJar("depmod.jar")
.withModsToml(builder -> {
builder.unlicensedJavaMod();
builder.addMod("depmod", "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");
c.set("dependencies.targetmod", new ArrayList<>(Arrays.asList(sub)));
});
});
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 @@ -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

0 comments on commit b3d6c05

Please sign in to comment.