diff --git a/build.gradle b/build.gradle index b5b1348e0..3968d2033 100644 --- a/build.gradle +++ b/build.gradle @@ -112,6 +112,17 @@ project(':forge') { srcDir "$rootDir/src/main/resources" } } + test { + compileClasspath += sourceSets.fmllauncher.runtimeClasspath + runtimeClasspath += sourceSets.fmllauncher.runtimeClasspath + java { + // srcDir "$rootDir/src/test/java" TODO fix later + srcDir "$rootDir/src/fmllaunchertest/java" + } + resources { + srcDir "$rootDir/src/test/resources" + } + } userdev { compileClasspath += sourceSets.main.runtimeClasspath runtimeClasspath += sourceSets.main.runtimeClasspath @@ -273,6 +284,9 @@ project(':forge') { installer 'org.apache.logging.log4j:log4j-core:2.11.1' fmllauncherImplementation 'com.google.guava:guava:21.0' fmllauncherImplementation 'com.google.code.gson:gson:2.8.0' + testImplementation "org.junit.jupiter:junit-jupiter-api:5.0.0" + testImplementation "org.opentest4j:opentest4j:1.0.0" // needed for junit 5 + testImplementation "org.hamcrest:hamcrest-all:1.3" // needs advanced matching for list order } task runclient(type: JavaExec, dependsOn: [":forge:downloadAssets", ":forge:extractNatives"]) { diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java index d62261ab4..7647deffd 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/ModSorter.java @@ -19,19 +19,30 @@ package net.minecraftforge.fml.loading; -import net.minecraftforge.fml.loading.toposort.TopologicalSort; +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.MutableGraph; +import net.minecraftforge.forgespi.language.IModFileInfo; import net.minecraftforge.forgespi.language.IModInfo; +import net.minecraftforge.fml.loading.EarlyLoadingException.ExceptionData; import net.minecraftforge.fml.loading.moddiscovery.ModFile; import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; import net.minecraftforge.fml.loading.moddiscovery.ModInfo; +import net.minecraftforge.fml.loading.toposort.CyclePresentException; +import net.minecraftforge.fml.loading.toposort.TopologicalSort; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.util.StringBuilderFormattable; import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -71,35 +82,53 @@ public class ModSorter private void sort() { - final TopologicalSort.DirectedGraph> topoGraph = new TopologicalSort.DirectedGraph<>(); - modFiles.stream().map(ModFile::getModFileInfo).map(ModFileInfo.class::cast).forEach(mi -> topoGraph.addNode(() -> mi)); + // lambdas are identity based, so sorting them is impossible unless you hold reference to them + final MutableGraph graph = GraphBuilder.directed().build(); + AtomicInteger counter = new AtomicInteger(); + Map infos = modFiles.stream().map(ModFile::getModFileInfo).collect(Collectors.toMap(Function.identity(), (e) -> counter.incrementAndGet())); modFiles.stream().map(ModFile::getModInfos).flatMap(Collection::stream). map(IModInfo::getDependencies).flatMap(Collection::stream). - forEach(dep -> addDependency(topoGraph, dep)); - final List> sorted; + forEach(dep -> addDependency(graph, dep)); + final List sorted; try { - sorted = TopologicalSort.topologicalSort(topoGraph); + sorted = TopologicalSort.topologicalSort(graph, Comparator.comparing(infos::get)); } - catch (TopologicalSort.TopoSortException e) + catch (CyclePresentException e) { - TopologicalSort.TopoSortException.TopoSortExceptionData> data = e.getData(); - LOGGER.error(LOADING, ()-> data); - throw new EarlyLoadingException("Sorting error", e, data.toExceptionData(mi-> mi.get().getModId())); + Set> cycles = e.getCycles(); + LOGGER.error(LOADING, () -> ((StringBuilderFormattable) (buffer -> { + buffer.append("Mod Sorting failed.\n"); + buffer.append("Detected Cycles: "); + buffer.append(cycles); + buffer.append('\n'); + }))); + List dataList = cycles.stream() + .map(Set::stream) + .map(stream -> stream + .flatMap(modFileInfo -> modFileInfo.getMods().stream() + .map(IModInfo::getModId)).collect(Collectors.toList())) + .map(list -> new ExceptionData("fml.modloading.cycle", list)) + .collect(Collectors.toList()); + throw new EarlyLoadingException("Sorting error", e, dataList); } - this.sortedList = sorted.stream().map(Supplier::get).map(ModFileInfo::getMods). + this.sortedList = sorted.stream().map(ModFileInfo::getMods). flatMap(Collection::stream).map(ModInfo.class::cast).collect(Collectors.toList()); - this.modFiles = sorted.stream().map(Supplier::get).map(ModFileInfo::getFile).collect(Collectors.toList()); + this.modFiles = sorted.stream().map(ModFileInfo::getFile).collect(Collectors.toList()); } - private void addDependency(TopologicalSort.DirectedGraph> topoGraph,IModInfo.ModVersion dep) + private void addDependency(MutableGraph topoGraph, IModInfo.ModVersion dep) { + ModFileInfo self = (ModFileInfo)dep.getOwner().getOwningFile(); + ModFileInfo target = modIdNameLookup.get(dep.getModId()).getOwningFile(); + if (self == target) + return; // in case a jar has two mods that have dependencies between switch (dep.getOrdering()) { case BEFORE: - topoGraph.addEdge(()->(ModFileInfo)dep.getOwner().getOwningFile(), ()->modIdNameLookup.get(dep.getModId()).getOwningFile()); + topoGraph.putEdge(self, target); break; case AFTER: - topoGraph.addEdge(()->modIdNameLookup.get(dep.getModId()).getOwningFile(), ()->(ModFileInfo)dep.getOwner().getOwningFile()); + topoGraph.putEdge(target, self); break; case NONE: break; diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/CyclePresentException.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/CyclePresentException.java new file mode 100644 index 000000000..31edacf22 --- /dev/null +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/CyclePresentException.java @@ -0,0 +1,52 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.fml.loading.toposort; + +import java.util.Set; + +/** + * An exception thrown for graphs with cycles as an argument for topological sort. + */ +public final class CyclePresentException extends IllegalArgumentException { + private final Set> cycles; + + /** + * Creates the exception. + * + * @param cycles the cycles present + */ + CyclePresentException(Set> cycles) { + this.cycles = cycles; + } + + /** + * Accesses the cycles present in the sorted graph. + * + *

Each element in the outer set represents a cycle; each cycle, or the inner set, + * forms a strongly connected component with two or more elements. + * + * @param the type of node sorted + * @return the cycles identified + */ + @SuppressWarnings("unchecked") + public Set> getCycles() { + return (Set>) (Set) cycles; + } +} diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/StronglyConnectedComponentDetector.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/StronglyConnectedComponentDetector.java new file mode 100644 index 000000000..10c8dd553 --- /dev/null +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/StronglyConnectedComponentDetector.java @@ -0,0 +1,123 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.fml.loading.toposort; + +import com.google.common.graph.Graph; + +import java.util.BitSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * An object that splits a graph into strongly connected components lazily with + * Tarjan's Strongly Connected Components Algorithm. + * + *

This algorithm allows to detect all cycles in dependencies that prevent topological + * sorting. + * + *

This detector evaluates the graph lazily and won't reflect the modifications in the + * graph after initial evaluation. + */ +public class StronglyConnectedComponentDetector { + private final Graph graph; + private Map ids; + private T[] elements; + private int[] dfn; + private int[] low; + private int[] stack; + private int top; + private BitSet onStack; + private Set> components; + + public StronglyConnectedComponentDetector(Graph graph) { + this.graph = graph; + } + + public Set> getComponents() { + if (components == null) { + calculate(); + } + return components; + } + + @SuppressWarnings("unchecked") + private void calculate() { + components = new HashSet<>(); + int t = 0; + ids = new HashMap<>(); + Set nodes = graph.nodes(); + elements = (T[]) new Object[nodes.size()]; + for (T node : nodes) { + ids.put(node, t); + elements[t] = node; + t++; + } + + final int n = nodes.size(); + dfn = new int[n]; + low = new int[n]; + stack = new int[n]; + onStack = new BitSet(n); + top = -1; + for (int i = 0; i < n; i++) { + if (dfn[i] == 0) { + dfs(i, 1); + } + } + } + + private void dfs(int now, int depth) { + dfn[now] = depth; + low[now] = depth; + top++; + stack[top] = now; + onStack.set(now); + for (T each : graph.successors(elements[now])) { + int to = ids.get(each); + if (dfn[to] != 0) { + if (low[now] > dfn[to]) { + low[now] = dfn[to]; + } + } else { + dfs(to, depth + 1); + if (low[now] > low[to]) { + low[now] = low[to]; + } + } + } + + if (dfn[now] == low[now]) { + Set component = new HashSet<>(); + while (top >= 0) { + final int t = stack[top]; + component.add(elements[t]); + onStack.clear(t); + top--; + if (t == now) { + break; + } + } + components.add(component); + } + } + +} diff --git a/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/TopologicalSort.java b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/TopologicalSort.java index 93c2828a2..dac32956e 100644 --- a/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/TopologicalSort.java +++ b/src/fmllauncher/java/net/minecraftforge/fml/loading/toposort/TopologicalSort.java @@ -19,257 +19,101 @@ package net.minecraftforge.fml.loading.toposort; -import com.google.common.collect.Sets; -import net.minecraftforge.fml.loading.EarlyLoadingException; -import org.apache.logging.log4j.message.Message; -import org.apache.logging.log4j.util.StringBuilderFormattable; +import com.google.common.base.Preconditions; +import com.google.common.graph.Graph; +import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.Queue; import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.function.Function; -import java.util.stream.Collectors; + +import javax.annotation.Nullable; /** - * Topological sort for mod loading - * - * Based on a variety of sources, including http://keithschwarz.com/interesting/code/?dir=topological-sort - * @author cpw + * Provides a topological sort algorithm. * + *

While this algorithm is used for mod loading in forge, it can be + * utilized in other fashions, e.g. topology-based registry loading, prioritization + * for renderers, and even mod module loading. */ -public class TopologicalSort -{ - public static class DirectedGraph implements Iterable - { - private final Map> graph = new HashMap<>(); - private List orderedNodes = new ArrayList<>(); - - public boolean addNode(T node) - { - // Ignore nodes already added - if (graph.containsKey(node)) - { - return false; - } - - orderedNodes.add(node); - graph.put(node, new TreeSet<>(Comparator.comparingInt(o -> orderedNodes.indexOf(o)))); - return true; - } - - public void addEdge(T from, T to) - { - if (!(graph.containsKey(from) && graph.containsKey(to))) - { - throw new NoSuchElementException("Missing nodes from graph"); - } - - graph.get(from).add(to); - } - - public void removeEdge(T from, T to) - { - if (!(graph.containsKey(from) && graph.containsKey(to))) - { - throw new NoSuchElementException("Missing nodes from graph"); - } - - graph.get(from).remove(to); - } - - public boolean edgeExists(T from, T to) - { - if (!(graph.containsKey(from) && graph.containsKey(to))) - { - throw new NoSuchElementException("Missing nodes from graph"); - } - - return graph.get(from).contains(to); - } - - public Set edgesFrom(T from) - { - if (!graph.containsKey(from)) - { - throw new NoSuchElementException("Missing node from graph"); - } - - return Collections.unmodifiableSortedSet(graph.get(from)); - } - @Override - public Iterator iterator() - { - return orderedNodes.iterator(); - } - - public int size() - { - return graph.size(); - } - - public boolean isEmpty() - { - return graph.isEmpty(); - } - - @Override - public String toString() - { - return graph.toString(); - } - } +public final class TopologicalSort { /** - * Sort the input graph into a topologically sorted list + * A breath-first-search based topological sort. * - * Uses the reverse depth first search as outlined in ... - * @param graph - * @return The sorted mods list. + *

Compared to the depth-first-search version, it does not reverse the graph + * and supports custom secondary ordering specified by a comparator. It also utilizes the + * recently introduced Guava Graph API, which is more straightforward than the old directed + * graph. + * + *

The graph to sort must be directed, must not allow self loops, and must not contain + * cycles. {@link IllegalArgumentException} will be thrown otherwise. + * + *

When {@code null} is used for the comparator and multiple nodes have no + * prerequisites, the order depends on the iteration order of the set returned by the + * {@link Graph#successors(Object)} call, which is random by default. + * + *

Given the number of edges {@code E} and the number of vertexes {@code V}, + * the time complexity of a sort without a secondary comparator is {@code O(E + V)}. + * With a secondary comparator of time complexity {@code O(T)}, the overall time + * complexity would be {@code O(E + TV log(V))}. As a result, the comparator should + * be as efficient as possible. + * + *

Examples of topological sort usage can be found in Forge test code. + * + * @param graph the graph to sort + * @param comparator the secondary comparator, may be null + * @param the node type of the graph + * @return the ordered nodes from the graph + * @throws IllegalArgumentException if the graph is undirected or allows self loops + * @throws CyclePresentException if the graph contains cycles */ - public static List topologicalSort(DirectedGraph graph) - { - DirectedGraph rGraph = reverse(graph); - List sortedResult = new ArrayList<>(); - Set visitedNodes = new HashSet<>(); - // A list of "fully explored" nodes. Leftovers in here indicate cycles in the graph - Set expandedNodes = new HashSet<>(); + public static List topologicalSort(Graph graph, @Nullable Comparator comparator) throws IllegalArgumentException { + Preconditions.checkArgument(graph.isDirected(), "Cannot topologically sort an undirected graph!"); + Preconditions.checkArgument(!graph.allowsSelfLoops(), "Cannot topologically sort a graph with self loops!"); - for (T node : rGraph) - { - explore(node, rGraph, sortedResult, visitedNodes, expandedNodes); + final Queue queue = comparator == null ? new ArrayDeque<>() : new PriorityQueue<>(comparator); + final Map degrees = new HashMap<>(); + final List results = new ArrayList<>(); + + for (final T node : graph.nodes()) { + final int degree = graph.inDegree(node); + if (degree == 0) { + queue.add(node); + } else { + degrees.put(node, degree); + } } - return sortedResult; + while (!queue.isEmpty()) { + final T current = queue.remove(); + results.add(current); + for (final T successor : graph.successors(current)) { + final int updated = degrees.compute(successor, (node, degree) -> Objects.requireNonNull(degree, () -> "Invalid degree present for " + node) - 1); + if (updated == 0) { + queue.add(successor); + degrees.remove(successor); + } + } + } + + if (!degrees.isEmpty()) { + Set> components = new StronglyConnectedComponentDetector<>(graph).getComponents(); + components.removeIf(set -> set.size() < 2); + throwCyclePresentException(components); + } + + return results; } - public static DirectedGraph reverse(DirectedGraph graph) - { - DirectedGraph result = new DirectedGraph<>(); - - for (T node : graph) - { - result.addNode(node); - } - - for (T from : graph) - { - for (T to : graph.edgesFrom(from)) - { - result.addEdge(to, from); - } - } - - return result; - } - - private static void explore(T node, DirectedGraph graph, List sortedResult, Set visitedNodes, Set expandedNodes) - { - // Have we been here before? - if (visitedNodes.contains(node)) - { - // And have completed this node before - if (expandedNodes.contains(node)) - { - // Then we're fine - return; - } - - throw new TopoSortException(new TopoSortException.TopoSortExceptionData<>(node, sortedResult, visitedNodes, expandedNodes)); - } - - // Visit this node - visitedNodes.add(node); - - // Recursively explore inbound edges - for (T inbound : graph.edgesFrom(node)) - { - explore(inbound, graph, sortedResult, visitedNodes, expandedNodes); - } - - // Add ourselves now - sortedResult.add(node); - // And mark ourselves as explored - expandedNodes.add(node); - } - - public static class TopoSortException extends RuntimeException { - private final TopoSortExceptionData topoSortData; - - public static class TopoSortExceptionData implements Message, StringBuilderFormattable - { - private final List sortedResult; - private final Set visitedNodes; - private final Set expandedNodes; - private final T node; - - TopoSortExceptionData(T node, List sortedResult, Set visitedNodes, Set expandedNodes) - { - this.node = node; - this.sortedResult = sortedResult; - this.visitedNodes = visitedNodes; - this.expandedNodes = expandedNodes; - } - - @Override - public String getFormattedMessage() - { - return ""; - } - - @Override - public String getFormat() - { - return ""; - } - - @Override - public Object[] getParameters() - { - return new Object[0]; - } - - @Override - public Throwable getThrowable() - { - return null; - } - - @Override - public void formatTo(StringBuilder buffer) - { - buffer.append("Mod Sorting failed.\n"); - buffer.append("Visiting node {}\n").append(String.valueOf(node)); - buffer.append("Current sorted list : {}\n").append(String.valueOf(sortedResult)); - buffer.append("Visited set for this node : {}\n").append(String.valueOf(visitedNodes)); - buffer.append("Explored node set : {}\n").append(expandedNodes); - buffer.append("Likely cycle is in : {}\n").append(Sets.difference(visitedNodes, expandedNodes)); - } - - public List toExceptionData(Function nodeMapper) { - return Collections.singletonList( - new EarlyLoadingException.ExceptionData("fml.messages.cycleproblem", - nodeMapper.apply(node), - visitedNodes.stream().map(nodeMapper).collect(Collectors.joining(",")))); - } - } - - public TopoSortException(TopoSortExceptionData data) - { - this.topoSortData = data; - } - - @SuppressWarnings("unchecked") - public TopoSortExceptionData getData() { - return (TopoSortExceptionData)this.topoSortData; - } + @SuppressWarnings("unchecked") // for unchecked annotation + private static void throwCyclePresentException(Set> components) { + throw new CyclePresentException((Set>) (Set) components); } } diff --git a/src/fmllaunchertest/java/net/minecraftforge/fml/test/TopologicalSortTests.java b/src/fmllaunchertest/java/net/minecraftforge/fml/test/TopologicalSortTests.java new file mode 100644 index 000000000..57c3088e0 --- /dev/null +++ b/src/fmllaunchertest/java/net/minecraftforge/fml/test/TopologicalSortTests.java @@ -0,0 +1,140 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2018. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.fml.test; + +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.MutableGraph; +import net.minecraftforge.fml.loading.toposort.CyclePresentException; +import net.minecraftforge.fml.loading.toposort.StronglyConnectedComponentDetector; +import net.minecraftforge.fml.loading.toposort.TopologicalSort; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; + +/** + * Tests for topological sort. + */ +public class TopologicalSortTests { + + @Test + @DisplayName("strongly connected components") + @SuppressWarnings("unchecked") + void testScc() { + MutableGraph graph = GraphBuilder.directed().build(); + graph.putEdge(2, 4); + graph.putEdge(4, 7); + graph.putEdge(4, 6); + graph.putEdge(6, 1); + graph.putEdge(3, 4); + graph.putEdge(1, 4); + graph.putEdge(3, 1); + Set> components = new StronglyConnectedComponentDetector<>(graph).getComponents(); + assertThat(components, containsInAnyOrder(contains(2), contains(3), contains(7), containsInAnyOrder(1, 4, 6))); + } + + @Test + @DisplayName("strongly connected components 2") + @SuppressWarnings("unchecked") + void testScc2() { + MutableGraph graph = GraphBuilder.directed().build(); + graph.putEdge(2, 4); + graph.putEdge(4, 8); + graph.putEdge(8, 2); + graph.putEdge(2, 1); + graph.putEdge(2, 3); + graph.putEdge(1, 9); + graph.putEdge(9, 7); + graph.putEdge(7, 5); + graph.putEdge(5, 1); + graph.putEdge(5, 3); + graph.putEdge(3, 6); + graph.putEdge(6, 5); + graph.putEdge(9, 3); + Set> components = new StronglyConnectedComponentDetector<>(graph).getComponents(); + assertThat(components, containsInAnyOrder(containsInAnyOrder(1, 3, 5, 6, 7, 9), containsInAnyOrder(2, 4, 8))); + } + + @Test + @DisplayName("basic sort") + void testBasicSort() { + MutableGraph graph = GraphBuilder.directed().build(); + graph.putEdge(2, 4); + graph.putEdge(4, 7); + graph.putEdge(4, 6); + graph.putEdge(3, 4); + graph.putEdge(1, 4); + graph.putEdge(3, 1); + List list = TopologicalSort.topologicalSort(graph, Comparator.naturalOrder()); + assertThat(list, contains(2, 3, 1, 4, 6, 7)); + } + + @Test + @DisplayName("basic sort 2") + void testBasicSort2() { + MutableGraph graph = GraphBuilder.directed().build(); + graph.putEdge(7, 6); + graph.putEdge(8, 6); + graph.putEdge(8, 2); + graph.putEdge(12, 2); + graph.putEdge(2, 6); + graph.putEdge(6, 1); + graph.putEdge(2, 4); + graph.putEdge(2, 1); + graph.putEdge(1, 4); + graph.putEdge(1, 3); + graph.putEdge(4, 5); + graph.putEdge(1, 5); + graph.putEdge(5, 3); + graph.putEdge(1, 10); + graph.putEdge(3, 11); + graph.putEdge(5, 9); + graph.putEdge(11, 9); + graph.putEdge(11, 13); + graph.putEdge(10, 13); + List list = TopologicalSort.topologicalSort(graph, Collections.reverseOrder()); + assertThat(list, contains(12, 8, 7, 2, 6, 1, 10, 4, 5, 3, 11, 13, 9)); + } + + @Test + @DisplayName("loop sort") + void testLoopSort() { + MutableGraph graph = GraphBuilder.directed().build(); + graph.putEdge(2, 4); + graph.putEdge(4, 7); + graph.putEdge(4, 6); + graph.putEdge(6, 1); + graph.putEdge(3, 4); + graph.putEdge(1, 4); + graph.putEdge(3, 1); + CyclePresentException ex = Assertions.assertThrows(CyclePresentException.class, () -> { + TopologicalSort.topologicalSort(graph, Comparator.naturalOrder()); + }); + assertThat(ex.getCycles(), contains(containsInAnyOrder(1, 4, 6))); + } +} diff --git a/src/main/resources/assets/forge/lang/en_us.json b/src/main/resources/assets/forge/lang/en_us.json index 6cf899a7d..f4e09daad 100644 --- a/src/main/resources/assets/forge/lang/en_us.json +++ b/src/main/resources/assets/forge/lang/en_us.json @@ -25,6 +25,7 @@ "fml.modloading.errorduringevent":"{0,modinfo,name} ({0,modinfo,id}) encountered an error during the {1,lower} event phase\n\u00a77{2,exc,msg}", "fml.modloading.failedtoloadforge": "Failed to load forge", "fml.modloading.missingdependency": "Mod \u00a7e{4}\u00a7r requires \u00a76{3}\u00a7r \u00a7o{5,vr}\u00a7r\n\u00a77Currently, \u00a76{3}\u00a7r\u00a77 is \u00a7o{6,i18n,fml.messages.artifactversion.ornotinstalled}", + "fml.modloading.cycle": "Detected a mod dependency cycle: {0}", "fml.modloading.failedtoprocesswork":"{0,modinfo,name} ({0,modinfo,id}) encountered an error processing deferred work\n\u00a77{2,exc,msg}", "fml.messages.artifactversion.ornotinstalled":"{0,ornull,fml.messages.artifactversion.notinstalled}",