diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java index 168ea8523..7690ce445 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java @@ -63,6 +63,7 @@ import me.lucko.luckperms.common.dependencies.DependencyRegistry; import me.lucko.luckperms.common.dependencies.classloader.PluginClassLoader; import me.lucko.luckperms.common.dependencies.classloader.ReflectionClassLoader; import me.lucko.luckperms.common.event.EventFactory; +import me.lucko.luckperms.common.inheritance.InheritanceHandler; import me.lucko.luckperms.common.locale.LocaleManager; import me.lucko.luckperms.common.locale.NoopLocaleManager; import me.lucko.luckperms.common.locale.SimpleLocaleManager; @@ -131,6 +132,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { private LocaleManager localeManager; private PluginClassLoader pluginClassLoader; private DependencyManager dependencyManager; + private InheritanceHandler inheritanceHandler; private CachedStateManager cachedStateManager; private ContextManager contextManager; private CalculatorFactory calculatorFactory; @@ -233,6 +235,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { // load internal managers getLog().info("Loading internal permission managers..."); + this.inheritanceHandler = new InheritanceHandler(this); this.userManager = new StandardUserManager(this); this.groupManager = new StandardGroupManager(this); this.trackManager = new StandardTrackManager(this); @@ -673,6 +676,11 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin { return this.contextManager; } + @Override + public InheritanceHandler getInheritanceHandler() { + return this.inheritanceHandler; + } + @Override public CalculatorFactory getCalculatorFactory() { return this.calculatorFactory; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/VaultChatHook.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/VaultChatHook.java index ba6ab3d84..79a5dde1a 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/VaultChatHook.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/VaultChatHook.java @@ -279,7 +279,7 @@ public class VaultChatHook extends AbstractVaultChat { } // find the max inherited priority & add 10 - MetaAccumulator metaAccumulator = holder.accumulateMeta(null, null, createContextForWorldSet(world)); + MetaAccumulator metaAccumulator = holder.accumulateMeta(null, createContextForWorldSet(world)); int priority = (type == ChatMetaType.PREFIX ? metaAccumulator.getPrefixes() : metaAccumulator.getSuffixes()).keySet().stream() .mapToInt(e -> e).max().orElse(0) + 10; diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index 74dc65bc2..6fdd836ed 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -134,6 +134,18 @@ apply-bukkit-default-permissions: true # permissions when considering if a player should have access to a certain permission. apply-bukkit-attachment-permissions: true +# The algorithm LuckPerms should use when traversing the "inheritance tree". +# +# The valid options are: +# - breadth-first +# - depth-first-pre-order +# - depth-first-post-order +# +# See here for information about the differences between each algorithm. +# - https://en.wikipedia.org/wiki/Breadth-first_search +# - https://en.wikipedia.org/wiki/Depth-first_search +inheritance-traversal-algorithm: depth-first-pre-order + # Define special group weights for this server. # Default is just 0. group-weight: diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java index 893edcd83..3ebc9e285 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java @@ -54,6 +54,7 @@ import me.lucko.luckperms.common.dependencies.DependencyRegistry; import me.lucko.luckperms.common.dependencies.classloader.PluginClassLoader; import me.lucko.luckperms.common.dependencies.classloader.ReflectionClassLoader; import me.lucko.luckperms.common.event.EventFactory; +import me.lucko.luckperms.common.inheritance.InheritanceHandler; import me.lucko.luckperms.common.locale.LocaleManager; import me.lucko.luckperms.common.locale.NoopLocaleManager; import me.lucko.luckperms.common.locale.SimpleLocaleManager; @@ -112,6 +113,7 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { private LocaleManager localeManager; private PluginClassLoader pluginClassLoader; private DependencyManager dependencyManager; + private InheritanceHandler inheritanceHandler; private CachedStateManager cachedStateManager; private ContextManager contextManager; private CalculatorFactory calculatorFactory; @@ -182,6 +184,7 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { // load internal managers getLog().info("Loading internal permission managers..."); + this.inheritanceHandler = new InheritanceHandler(this); this.userManager = new StandardUserManager(this); this.groupManager = new StandardGroupManager(this); this.trackManager = new StandardTrackManager(this); @@ -460,6 +463,11 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin { return this.contextManager; } + @Override + public InheritanceHandler getInheritanceHandler() { + return this.inheritanceHandler; + } + @Override public CalculatorFactory getCalculatorFactory() { return this.calculatorFactory; diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/event/TristateCheckEvent.java b/bungee/src/main/java/me/lucko/luckperms/bungee/event/TristateCheckEvent.java index 17b0fc847..51bacd9da 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/event/TristateCheckEvent.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/event/TristateCheckEvent.java @@ -27,35 +27,35 @@ package me.lucko.luckperms.bungee.event; import me.lucko.luckperms.api.Tristate; +import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.ProxyServer; -import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.plugin.Event; /** * Copy of the internal BungeeCord PermissionCheckEvent, returning a tristate instead of a boolean. */ public class TristateCheckEvent extends Event { - public static Tristate call(ProxiedPlayer player, String permission) { - return ProxyServer.getInstance().getPluginManager().callEvent(new TristateCheckEvent(player, permission)).getResult(); + public static Tristate call(CommandSender sender, String permission) { + return ProxyServer.getInstance().getPluginManager().callEvent(new TristateCheckEvent(sender, permission)).getResult(); } - private final ProxiedPlayer player; + private final CommandSender sender; private final String permission; private Tristate result; - public TristateCheckEvent(ProxiedPlayer player, String permission) { - this(player, permission, player.getPermissions().contains(permission) ? Tristate.TRUE : Tristate.UNDEFINED); + public TristateCheckEvent(CommandSender sender, String permission) { + this(sender, permission, sender.getPermissions().contains(permission) ? Tristate.TRUE : Tristate.UNDEFINED); } - public TristateCheckEvent(ProxiedPlayer player, String permission, Tristate result) { - this.player = player; + public TristateCheckEvent(CommandSender sender, String permission, Tristate result) { + this.sender = sender; this.permission = permission; this.result = result; } - public ProxiedPlayer getPlayer() { - return this.player; + public CommandSender getSender() { + return this.sender; } public String getPermission() { @@ -73,7 +73,7 @@ public class TristateCheckEvent extends Event { @Override public String toString() { return "TristateCheckEvent(" + - "player=" + this.player + ", " + + "sender=" + this.sender + ", " + "permission=" + this.permission + ", " + "result=" + this.result + ")"; } diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeePermissionCheckListener.java b/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeePermissionCheckListener.java index 55ddb6412..0bfa304a6 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeePermissionCheckListener.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeePermissionCheckListener.java @@ -77,10 +77,14 @@ public class BungeePermissionCheckListener implements Listener { @EventHandler public void onPlayerTristateCheck(TristateCheckEvent e) { - ProxiedPlayer player = e.getPlayer(); + if (!(e.getSender() instanceof ProxiedPlayer)) { + return; + } Objects.requireNonNull(e.getPermission(), "permission"); - Objects.requireNonNull(player, "player"); + Objects.requireNonNull(e.getSender(), "sender"); + + ProxiedPlayer player = ((ProxiedPlayer) e.getSender()); User user = this.plugin.getUserManager().getIfLoaded(player.getUniqueId()); if (user == null) { diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index 2531f8a28..67cd39d28 100644 --- a/bungee/src/main/resources/config.yml +++ b/bungee/src/main/resources/config.yml @@ -131,6 +131,18 @@ apply-shorthand: true # If set to false, LuckPerms will ignore these values. apply-bungee-config-permissions: false +# The algorithm LuckPerms should use when traversing the "inheritance tree". +# +# The valid options are: +# - breadth-first +# - depth-first-pre-order +# - depth-first-post-order +# +# See here for information about the differences between each algorithm. +# - https://en.wikipedia.org/wiki/Breadth-first_search +# - https://en.wikipedia.org/wiki/Depth-first_search +inheritance-traversal-algorithm: depth-first-pre-order + # Define special group weights for this server. # Default is just 0. group-weight: diff --git a/common/src/main/java/me/lucko/luckperms/common/caching/HolderCachedData.java b/common/src/main/java/me/lucko/luckperms/common/caching/HolderCachedData.java index b3bedf993..3906b0c41 100644 --- a/common/src/main/java/me/lucko/luckperms/common/caching/HolderCachedData.java +++ b/common/src/main/java/me/lucko/luckperms/common/caching/HolderCachedData.java @@ -117,9 +117,9 @@ public abstract class HolderCachedData implements Ca } if (contexts.getContexts() == Contexts.allowAll()) { - data.loadMeta(this.holder.accumulateMeta(newAccumulator(contexts), null)); + data.loadMeta(this.holder.accumulateMeta(newAccumulator(contexts))); } else { - data.loadMeta(this.holder.accumulateMeta(newAccumulator(contexts), null, contexts.getContexts())); + data.loadMeta(this.holder.accumulateMeta(newAccumulator(contexts), contexts.getContexts())); } return data; diff --git a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java index 0e2ea640d..6a6b58fab 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java @@ -37,6 +37,7 @@ import me.lucko.luckperms.common.config.keys.IntegerKey; import me.lucko.luckperms.common.config.keys.LowercaseStringKey; import me.lucko.luckperms.common.config.keys.MapKey; import me.lucko.luckperms.common.config.keys.StringKey; +import me.lucko.luckperms.common.inheritance.graph.TraversalAlgorithm; import me.lucko.luckperms.common.metastacking.SimpleMetaStackDefinition; import me.lucko.luckperms.common.metastacking.StandardStackElements; import me.lucko.luckperms.common.model.TemporaryModifier; @@ -222,6 +223,21 @@ public class ConfigKeys { */ public static final ConfigKey APPLY_SPONGE_DEFAULT_SUBJECTS = EnduringKey.wrap(BooleanKey.of("apply-sponge-default-subjects", true)); + /** + * The algorithm LuckPerms should use when traversing the "inheritance tree" + */ + public static final ConfigKey INHERITANCE_TRAVERSAL_ALGORITHM = AbstractKey.of(c -> { + String value = c.getString("inheritance-traversal-algorithm", "depth-first-pre-order"); + switch (value.toLowerCase()) { + case "breadth-first": + return TraversalAlgorithm.BREADTH_FIRST; + case "depth-first-post-order": + return TraversalAlgorithm.DEPTH_FIRST_POST_ORDER; + default: + return TraversalAlgorithm.DEPTH_FIRST_PRE_ORDER; + } + }); + /** * The configured group weightings */ diff --git a/common/src/main/java/me/lucko/luckperms/common/primarygroup/GroupInheritanceComparator.java b/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceComparator.java similarity index 89% rename from common/src/main/java/me/lucko/luckperms/common/primarygroup/GroupInheritanceComparator.java rename to common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceComparator.java index 26114e8f2..6df485e35 100644 --- a/common/src/main/java/me/lucko/luckperms/common/primarygroup/GroupInheritanceComparator.java +++ b/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceComparator.java @@ -23,7 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.primarygroup; +package me.lucko.luckperms.common.inheritance; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.PermissionHolder; @@ -35,19 +35,19 @@ import java.util.Comparator; /** * Determines the order of group inheritance in {@link PermissionHolder}. */ -public class GroupInheritanceComparator implements Comparator { - private static final Comparator NULL_ORIGIN = new GroupInheritanceComparator(null); +public class InheritanceComparator implements Comparator { + private static final Comparator NULL_ORIGIN = new InheritanceComparator(null); public static Comparator getFor(PermissionHolder origin) { if (origin.getType().isUser()) { - return new GroupInheritanceComparator(((User) origin)); + return new InheritanceComparator(((User) origin)); } return NULL_ORIGIN; } private final User origin; - private GroupInheritanceComparator(User origin) { + private InheritanceComparator(User origin) { this.origin = origin; } diff --git a/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraph.java b/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraph.java new file mode 100644 index 000000000..9c96d5dca --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraph.java @@ -0,0 +1,111 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.inheritance; + +import me.lucko.luckperms.api.Contexts; +import me.lucko.luckperms.api.Node; +import me.lucko.luckperms.common.inheritance.graph.Graph; +import me.lucko.luckperms.common.inheritance.graph.GraphTraversers; +import me.lucko.luckperms.common.inheritance.graph.TraversalAlgorithm; +import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.model.PermissionHolder; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; + +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * A {@link Graph} which represents an "inheritance tree". + */ +public interface InheritanceGraph extends Graph { + + /** + * Returns an iterable which will traverse this inheritance graph using the + * specified algorithm starting at the given node. + * + * @param algorithm the algorithm to use when traversing + * @param startNode the start node in the inheritance graph + * @return an iterable + */ + default Iterable traverse(TraversalAlgorithm algorithm, PermissionHolder startNode) { + return GraphTraversers.traverseUsing(algorithm, this, startNode); + } + + final class NonContextual implements InheritanceGraph { + private final LuckPermsPlugin plugin; + + NonContextual(LuckPermsPlugin plugin) { + this.plugin = plugin; + } + + @Override + public Iterable successors(PermissionHolder holder) { + Set successors = new TreeSet<>(holder.getInheritanceComparator()); + List nodes = holder.getOwnGroupNodes(); + for (Node n : nodes) { + Group g = this.plugin.getGroupManager().getIfLoaded(n.getGroupName()); + if (g != null) { + successors.add(g); + } + } + return successors; + } + } + + final class Contextual implements InheritanceGraph { + private final LuckPermsPlugin plugin; + + /** + * The contexts to resolve inheritance in. + */ + private final Contexts context; + + Contextual(LuckPermsPlugin plugin, Contexts context) { + this.plugin = plugin; + this.context = context; + } + + @Override + public Iterable successors(PermissionHolder holder) { + Set successors = new TreeSet<>(holder.getInheritanceComparator()); + List nodes = holder.getOwnGroupNodes(this.context.getContexts()); + for (Node n : nodes) { + // effectively: if not (we're applying global groups or it's specific anyways) + if (!((this.context.isApplyGlobalGroups() || n.isServerSpecific()) && (this.context.isApplyGlobalWorldGroups() || n.isWorldSpecific()))) { + continue; + } + + Group g = this.plugin.getGroupManager().getIfLoaded(n.getGroupName()); + if (g != null) { + successors.add(g); + } + } + return successors; + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceHandler.java b/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceHandler.java new file mode 100644 index 000000000..f298ce117 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceHandler.java @@ -0,0 +1,68 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.inheritance; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +import me.lucko.luckperms.api.Contexts; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; + +import java.util.concurrent.TimeUnit; + +/** + * Provides {@link InheritanceGraph}s. + */ +public class InheritanceHandler { + private final LuckPermsPlugin plugin; + + /** + * An inheritance graph which doesn't consider contexts + */ + private final InheritanceGraph nonContextualGraph; + + /** + * Cache of contextual inheritance graph instances + */ + private final LoadingCache contextualGraphs; + + public InheritanceHandler(LuckPermsPlugin plugin) { + this.plugin = plugin; + this.nonContextualGraph = new InheritanceGraph.NonContextual(plugin); + this.contextualGraphs = Caffeine.newBuilder() + .expireAfterAccess(10, TimeUnit.MINUTES) + .build(key -> new InheritanceGraph.Contextual(this.plugin, key)); + } + + public InheritanceGraph getGraph() { + return this.nonContextualGraph; + } + + public InheritanceGraph getGraph(Contexts contexts) { + return this.contextualGraphs.get(contexts); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/inheritance/graph/Graph.java b/common/src/main/java/me/lucko/luckperms/common/inheritance/graph/Graph.java new file mode 100644 index 000000000..b223d0add --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/inheritance/graph/Graph.java @@ -0,0 +1,44 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.inheritance.graph; + +/** + * A minimal functional interface for graph-structured data. + * + * @param the node parameter type + */ +@FunctionalInterface +public interface Graph { + + /** + * Returns all nodes in this graph directly adjacent to {@code node} which + * can be reached by traversing {@code node}'s outgoing edges. + * + * @throws IllegalArgumentException if {@code node} is not an element of this graph + */ + Iterable successors(N node); + +} \ No newline at end of file diff --git a/common/src/main/java/me/lucko/luckperms/common/inheritance/graph/GraphTraversers.java b/common/src/main/java/me/lucko/luckperms/common/inheritance/graph/GraphTraversers.java new file mode 100644 index 000000000..1453333ef --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/inheritance/graph/GraphTraversers.java @@ -0,0 +1,215 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * Copyright (C) 2017 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.lucko.luckperms.common.inheritance.graph; + +import com.google.common.collect.AbstractIterator; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; + +/** + * A collection of graph traversal algorithms. + * + * @author Jens Nyman + */ +public final class GraphTraversers { + + /** + * Returns an unmodifiable {@code Iterable} over the nodes reachable from + * {@code startNode}, in the order defined by the {@code algorithm}. + */ + public static Iterable traverseUsing(TraversalAlgorithm algorithm, Graph graph, N startNode) { + Objects.requireNonNull(algorithm, "algorithm"); + switch (algorithm) { + case BREADTH_FIRST: + return breadthFirst(graph, startNode); + case DEPTH_FIRST_PRE_ORDER: + return depthFirstPreOrder(graph, startNode); + case DEPTH_FIRST_POST_ORDER: + return depthFirstPostOrder(graph, startNode); + default: + throw new AssertionError(); + } + } + + /** + * Returns an unmodifiable {@code Iterable} over the nodes reachable from + * {@code startNode}, in the order of a breadth-first traversal. That is, + * all the nodes of depth 0 are returned, then depth 1, then 2, and so on. + * + *

See Wikipedia for more info.

+ */ + public static Iterable breadthFirst(Graph graph, N startNode) { + Objects.requireNonNull(graph, "graph"); + Objects.requireNonNull(startNode, "startNode"); + return () -> new BreadthFirstIterator<>(graph, startNode); + } + + /** + * Returns an unmodifiable {@code Iterable} over the nodes reachable from + * {@code startNode}, in the order of a depth-first pre-order traversal. + * "Pre-order" implies that nodes appear in the {@code Iterable} in the + * order in which they are first visited. + * + *

See Wikipedia for more info.

+ */ + public static Iterable depthFirstPreOrder(Graph graph, N startNode) { + Objects.requireNonNull(graph, "graph"); + Objects.requireNonNull(startNode, "startNode"); + return () -> new DepthFirstIterator<>(graph, startNode, Order.PRE_ORDER); + } + + /** + * Returns an unmodifiable {@code Iterable} over the nodes reachable from {@code startNode}, in + * the order of a depth-first post-order traversal. "Post-order" implies that nodes appear in the + * {@code Iterable} in the order in which they are visited for the last time. + * + *

See Wikipedia for more info.

+ */ + public static Iterable depthFirstPostOrder(Graph graph, N startNode) { + Objects.requireNonNull(graph, "graph"); + Objects.requireNonNull(startNode, "startNode"); + return () -> new DepthFirstIterator<>(graph, startNode, Order.POST_ORDER); + } + + private static final class BreadthFirstIterator implements Iterator { + private final Graph graph; + + private final Queue queue = new ArrayDeque<>(); + private final Set visited = new HashSet<>(); + + BreadthFirstIterator(Graph graph, N root) { + this.graph = graph; + this.queue.add(root); + this.visited.add(root); + } + + @Override + public boolean hasNext() { + return !this.queue.isEmpty(); + } + + @Override + public N next() { + N current = this.queue.remove(); + for (N neighbor : this.graph.successors(current)) { + if (this.visited.add(neighbor)) { + this.queue.add(neighbor); + } + } + return current; + } + } + + private static final class DepthFirstIterator extends AbstractIterator { + private final Graph graph; + + private final Deque stack = new ArrayDeque<>(); + private final Set visited = new HashSet<>(); + private final Order order; + + DepthFirstIterator(Graph graph, N root, Order order) { + this.graph = graph; + + // our invariant is that in computeNext we call next on the iterator at the top first, so we + // need to start with one additional item on that iterator + this.stack.push(withSuccessors(root)); + this.order = order; + } + + @Override + protected N computeNext() { + while (true) { + if (this.stack.isEmpty()) { + return endOfData(); + } + NodeAndSuccessors node = this.stack.getFirst(); + boolean firstVisit = this.visited.add(node.node); + boolean lastVisit = !node.successorIterator.hasNext(); + boolean produceNode = (firstVisit && this.order == Order.PRE_ORDER) || (lastVisit && this.order == Order.POST_ORDER); + if (lastVisit) { + this.stack.pop(); + } else { + // we need to push a neighbor, but only if we haven't already seen it + N successor = node.successorIterator.next(); + if (!this.visited.contains(successor)) { + this.stack.push(withSuccessors(successor)); + } + } + if (produceNode) { + return node.node; + } + } + } + + NodeAndSuccessors withSuccessors(N node) { + return new NodeAndSuccessors(node, this.graph.successors(node)); + } + + /** + * A simple tuple of a node and a partially iterated {@link Iterator} of + * its successors + */ + private final class NodeAndSuccessors { + final N node; + final Iterator successorIterator; + + NodeAndSuccessors(N node, Iterable successors) { + this.node = node; + this.successorIterator = successors.iterator(); + } + } + } + + private enum Order { + PRE_ORDER, + POST_ORDER + } + + private GraphTraversers() {} + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/inheritance/graph/TraversalAlgorithm.java b/common/src/main/java/me/lucko/luckperms/common/inheritance/graph/TraversalAlgorithm.java new file mode 100644 index 000000000..cd058f253 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/inheritance/graph/TraversalAlgorithm.java @@ -0,0 +1,59 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.inheritance.graph; + +public enum TraversalAlgorithm { + + /** + * Traverses in breadth-first order. + * + *

That is, all the nodes of depth 0 are returned, then depth 1, then 2, and so on.

+ * + *

See Wikipedia for more info.

+ */ + BREADTH_FIRST, + + /** + * Traverses in depth-first pre-order. + * + *

"Pre-order" implies that nodes appear in the order in which they are + * first visited.

+ * + *

See Wikipedia for more info.

+ */ + DEPTH_FIRST_PRE_ORDER, + + /** + * Traverses in depth-first post-order. + * + *

"Post-order" implies that nodes appear in the order in which they are + * visited for the last time.

+ * + *

See Wikipedia for more info.

+ */ + DEPTH_FIRST_POST_ORDER + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java b/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java index 948fe2fa6..4f0532a27 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/NodeMap.java @@ -83,6 +83,15 @@ public final class NodeMap { .treeSetValues(NodeComparator.reverse()) .build(); + /** + * Copy of {@link #map} which only contains group nodes + * @see Node#isGroupNode() + */ + private final SortedSetMultimap inheritanceMap = MultimapBuilder + .treeKeys(ContextSetComparator.reverse()) + .treeSetValues(NodeComparator.reverse()) + .build(); + /** * The lock which synchronizes the instance */ @@ -143,6 +152,28 @@ public final class NodeMap { } } + public void copyGroupNodesTo(Collection collection) { + this.lock.lock(); + try { + collection.addAll(this.inheritanceMap.values()); + } finally { + this.lock.unlock(); + } + } + + public void copyGroupNodesTo(Collection collection, ContextSet filter) { + this.lock.lock(); + try { + for (Map.Entry> e : this.inheritanceMap.asMap().entrySet()) { + if (e.getKey().isSatisfiedBy(filter)) { + collection.addAll(e.getValue()); + } + } + } finally { + this.lock.unlock(); + } + } + public void copyToLocalized(Collection collection) { this.lock.lock(); try { @@ -173,17 +204,11 @@ public final class NodeMap { void add(Node node) { this.lock.lock(); try { - this.map.put(node.getFullContexts().makeImmutable(), node); - } finally { - this.lock.unlock(); - } - } - - void replace(Node node, Node previous) { - this.lock.lock(); - try { - this.map.remove(previous.getFullContexts().makeImmutable(), previous); - this.map.put(node.getFullContexts().makeImmutable(), node); + ImmutableContextSet context = node.getFullContexts().makeImmutable(); + this.map.put(context, node); + if (node.isGroupNode() && node.getValuePrimitive()) { + this.inheritanceMap.put(context, node); + } } finally { this.lock.unlock(); } @@ -192,7 +217,34 @@ public final class NodeMap { void remove(Node node) { this.lock.lock(); try { - this.map.get(node.getFullContexts().makeImmutable()).removeIf(e -> e.almostEquals(node)); + ImmutableContextSet context = node.getFullContexts().makeImmutable(); + this.map.get(context).removeIf(e -> e.almostEquals(node)); + if (node.isGroupNode()) { + this.inheritanceMap.get(context).removeIf(e -> e.almostEquals(node)); + } + } finally { + this.lock.unlock(); + } + } + + private void removeExact(Node node) { + this.lock.lock(); + try { + ImmutableContextSet context = node.getFullContexts().makeImmutable(); + this.map.remove(context, node); + if (node.isGroupNode() && node.getValuePrimitive()) { + this.inheritanceMap.remove(context, node); + } + } finally { + this.lock.unlock(); + } + } + + void replace(Node node, Node previous) { + this.lock.lock(); + try { + removeExact(previous); + add(node); } finally { this.lock.unlock(); } @@ -202,6 +254,7 @@ public final class NodeMap { this.lock.lock(); try { this.map.clear(); + this.inheritanceMap.clear(); } finally { this.lock.unlock(); } @@ -210,7 +263,9 @@ public final class NodeMap { void clear(ContextSet contextSet) { this.lock.lock(); try { - this.map.removeAll(contextSet.makeImmutable()); + ImmutableContextSet context = contextSet.makeImmutable(); + this.map.removeAll(context); + this.inheritanceMap.removeAll(context); } finally { this.lock.unlock(); } @@ -220,8 +275,9 @@ public final class NodeMap { this.lock.lock(); try { this.map.clear(); + this.inheritanceMap.clear(); for (Node n : set) { - this.map.put(n.getFullContexts().makeImmutable(), n); + add(n); } } finally { this.lock.unlock(); @@ -232,7 +288,14 @@ public final class NodeMap { this.lock.lock(); try { this.map.clear(); + this.inheritanceMap.clear(); + this.map.putAll(multimap); + for (Map.Entry entry : this.map.entries()) { + if (entry.getValue().isGroupNode() && entry.getValue().getValuePrimitive()) { + this.inheritanceMap.put(entry.getKey(), entry.getValue()); + } + } } finally { this.lock.unlock(); } @@ -241,7 +304,11 @@ public final class NodeMap { boolean removeIf(Predicate predicate) { this.lock.lock(); try { - return this.map.values().removeIf(predicate); + boolean ret = this.map.values().removeIf(predicate); + if (ret) { + this.inheritanceMap.values().removeIf(predicate); + } + return ret; } finally { this.lock.unlock(); } @@ -250,8 +317,13 @@ public final class NodeMap { boolean removeIf(ContextSet contextSet, Predicate predicate) { this.lock.lock(); try { - SortedSet nodes = this.map.get(contextSet.makeImmutable()); - return nodes != null && nodes.removeIf(predicate); + ImmutableContextSet context = contextSet.makeImmutable(); + SortedSet nodes = this.map.get(context); + boolean ret = nodes.removeIf(predicate); + if (ret) { + this.inheritanceMap.get(context).removeIf(predicate); + } + return ret; } finally { this.lock.unlock(); } @@ -269,6 +341,9 @@ public final class NodeMap { if (removed != null) { removed.add(entry); } + if (entry.isGroupNode() && entry.getValuePrimitive()) { + this.inheritanceMap.remove(entry.getFullContexts().makeImmutable(), entry); + } work = true; it.remove(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java b/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java index 45bbc9dbb..356e57bb5 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java @@ -44,6 +44,8 @@ import me.lucko.luckperms.common.caching.HolderCachedData; import me.lucko.luckperms.common.caching.handlers.StateListener; import me.lucko.luckperms.common.caching.type.MetaAccumulator; import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.inheritance.InheritanceComparator; +import me.lucko.luckperms.common.inheritance.InheritanceGraph; import me.lucko.luckperms.common.node.ImmutableLocalizedNode; import me.lucko.luckperms.common.node.InheritanceInfo; import me.lucko.luckperms.common.node.MetaType; @@ -51,7 +53,6 @@ import me.lucko.luckperms.common.node.NodeFactory; import me.lucko.luckperms.common.node.NodeTools; import me.lucko.luckperms.common.node.NodeWithContextComparator; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.primarygroup.GroupInheritanceComparator; import me.lucko.luckperms.common.references.GroupReference; import me.lucko.luckperms.common.references.HolderReference; import me.lucko.luckperms.common.references.HolderType; @@ -151,7 +152,7 @@ public abstract class PermissionHolder { /** * Comparator used to ordering groups when calculating inheritance */ - private final Comparator inheritanceComparator = GroupInheritanceComparator.getFor(this); + private final Comparator inheritanceComparator = InheritanceComparator.getFor(this); /** * A set of runnables which are called when this objects state changes. @@ -235,6 +236,10 @@ public abstract class PermissionHolder { */ public abstract HolderType getType(); + public Comparator getInheritanceComparator() { + return this.inheritanceComparator; + } + public NodeMap getData(NodeMapType type) { switch (type) { case ENDURING: @@ -296,6 +301,20 @@ public abstract class PermissionHolder { return ret; } + public List getOwnGroupNodes() { + List ret = new ArrayList<>(); + this.transientNodes.copyGroupNodesTo(ret); + this.enduringNodes.copyGroupNodesTo(ret); + return ret; + } + + public List getOwnGroupNodes(ContextSet filter) { + List ret = new ArrayList<>(); + this.transientNodes.copyGroupNodesTo(ret, filter); + this.enduringNodes.copyGroupNodesTo(ret, filter); + return ret; + } + public SortedSet getOwnNodesSorted() { SortedSet ret = new TreeSet<>(NodeWithContextComparator.reverse()); this.transientNodes.copyToLocalized(ret); @@ -323,69 +342,28 @@ public abstract class PermissionHolder { return result; } - /** - * Resolves inherited nodes and returns them - * - * @param excludedGroups a list of groups to exclude - * @param context context to decide if groups should be applied - * @return a set of nodes - */ - public List resolveInheritances(List accumulator, Set excludedGroups, Contexts context) { - if (accumulator == null) { - accumulator = new ArrayList<>(); - } - - if (excludedGroups == null) { - excludedGroups = new HashSet<>(); - } - - if (this.getType().isGroup()) { - excludedGroups.add(getObjectName().toLowerCase()); - } - - // get and add the objects own nodes - List nodes = getOwnNodes(context.getContexts()); - for (Node node : nodes) { - ImmutableLocalizedNode localizedNode = ImmutableLocalizedNode.of(node, getObjectName()); - accumulator.add(localizedNode); - } - - // resolve and process the objects parents - List resolvedGroups = new ArrayList<>(); - Set processedGroups = new HashSet<>(); - - for (Node n : nodes) { - if (!n.isGroupNode()) continue; - String groupName = n.getGroupName(); - - if (!processedGroups.add(groupName) || excludedGroups.contains(groupName) || !n.getValuePrimitive()) continue; - - if (!((context.isApplyGlobalGroups() || n.isServerSpecific()) && (context.isApplyGlobalWorldGroups() || n.isWorldSpecific()))) { - continue; - } - - Group g = this.plugin.getGroupManager().getIfLoaded(groupName); - if (g != null) { - resolvedGroups.add(g); + private void accumulateInheritancesTo(List accumulator, Contexts context) { + InheritanceGraph graph = this.plugin.getInheritanceHandler().getGraph(context); + Iterable traversal = graph.traverse(this.plugin.getConfiguration().get(ConfigKeys.INHERITANCE_TRAVERSAL_ALGORITHM), this); + for (PermissionHolder holder : traversal) { + List nodes = holder.getOwnNodes(context.getContexts()); + for (Node node : nodes) { + ImmutableLocalizedNode localizedNode = ImmutableLocalizedNode.of(node, holder.getObjectName()); + accumulator.add(localizedNode); } } - - // sort the groups according to weight + other factors. - resolvedGroups.sort(this.inheritanceComparator); - - for (Group g : resolvedGroups) { - g.resolveInheritances(accumulator, excludedGroups, context); - } - - return accumulator; } public List resolveInheritances(Contexts context) { - return resolveInheritances(null, null, context); + List accumulator = new ArrayList<>(); + accumulateInheritancesTo(accumulator, context); + return accumulator; } public SortedSet resolveInheritancesAlmostEqual(Contexts contexts) { - List nodes = resolveInheritances(new LinkedList<>(), null, contexts); + List nodes = new LinkedList<>(); + accumulateInheritancesTo(nodes, contexts); + NodeTools.removeAlmostEqual(nodes.iterator()); SortedSet ret = new TreeSet<>(NodeWithContextComparator.reverse()); ret.addAll(nodes); @@ -393,71 +371,37 @@ public abstract class PermissionHolder { } public SortedSet resolveInheritancesMergeTemp(Contexts contexts) { - List nodes = resolveInheritances(new LinkedList<>(), null, contexts); + List nodes = new LinkedList<>(); + accumulateInheritancesTo(nodes, contexts); + NodeTools.removeIgnoreValueOrTemp(nodes.iterator()); SortedSet ret = new TreeSet<>(NodeWithContextComparator.reverse()); ret.addAll(nodes); return ret; } - /** - * Resolves inherited nodes and returns them - * - * @param excludedGroups a list of groups to exclude - * @return a set of nodes - */ - public List resolveInheritances(List accumulator, Set excludedGroups) { - if (accumulator == null) { - accumulator = new ArrayList<>(); - } - - if (excludedGroups == null) { - excludedGroups = new HashSet<>(); - } - - if (this.getType().isGroup()) { - excludedGroups.add(getObjectName().toLowerCase()); - } - - // get and add the objects own nodes - List nodes = getOwnNodes(); - for (Node node : nodes) { - ImmutableLocalizedNode localizedNode = ImmutableLocalizedNode.of(node, getObjectName()); - accumulator.add(localizedNode); - } - - // resolve and process the objects parents - List resolvedGroups = new ArrayList<>(); - Set processedGroups = new HashSet<>(); - - for (Node n : nodes) { - if (!n.isGroupNode()) continue; - String groupName = n.getGroupName(); - - if (!processedGroups.add(groupName) || excludedGroups.contains(groupName) || !n.getValuePrimitive()) continue; - - Group g = this.plugin.getGroupManager().getIfLoaded(groupName); - if (g != null) { - resolvedGroups.add(g); + private void accumulateInheritancesTo(List accumulator) { + InheritanceGraph graph = this.plugin.getInheritanceHandler().getGraph(); + Iterable traversal = graph.traverse(this.plugin.getConfiguration().get(ConfigKeys.INHERITANCE_TRAVERSAL_ALGORITHM), this); + for (PermissionHolder holder : traversal) { + List nodes = holder.getOwnNodes(); + for (Node node : nodes) { + ImmutableLocalizedNode localizedNode = ImmutableLocalizedNode.of(node, holder.getObjectName()); + accumulator.add(localizedNode); } } - - // sort the groups according to weight + other factors. - resolvedGroups.sort(this.inheritanceComparator); - - for (Group g : resolvedGroups) { - g.resolveInheritances(accumulator, excludedGroups); - } - - return accumulator; } public List resolveInheritances() { - return resolveInheritances(null, null); + List accumulator = new ArrayList<>(); + accumulateInheritancesTo(accumulator); + return accumulator; } public SortedSet resolveInheritancesAlmostEqual() { - List nodes = resolveInheritances(new LinkedList<>(), null); + List nodes = new LinkedList<>(); + accumulateInheritancesTo(nodes); + NodeTools.removeAlmostEqual(nodes.iterator()); SortedSet ret = new TreeSet<>(NodeWithContextComparator.reverse()); ret.addAll(nodes); @@ -465,19 +409,20 @@ public abstract class PermissionHolder { } public SortedSet resolveInheritancesMergeTemp() { - List nodes = resolveInheritances(new LinkedList<>(), null); + List nodes = new LinkedList<>(); + accumulateInheritancesTo(nodes); + NodeTools.removeIgnoreValueOrTemp(nodes.iterator()); SortedSet ret = new TreeSet<>(NodeWithContextComparator.reverse()); ret.addAll(nodes); return ret; } - public SortedSet getAllNodes(Contexts context) { - List entries; + private List getAllEntries(Contexts context) { + List entries = new LinkedList<>(); if (context.isApplyGroups()) { - entries = resolveInheritances(new LinkedList<>(), null, context); + accumulateInheritancesTo(entries, context); } else { - entries = new LinkedList<>(); for (Node n : getOwnNodes(context.getContexts())) { ImmutableLocalizedNode localizedNode = ImmutableLocalizedNode.of(n, getObjectName()); entries.add(localizedNode); @@ -491,6 +436,12 @@ public abstract class PermissionHolder { entries.removeIf(n -> !n.isGroupNode() && !n.isWorldSpecific()); } + return entries; + } + + public SortedSet getAllNodes(Contexts context) { + List entries = getAllEntries(context); + NodeTools.removeSamePermission(entries.iterator()); SortedSet ret = new TreeSet<>(NodeWithContextComparator.reverse()); ret.addAll(entries); @@ -498,23 +449,7 @@ public abstract class PermissionHolder { } public Map exportNodesAndShorthand(Contexts context, boolean lowerCase) { - List entries; - if (context.isApplyGroups()) { - entries = resolveInheritances(new LinkedList<>(), null, context); - } else { - entries = new LinkedList<>(); - for (Node n : getOwnNodes(context.getContexts())) { - ImmutableLocalizedNode localizedNode = ImmutableLocalizedNode.of(n, getObjectName()); - entries.add(localizedNode); - } - } - - if (!context.isIncludeGlobal()) { - entries.removeIf(n -> !n.isGroupNode() && !n.isServerSpecific()); - } - if (!context.isApplyGlobalWorldGroups()) { - entries.removeIf(n -> !n.isGroupNode() && !n.isWorldSpecific()); - } + List entries = getAllEntries(context); Map perms = new HashMap<>(); boolean applyShorthand = this.plugin.getConfiguration().get(ConfigKeys.APPLYING_SHORTHAND); @@ -557,117 +492,55 @@ public abstract class PermissionHolder { return ImmutableMap.copyOf(perms); } - public MetaAccumulator accumulateMeta(MetaAccumulator accumulator, Set excludedGroups, Contexts context) { + public MetaAccumulator accumulateMeta(MetaAccumulator accumulator, Contexts context) { if (accumulator == null) { accumulator = MetaAccumulator.makeFromConfig(this.plugin); } - if (excludedGroups == null) { - excludedGroups = new HashSet<>(); - } + InheritanceGraph graph = this.plugin.getInheritanceHandler().getGraph(context); + Iterable traversal = graph.traverse(this.plugin.getConfiguration().get(ConfigKeys.INHERITANCE_TRAVERSAL_ALGORITHM), this); + for (PermissionHolder holder : traversal) { + List nodes = holder.getOwnNodes(context.getContexts()); + for (Node node : nodes) { + if (!node.getValuePrimitive()) continue; + if (!node.isMeta() && !node.isPrefix() && !node.isSuffix()) continue; - if (this.getType().isGroup()) { - excludedGroups.add(getObjectName().toLowerCase()); - } + if (!((context.isIncludeGlobal() || node.isServerSpecific()) && (context.isIncludeGlobalWorld() || node.isWorldSpecific()))) { + continue; + } - // get and add the objects own nodes - List nodes = getOwnNodes(context.getContexts()); - - for (Node node : nodes) { - if (!node.getValuePrimitive()) continue; - if (!node.isMeta() && !node.isPrefix() && !node.isSuffix()) continue; - - if (!((context.isIncludeGlobal() || node.isServerSpecific()) && (context.isIncludeGlobalWorld() || node.isWorldSpecific()))) { - continue; + accumulator.accumulateNode(ImmutableLocalizedNode.of(node, holder.getObjectName())); } - accumulator.accumulateNode(ImmutableLocalizedNode.of(node, getObjectName())); - } - - OptionalInt w = getWeight(); - if (w.isPresent()) { - accumulator.accumulateWeight(w.getAsInt()); - } - - // resolve and process the objects parents - List resolvedGroups = new ArrayList<>(); - Set processedGroups = new HashSet<>(); - - for (Node n : nodes) { - if (!n.isGroupNode()) continue; - String groupName = n.getGroupName(); - - if (!processedGroups.add(groupName) || excludedGroups.contains(groupName) || !n.getValuePrimitive()) continue; - - if (!((context.isApplyGlobalGroups() || n.isServerSpecific()) && (context.isApplyGlobalWorldGroups() || n.isWorldSpecific()))) { - continue; + OptionalInt w = holder.getWeight(); + if (w.isPresent()) { + accumulator.accumulateWeight(w.getAsInt()); } - - Group g = this.plugin.getGroupManager().getIfLoaded(groupName); - if (g != null) { - resolvedGroups.add(g); - } - } - - // sort the groups according to weight + other factors. - resolvedGroups.sort(this.inheritanceComparator); - - for (Group g : resolvedGroups) { - g.accumulateMeta(accumulator, excludedGroups, context); } return accumulator; } - public MetaAccumulator accumulateMeta(MetaAccumulator accumulator, Set excludedGroups) { + public MetaAccumulator accumulateMeta(MetaAccumulator accumulator) { if (accumulator == null) { accumulator = MetaAccumulator.makeFromConfig(this.plugin); } - if (excludedGroups == null) { - excludedGroups = new HashSet<>(); - } + InheritanceGraph graph = this.plugin.getInheritanceHandler().getGraph(); + Iterable traversal = graph.traverse(this.plugin.getConfiguration().get(ConfigKeys.INHERITANCE_TRAVERSAL_ALGORITHM), this); + for (PermissionHolder holder : traversal) { + List nodes = holder.getOwnNodes(); + for (Node node : nodes) { + if (!node.getValuePrimitive()) continue; + if (!node.isMeta() && !node.isPrefix() && !node.isSuffix()) continue; - if (this.getType().isGroup()) { - excludedGroups.add(getObjectName().toLowerCase()); - } - - // get and add the objects own nodes - List nodes = getOwnNodes(); - - for (Node node : nodes) { - if (!node.getValuePrimitive()) continue; - if (!node.isMeta() && !node.isPrefix() && !node.isSuffix()) continue; - - accumulator.accumulateNode(ImmutableLocalizedNode.of(node, getObjectName())); - } - - OptionalInt w = getWeight(); - if (w.isPresent()) { - accumulator.accumulateWeight(w.getAsInt()); - } - - // resolve and process the objects parents - List resolvedGroups = new ArrayList<>(); - Set processedGroups = new HashSet<>(); - - for (Node n : nodes) { - if (!n.isGroupNode()) continue; - String groupName = n.getGroupName(); - - if (!processedGroups.add(groupName) || excludedGroups.contains(groupName) || !n.getValuePrimitive()) continue; - - Group g = this.plugin.getGroupManager().getIfLoaded(groupName); - if (g != null) { - resolvedGroups.add(g); + accumulator.accumulateNode(ImmutableLocalizedNode.of(node, holder.getObjectName())); } - } - // sort the groups according to weight + other factors. - resolvedGroups.sort(this.inheritanceComparator); - - for (Group g : resolvedGroups) { - g.accumulateMeta(accumulator, excludedGroups); + OptionalInt w = getWeight(); + if (w.isPresent()) { + accumulator.accumulateWeight(w.getAsInt()); + } } return accumulator; @@ -1019,8 +892,7 @@ public abstract class PermissionHolder { } public Set getGroupReferences() { - return getOwnNodes().stream() - .filter(Node::isGroupNode) + return getOwnGroupNodes().stream() .map(Node::getGroupName) .map(String::toLowerCase) .map(GroupReference::of) diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java index 821291bbb..258f8ecf3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java @@ -41,6 +41,7 @@ import me.lucko.luckperms.common.contexts.ContextManager; import me.lucko.luckperms.common.dependencies.DependencyManager; import me.lucko.luckperms.common.dependencies.classloader.PluginClassLoader; import me.lucko.luckperms.common.event.EventFactory; +import me.lucko.luckperms.common.inheritance.InheritanceHandler; import me.lucko.luckperms.common.locale.LocaleManager; import me.lucko.luckperms.common.logging.Logger; import me.lucko.luckperms.common.managers.group.GroupManager; @@ -178,6 +179,13 @@ public interface LuckPermsPlugin { */ ContextManager getContextManager(); + /** + * Gets the inheritance handler + * + * @return the inheritance handler + */ + InheritanceHandler getInheritanceHandler(); + /** * Gets the cached state manager for the platform. * diff --git a/common/src/main/java/me/lucko/luckperms/common/primarygroup/AllParentsByWeightHolder.java b/common/src/main/java/me/lucko/luckperms/common/primarygroup/AllParentsByWeightHolder.java index d6538ed7f..49a252541 100644 --- a/common/src/main/java/me/lucko/luckperms/common/primarygroup/AllParentsByWeightHolder.java +++ b/common/src/main/java/me/lucko/luckperms/common/primarygroup/AllParentsByWeightHolder.java @@ -25,15 +25,16 @@ package me.lucko.luckperms.common.primarygroup; +import com.google.common.collect.ImmutableList; + import me.lucko.luckperms.api.Contexts; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.inheritance.InheritanceGraph; import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.model.User; -import java.util.AbstractList; -import java.util.ArrayList; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; public class AllParentsByWeightHolder extends CachedPrimaryGroupHolder { public AllParentsByWeightHolder(User user) { @@ -47,23 +48,21 @@ public class AllParentsByWeightHolder extends CachedPrimaryGroupHolder { contexts = this.user.getPlugin().getContextManager().getStaticContexts(); } - // hack to get a list of groups the holder is inheriting from - Set groupNames = new LinkedHashSet<>(); - this.user.resolveInheritances(new NoopList<>(), groupNames, contexts); + InheritanceGraph graph = this.user.getPlugin().getInheritanceHandler().getGraph(contexts); - List groups = new ArrayList<>(); - for (String groupName : groupNames) { - Group group = this.user.getPlugin().getGroupManager().getIfLoaded(groupName); - if (group != null) { - groups.add(group); - } - } + // fully traverse the graph, obtain a list of permission holders the user inherits from + List traversal = ImmutableList.copyOf(graph.traverse(this.user.getPlugin().getConfiguration().get(ConfigKeys.INHERITANCE_TRAVERSAL_ALGORITHM), this.user)); Group bestGroup = null; - if (!groups.isEmpty()) { + if (!traversal.isEmpty()) { int best = 0; - for (Group g : groups) { + for (PermissionHolder holder : traversal) { + if (!(holder instanceof Group)) { + continue; + } + Group g = ((Group) holder); + int weight = g.getWeight().orElse(0); if (bestGroup == null || g.getWeight().orElse(0) > best) { bestGroup = g; @@ -74,22 +73,4 @@ public class AllParentsByWeightHolder extends CachedPrimaryGroupHolder { return bestGroup == null ? null : bestGroup.getName(); } - - private static final class NoopList extends AbstractList implements List { - - @Override - public boolean add(E e) { - return true; - } - - @Override - public E get(int index) { - return null; - } - - @Override - public int size() { - return 0; - } - } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java index 54fa04100..2371da2b8 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java @@ -51,6 +51,7 @@ import me.lucko.luckperms.common.dependencies.DependencyRegistry; import me.lucko.luckperms.common.dependencies.classloader.PluginClassLoader; import me.lucko.luckperms.common.dependencies.classloader.ReflectionClassLoader; import me.lucko.luckperms.common.event.EventFactory; +import me.lucko.luckperms.common.inheritance.InheritanceHandler; import me.lucko.luckperms.common.locale.LocaleManager; import me.lucko.luckperms.common.locale.NoopLocaleManager; import me.lucko.luckperms.common.locale.SimpleLocaleManager; @@ -179,6 +180,7 @@ public class LPSpongePlugin implements LuckPermsSpongePlugin { private LocaleManager localeManager; private PluginClassLoader pluginClassLoader; private DependencyManager dependencyManager; + private InheritanceHandler inheritanceHandler; private CachedStateManager cachedStateManager; private ContextManager contextManager; private CalculatorFactory calculatorFactory; @@ -242,6 +244,7 @@ public class LPSpongePlugin implements LuckPermsSpongePlugin { // load internal managers getLog().info("Loading internal permission managers..."); + this.inheritanceHandler = new InheritanceHandler(this); this.userManager = new SpongeUserManager(this); this.groupManager = new SpongeGroupManager(this); this.trackManager = new StandardTrackManager(this); @@ -609,6 +612,11 @@ public class LPSpongePlugin implements LuckPermsSpongePlugin { return this.contextManager; } + @Override + public InheritanceHandler getInheritanceHandler() { + return this.inheritanceHandler; + } + @Override public CalculatorFactory getCalculatorFactory() { return this.calculatorFactory; diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsSubjectData.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsSubjectData.java index 3104fa40a..eb78b3115 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsSubjectData.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsSubjectData.java @@ -376,7 +376,7 @@ public class LuckPermsSubjectData implements LPSubjectData { toRemove.forEach(makeUnsetConsumer(this.enduring)); - MetaAccumulator metaAccumulator = this.holder.accumulateMeta(null, null, this.service.getPlugin().getContextManager().formContexts(contexts)); + MetaAccumulator metaAccumulator = this.holder.accumulateMeta(null, this.service.getPlugin().getContextManager().formContexts(contexts)); int priority = metaAccumulator.getChatMeta(type).keySet().stream().mapToInt(e -> e).max().orElse(0); priority += 10; diff --git a/sponge/src/main/resources/luckperms.conf b/sponge/src/main/resources/luckperms.conf index 24695d4f6..127b8ab4f 100644 --- a/sponge/src/main/resources/luckperms.conf +++ b/sponge/src/main/resources/luckperms.conf @@ -128,6 +128,18 @@ apply-sponge-implicit-wildcards=true # false, LuckPerms will ignore this data when considering if a player has a permission. apply-sponge-default-subjects=true +# The algorithm LuckPerms should use when traversing the "inheritance tree". +# +# The valid options are: +# - breadth-first +# - depth-first-pre-order +# - depth-first-post-order +# +# See here for information about the differences between each algorithm. +# - https://en.wikipedia.org/wiki/Breadth-first_search +# - https://en.wikipedia.org/wiki/Depth-first_search +inheritance-traversal-algorithm="depth-first-pre-order" + # Define special group weights for this server. # Default is just 0. group-weight {