diff --git a/src/main/java/de/craftsblock/craftsnet/addon/loaders/AddonLoadOrder.java b/src/main/java/de/craftsblock/craftsnet/addon/loaders/AddonLoadOrder.java index 6381de42..09aad062 100644 --- a/src/main/java/de/craftsblock/craftsnet/addon/loaders/AddonLoadOrder.java +++ b/src/main/java/de/craftsblock/craftsnet/addon/loaders/AddonLoadOrder.java @@ -1,208 +1,277 @@ package de.craftsblock.craftsnet.addon.loaders; import de.craftsblock.craftsnet.addon.Addon; -import de.craftsblock.craftsnet.autoregister.loaders.AutoRegisterLoader; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.io.Closeable; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Represents a system for managing the load order of addons. - * This class may be used to organize and control the order in which addons are loaded or executed. - * It provides methods for adding addons, specifying dependencies, and retrieving the load order. * - * @author CraftsBlock + *

This implementation is based on a directed dependency graph and uses + * a topological sorting algorithm (Kahn's Algorithm) to determine a valid + * and efficient load order.

+ * * @author Philipp Maywald + * @author CraftsBlock * @since 3.0.2-SNAPSHOT */ -final class AddonLoadOrder implements Closeable { +public final class AddonLoadOrder implements Closeable { + + /** + * Stores all known addons mapped by their name. + */ + private final Map addonLoadOrder = + Collections.synchronizedMap(new LinkedHashMap<>()); + + /** + * Stores required dependencies. + */ + private final Map> dependencies = new HashMap<>(); - private final Map addonLoadOrder = Collections.synchronizedMap(new LinkedHashMap<>()); + /** + * Stores optional dependencies. + */ + private final Map> softDependencies = new HashMap<>(); /** - * Adds an Addon to the system, considering its load order. - * The method utilizes a ConcurrentHashMap to efficiently manage the load order of addons based on their names. - * If an addon with the same name already exists, the load order is updated accordingly. + * Adds an Addon to the system. * - * @param addon The Addon object to be added to the system. + * @param addon The Addon to add */ public void addAddon(Addon addon) { - addonLoadOrder.compute(addon.getName(), (name, bootMapping) -> - (bootMapping == null) ? new BootMapping(name, 0, addon, true) : bootMapping.addon(addon)); + addonLoadOrder.compute( + addon.getName(), + (name, mapping) -> { + if (mapping == null) { + return new BootMapping(name, addon, true); + } + + return mapping.setAddon(addon); + } + ); } /** - * Perform cleanup for the current {@link AddonLoadOrder} - * when it is no longer used. + * Clears all stored data. */ @Override public void close() { addonLoadOrder.clear(); + dependencies.clear(); + softDependencies.clear(); } /** - * Checks if the specified addon is present in the addon load order and has a non-null association. + * Checks if the given addon exists. * - * @param addon The addon to be checked for presence. - * @return true if the addon is present in the addon load order and has a non-null association, false otherwise. + * @param addon The addon instance + * @return true if present */ public boolean contains(Addon addon) { return contains(addon.getName()); } /** - * Checks if the specified addon is present in the addon load order and has a non-null association. + * Checks if the given addon exists. * - * @param addon The addon to be checked for presence. - * @return true if the addon is present in the addon load order and has a non-null association, false otherwise. + * @param addon The addon name + * @return true if present */ public boolean contains(String addon) { - return addonLoadOrder.containsKey(addon) && addonLoadOrder.get(addon).addon() != null; + return addonLoadOrder.containsKey(addon) + && addonLoadOrder.get(addon).getAddon() != null; } /** - * Registers a required dependency for the specified addon. + * Registers a required dependency. * - * @param addon the addon that declares the dependency - * @param dependsOn the name of the addon that is depended on + * @param addon The addon declaring the dependency + * @param dependsOn The required dependency */ public void depends(String addon, String dependsOn) { - this.dryDepends(addon, dependsOn, true); + dryDepends(addon, dependsOn, dependencies, true); } /** - * Registers an optional (soft) dependency for the specified addon. + * Registers an optional dependency. * - * @param addon the addon that declares the dependency - * @param dependsOn the name of the addon that is optionally depended on - * @since 3.3.4-SNAPSHOT + * @param addon The addon declaring the dependency + * @param dependsOn The optional dependency */ public void softDepends(String addon, String dependsOn) { - this.dryDepends(addon, dependsOn, false); + this.dryDepends(addon, dependsOn, softDependencies, false); } /** - * Registers a dependency for the specified addon on another addon. - *

- * The dependency can be either required or optional based on the {@code required} parameter. - * This method updates the internal addon load order by merging a new {@link BootMapping} into the existing mapping. - * If a dependency mapping for {@code dependsOn} already exists, the method adjusts its priority and, if necessary, - * marks it as required. - *

+ * Registers a dependency with the specified required state. * - * @param addon the addon that declares the dependency - * @param dependsOn the name of the addon that is being depended on - * @param required {@code true} if the dependency is required, {@code false} otherwise. - * @throws IllegalStateException if the addon attempts to depend on itself - * @since 3.3.4-SNAPSHOT + * @param addon The addon declaring the dependency. + * @param dependsOn The dependency. + * @param dependencies The dependencies the dependency should be registered to. + * @param required Whether the dependency is forcibly required. */ - private void dryDepends(String addon, String dependsOn, boolean required) { - if (addon.equalsIgnoreCase(dependsOn)) { - throw new IllegalStateException("Can not add " + addon + " as depends to itself!"); + private void dryDepends(String addon, String dependsOn, + Map> dependencies, + boolean required) { + validateSelfDependency(addon, dependsOn); + + dependencies.computeIfAbsent(addon, k -> new HashSet<>()) + .add(dependsOn); + + addonLoadOrder.putIfAbsent(addon, new BootMapping(addon, null, false)); + + if (addonLoadOrder.containsKey(dependsOn)) { + addonLoadOrder.get(dependsOn).mergeRequired(required); + } else { + addonLoadOrder.put(dependsOn, new BootMapping(dependsOn, null, required)); } + } - final int addonPriority = getPriority(addon); - addonLoadOrder.merge(dependsOn, new BootMapping(dependsOn, addonPriority + 1, null, required), - (existingMapping, newMapping) -> { - int dependsOnPriority = getPriority(dependsOn); + /** + * Creates a new sorted stream of the currently registered + * {@link BootMapping}'s. + * + * @return The created and sorted stream. + */ + private @Unmodifiable Stream createSortedBootMappingStream() { + Map> graph = new HashMap<>(); + Map inDegree = new HashMap<>(); - if (required) { - existingMapping.require(); - } + for (String name : addonLoadOrder.keySet()) { + graph.putIfAbsent(name, new ArrayList<>()); + inDegree.putIfAbsent(name, 0); + } + + for (Map.Entry> entry : dependencies.entrySet()) { + String addon = entry.getKey(); - return (addonPriority <= dependsOnPriority) ? - existingMapping.priority(addonPriority + 1) : - existingMapping; - }); + for (String dep : entry.getValue()) { + if (!addonLoadOrder.containsKey(dep)) { + throw new IllegalStateException("Missing required dependency: " + dep); + } + + graph.computeIfAbsent(dep, k -> new ArrayList<>()).add(addon); + inDegree.put(addon, inDegree.getOrDefault(addon, 0) + 1); + } + } + + for (Map.Entry> entry : softDependencies.entrySet()) { + String addon = entry.getKey(); + + for (String dep : entry.getValue()) { + if (!addonLoadOrder.containsKey(dep)) { + continue; + } + + graph.computeIfAbsent(dep, k -> new ArrayList<>()).add(addon); + inDegree.put(addon, inDegree.getOrDefault(addon, 0) + 1); + } + } + + Queue queue = new ArrayDeque<>(); + for (Map.Entry entry : inDegree.entrySet()) { + if (entry.getValue() == 0) { + queue.add(entry.getKey()); + } + } + + List sorted = new ArrayList<>(); + while (!queue.isEmpty()) { + String current = queue.poll(); + sorted.add(current); + + for (String next : graph.getOrDefault(current, Collections.emptyList())) { + int newDegree = inDegree.merge(next, -1, Integer::sum); + if (newDegree == 0) { + queue.add(next); + } + } + } + + if (sorted.size() != addonLoadOrder.size()) { + throw new IllegalStateException("Dependency cycle detected!"); + } + + return sorted.stream().map(addonLoadOrder::get); } /** - * Retrieves an unmodifiable collection representing the load order of addons names. - * The method generates a sorted list of addon names based on their load priorities, - * and then maps the names to their corresponding addons using the addonLoadOrder map. + * Builds and returns the final load order. + * + *

This method performs a topological sort over the dependency graph.

* - * @return An unmodifiable Collection of addons, representing the load order. - * @since 3.4.3 + * @return ordered list of addons + * @throws IllegalStateException if dependencies are missing or cyclic */ public @Unmodifiable List getPreLoadOrder() { - List bootOrderList = new ArrayList<>(addonLoadOrder.keySet()); - bootOrderList.sort(Comparator.comparingInt(value -> addonLoadOrder.get(value.toString()).priority()).reversed()); - return bootOrderList; + return createSortedBootMappingStream() + .filter(Objects::nonNull) + .map(BootMapping::getName) + .toList(); } /** - * Retrieves an unmodifiable collection representing the load order of addons. - * The resulting list of addons is filtered to exclude any null values, and the final collection is returned. + * Builds and returns the final load order. + * + *

This method performs a topological sort over the dependency graph.

* - * @return An unmodifiable Collection of addons, representing the load order. + * @return ordered list of addons + * @throws IllegalStateException if dependencies are missing or cyclic */ public @Unmodifiable List getLoadOrder() { - return getPreLoadOrder().stream() - .map(addonLoadOrder::get) + return createSortedBootMappingStream() .filter(Objects::nonNull) .filter(BootMapping::presenceFilter) - .sorted((o1, o2) -> Integer.compare(o2.priority(), o1.priority())) - .map(BootMapping::addon) + .map(BootMapping::getAddon) .filter(Objects::nonNull) - .collect(Collectors.toCollection(ArrayList::new)); + .toList(); } /** - * Retrieves the priority of an addon in the load order. - * If the addon is not found in the load order, a default BootMapping with priority 0 is used. - * - * @param addon The name of the addon for which the priority is to be retrieved. - * @return The priority of the specified addon in the load order. + * Validates that an addon does not depend on itself. */ - private int getPriority(String addon) { - if (addonLoadOrder.containsKey(addon)) { - return addonLoadOrder.get(addon).priority(); + private void validateSelfDependency(String addon, String dependsOn) { + if (addon.equalsIgnoreCase(dependsOn)) { + throw new IllegalStateException("Can not add " + addon + " as depends to itself!"); } - - return 0; } /** - * Represents a mapping used in the context of addon load order, associating a priority level with an addon. - * This class is utilized for organizing and managing the load order of addons within the system. + * Represents an internal mapping for an addon. */ private static class BootMapping { private final String name; - private int priority; - private Addon addon; private boolean required; + private Addon addon; /** - * Constructs a BootMapping with the specified priority and addon. + * Constructs a BootMapping for the specified addon. * * @param name The name of the addon. - * @param priority The priority level assigned to the addon in the load order. * @param addon The addon associated with this mapping. * @param required Whether this {@link BootMapping} is required to properly start or not. */ - public BootMapping(@NotNull String name, int priority, @Nullable Addon addon, boolean required) { + public BootMapping(@NotNull String name, + @Nullable Addon addon, + boolean required) { this.name = name; - this.priority = priority; this.addon = addon; this.required = required; } /** - * Sets the priority level for the addon in the load order. + * Gets the addon name associated with this mapping. * - * @param priority The new priority level to be set. - * @return The modified BootMapping instance with the updated priority. + * @return The addon name. */ - public BootMapping priority(int priority) { - this.priority = priority; - return this; + public String getName() { + return name; } /** @@ -211,56 +280,46 @@ public BootMapping priority(int priority) { * @param addon The addon to be associated with this mapping. * @return The modified BootMapping instance with the updated addon. */ - public BootMapping addon(Addon addon) { + public BootMapping setAddon(Addon addon) { this.addon = addon; return this; } /** - * Marks this {@link BootMapping} as required to properly start. + * Retrieves the addon associated with this mapping. * - * @since 3.3.4-SNAPSHOT + * @return The addon associated with this mapping. */ - public void require() { - this.required = true; + public Addon getAddon() { + return addon; } /** - * Retrieves the priority level assigned to the addon in the load order. + * Merges the required state into this mapping. * - * @return The priority level of the addon in the load order. + * @param required The required state to apply if this mapping is not already required. */ - public int priority() { - return priority; - } + public void mergeRequired(boolean required) { + if (this.required) { + return; + } - /** - * Retrieves the addon associated with this mapping. - * - * @return The addon associated with this mapping. - */ - public Addon addon() { - return addon; + this.required = required; } /** - * Filter for the presence check of this {@link BootMapping}. + * Ensures required addons are present. * - * @return {@code true} if the {@link BootMapping} is properly present. - * @throws IllegalStateException If the addon is required but not present. - * @since 3.3.4-SNAPSHOT + * @return true if valid */ public boolean presenceFilter() { - if (!required) { + if (!required || addon != null) { return true; } - if (addon != null) { - return true; - } - - throw new IllegalStateException("The addon \"" + this.name + "\" is required but not found!"); + throw new IllegalStateException( + "The addon " + this.name + " is required but not found!" + ); } - } -} +} \ No newline at end of file