diff --git a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts index 398d1e429..d349dc40e 100644 --- a/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/cc-tweaked.java-convention.gradle.kts @@ -66,6 +66,7 @@ repositories { includeGroup("me.shedaniel") includeGroup("mezz.jei") includeModule("com.terraformersmc", "modmenu") + includeModule("me.lucko", "fabric-permissions-api") } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e0fa896a4..ca34fa6b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ slf4j = "1.7.36" # Minecraft mods emi = "1.0.8+1.19.4" +fabricPermissions = "0.2.20221016" iris = "1.5.2+1.19.4" jei = "13.1.0.11" modmenu = "6.1.0-rc.1" @@ -93,6 +94,7 @@ slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } # Minecraft mods fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } +fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" } emi = { module = "dev.emi:emi-xplat-mojmap", version.ref = "emi" } iris = { module = "maven.modrinth:iris", version.ref = "iris" } jei-api = { module = "mezz.jei:jei-1.19.4-common-api", version.ref = "jei" } @@ -154,7 +156,7 @@ externalMods-common = ["jei-api", "nightConfig-core", "nightConfig-toml"] externalMods-forge-compile = ["oculus", "jei-api"] externalMods-forge-runtime = ["jei-forge"] externalMods-fabric = ["nightConfig-core", "nightConfig-toml"] -externalMods-fabric-compile = ["iris", "jei-api", "rei-api", "rei-builtin"] +externalMods-fabric-compile = ["fabricPermissions", "iris", "jei-api", "rei-api", "rei-builtin"] externalMods-fabric-runtime = ["jei-fabric", "modmenu"] # Testing diff --git a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java index a903eed6e..01248317e 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/ModRegistry.java @@ -15,6 +15,7 @@ import dan200.computercraft.core.util.Colour; import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.impl.TurtleUpgrades; +import dan200.computercraft.shared.command.UserLevel; import dan200.computercraft.shared.command.arguments.ComputerArgumentType; import dan200.computercraft.shared.command.arguments.ComputersArgumentType; import dan200.computercraft.shared.command.arguments.RepeatArgumentType; @@ -37,6 +38,7 @@ import dan200.computercraft.shared.data.PlayerCreativeLootCondition; import dan200.computercraft.shared.details.BlockDetails; import dan200.computercraft.shared.details.ItemDetails; +import dan200.computercraft.shared.integration.PermissionRegistry; import dan200.computercraft.shared.media.items.DiskItem; import dan200.computercraft.shared.media.items.PrintoutItem; import dan200.computercraft.shared.media.items.RecordMedia; @@ -77,6 +79,7 @@ import dan200.computercraft.shared.turtle.upgrades.*; import dan200.computercraft.shared.util.ImpostorRecipe; import dan200.computercraft.shared.util.ImpostorShapelessRecipe; +import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.commands.synchronization.SingletonArgumentInfo; import net.minecraft.core.BlockPos; @@ -98,6 +101,7 @@ import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType; import java.util.function.BiFunction; +import java.util.function.Predicate; /** * Registers ComputerCraft's registry entries and additional objects, such as {@link CauldronInteraction}s and @@ -366,6 +370,18 @@ private static RegistryEntry IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", ImpostorShapelessRecipe.Serializer::new); } + public static class Permissions { + static final PermissionRegistry REGISTRY = PermissionRegistry.create(); + + public static final Predicate PERMISSION_DUMP = REGISTRY.registerCommand("dump", UserLevel.OWNER_OP); + public static final Predicate PERMISSION_SHUTDOWN = REGISTRY.registerCommand("shutdown", UserLevel.OWNER_OP); + public static final Predicate PERMISSION_TURN_ON = REGISTRY.registerCommand("turn_on", UserLevel.OWNER_OP); + public static final Predicate PERMISSION_TP = REGISTRY.registerCommand("tp", UserLevel.OP); + public static final Predicate PERMISSION_TRACK = REGISTRY.registerCommand("track", UserLevel.OWNER_OP); + public static final Predicate PERMISSION_QUEUE = REGISTRY.registerCommand("queue", UserLevel.ANYONE); + public static final Predicate PERMISSION_VIEW = REGISTRY.registerCommand("view", UserLevel.OP); + } + /** * Register any objects which don't have to be done on the main thread. */ @@ -379,6 +395,7 @@ public static void register() { ArgumentTypes.REGISTRY.register(); LootItemConditionTypes.REGISTRY.register(); RecipeSerializers.REGISTRY.register(); + Permissions.REGISTRY.register(); // Register bundled power providers ComputerCraftAPI.registerBundledRedstoneProvider(new DefaultBundledRedstoneProvider()); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java index 93b96d069..77e36447f 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java @@ -11,6 +11,7 @@ import com.mojang.brigadier.suggestion.Suggestions; import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.metrics.Metrics; +import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.command.arguments.ComputersArgumentType; import dan200.computercraft.shared.command.text.TableBuilder; import dan200.computercraft.shared.computer.core.ComputerFamily; @@ -60,7 +61,7 @@ private CommandComputerCraft() { public static void register(CommandDispatcher dispatcher) { dispatcher.register(choice("computercraft") .then(literal("dump") - .requires(UserLevel.OWNER_OP) + .requires(ModRegistry.Permissions.PERMISSION_DUMP) .executes(context -> { var table = new TableBuilder("DumpAll", "Computer", "On", "Position"); @@ -118,7 +119,7 @@ public static void register(CommandDispatcher dispatcher) { }))) .then(command("shutdown") - .requires(UserLevel.OWNER_OP) + .requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN) .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers()) .executes((context, computerSelectors) -> { var shutdown = 0; @@ -132,7 +133,7 @@ public static void register(CommandDispatcher dispatcher) { })) .then(command("turn-on") - .requires(UserLevel.OWNER_OP) + .requires(ModRegistry.Permissions.PERMISSION_TURN_ON) .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers()) .executes((context, computerSelectors) -> { var on = 0; @@ -146,7 +147,7 @@ public static void register(CommandDispatcher dispatcher) { })) .then(command("tp") - .requires(UserLevel.OP) + .requires(ModRegistry.Permissions.PERMISSION_TP) .arg("computer", oneComputer()) .executes(context -> { var computer = getComputerArgument(context, "computer"); @@ -171,7 +172,7 @@ public static void register(CommandDispatcher dispatcher) { })) .then(command("queue") - .requires(UserLevel.ANYONE) + .requires(ModRegistry.Permissions.PERMISSION_QUEUE) .arg( RequiredArgumentBuilder.argument("computer", manyComputers()) .suggests((context, builder) -> Suggestions.empty()) @@ -193,7 +194,7 @@ public static void register(CommandDispatcher dispatcher) { })) .then(command("view") - .requires(UserLevel.OP) + .requires(ModRegistry.Permissions.PERMISSION_VIEW) .arg("computer", oneComputer()) .executes(context -> { var player = context.getSource().getPlayerOrException(); @@ -213,8 +214,8 @@ public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) })) .then(choice("track") + .requires(ModRegistry.Permissions.PERMISSION_TRACK) .then(command("start") - .requires(UserLevel.OWNER_OP) .executes(context -> { getMetricsInstance(context.getSource()).start(); @@ -227,7 +228,6 @@ public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) })) .then(command("stop") - .requires(UserLevel.OWNER_OP) .executes(context -> { var timings = getMetricsInstance(context.getSource()); if (!timings.stop()) throw NOT_TRACKING_EXCEPTION.create(); @@ -236,7 +236,6 @@ public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) })) .then(command("dump") - .requires(UserLevel.OWNER_OP) .argManyValue("fields", metric(), DEFAULT_FIELDS) .executes((context, fields) -> { AggregatedMetric sort; @@ -270,23 +269,25 @@ private static Component linkComputer(CommandSourceStack source, @Nullable Serve out.append(" (id " + computerId + ")"); // And, if we're a player, some useful links - if (serverComputer != null && UserLevel.OP.test(source) && isPlayer(source)) { - out - .append(" ") - .append(link( + if (serverComputer != null && isPlayer(source)) { + if (ModRegistry.Permissions.PERMISSION_TP.test(source)) { + out.append(" ").append(link( text("\u261b"), "/computercraft tp " + serverComputer.getInstanceID(), Component.translatable("commands.computercraft.tp.action") - )) - .append(" ") - .append(link( + )); + } + + if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) { + out.append(" ").append(link( text("\u20e2"), "/computercraft view " + serverComputer.getInstanceID(), Component.translatable("commands.computercraft.view.action") )); + } } - if (UserLevel.OWNER.test(source) && isPlayer(source)) { + if (isPlayer(source) && UserLevel.isOwner(source)) { var linkPath = linkStorage(source, computerId); if (linkPath != null) out.append(" ").append(linkPath); } @@ -295,7 +296,7 @@ private static Component linkComputer(CommandSourceStack source, @Nullable Serve } private static Component linkPosition(CommandSourceStack context, ServerComputer computer) { - if (UserLevel.OP.test(context)) { + if (ModRegistry.Permissions.PERMISSION_TP.test(context)) { return link( position(computer.getPosition()), "/computercraft tp " + computer.getInstanceID(), diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java index 2494759c9..57d935dab 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/CommandUtils.java @@ -10,7 +10,6 @@ import dan200.computercraft.shared.platform.PlatformHelper; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.SharedSuggestionProvider; -import net.minecraft.server.level.ServerPlayer; import java.util.Arrays; import java.util.Locale; @@ -22,8 +21,8 @@ private CommandUtils() { } public static boolean isPlayer(CommandSourceStack output) { - var sender = output.getEntity(); - return sender instanceof ServerPlayer player && !PlatformHelper.get().isFakePlayer(player); + var player = output.getPlayer(); + return player != null && !PlatformHelper.get().isFakePlayer(player); } @SuppressWarnings("unchecked") diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java b/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java index c8bdbb0f2..ab7709b90 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/UserLevel.java @@ -5,7 +5,7 @@ package dan200.computercraft.shared.command; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.world.entity.player.Player; +import net.minecraft.server.level.ServerPlayer; import java.util.function.Predicate; @@ -13,11 +13,6 @@ * The level a user must be at in order to execute a command. */ public enum UserLevel implements Predicate { - /** - * Only can be used by the owner of the server: namely the server console or the player in SSP. - */ - OWNER, - /** * Can only be used by ops. */ @@ -35,7 +30,6 @@ public enum UserLevel implements Predicate { public int toLevel() { return switch (this) { - case OWNER -> 4; case OP, OWNER_OP -> 2; case ANYONE -> 0; }; @@ -44,39 +38,26 @@ public int toLevel() { @Override public boolean test(CommandSourceStack source) { if (this == ANYONE) return true; - if (this == OWNER) return isOwner(source); if (this == OWNER_OP && isOwner(source)) return true; return source.hasPermission(toLevel()); } - /** - * Take the union of two {@link UserLevel}s. - *

- * This satisfies the property that for all sources {@code s}, {@code a.test(s) || b.test(s) == (a ∪ b).test(s)}. - * - * @param left The first user level to take the union of. - * @param right The second user level to take the union of. - * @return The union of two levels. - */ - public static UserLevel union(UserLevel left, UserLevel right) { - if (left == right) return left; - - // x ∪ ANYONE = ANYONE - if (left == ANYONE || right == ANYONE) return ANYONE; - - // x ∪ OWNER = OWNER - if (left == OWNER) return right; - if (right == OWNER) return left; - - // At this point, we have x != y and x, y ∈ { OP, OWNER_OP }. - return OWNER_OP; + public boolean test(ServerPlayer source) { + if (this == ANYONE) return true; + if (this == OWNER_OP && isOwner(source)) return true; + return source.hasPermissions(toLevel()); } - private static boolean isOwner(CommandSourceStack source) { + public static boolean isOwner(CommandSourceStack source) { var server = source.getServer(); - var sender = source.getEntity(); + var player = source.getPlayer(); return server.isDedicatedServer() ? source.getEntity() == null && source.hasPermission(4) && source.getTextName().equals("Server") - : sender instanceof Player player && server.isSingleplayerOwner(player.getGameProfile()); + : player != null && server.isSingleplayerOwner(player.getGameProfile()); + } + + public static boolean isOwner(ServerPlayer player) { + var server = player.getServer(); + return server != null && server.isSingleplayerOwner(player.getGameProfile()); } } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java index 2a395ff7b..a150b8c88 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java @@ -44,7 +44,8 @@ public static CommandBuilder command(String literal) { } public CommandBuilder requires(Predicate predicate) { - requires = requires == null ? predicate : requires.and(predicate); + if (requires != null) throw new IllegalStateException("Requires already set"); + requires = predicate; return this; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java index dfe2fecc3..9bd797108 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java @@ -10,7 +10,6 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; -import dan200.computercraft.shared.command.UserLevel; import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.ClickEvent; @@ -31,6 +30,7 @@ */ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder { private final Collection children = new ArrayList<>(); + private @Nullable Predicate requirement; private HelpingArgumentBuilder(String literal) { super(literal); @@ -41,26 +41,20 @@ public static HelpingArgumentBuilder choice(String literal) { } @Override - public LiteralArgumentBuilder requires(Predicate requirement) { - throw new IllegalStateException("Cannot use requires on a HelpingArgumentBuilder"); + public HelpingArgumentBuilder requires(Predicate requirement) { + this.requirement = requirement; + return this; } @Override public Predicate getRequirement() { - // The requirement of this node is the union of all child's requirements. + if (requirement != null) return requirement; + var requirements = Stream.concat( children.stream().map(ArgumentBuilder::getRequirement), getArguments().stream().map(CommandNode::getRequirement) ).toList(); - - // If all requirements are a UserLevel, take the union of those instead. - var userLevel = UserLevel.OWNER; - for (var requirement : requirements) { - if (!(requirement instanceof UserLevel level)) return x -> requirements.stream().anyMatch(y -> y.test(x)); - userLevel = UserLevel.union(userLevel, level); - } - - return userLevel; + return x -> requirements.stream().anyMatch(y -> y.test(x)); } @Override @@ -181,7 +175,7 @@ private static Component getHelp(CommandContext context, Com .append(Component.translatable("commands." + id + ".desc")); for (var child : node.getChildren()) { - if (!child.getRequirement().test(context.getSource()) || !(child instanceof LiteralCommandNode)) { + if (!child.canUse(context.getSource()) || !(child instanceof LiteralCommandNode)) { continue; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/integration/PermissionRegistry.java b/projects/common/src/main/java/dan200/computercraft/shared/integration/PermissionRegistry.java new file mode 100644 index 000000000..5360f4445 --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/integration/PermissionRegistry.java @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration; + +import com.mojang.brigadier.builder.ArgumentBuilder; +import dan200.computercraft.shared.command.CommandComputerCraft; +import dan200.computercraft.shared.command.UserLevel; +import dan200.computercraft.shared.platform.RegistrationHelper; +import net.minecraft.commands.CommandSourceStack; + +import javax.annotation.OverridingMethodsMustInvokeSuper; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.function.Predicate; + +/** + * A registry of nodes in a permission system. + *

+ * This acts as an abstraction layer over permission systems such Forge's built-in permissions API, or Fabric's + * unofficial fabric-permissions-api-v0. + *

+ * This behaves similarly to {@link RegistrationHelper} (aka Forge's deferred registry), in that you {@linkplain #create() + * create a registry}, {@linkplain #registerCommand(String, UserLevel) add nodes to it} and then finally {@linkplain + * #register()} all created nodes. + * + * @see dan200.computercraft.shared.ModRegistry.Permissions + */ +public abstract class PermissionRegistry { + private boolean frozen = false; + + /** + * Register a permission node for a command. The registered node should be of the form {@code "command." + command}. + * + * @param command The name of the command. This should be one of the subcommands under the {@code /computercraft} + * subcommand, and not something general. + * @param fallback The default/fallback permission check. + * @return The resulting predicate which should be passed to {@link ArgumentBuilder#requires(Predicate)}. + * @see CommandComputerCraft + */ + public abstract Predicate registerCommand(String command, UserLevel fallback); + + /** + * Check that the registry has not been frozen (namely {@link #register()} has been called). This should be called + * before registering each node. + */ + protected void checkNotFrozen() { + if (frozen) throw new IllegalStateException("Permission registry has been frozen."); + } + + /** + * Freeze the permissions registry and register the underlying nodes. + */ + @OverridingMethodsMustInvokeSuper + public void register() { + frozen = true; + } + + public interface Provider { + Optional get(); + } + + public static PermissionRegistry create() { + return ServiceLoader.load(Provider.class) + .stream() + .flatMap(x -> x.get().get().stream()) + .findFirst() + .orElseGet(DefaultPermissionRegistry::new); + } + + private static class DefaultPermissionRegistry extends PermissionRegistry { + @Override + public Predicate registerCommand(String command, UserLevel fallback) { + checkNotFrozen(); + return fallback; + } + } +} diff --git a/projects/fabric/src/main/java/dan200/computercraft/shared/integration/FabricPermissionRegistry.java b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/FabricPermissionRegistry.java new file mode 100644 index 000000000..7d0705a54 --- /dev/null +++ b/projects/fabric/src/main/java/dan200/computercraft/shared/integration/FabricPermissionRegistry.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration; + +import com.google.auto.service.AutoService; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.shared.command.UserLevel; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.commands.CommandSourceStack; + +import java.util.Optional; +import java.util.function.Predicate; + +/** + * An implementation of {@link PermissionRegistry} using Fabric's unofficial {@linkplain Permissions permissions api}. + */ +public final class FabricPermissionRegistry extends PermissionRegistry { + private FabricPermissionRegistry() { + } + + @Override + public Predicate registerCommand(String command, UserLevel fallback) { + checkNotFrozen(); + var name = ComputerCraftAPI.MOD_ID + ".command." + command; + return source -> Permissions.getPermissionValue(source, name).orElseGet(() -> fallback.test(source)); + } + + @AutoService(PermissionRegistry.Provider.class) + public static final class Provider implements PermissionRegistry.Provider { + @Override + public Optional get() { + return FabricLoader.getInstance().isModLoaded("fabric-permissions-api-v0") + ? Optional.of(new FabricPermissionRegistry()) + : Optional.empty(); + } + } +} diff --git a/projects/forge/src/main/java/dan200/computercraft/shared/integration/ForgePermissionRegistry.java b/projects/forge/src/main/java/dan200/computercraft/shared/integration/ForgePermissionRegistry.java new file mode 100644 index 000000000..eaa734ea8 --- /dev/null +++ b/projects/forge/src/main/java/dan200/computercraft/shared/integration/ForgePermissionRegistry.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.integration; + +import com.google.auto.service.AutoService; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.shared.command.UserLevel; +import net.minecraft.commands.CommandSourceStack; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.server.permission.PermissionAPI; +import net.minecraftforge.server.permission.events.PermissionGatherEvent; +import net.minecraftforge.server.permission.nodes.PermissionNode; +import net.minecraftforge.server.permission.nodes.PermissionType; +import net.minecraftforge.server.permission.nodes.PermissionTypes; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +/** + * An implementation of {@link PermissionRegistry} using Forge's {@link PermissionAPI}. + */ +public final class ForgePermissionRegistry extends PermissionRegistry { + private final List> nodes = new ArrayList<>(); + + private ForgePermissionRegistry() { + } + + private PermissionNode registerNode(String nodeName, PermissionType type, PermissionNode.PermissionResolver defaultResolver) { + checkNotFrozen(); + var node = new PermissionNode<>(ComputerCraftAPI.MOD_ID, nodeName, type, defaultResolver); + nodes.add(node); + return node; + } + + @Override + public Predicate registerCommand(String command, UserLevel fallback) { + var node = registerNode( + "command." + command, PermissionTypes.BOOLEAN, + (player, uuid, context) -> player != null && fallback.test(player) + ); + + return source -> { + var player = source.getPlayer(); + return player == null ? fallback.test(source) : PermissionAPI.getPermission(player, node); + }; + } + + @Override + public void register() { + super.register(); + MinecraftForge.EVENT_BUS.addListener((PermissionGatherEvent.Nodes event) -> event.addNodes(nodes)); + } + + @AutoService(PermissionRegistry.Provider.class) + public static final class Provider implements PermissionRegistry.Provider { + @Override + public Optional get() { + return Optional.of(new ForgePermissionRegistry()); + } + } +}