Skip to content

Commit

Permalink
Validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Matyrobbrt committed Nov 15, 2024
1 parent b3eb5bc commit 3ca3e7b
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 20 deletions.
27 changes: 20 additions & 7 deletions loader/src/main/java/net/neoforged/fml/loading/FMLConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Unmodifiable;
import org.jetbrains.annotations.UnmodifiableView;
import org.slf4j.Logger;

public class FMLConfig {
Expand Down Expand Up @@ -87,13 +89,16 @@ 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 Map<String, List<DependencyOverride>> dependencyOverrides;
private static final ConfigSpec configSpec = new ConfigSpec();
private static final CommentedConfig configComments = CommentedConfig.inMemory();
static {
for (ConfigValue cv : ConfigValue.values()) {
cv.buildConfigEntry(configSpec, configComments);
}

// Make sure that we don't end up "correcting" the config and removing dependency overrides
// Since we're not writing them by default, the default value can be null and we accept any objects (parsing and validation is done when the config is loaded)
configSpec.define("dependencyOverrides", () -> null, object -> true);
}

Expand Down Expand Up @@ -128,7 +133,8 @@ public static void load() {
}
FMLPaths.getOrCreateGameRelativePath(Paths.get(FMLConfig.getConfigValue(ConfigValue.DEFAULT_CONFIG_PATH)));

DEPENDENCY_OVERRIDES.clear();
// load dependency overrides
Map<String, List<DependencyOverride>> dependencyOverrides = new HashMap<>();
var overridesObject = INSTANCE.configData.get("dependencyOverrides");
if (overridesObject != null) {
if (!(overridesObject instanceof Config cfg)) {
Expand All @@ -137,8 +143,9 @@ public static void load() {
}

cfg.valueMap().forEach((modId, object) -> {
// We accept both dependencyOverrides.target = "-dep" and dependencyOverrides.target = ["-dep"]
var asList = object instanceof List<?> ls ? ls : List.of(object);
var overrides = DEPENDENCY_OVERRIDES.computeIfAbsent(modId, k -> new ArrayList<>());
var overrides = dependencyOverrides.computeIfAbsent(modId, k -> new ArrayList<>());
for (Object o : asList) {
var str = (String) o;
var start = str.charAt(0);
Expand All @@ -153,11 +160,15 @@ public static void load() {
});
}

if (!DEPENDENCY_OVERRIDES.isEmpty()) {
if (!dependencyOverrides.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(", "))));
dependencyOverrides.forEach((modId, ov) -> LOGGER.warn("Dependency overrides for mod '{}': {}", modId, ov.stream().map(DependencyOverride::getMessage).collect(Collectors.joining(", "))));
LOGGER.warn("*".repeat(88));
}

// Make the overrides immutable
dependencyOverrides.replaceAll((id, list) -> List.copyOf(list));
FMLConfig.dependencyOverrides = Collections.unmodifiableMap(dependencyOverrides);
}

public static String getConfigValue(ConfigValue v) {
Expand Down Expand Up @@ -187,14 +198,16 @@ public static String defaultConfigPath() {
return getConfigValue(ConfigValue.DEFAULT_CONFIG_PATH);
}

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

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

public record DependencyOverride(String modId, boolean remove) {
Expand Down
23 changes: 18 additions & 5 deletions loader/src/main/java/net/neoforged/fml/loading/ModSorter.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public static LoadingModList sort(List<ModFile> plugins, List<ModFile> mods, fin
// Otherwise, lets try and sort the modlist and proceed
ModLoadingException modLoadingException = null;
try {
ms.sort();
ms.sort(issues);
} catch (ModLoadingException e) {
modLoadingException = e;
}
Expand Down Expand Up @@ -105,7 +105,7 @@ private static <T> List<T> concat(List<T>... lists) {
}

@SuppressWarnings("UnstableApiUsage")
private void sort() {
private void sort(List<ModLoadingIssue> issues) {
// lambdas are identity based, so sorting them is impossible unless you hold reference to them
final MutableGraph<ModInfo> graph = GraphBuilder.directed().build();
AtomicInteger counter = new AtomicInteger();
Expand All @@ -120,10 +120,22 @@ private void sort() {
.map(IModInfo::getDependencies).<IModInfo.ModVersion>mapMulti(Iterable::forEach)
.forEach(dep -> addDependency(graph, dep));

// now consider dependency overrides
// we also check their validity here, and report unknown mods as warnings
FMLConfig.getDependencyOverrides().forEach((id, overrides) -> {
for (FMLConfig.DependencyOverride override : overrides) {
if (!override.remove()) {
graph.putEdge((ModInfo) modIdNameLookup.get(override.modId()), (ModInfo) modIdNameLookup.get(id));
var target = (ModInfo) modIdNameLookup.get(id);
if (target == null) {
issues.add(ModLoadingIssue.warning("fml.modloadingissue.depoverride.unknown_target", id));
} else {
for (FMLConfig.DependencyOverride override : overrides) {
var dep = (ModInfo) modIdNameLookup.get(override.modId());
if (dep == null) {
issues.add(ModLoadingIssue.warning("fml.modloadingissue.depoverride.unknown_dependency", override.modId(), id));
} else if (!override.remove()) {
// Add ordering dependency overrides (random order -> target AFTER dependency)
// We do not need to check for overrides that attempt to change the declared order as the sorter will detect the cycle itself and error
graph.putEdge(dep, target);
}
}
}
});
Expand Down Expand Up @@ -247,6 +259,7 @@ private DependencyResolutionResult verifyDependencyVersions() {
.<IModInfo>mapMulti(Iterable::forEach)
.collect(groupingBy(Function.identity(), flatMapping(e -> {
var overrides = FMLConfig.getOverrides(e.getModId());
// consider overrides and invalidate dependencies that are removed
if (!overrides.isEmpty()) {
var ids = overrides.stream()
.filter(FMLConfig.DependencyOverride::remove)
Expand Down
2 changes: 2 additions & 0 deletions loader/src/main/resources/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"fml.modloadingissue.discouragedmod": "Mod §e{1}§r §ddiscourages§r the use of §3{0}§r §o{2,vr}§r\n§7Currently, §3{0}§r§7 is §o{3}§r\n§7The reason is:§r §o{4,i18ntranslate}§r",
"fml.modloadingissue.discouragedmod.proceed": "Proceed at your own risk",
"fml.modloadingissue.duplicate_mod": "Mod §e{0}§r is present in multiple files: {1}",
"fml.modloadingissue.depoverride.unknown_target": "Unknown dependency override target with id §e{0}§r",
"fml.modloadingissue.depoverride.unknown_dependency": "Unknown mod §e{0}§r referenced in dependency overrides for mod §e{1}§r",
"fml.modloading.duplicate_library": "Library §e{3}§r is present in multiple files: {4}",
"fml.modloading.incompatiblemod.noreason": "§eNo reason provided§r",
"fml.modloading.discouragedmod.noreason": "§eNo reason provided§r",
Expand Down
22 changes: 19 additions & 3 deletions loader/src/test/java/net/neoforged/fml/loading/FMLLoaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,8 @@ void testUnsatisfiedNeoForgeRange() throws Exception {
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("depmod.jar").withMod("depmod", "1.0").build();
installation.buildModJar("incompatiblemod.jar").withMod("incompatiblemod", "1.0").build();
installation.buildModJar("targetmod.jar")
.withModsToml(builder -> {
builder.unlicensedJavaMod();
Expand All @@ -416,10 +416,26 @@ void testDependencyOverride() throws Exception {
sub2.set("type", "incompatible");
c.set("dependencies.targetmod", new ArrayList<>(Arrays.asList(sub, sub2)));
});
});
})
.build();
assertThat(launchAndLoad("forgeclient").issues()).isEmpty();
}

@Test
void testInvalidDependencyOverride() throws Exception {
installation.setupProductionClient();

// Test that invalid targets and dependencies warn
installation.appendToConfig("dependencyOverrides.unknownmod = [\"-testmod\"]");
installation.appendToConfig("dependencyOverrides.testmod = [\"+depdoesntexist\"]");
installation.buildModJar("testmod.jar").withMod("testmod", "1.0").build();

var r = launchAndLoad("forgeclient");
assertThat(getTranslatedIssues(r.issues())).containsOnly(
"WARNING: Unknown dependency override target with id unknownmod",
"WARNING: Unknown mod depdoesntexist referenced in dependency overrides for mod testmod");
}

@Test
void testDuplicateMods() throws Exception {
installation.setupProductionClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -318,12 +319,17 @@ public ModFileBuilder buildModJar(String filename) throws IOException {
}

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);

try {
Files.writeString(file, Files.readString(file) + '\n' + text);
} catch (NoSuchFileException ex) {
var in = Objects.requireNonNull(FMLConfig.class.getResourceAsStream("/META-INF/defaultfmlconfig.toml"));
text = new String(in.readAllBytes()) + '\n' + text;
in.close();
Files.createDirectories(file.getParent());
Files.writeString(file, text);
}
}

public static void writeJarFile(Path file, IdentifiableContent... content) throws IOException {
Expand Down

0 comments on commit 3ca3e7b

Please sign in to comment.