diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30037813a..f55363882 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,4 +23,4 @@ The project is split up into a few separate modules. * **API** - The public, semantically versioned API used by other plugins wishing to integrate with and retrieve data from LuckPerms. This module (for the most part) does not contain any implementation itself, and is provided by the plugin. * **Common** - The common module contains most of the code which implements the respective LuckPerms plugins. This abstract module reduces duplicated code throughout the project. -* **Bukkit, BungeeCord, Sponge, Nukkit, Velocity & Fabric** - Each use the common module to implement plugins on the respective server platforms. +* **Bukkit, BungeeCord, Fabric, Forge, Nukkit, Sponge & Velocity** - Each use the common module to implement plugins on the respective server platforms. diff --git a/README.md b/README.md index 56ac8b795..2d266a359 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The project is split up into a few separate modules. * **API** - The public, semantically versioned API used by other plugins wishing to integrate with and retrieve data from LuckPerms. This module (for the most part) does not contain any implementation itself, and is provided by the plugin. * **Common** - The common module contains most of the code which implements the respective LuckPerms plugins. This abstract module reduces duplicated code throughout the project. -* **Bukkit, BungeeCord, Sponge, Fabric, Nukkit & Velocity** - Each use the common module to implement plugins on the respective server platforms. +* **Bukkit, BungeeCord, Fabric, Forge, Nukkit, Sponge & Velocity** - Each use the common module to implement plugins on the respective server platforms. ## License LuckPerms is licensed under the permissive MIT license. Please see [`LICENSE.txt`](https://github.com/lucko/LuckPerms/blob/master/LICENSE.txt) for more info. diff --git a/api/src/main/java/net/luckperms/api/platform/Platform.java b/api/src/main/java/net/luckperms/api/platform/Platform.java index 9ed38beee..92b4fd09f 100644 --- a/api/src/main/java/net/luckperms/api/platform/Platform.java +++ b/api/src/main/java/net/luckperms/api/platform/Platform.java @@ -75,7 +75,8 @@ public interface Platform { SPONGE("Sponge"), NUKKIT("Nukkit"), VELOCITY("Velocity"), - FABRIC("Fabric"); + FABRIC("Fabric"), + FORGE("Forge"); private final String friendlyName; diff --git a/build.gradle b/build.gradle index 4a17698b8..44022515d 100644 --- a/build.gradle +++ b/build.gradle @@ -54,5 +54,6 @@ subprojects { mavenCentral() maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url 'https://repo.lucko.me/' } + maven { url 'https://libraries.minecraft.net/' } } } diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 7dcf52230..fcdbe4ef4 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -4,7 +4,6 @@ plugins { repositories { maven { url 'https://papermc.io/repo/repository/maven-public/' } - maven { url 'https://libraries.minecraft.net/' } } dependencies { diff --git a/common/build.gradle b/common/build.gradle index b2137a5f0..374a0ade5 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -17,6 +17,7 @@ dependencies { compileOnly project(':common:loader-utils') + compileOnly 'com.mojang:brigadier:1.0.18' compileOnly 'org.slf4j:slf4j-api:1.7.30' compileOnly 'org.apache.logging.log4j:log4j-api:2.14.0' diff --git a/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/LoaderBootstrap.java b/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/LoaderBootstrap.java index 875a2b153..910a51f01 100644 --- a/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/LoaderBootstrap.java +++ b/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/LoaderBootstrap.java @@ -32,8 +32,8 @@ public interface LoaderBootstrap { void onLoad(); - void onEnable(); + default void onEnable() {} - void onDisable(); + default void onDisable() {} } diff --git a/common/src/main/java/me/lucko/luckperms/common/command/BrigadierCommandExecutor.java b/common/src/main/java/me/lucko/luckperms/common/command/BrigadierCommandExecutor.java new file mode 100644 index 000000000..3ecb146ff --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/command/BrigadierCommandExecutor.java @@ -0,0 +1,114 @@ +/* + * 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.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.sender.Sender; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public abstract class BrigadierCommandExecutor extends CommandManager implements Command, SuggestionProvider { + + protected static final String[] COMMAND_ALIASES = new String[]{"luckperms", "lp", "perm", "perms", "permission", "permissions"}; + + private final LuckPermsPlugin plugin; + + protected BrigadierCommandExecutor(LuckPermsPlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + public abstract Sender getSender(S source); + + public abstract List resolveSelectors(S source, List args); + + @Override + public int run(CommandContext context) throws CommandSyntaxException { + S source = context.getSource(); + Sender sender = getSender(source); + + int start = context.getRange().getStart(); + String buffer = context.getInput().substring(start); + + List arguments; + if (this.plugin.getConfiguration().get(ConfigKeys.RESOLVE_COMMAND_SELECTORS)) { + arguments = resolveSelectors(source, ArgumentTokenizer.EXECUTE.tokenizeInput(buffer)); + } else { + arguments = ArgumentTokenizer.EXECUTE.tokenizeInput(buffer); + } + + String label = arguments.remove(0); + if (label.startsWith("/")) { + label = label.substring(1); + } + + executeCommand(sender, label, arguments); + return Command.SINGLE_SUCCESS; + } + + @Override + public CompletableFuture getSuggestions(CommandContext context, SuggestionsBuilder builder) throws CommandSyntaxException { + S source = context.getSource(); + Sender sender = getSender(source); + + int idx = builder.getStart(); + + String buffer = builder.getInput().substring(idx); + idx += buffer.length(); + + List arguments = ArgumentTokenizer.TAB_COMPLETE.tokenizeInput(buffer); + List resolvedArguments; + if (this.plugin.getConfiguration().get(ConfigKeys.RESOLVE_COMMAND_SELECTORS)) { + resolvedArguments = resolveSelectors(source, new ArrayList<>(arguments)); + } else { + resolvedArguments = arguments; + } + + if (!arguments.isEmpty() && !resolvedArguments.isEmpty()) { + idx -= arguments.get(arguments.size() - 1).length(); + } + + List completions = tabCompleteCommand(sender, resolvedArguments); + + // Offset the builder from the current string range so suggestions are placed in the right spot + builder = builder.createOffset(idx); + for (String completion : completions) { + builder.suggest(completion); + } + return builder.buildFuture(); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/command/access/CommandPermission.java b/common/src/main/java/me/lucko/luckperms/common/command/access/CommandPermission.java index dfb5efc3a..ed1ed15e3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/access/CommandPermission.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/access/CommandPermission.java @@ -173,6 +173,7 @@ public enum CommandPermission { public static final String ROOT = "luckperms."; private final String node; + private final String permission; private final Type type; @@ -180,14 +181,20 @@ public enum CommandPermission { this.type = type; if (type == Type.NONE) { - this.node = ROOT + node; + this.node = node; } else { - this.node = ROOT + type.getTag() + "." + node; + this.node = type.getTag() + "." + node; } + + this.permission = ROOT + this.node; + } + + public String getNode() { + return this.node; } public String getPermission() { - return this.node; + return this.permission; } public boolean isAuthorized(Sender sender) { 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 2c5ac5a31..28cc6c1ad 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 @@ -505,9 +505,9 @@ public final class ConfigKeys { public static final ConfigKey VAULT_IGNORE_WORLD = booleanKey("vault-ignore-world", false); /** - * If the owner of an integrated server should automatically bypasses all permission checks. On fabric, this only applies on an Integrated Server. + * If the owner of an integrated server should automatically bypass all permission checks. On fabric and forge, this only applies on an Integrated Server. */ - public static final ConfigKey FABRIC_INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS = booleanKey("integrated-server-owner-bypasses-checks", true); + public static final ConfigKey INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS = booleanKey("integrated-server-owner-bypasses-checks", true); /** * Disabled context calculators diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java index 24ed279c7..0650fcb47 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java @@ -117,8 +117,12 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { this.dependencyManager = new DependencyManager(this); this.dependencyManager.loadDependencies(getGlobalDependencies()); + // load translations this.translationManager = new TranslationManager(this); this.translationManager.reload(); + + // load some utilities early + this.permissionRegistry = new PermissionRegistry(getBootstrap().getScheduler()); } public final void enable() { @@ -130,7 +134,6 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { // load some utilities early this.verboseHandler = new VerboseHandler(getBootstrap().getScheduler()); - this.permissionRegistry = new PermissionRegistry(getBootstrap().getScheduler()); this.logDispatcher = new LogDispatcher(this); // load configuration diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java b/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java index 2680ab00e..df8fbcb53 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java @@ -135,10 +135,12 @@ public interface LuckPermsBootstrap { /** * Gets the plugins main data storage directory * - *

Bukkit: /root/plugins/LuckPerms

- *

Bungee: /root/plugins/LuckPerms

- *

Sponge: /root/luckperms/

- *

Fabric: /root/mods/LuckPerms

+ *

Bukkit: ./plugins/LuckPerms

+ *

BungeeCord: ./plugins/LuckPerms

+ *

Sponge: ./luckperms/

+ *

Velocity: ./plugins/luckperms

+ *

Fabric: ./mods/LuckPerms

+ *

Forge: ./config/luckperms

* * @return the platforms data folder */ diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java index 00080670a..6fca4aceb 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java @@ -68,7 +68,7 @@ public class FabricCalculatorFactory implements CalculatorFactory { } boolean integratedOwner = queryOptions.option(FabricContextManager.INTEGRATED_SERVER_OWNER).orElse(false); - if (integratedOwner && this.plugin.getConfiguration().get(ConfigKeys.FABRIC_INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS)) { + if (integratedOwner && this.plugin.getConfiguration().get(ConfigKeys.INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS)) { processors.add(ServerOwnerProcessor.INSTANCE); } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java index 3fcead276..f7b42aa97 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java @@ -25,21 +25,12 @@ package me.lucko.luckperms.fabric; -import com.mojang.brigadier.Command; import com.mojang.brigadier.StringReader; -import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; -import com.mojang.brigadier.suggestion.SuggestionProvider; -import com.mojang.brigadier.suggestion.Suggestions; -import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.tree.ArgumentCommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; - -import me.lucko.luckperms.common.command.CommandManager; -import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; -import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.command.BrigadierCommandExecutor; import me.lucko.luckperms.common.sender.Sender; - import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; import net.minecraft.command.argument.EntityArgumentType; import net.minecraft.server.command.ServerCommandSource; @@ -47,14 +38,12 @@ import net.minecraft.server.network.ServerPlayerEntity; import java.util.List; import java.util.ListIterator; -import java.util.concurrent.CompletableFuture; import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; import static net.minecraft.server.command.CommandManager.argument; import static net.minecraft.server.command.CommandManager.literal; -public class FabricCommandExecutor extends CommandManager implements Command, SuggestionProvider { - private static final String[] COMMAND_ALIASES = new String[] {"luckperms", "lp", "perm", "perms", "permission", "permissions"}; +public class FabricCommandExecutor extends BrigadierCommandExecutor { private final LPFabricPlugin plugin; @@ -82,52 +71,12 @@ public class FabricCommandExecutor extends CommandManager implements Command ctx) { - ServerCommandSource source = ctx.getSource(); - Sender wrapped = this.plugin.getSenderFactory().wrap(source); - - int start = ctx.getRange().getStart(); - List arguments = resolveSelectors(source, ArgumentTokenizer.EXECUTE.tokenizeInput(ctx.getInput().substring(start))); - - String label = arguments.remove(0); - if (label.startsWith("/")) { - label = label.substring(1); - } - - executeCommand(wrapped, label, arguments); - return Command.SINGLE_SUCCESS; + public Sender getSender(ServerCommandSource source) { + return this.plugin.getSenderFactory().wrap(source); } @Override - public CompletableFuture getSuggestions(CommandContext ctx, SuggestionsBuilder builder) { - ServerCommandSource source = ctx.getSource(); - Sender wrapped = this.plugin.getSenderFactory().wrap(source); - - int idx = builder.getStart(); - - String buffer = ctx.getInput().substring(idx); - idx += buffer.length(); - - List arguments = resolveSelectors(source, ArgumentTokenizer.TAB_COMPLETE.tokenizeInput(buffer)); - if (!arguments.isEmpty()) { - idx -= arguments.get(arguments.size() - 1).length(); - } - - List completions = tabCompleteCommand(wrapped, arguments); - - // Offset the builder from the current string range so suggestions are placed in the right spot - builder = builder.createOffset(idx); - for (String completion : completions) { - builder.suggest(completion); - } - return builder.buildFuture(); - } - - private List resolveSelectors(ServerCommandSource source, List args) { - if (!this.plugin.getConfiguration().get(ConfigKeys.RESOLVE_COMMAND_SELECTORS)) { - return args; - } - + public List resolveSelectors(ServerCommandSource source, List args) { // usage of @ selectors requires at least level 2 permission ServerCommandSource atAllowedSource = source.hasPermissionLevel(2) ? source : source.withLevel(2); for (ListIterator it = args.listIterator(); it.hasNext(); ) { diff --git a/forge/build.gradle b/forge/build.gradle new file mode 100644 index 000000000..4b12a22dd --- /dev/null +++ b/forge/build.gradle @@ -0,0 +1,75 @@ +buildscript { + repositories { + maven { url 'https://plugins.gradle.org/m2' } + maven { url 'https://maven.minecraftforge.net/' } + } + + dependencies { + classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2' + classpath 'net.kyori:blossom:1.3.0' + classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.+' + } +} + +apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'net.kyori.blossom' +apply plugin: 'net.minecraftforge.gradle' + +sourceCompatibility = 1.8 +targetCompatibility = 17 + +blossom { + replaceTokenIn 'src/main/java/me/lucko/luckperms/forge/LPForgeBootstrap.java' + replaceToken '@version@', project.ext.fullVersion +} + +minecraft { + mappings channel: 'official', version: minecraftVersion +} + +configurations { +} + +repositories { +} + +dependencies { + minecraft "net.minecraftforge:forge:${minecraftVersion}-${forgeVersion}" + implementation project(':common') + compileOnly project(':common:loader-utils') + compileOnly project(':forge:forge-api') +} + +reobf { + shadowJar {} +} + +shadowJar { + archiveFileName = "luckperms-forge.jarinjar" + + dependencies { + include(dependency('me.lucko.luckperms:.*')) + } + + relocate 'net.kyori.adventure', 'me.lucko.luckperms.lib.adventure' + relocate 'net.kyori.event', 'me.lucko.luckperms.lib.eventbus' + relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine' + relocate 'okio', 'me.lucko.luckperms.lib.okio' + relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3' + relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy' + relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' + relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' + relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' + relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' + relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' + relocate 'org.bson', 'me.lucko.luckperms.lib.bson' + relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' + relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' + relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' +} + +artifacts { + archives shadowJar +} diff --git a/forge/forge-api/build.gradle b/forge/forge-api/build.gradle new file mode 100644 index 000000000..8edccffbe --- /dev/null +++ b/forge/forge-api/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + maven { url 'https://maven.minecraftforge.net/' } + } + + dependencies { + classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.+' + } +} + +apply plugin: 'net.minecraftforge.gradle' + +sourceCompatibility = 1.8 +targetCompatibility = 17 + +minecraft { + mappings channel: 'official', version: minecraftVersion +} + +configurations { +} + +repositories { +} + +dependencies { + minecraft "net.minecraftforge:forge:${minecraftVersion}-${forgeVersion}" + implementation project(':api') +} diff --git a/forge/forge-api/src/main/java/me/lucko/luckperms/forge/capabilities/UserCapability.java b/forge/forge-api/src/main/java/me/lucko/luckperms/forge/capabilities/UserCapability.java new file mode 100644 index 000000000..9a1323837 --- /dev/null +++ b/forge/forge-api/src/main/java/me/lucko/luckperms/forge/capabilities/UserCapability.java @@ -0,0 +1,85 @@ +/* + * 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.forge.capabilities; + +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.common.capabilities.CapabilityToken; + +/** + * A Forge {@link Capability} that attaches LuckPerms functionality onto {@link ServerPlayer}s. + */ +public interface UserCapability { + + /** + * The identifier used for the capability + */ + ResourceLocation IDENTIFIER = new ResourceLocation("luckperms", "user"); + + /** + * The capability instance. + */ + Capability CAPABILITY = CapabilityManager.get(new CapabilityToken(){}); + + /** + * Checks for a permission. + * + * @param permission the permission + * @return the result + */ + default boolean hasPermission(String permission) { + return checkPermission(permission).asBoolean(); + } + + /** + * Runs a permission check. + * + * @param permission the permission + * @return the result + */ + Tristate checkPermission(String permission); + + /** + * Runs a permission check. + * + * @param permission the permission + * @param queryOptions the query options + * @return the result + */ + Tristate checkPermission(String permission, QueryOptions queryOptions); + + /** + * Gets the user's currently query options. + * + * @return the current query options for the user + */ + QueryOptions getQueryOptions(); + +} diff --git a/forge/gradle.properties b/forge/gradle.properties new file mode 100644 index 000000000..7c8b2904a --- /dev/null +++ b/forge/gradle.properties @@ -0,0 +1,2 @@ +minecraftVersion=1.18.2 +forgeVersion=40.1.20 \ No newline at end of file diff --git a/forge/loader/build.gradle b/forge/loader/build.gradle new file mode 100644 index 000000000..c66d7276b --- /dev/null +++ b/forge/loader/build.gradle @@ -0,0 +1,78 @@ +buildscript { + repositories { + maven { url 'https://plugins.gradle.org/m2' } + maven { url 'https://maven.minecraftforge.net/' } + } + + dependencies { + classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2' + classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.+' + } +} + +apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'java-library' +apply plugin: 'net.minecraftforge.gradle' + +sourceCompatibility = 1.8 +targetCompatibility = 17 + +minecraft { + mappings channel: 'official', version: minecraftVersion +} + +repositories { +} + +dependencies { + minecraft "net.minecraftforge:forge:${minecraftVersion}-${forgeVersion}" + implementation project(':api') + implementation project(':common:loader-utils') + implementation project(':forge:forge-api') +} + +build { + dependsOn(":forge:build") + dependsOn(":forge-api:build") +} + +jar { + manifest { + attributes( + 'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), + 'Implementation-Title': 'LuckPerms', + 'Implementation-Vendor': 'LuckPerms', + 'Implementation-Version': project.ext.fullVersion, + 'Specification-Title': 'luckperms', + 'Specification-Vendor': 'LuckPerms', + 'Specification-Version': '1' + ) + } +} + +processResources { + filesMatching('META-INF/mods.toml') { + expand 'version': project.ext.fullVersion + } +} + +reobf { + shadowJar {} +} + +shadowJar { + archiveFileName = "LuckPerms-Forge-${project.ext.fullVersion}.jar" + + from { + project(':forge').tasks.shadowJar.archiveFile + } + + dependencies { + include(dependency('net.luckperms:.*')) + include(dependency('me.lucko.luckperms:.*')) + } +} + +artifacts { + archives shadowJar +} diff --git a/forge/loader/src/main/java/me/lucko/luckperms/forge/loader/ForgeLoaderPlugin.java b/forge/loader/src/main/java/me/lucko/luckperms/forge/loader/ForgeLoaderPlugin.java new file mode 100644 index 000000000..951311603 --- /dev/null +++ b/forge/loader/src/main/java/me/lucko/luckperms/forge/loader/ForgeLoaderPlugin.java @@ -0,0 +1,100 @@ +/* + * 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.forge.loader; + +import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import me.lucko.luckperms.common.loader.LoaderBootstrap; + +import net.minecraftforge.fml.IExtensionPoint; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.network.NetworkConstants; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.reflect.InvocationTargetException; +import java.util.function.Supplier; + +@Mod(value = "luckperms") +public class ForgeLoaderPlugin implements Supplier { + private static final Logger LOGGER = LogManager.getLogger("luckperms"); + + private static final String JAR_NAME = "luckperms-forge.jarinjar"; + private static final String BOOTSTRAP_CLASS = "me.lucko.luckperms.forge.LPForgeBootstrap"; + + private final ModContainer container; + + private JarInJarClassLoader loader; + private LoaderBootstrap plugin; + + public ForgeLoaderPlugin() { + this.container = ModList.get().getModContainerByObject(this).orElse(null); + + markAsNotRequiredClientSide(); + + if (FMLEnvironment.dist.isClient()) { + LOGGER.info("Skipping LuckPerms init (not supported on the client!)"); + return; + } + + this.loader = new JarInJarClassLoader(getClass().getClassLoader(), JAR_NAME); + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::onCommonSetup); + } + + @Override + public ModContainer get() { + return this.container; + } + + public void onCommonSetup(FMLCommonSetupEvent event) { + this.plugin = this.loader.instantiatePlugin(BOOTSTRAP_CLASS, Supplier.class, this); + this.plugin.onLoad(); + } + + private static void markAsNotRequiredClientSide() { + try { + // workaround as we don't compile against java 17 + ModLoadingContext.class.getDeclaredMethod("registerExtensionPoint", Class.class, Supplier.class) + .invoke( + ModLoadingContext.get(), + IExtensionPoint.DisplayTest.class, + (Supplier) () -> new IExtensionPoint.DisplayTest( + () -> NetworkConstants.IGNORESERVERONLY, + (a, b) -> true + ) + ); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/forge/loader/src/main/resources/META-INF/mods.toml b/forge/loader/src/main/resources/META-INF/mods.toml new file mode 100644 index 000000000..b773ff9c8 --- /dev/null +++ b/forge/loader/src/main/resources/META-INF/mods.toml @@ -0,0 +1,20 @@ +modLoader="javafml" +loaderVersion="[40,)" +license="MIT" +issueTrackerURL="https://github.com/LuckPerms/LuckPerms/issues" + +[[mods]] + modId="luckperms" + version="${version}" + displayName="LuckPerms" + displayURL="https://luckperms.net/" + logoFile="luckperms.png" + credits="Luck" + authors="Luck" + description="A permissions plugin for Minecraft servers." +[[dependencies.luckperms]] + modId="forge" + mandatory=true + versionRange="[40.1.20,)" + ordering="NONE" + side="BOTH" \ No newline at end of file diff --git a/forge/loader/src/main/resources/luckperms.png b/forge/loader/src/main/resources/luckperms.png new file mode 100644 index 000000000..2e0ea669a Binary files /dev/null and b/forge/loader/src/main/resources/luckperms.png differ diff --git a/forge/loader/src/main/resources/pack.mcmeta b/forge/loader/src/main/resources/pack.mcmeta new file mode 100644 index 000000000..fc492580a --- /dev/null +++ b/forge/loader/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "LuckPerms resources", + "pack_format": 8 + } +} \ No newline at end of file diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeCommandExecutor.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeCommandExecutor.java new file mode 100644 index 000000000..4f959c6c1 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeCommandExecutor.java @@ -0,0 +1,110 @@ +/* + * 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.forge; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import me.lucko.luckperms.common.command.BrigadierCommandExecutor; +import me.lucko.luckperms.common.sender.Sender; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +import java.util.List; +import java.util.ListIterator; + +public class ForgeCommandExecutor extends BrigadierCommandExecutor { + + private final LPForgePlugin plugin; + + public ForgeCommandExecutor(LPForgePlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + @SubscribeEvent + public void onRegisterCommands(RegisterCommandsEvent event) { + for (String alias : COMMAND_ALIASES) { + LiteralCommandNode command = Commands.literal(alias).executes(this).build(); + ArgumentCommandNode argument = Commands.argument("args", StringArgumentType.greedyString()) + .suggests(this) + .executes(this) + .build(); + + command.addChild(argument); + event.getDispatcher().getRoot().addChild(command); + } + } + + @Override + public Sender getSender(CommandSourceStack source) { + return this.plugin.getSenderFactory().wrap(source); + } + + @Override + public List resolveSelectors(CommandSourceStack source, List args) { + // usage of @ selectors requires at least level 2 permission + CommandSourceStack atAllowedSource = source.hasPermission(2) ? source : source.withPermission(2); + for (ListIterator it = args.listIterator(); it.hasNext(); ) { + String arg = it.next(); + if (arg.isEmpty() || arg.charAt(0) != '@') { + continue; + } + + List matchedPlayers; + try { + matchedPlayers = EntityArgument.entities().parse(new StringReader(arg)).findPlayers(atAllowedSource); + } catch (CommandSyntaxException e) { + this.plugin.getLogger().warn("Error parsing selector '" + arg + "' for " + source + " executing " + args, e); + continue; + } + + if (matchedPlayers.isEmpty()) { + continue; + } + + if (matchedPlayers.size() > 1) { + this.plugin.getLogger().warn("Error parsing selector '" + arg + "' for " + source + " executing " + args + + ": ambiguous result (more than one player matched) - " + matchedPlayers); + continue; + } + + ServerPlayer player = matchedPlayers.get(0); + it.set(player.getStringUUID()); + } + + return args; + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeConfigAdapter.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeConfigAdapter.java new file mode 100644 index 000000000..a5d431a81 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeConfigAdapter.java @@ -0,0 +1,47 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.config.generic.adapter.ConfigurateConfigAdapter; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; + +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.hocon.HoconConfigurationLoader; +import ninja.leaping.configurate.loader.ConfigurationLoader; + +import java.nio.file.Path; + +public class ForgeConfigAdapter extends ConfigurateConfigAdapter { + public ForgeConfigAdapter(LuckPermsPlugin plugin, Path path) { + super(plugin, path); + } + + @Override + protected ConfigurationLoader createLoader(Path path) { + return HoconConfigurationLoader.builder().setPath(path).build(); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeEventBus.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeEventBus.java new file mode 100644 index 000000000..18228958b --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeEventBus.java @@ -0,0 +1,50 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; + +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModList; + +public class ForgeEventBus extends AbstractEventBus { + public ForgeEventBus(LuckPermsPlugin plugin, LuckPermsApiProvider apiProvider) { + super(plugin, apiProvider); + } + + @Override + protected ModContainer checkPlugin(Object mod) throws IllegalArgumentException { + ModContainer modContainer = ModList.get().getModContainerByObject(mod).orElse(null); + if (modContainer != null) { + return modContainer; + } + + throw new IllegalArgumentException("Object " + mod + " (" + mod.getClass().getName() + ") is not a ModContainer."); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeSchedulerAdapter.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeSchedulerAdapter.java new file mode 100644 index 000000000..9d3d76cd4 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeSchedulerAdapter.java @@ -0,0 +1,45 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.plugin.scheduler.AbstractJavaScheduler; + +import java.util.concurrent.Executor; + +public class ForgeSchedulerAdapter extends AbstractJavaScheduler { + private final Executor sync; + + public ForgeSchedulerAdapter(LPForgeBootstrap bootstrap) { + super(bootstrap); + this.sync = r -> bootstrap.getServer().orElseThrow(() -> new IllegalStateException("Server not ready")).executeBlocking(r); + } + + @Override + public Executor sync() { + return this.sync; + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java new file mode 100644 index 000000000..d6a364037 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java @@ -0,0 +1,116 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.sender.SenderFactory; +import me.lucko.luckperms.common.verbose.VerboseCheckTarget; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.forge.capabilities.UserCapability; +import me.lucko.luckperms.forge.capabilities.UserCapabilityImpl; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.luckperms.api.util.Tristate; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; + +import java.util.Locale; +import java.util.UUID; + +public class ForgeSenderFactory extends SenderFactory { + public ForgeSenderFactory(LPForgePlugin plugin) { + super(plugin); + } + + @Override + protected UUID getUniqueId(CommandSourceStack commandSource) { + if (commandSource.getEntity() instanceof Player) { + return commandSource.getEntity().getUUID(); + } + return Sender.CONSOLE_UUID; + } + + @Override + protected String getName(CommandSourceStack commandSource) { + if (commandSource.getEntity() instanceof Player) { + return commandSource.getTextName(); + } + return Sender.CONSOLE_NAME; + } + + @Override + protected void sendMessage(CommandSourceStack sender, Component message) { + Locale locale; + if (sender.getEntity() instanceof ServerPlayer) { + ServerPlayer player = (ServerPlayer) sender.getEntity(); + UserCapabilityImpl user = UserCapabilityImpl.get(player); + locale = user.getLocale(player); + } else { + locale = null; + } + + sender.sendSuccess(toNativeText(TranslationManager.render(message, locale)), false); + } + + @Override + protected Tristate getPermissionValue(CommandSourceStack commandSource, String node) { + if (commandSource.getEntity() instanceof ServerPlayer) { + ServerPlayer player = (ServerPlayer) commandSource.getEntity(); + UserCapability user = UserCapabilityImpl.get(player); + return user.checkPermission(node); + } + + VerboseCheckTarget target = VerboseCheckTarget.internal(commandSource.getTextName()); + getPlugin().getVerboseHandler().offerPermissionCheckEvent(CheckOrigin.PLATFORM_API_HAS_PERMISSION, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, node, TristateResult.UNDEFINED); + getPlugin().getPermissionRegistry().offer(node); + return Tristate.UNDEFINED; + } + + @Override + protected boolean hasPermission(CommandSourceStack commandSource, String node) { + return getPermissionValue(commandSource, node).asBoolean(); + } + + @Override + protected void performCommand(CommandSourceStack sender, String command) { + sender.getServer().getCommands().performCommand(sender, command); + } + + @Override + protected boolean isConsole(CommandSourceStack sender) { + return !(sender.getEntity() instanceof Player); + } + + public static net.minecraft.network.chat.Component toNativeText(Component component) { + return net.minecraft.network.chat.Component.Serializer.fromJson(GsonComponentSerializer.gson().serialize(component)); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/LPForgeBootstrap.java b/forge/src/main/java/me/lucko/luckperms/forge/LPForgeBootstrap.java new file mode 100644 index 000000000..54bad0744 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/LPForgeBootstrap.java @@ -0,0 +1,290 @@ +/* + * 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.forge; + +import com.mojang.authlib.GameProfile; + +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; +import me.lucko.luckperms.common.plugin.classpath.JarInJarClassPathAppender; +import me.lucko.luckperms.common.plugin.logging.Log4jPluginLogger; +import me.lucko.luckperms.common.plugin.logging.PluginLogger; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.forge.util.ForgeEventBusFacade; + +import net.luckperms.api.platform.Platform; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.forgespi.language.IModInfo; + +import org.apache.logging.log4j.LogManager; +import org.apache.maven.artifact.versioning.ArtifactVersion; + +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.function.Supplier; + +/** + * Bootstrap plugin for LuckPerms running on Forge. + */ +public final class LPForgeBootstrap implements LuckPermsBootstrap, LoaderBootstrap, BootstrappedWithLoader { + public static final String ID = "luckperms"; + + /** + * The plugin loader + */ + private final Supplier loader; + + /** + * The plugin logger + */ + private final PluginLogger logger; + + /** + * A scheduler adapter for the platform + */ + private final SchedulerAdapter schedulerAdapter; + + /** + * The plugin class path appender + */ + private final ClassPathAppender classPathAppender; + + /** + * A facade for the forge event bus, compatible with LP's jar-in-jar packaging + */ + private final ForgeEventBusFacade forgeEventBus; + + /** + * The plugin instance + */ + private final LPForgePlugin plugin; + + /** + * The time when the plugin was enabled + */ + private Instant startTime; + + // load/enable latches + private final CountDownLatch loadLatch = new CountDownLatch(1); + private final CountDownLatch enableLatch = new CountDownLatch(1); + + /** + * The Minecraft server instance + */ + private MinecraftServer server; + + public LPForgeBootstrap(Supplier loader) { + this.loader = loader; + this.logger = new Log4jPluginLogger(LogManager.getLogger(LPForgeBootstrap.ID)); + this.schedulerAdapter = new ForgeSchedulerAdapter(this); + this.classPathAppender = new JarInJarClassPathAppender(getClass().getClassLoader()); + this.forgeEventBus = new ForgeEventBusFacade(); + this.plugin = new LPForgePlugin(this); + } + + // provide adapters + + @Override + public Object getLoader() { + return this.loader; + } + + @Override + public PluginLogger getPluginLogger() { + return this.logger; + } + + @Override + public SchedulerAdapter getScheduler() { + return this.schedulerAdapter; + } + + @Override + public ClassPathAppender getClassPathAppender() { + return this.classPathAppender; + } + + public void registerListeners(Object target) { + this.forgeEventBus.register(target); + } + + // lifecycle + + @Override + public void onLoad() { // called by the loader on FMLCommonSetupEvent + this.startTime = Instant.now(); + try { + this.plugin.load(); + } finally { + this.loadLatch.countDown(); + } + + this.forgeEventBus.register(this); + this.plugin.registerEarlyListeners(); + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public void onServerAboutToStart(ServerAboutToStartEvent event) { + this.server = event.getServer(); + try { + this.plugin.enable(); + } finally { + this.enableLatch.countDown(); + } + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onServerStopping(ServerStoppingEvent event) { + this.plugin.disable(); + this.forgeEventBus.unregisterAll(); + this.server = null; + } + + @Override + public CountDownLatch getLoadLatch() { + return this.loadLatch; + } + + @Override + public CountDownLatch getEnableLatch() { + return this.enableLatch; + } + + // MinecraftServer singleton getter + + public Optional getServer() { + return Optional.ofNullable(this.server); + } + + // provide information about the plugin + + @Override + public String getVersion() { + return "@version@"; + } + + @Override + public Instant getStartupTime() { + return this.startTime; + } + + // provide information about the platform + + @Override + public Platform.Type getType() { + return Platform.Type.FORGE; + } + + @Override + public String getServerBrand() { + return ModList.get().getModContainerById("forge") + .map(ModContainer::getModInfo) + .map(IModInfo::getDisplayName) + .orElse("null"); + } + + @Override + public String getServerVersion() { + String forgeVersion = ModList.get().getModContainerById("forge") + .map(ModContainer::getModInfo) + .map(IModInfo::getVersion) + .map(ArtifactVersion::toString) + .orElse("null"); + + return getServer().map(MinecraftServer::getServerVersion).orElse("null") + "-" + forgeVersion; + } + + @Override + public Path getDataDirectory() { + return FMLPaths.CONFIGDIR.get().resolve(LPForgeBootstrap.ID).toAbsolutePath(); + } + + @Override + public Optional getPlayer(UUID uniqueId) { + return getServer().map(MinecraftServer::getPlayerList).map(playerList -> playerList.getPlayer(uniqueId)); + } + + @Override + public Optional lookupUniqueId(String username) { + return getServer().map(MinecraftServer::getProfileCache).flatMap(profileCache -> profileCache.get(username)).map(GameProfile::getId); + } + + @Override + public Optional lookupUsername(UUID uniqueId) { + return getServer().map(MinecraftServer::getProfileCache).flatMap(profileCache -> profileCache.get(uniqueId)).map(GameProfile::getName); + } + + @Override + public int getPlayerCount() { + return getServer().map(MinecraftServer::getPlayerCount).orElse(0); + } + + @Override + public Collection getPlayerList() { + return getServer().map(MinecraftServer::getPlayerList).map(PlayerList::getPlayers).map(players -> { + List list = new ArrayList<>(players.size()); + for (ServerPlayer player : players) { + list.add(player.getGameProfile().getName()); + } + return list; + }).orElse(Collections.emptyList()); + } + + @Override + public Collection getOnlinePlayers() { + return getServer().map(MinecraftServer::getPlayerList).map(PlayerList::getPlayers).map(players -> { + List list = new ArrayList<>(players.size()); + for (ServerPlayer player : players) { + list.add(player.getGameProfile().getId()); + } + return list; + }).orElse(Collections.emptyList()); + } + + @Override + public boolean isPlayerOnline(UUID uniqueId) { + return getServer().map(MinecraftServer::getPlayerList).map(playerList -> playerList.getPlayer(uniqueId)).isPresent(); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/LPForgePlugin.java b/forge/src/main/java/me/lucko/luckperms/forge/LPForgePlugin.java new file mode 100644 index 000000000..0e2326cdf --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/LPForgePlugin.java @@ -0,0 +1,249 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.calculator.CalculatorFactory; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import me.lucko.luckperms.common.dependencies.Dependency; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.messaging.MessagingFactory; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.model.manager.track.StandardTrackManager; +import me.lucko.luckperms.common.model.manager.user.StandardUserManager; +import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; +import me.lucko.luckperms.common.sender.DummyConsoleSender; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.forge.calculator.ForgeCalculatorFactory; +import me.lucko.luckperms.forge.capabilities.UserCapabilityListener; +import me.lucko.luckperms.forge.context.ForgeContextManager; +import me.lucko.luckperms.forge.context.ForgePlayerCalculator; +import me.lucko.luckperms.forge.listeners.ForgeAutoOpListener; +import me.lucko.luckperms.forge.listeners.ForgeCommandListUpdater; +import me.lucko.luckperms.forge.listeners.ForgeConnectionListener; +import me.lucko.luckperms.forge.listeners.ForgePlatformListener; +import me.lucko.luckperms.forge.messaging.ForgeMessagingFactory; +import me.lucko.luckperms.forge.service.ForgePermissionHandlerListener; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.PlayerList; +import net.minecraftforge.fml.ModContainer; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * LuckPerms implementation for Forge. + */ +public class LPForgePlugin extends AbstractLuckPermsPlugin { + private final LPForgeBootstrap bootstrap; + + private ForgeSenderFactory senderFactory; + private ForgeConnectionListener connectionListener; + private ForgeCommandExecutor commandManager; + private StandardUserManager userManager; + private StandardGroupManager groupManager; + private StandardTrackManager trackManager; + private ForgeContextManager contextManager; + + public LPForgePlugin(LPForgeBootstrap bootstrap) { + this.bootstrap = bootstrap; + } + + @Override + public LPForgeBootstrap getBootstrap() { + return this.bootstrap; + } + + protected void registerEarlyListeners() { + this.connectionListener = new ForgeConnectionListener(this); + this.bootstrap.registerListeners(this.connectionListener); + + ForgePlatformListener platformListener = new ForgePlatformListener(this); + this.bootstrap.registerListeners(platformListener); + + UserCapabilityListener userCapabilityListener = new UserCapabilityListener(); + this.bootstrap.registerListeners(userCapabilityListener); + + ForgePermissionHandlerListener permissionHandlerListener = new ForgePermissionHandlerListener(this); + this.bootstrap.registerListeners(permissionHandlerListener); + + this.commandManager = new ForgeCommandExecutor(this); + this.bootstrap.registerListeners(this.commandManager); + } + + @Override + protected void setupSenderFactory() { + this.senderFactory = new ForgeSenderFactory(this); + } + + @Override + protected Set getGlobalDependencies() { + Set dependencies = super.getGlobalDependencies(); + dependencies.add(Dependency.CONFIGURATE_CORE); + dependencies.add(Dependency.CONFIGURATE_HOCON); + dependencies.add(Dependency.HOCON_CONFIG); + return dependencies; + } + + @Override + protected ConfigurationAdapter provideConfigurationAdapter() { + return new ForgeConfigAdapter(this, resolveConfig("luckperms.conf")); + } + + @Override + protected void registerPlatformListeners() { + // Too late for Forge, registered in #registerEarlyListeners + } + + @Override + protected MessagingFactory provideMessagingFactory() { + return new ForgeMessagingFactory(this); + } + + @Override + protected void registerCommands() { + // Too late for Forge, registered in #registerEarlyListeners + } + + @Override + protected void setupManagers() { + this.userManager = new StandardUserManager(this); + this.groupManager = new StandardGroupManager(this); + this.trackManager = new StandardTrackManager(this); + } + + @Override + protected CalculatorFactory provideCalculatorFactory() { + return new ForgeCalculatorFactory(this); + } + + @Override + protected void setupContextManager() { + this.contextManager = new ForgeContextManager(this); + + ForgePlayerCalculator playerCalculator = new ForgePlayerCalculator(this, getConfiguration().get(ConfigKeys.DISABLED_CONTEXTS)); + this.bootstrap.registerListeners(playerCalculator); + this.contextManager.registerCalculator(playerCalculator); + } + + @Override + protected void setupPlatformHooks() { + } + + @Override + protected AbstractEventBus provideEventBus(LuckPermsApiProvider provider) { + return new ForgeEventBus(this, provider); + } + + @Override + protected void registerApiOnPlatform(LuckPerms api) { + } + + @Override + protected void performFinalSetup() { + // register autoop listener + if (getConfiguration().get(ConfigKeys.AUTO_OP)) { + getApiProvider().getEventBus().subscribe(new ForgeAutoOpListener(this)); + } + + // register forge command list updater + if (getConfiguration().get(ConfigKeys.UPDATE_CLIENT_COMMAND_LIST)) { + getApiProvider().getEventBus().subscribe(new ForgeCommandListUpdater(this)); + } + } + + @Override + public Optional getQueryOptionsForUser(User user) { + return this.bootstrap.getPlayer(user.getUniqueId()).map(player -> this.contextManager.getQueryOptions(player)); + } + + @Override + public Stream getOnlineSenders() { + return Stream.concat( + Stream.of(getConsoleSender()), + this.bootstrap.getServer() + .map(MinecraftServer::getPlayerList) + .map(PlayerList::getPlayers) + .map(players -> players.stream().map(player -> this.senderFactory.wrap(player.createCommandSourceStack()))).orElseGet(Stream::empty) + ); + } + + @Override + public Sender getConsoleSender() { + return this.bootstrap.getServer() + .map(server -> this.senderFactory.wrap(server.createCommandSourceStack())) + .orElseGet(() -> new DummyConsoleSender(this) { + @Override + public void sendMessage(Component message) { + LPForgePlugin.this.bootstrap.getPluginLogger().info(PlainTextComponentSerializer.plainText().serialize(TranslationManager.render(message))); + } + }); + } + + public ForgeSenderFactory getSenderFactory() { + return this.senderFactory; + } + + @Override + public ForgeConnectionListener getConnectionListener() { + return this.connectionListener; + } + + @Override + public ForgeCommandExecutor getCommandManager() { + return this.commandManager; + } + + @Override + public StandardUserManager getUserManager() { + return this.userManager; + } + + @Override + public StandardGroupManager getGroupManager() { + return this.groupManager; + } + + @Override + public StandardTrackManager getTrackManager() { + return this.trackManager; + } + + @Override + public ForgeContextManager getContextManager() { + return this.contextManager; + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/calculator/ForgeCalculatorFactory.java b/forge/src/main/java/me/lucko/luckperms/forge/calculator/ForgeCalculatorFactory.java new file mode 100644 index 000000000..b4450dade --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/calculator/ForgeCalculatorFactory.java @@ -0,0 +1,78 @@ +/* + * 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.forge.calculator; + +import me.lucko.luckperms.common.cacheddata.CacheMetadata; +import me.lucko.luckperms.common.calculator.CalculatorFactory; +import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.processor.DirectProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.RegexProcessor; +import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.forge.LPForgePlugin; +import me.lucko.luckperms.forge.context.ForgeContextManager; + +import net.luckperms.api.query.QueryOptions; + +import java.util.ArrayList; +import java.util.List; + +public class ForgeCalculatorFactory implements CalculatorFactory { + private final LPForgePlugin plugin; + + public ForgeCalculatorFactory(LPForgePlugin plugin) { + this.plugin = plugin; + } + + @Override + public PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata) { + List processors = new ArrayList<>(5); + + processors.add(new DirectProcessor()); + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) { + processors.add(new RegexProcessor()); + } + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS)) { + processors.add(new WildcardProcessor()); + } + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS_SPONGE)) { + processors.add(new SpongeWildcardProcessor()); + } + + boolean integratedOwner = queryOptions.option(ForgeContextManager.INTEGRATED_SERVER_OWNER).orElse(false); + if (integratedOwner && this.plugin.getConfiguration().get(ConfigKeys.INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS)) { + processors.add(ServerOwnerProcessor.INSTANCE); + } + + return new PermissionCalculator(this.plugin, metadata, processors); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/calculator/ServerOwnerProcessor.java b/forge/src/main/java/me/lucko/luckperms/forge/calculator/ServerOwnerProcessor.java new file mode 100644 index 000000000..a78471135 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/calculator/ServerOwnerProcessor.java @@ -0,0 +1,52 @@ +/* + * 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.forge.calculator; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; + +import net.luckperms.api.util.Tristate; + +/** + * Permission processor which is added to the owner of an Integrated server to + * simply return true if no other processors match. + */ +public class ServerOwnerProcessor extends AbstractPermissionProcessor implements PermissionProcessor { + private static final TristateResult TRUE_RESULT = new TristateResult.Factory(ServerOwnerProcessor.class).result(Tristate.TRUE); + + public static final ServerOwnerProcessor INSTANCE = new ServerOwnerProcessor(); + + private ServerOwnerProcessor() { + + } + + @Override + public TristateResult hasPermission(String permission) { + return TRUE_RESULT; + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/capabilities/UserCapabilityImpl.java b/forge/src/main/java/me/lucko/luckperms/forge/capabilities/UserCapabilityImpl.java new file mode 100644 index 000000000..17a55f3c8 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/capabilities/UserCapabilityImpl.java @@ -0,0 +1,149 @@ +/* + * 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.forge.capabilities; + +import me.lucko.luckperms.common.cacheddata.type.PermissionCache; +import me.lucko.luckperms.common.context.manager.QueryOptionsCache; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.forge.context.ForgeContextManager; + +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +public class UserCapabilityImpl implements UserCapability { + + /** + * Gets a {@link UserCapability} for a given {@link ServerPlayer}. + * + * @param player the player + * @return the capability + */ + public static @NotNull UserCapabilityImpl get(@NotNull Player player) { + return (UserCapabilityImpl) player.getCapability(CAPABILITY) + .orElseThrow(() -> new IllegalStateException("Capability missing for " + player.getUUID())); + } + + /** + * Gets a {@link UserCapability} for a given {@link ServerPlayer}. + * + * @param player the player + * @return the capability, or null + */ + public static @Nullable UserCapabilityImpl getNullable(@NotNull Player player) { + return (UserCapabilityImpl) player.getCapability(CAPABILITY).resolve().orElse(null); + } + + private boolean initialised = false; + + private User user; + private QueryOptionsCache queryOptionsCache; + private String language; + private Locale locale; + + public UserCapabilityImpl() { + + } + + public void initialise(UserCapabilityImpl previous) { + this.user = previous.user; + this.queryOptionsCache = previous.queryOptionsCache; + this.language = previous.language; + this.locale = previous.locale; + this.initialised = true; + } + + public void initialise(User user, ServerPlayer player, ForgeContextManager contextManager) { + this.user = user; + this.queryOptionsCache = new QueryOptionsCache<>(player, contextManager); + this.initialised = true; + } + + private void assertInitialised() { + if (!this.initialised) { + throw new IllegalStateException("Capability has not been initialised"); + } + } + + @Override + public Tristate checkPermission(String permission) { + assertInitialised(); + + if (permission == null) { + throw new NullPointerException("permission"); + } + + return checkPermission(permission, this.queryOptionsCache.getQueryOptions()); + } + + @Override + public Tristate checkPermission(String permission, QueryOptions queryOptions) { + assertInitialised(); + + if (permission == null) { + throw new NullPointerException("permission"); + } + + if (queryOptions == null) { + throw new NullPointerException("queryOptions"); + } + + PermissionCache cache = this.user.getCachedData().getPermissionData(queryOptions); + return cache.checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + } + + public User getUser() { + assertInitialised(); + return this.user; + } + + @Override + public QueryOptions getQueryOptions() { + return getQueryOptionsCache().getQueryOptions(); + } + + public QueryOptionsCache getQueryOptionsCache() { + assertInitialised(); + return this.queryOptionsCache; + } + + public Locale getLocale(ServerPlayer player) { + if (this.language == null || !this.language.equals(player.getLanguage())) { + this.language = player.getLanguage(); + this.locale = TranslationManager.parseLocale(this.language); + } + + return this.locale; + } +} \ No newline at end of file diff --git a/forge/src/main/java/me/lucko/luckperms/forge/capabilities/UserCapabilityListener.java b/forge/src/main/java/me/lucko/luckperms/forge/capabilities/UserCapabilityListener.java new file mode 100644 index 000000000..fdbc06555 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/capabilities/UserCapabilityListener.java @@ -0,0 +1,99 @@ +/* + * 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.forge.capabilities; + +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class UserCapabilityListener { + + @SubscribeEvent + public void onRegisterCapabilities(RegisterCapabilitiesEvent event) { + event.register(UserCapabilityImpl.class); + } + + @SubscribeEvent + public void onAttachCapabilities(AttachCapabilitiesEvent event) { + if (!(event.getObject() instanceof ServerPlayer)) { + return; + } + + event.addCapability(UserCapability.IDENTIFIER, new UserCapabilityProvider(new UserCapabilityImpl())); + } + + @SubscribeEvent + public void onPlayerClone(PlayerEvent.Clone event) { + if (!event.isWasDeath()) { + return; + } + + Player previousPlayer = event.getOriginal(); + Player currentPlayer = event.getPlayer(); + + previousPlayer.reviveCaps(); + try { + UserCapabilityImpl previous = UserCapabilityImpl.get(previousPlayer); + UserCapabilityImpl current = UserCapabilityImpl.get(currentPlayer); + + current.initialise(previous); + current.getQueryOptionsCache().invalidate(); + } finally { + previousPlayer.invalidateCaps(); + } + } + + private static final class UserCapabilityProvider implements ICapabilityProvider { + private final UserCapabilityImpl userCapability; + + private UserCapabilityProvider(UserCapabilityImpl userCapability) { + this.userCapability = userCapability; + } + + @SuppressWarnings("unchecked") + @NotNull + @Override + public LazyOptional getCapability(@NotNull Capability cap, @Nullable Direction side) { + if (cap != UserCapability.CAPABILITY) { + return LazyOptional.empty(); + } + + return LazyOptional.of(() -> (T) this.userCapability); + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/context/ForgeContextManager.java b/forge/src/main/java/me/lucko/luckperms/forge/context/ForgeContextManager.java new file mode 100644 index 000000000..2a10fe4cf --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/context/ForgeContextManager.java @@ -0,0 +1,80 @@ +/* + * 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.forge.context; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.context.manager.ContextManager; +import me.lucko.luckperms.common.context.manager.QueryOptionsCache; +import me.lucko.luckperms.forge.LPForgePlugin; +import me.lucko.luckperms.forge.capabilities.UserCapabilityImpl; + +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.query.OptionKey; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.level.ServerPlayer; + +import java.util.UUID; + +public class ForgeContextManager extends ContextManager { + public static final OptionKey INTEGRATED_SERVER_OWNER = OptionKey.of("integrated_server_owner", Boolean.class); + + public ForgeContextManager(LPForgePlugin plugin) { + super(plugin, ServerPlayer.class, ServerPlayer.class); + } + + @Override + public UUID getUniqueId(ServerPlayer player) { + return player.getUUID(); + } + + @Override + public QueryOptionsCache getCacheFor(ServerPlayer subject) { + if (subject == null) { + throw new NullPointerException("subject"); + } + + return UserCapabilityImpl.get(subject).getQueryOptionsCache(); + } + + @Override + public QueryOptions formQueryOptions(ServerPlayer subject, ImmutableContextSet contextSet) { + QueryOptions.Builder builder = this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder(); + if (subject.getServer() != null && subject.getServer().isSingleplayerOwner(subject.getGameProfile())) { + builder.option(INTEGRATED_SERVER_OWNER, true); + } + + return builder.context(contextSet).build(); + } + + @Override + public void invalidateCache(ServerPlayer subject) { + UserCapabilityImpl capability = UserCapabilityImpl.getNullable(subject); + if (capability != null) { + capability.getQueryOptionsCache().invalidate(); + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/context/ForgePlayerCalculator.java b/forge/src/main/java/me/lucko/luckperms/forge/context/ForgePlayerCalculator.java new file mode 100644 index 000000000..61c86cd29 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/context/ForgePlayerCalculator.java @@ -0,0 +1,149 @@ +/* + * 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.forge.context; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.forge.LPForgePlugin; + +import net.luckperms.api.context.Context; +import net.luckperms.api.context.ContextCalculator; +import net.luckperms.api.context.ContextConsumer; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.DefaultContextKeys; +import net.luckperms.api.context.ImmutableContextSet; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Set; + +public class ForgePlayerCalculator implements ContextCalculator { + /** + * GameType.NOT_SET(-1, "") was removed in 1.17 + */ + private static final int GAME_MODE_NOT_SET = -1; + + private final LPForgePlugin plugin; + + private final boolean gamemode; + private final boolean world; + private final boolean dimensionType; + + public ForgePlayerCalculator(LPForgePlugin plugin, Set disabled) { + this.plugin = plugin; + this.gamemode = !disabled.contains(DefaultContextKeys.GAMEMODE_KEY); + this.world = !disabled.contains(DefaultContextKeys.WORLD_KEY); + this.dimensionType = !disabled.contains(DefaultContextKeys.DIMENSION_TYPE_KEY); + } + + @Override + public void calculate(@NonNull ServerPlayer target, @NonNull ContextConsumer consumer) { + ServerLevel level = target.getLevel(); + if (this.dimensionType) { + consumer.accept(DefaultContextKeys.DIMENSION_TYPE_KEY, getContextKey(level.dimension().location())); + } + + ServerLevelData levelData = (ServerLevelData) level.getLevelData(); + if (this.world) { + this.plugin.getConfiguration().get(ConfigKeys.WORLD_REWRITES).rewriteAndSubmit(levelData.getLevelName(), consumer); + } + + GameType gameMode = target.gameMode.getGameModeForPlayer(); + if (this.gamemode && gameMode.getId() != GAME_MODE_NOT_SET) { + consumer.accept(DefaultContextKeys.GAMEMODE_KEY, gameMode.getName()); + } + } + + @Override + public @NonNull ContextSet estimatePotentialContexts() { + ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); + + if (this.gamemode) { + for (GameType gameType : GameType.values()) { + if (gameType.getId() == GAME_MODE_NOT_SET) { + continue; + } + + builder.add(DefaultContextKeys.GAMEMODE_KEY, gameType.getName()); + } + } + + MinecraftServer server = this.plugin.getBootstrap().getServer().orElse(null); + if (this.dimensionType && server != null) { + server.registryAccess().registry(Registry.DIMENSION_TYPE_REGISTRY).ifPresent(registry -> { + for (ResourceLocation resourceLocation : registry.keySet()) { + builder.add(DefaultContextKeys.DIMENSION_TYPE_KEY, getContextKey(resourceLocation)); + } + }); + } + + if (this.world && server != null) { + for (ServerLevel level : server.getAllLevels()) { + ServerLevelData levelData = (ServerLevelData) level.getLevelData(); + if (Context.isValidValue(levelData.getLevelName())) { + builder.add(DefaultContextKeys.WORLD_KEY, levelData.getLevelName()); + } + } + } + + return builder.build(); + } + + private static String getContextKey(ResourceLocation key) { + if (key.getNamespace().equals("minecraft")) { + return key.getPath(); + } + return key.toString(); + } + + @SubscribeEvent + public void onPlayerChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) { + if (!(this.world || this.dimensionType)) { + return; + } + + this.plugin.getContextManager().signalContextUpdate((ServerPlayer) event.getPlayer()); + } + + @SubscribeEvent + public void onPlayerChangeGameMode(PlayerEvent.PlayerChangeGameModeEvent event) { + if (!this.gamemode || event.getNewGameMode().getId() == GAME_MODE_NOT_SET) { + return; + } + + this.plugin.getContextManager().signalContextUpdate((ServerPlayer) event.getPlayer()); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeAutoOpListener.java b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeAutoOpListener.java new file mode 100644 index 000000000..fbb975002 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeAutoOpListener.java @@ -0,0 +1,98 @@ +/* + * 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.forge.listeners; + +import me.lucko.luckperms.common.api.implementation.ApiUser; +import me.lucko.luckperms.common.event.LuckPermsEventListener; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.forge.LPForgePlugin; + +import net.luckperms.api.event.EventBus; +import net.luckperms.api.event.context.ContextUpdateEvent; +import net.luckperms.api.event.user.UserDataRecalculateEvent; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Map; + +public class ForgeAutoOpListener implements LuckPermsEventListener { + private static final String NODE = "luckperms.autoop"; + + private final LPForgePlugin plugin; + + public ForgeAutoOpListener(LPForgePlugin plugin) { + this.plugin = plugin; + } + + @Override + public void bind(EventBus bus) { + bus.subscribe(ContextUpdateEvent.class, this::onContextUpdate); + bus.subscribe(UserDataRecalculateEvent.class, this::onUserDataRecalculate); + } + + private void onContextUpdate(ContextUpdateEvent event) { + event.getSubject(ServerPlayer.class).ifPresent(player -> refreshAutoOp(player, true)); + } + + private void onUserDataRecalculate(UserDataRecalculateEvent event) { + User user = ApiUser.cast(event.getUser()); + this.plugin.getBootstrap().getPlayer(user.getUniqueId()).ifPresent(player -> refreshAutoOp(player, false)); + } + + private void refreshAutoOp(ServerPlayer player, boolean callerIsSync) { + if (!callerIsSync && !this.plugin.getBootstrap().getServer().isPresent()) { + return; + } + + User user = this.plugin.getUserManager().getIfLoaded(player.getUUID()); + + boolean value; + if (user != null) { + QueryOptions queryOptions = this.plugin.getContextManager().getQueryOptions(player); + Map permData = user.getCachedData().getPermissionData(queryOptions).getPermissionMap(); + value = permData.getOrDefault(NODE, false); + } else { + value = false; + } + + if (callerIsSync) { + setOp(player, value); + } else { + this.plugin.getBootstrap().getScheduler().executeSync(() -> setOp(player, value)); + } + } + + private void setOp(ServerPlayer player, boolean value) { + this.plugin.getBootstrap().getServer().ifPresent(server -> { + if (value) { + server.getPlayerList().op(player.getGameProfile()); + } else { + server.getPlayerList().deop(player.getGameProfile()); + } + }); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeCommandListUpdater.java b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeCommandListUpdater.java new file mode 100644 index 000000000..9a534a209 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeCommandListUpdater.java @@ -0,0 +1,121 @@ +/* + * 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.forge.listeners; + +import com.github.benmanes.caffeine.cache.LoadingCache; + +import me.lucko.luckperms.common.api.implementation.ApiGroup; +import me.lucko.luckperms.common.cache.BufferedRequest; +import me.lucko.luckperms.common.event.LuckPermsEventListener; +import me.lucko.luckperms.common.util.CaffeineFactory; +import me.lucko.luckperms.forge.LPForgePlugin; + +import net.luckperms.api.event.EventBus; +import net.luckperms.api.event.context.ContextUpdateEvent; +import net.luckperms.api.event.group.GroupDataRecalculateEvent; +import net.luckperms.api.event.user.UserDataRecalculateEvent; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Calls {@link net.minecraft.server.players.PlayerList#sendPlayerPermissionLevel(ServerPlayer)} when a players permissions change. + */ +public class ForgeCommandListUpdater implements LuckPermsEventListener { + private final LPForgePlugin plugin; + private final LoadingCache sendingBuffers = CaffeineFactory.newBuilder() + .expireAfterAccess(10, TimeUnit.SECONDS) + .build(SendBuffer::new); + + public ForgeCommandListUpdater(LPForgePlugin plugin) { + this.plugin = plugin; + } + + @Override + public void bind(EventBus bus) { + bus.subscribe(UserDataRecalculateEvent.class, this::onUserDataRecalculate); + bus.subscribe(GroupDataRecalculateEvent.class, this::onGroupDataRecalculate); + bus.subscribe(ContextUpdateEvent.class, this::onContextUpdate); + } + + private void onUserDataRecalculate(UserDataRecalculateEvent e) { + requestUpdate(e.getUser().getUniqueId()); + } + + private void onGroupDataRecalculate(GroupDataRecalculateEvent e) { + plugin.getUserManager().getAll().values().forEach(user -> { + if (user.resolveInheritanceTree(user.getQueryOptions()).contains(ApiGroup.cast(e.getGroup()))) { + requestUpdate(user.getUniqueId()); + } + }); + } + + private void onContextUpdate(ContextUpdateEvent e) { + e.getSubject(ServerPlayer.class).ifPresent(p -> requestUpdate(p.getUUID())); + } + + private void requestUpdate(UUID uniqueId) { + if (!this.plugin.getBootstrap().isPlayerOnline(uniqueId)) { + return; + } + + // Buffer the request to send a commands update. + SendBuffer sendBuffer = this.sendingBuffers.get(uniqueId); + if (sendBuffer != null) { + sendBuffer.request(); + } + } + + // Called when the buffer times out. + private void sendUpdate(UUID uniqueId) { + this.plugin.getBootstrap().getScheduler().sync().execute(() -> { + this.plugin.getBootstrap().getPlayer(uniqueId).ifPresent(player -> { + MinecraftServer server = player.getServer(); + if (server != null) { + server.getPlayerList().sendPlayerPermissionLevel(player); + } + }); + }); + } + + private final class SendBuffer extends BufferedRequest { + private final UUID uniqueId; + + SendBuffer(UUID uniqueId) { + super(500, TimeUnit.MILLISECONDS, ForgeCommandListUpdater.this.plugin.getBootstrap().getScheduler()); + this.uniqueId = uniqueId; + } + + @Override + protected Void perform() { + sendUpdate(this.uniqueId); + return null; + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeConnectionListener.java b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeConnectionListener.java new file mode 100644 index 000000000..4bda329da --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeConnectionListener.java @@ -0,0 +1,154 @@ +/* + * 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.forge.listeners; + +import com.mojang.authlib.GameProfile; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; +import me.lucko.luckperms.forge.ForgeSenderFactory; +import me.lucko.luckperms.forge.LPForgePlugin; +import me.lucko.luckperms.forge.capabilities.UserCapabilityImpl; + +import net.kyori.adventure.text.Component; +import net.minecraft.Util; +import net.minecraft.network.Connection; +import net.minecraft.network.chat.ChatType; +import net.minecraft.network.protocol.game.ClientboundChatPacket; +import net.minecraft.network.protocol.login.ClientboundLoginDisconnectPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.entity.player.PlayerNegotiationEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class ForgeConnectionListener extends AbstractConnectionListener { + private final LPForgePlugin plugin; + + public ForgeConnectionListener(LPForgePlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + @SubscribeEvent + public void onPlayerNegotiation(PlayerNegotiationEvent event) { + String username = event.getProfile().getName(); + UUID uniqueId = event.getProfile().isComplete() ? event.getProfile().getId() : Player.createPlayerUUID(username); + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing pre-login (sync phase) for " + uniqueId + " - " + username); + } + + event.enqueueWork(CompletableFuture.runAsync(() -> { + onPlayerNegotiationAsync(event.getConnection(), uniqueId, username); + }, this.plugin.getBootstrap().getScheduler().async())); + } + + private void onPlayerNegotiationAsync(Connection connection, UUID uniqueId, String username) { + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing pre-login (async phase) for " + uniqueId + " - " + username); + } + + /* Actually process the login for the connection. + We do this here to delay the login until the data is ready. + If the login gets cancelled later on, then this will be cleaned up. + + This includes: + - loading uuid data + - loading permissions + - creating a user instance in the UserManager for this connection. + - setting up cached data. */ + try { + User user = loadUser(uniqueId, username); + recordConnection(uniqueId); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(uniqueId, username, user); + } catch (Exception ex) { + this.plugin.getLogger().severe("Exception occurred whilst loading data for " + uniqueId + " - " + username, ex); + + if (this.plugin.getConfiguration().get(ConfigKeys.CANCEL_FAILED_LOGINS)) { + Component component = TranslationManager.render(Message.LOADING_DATABASE_ERROR.build()); + connection.send(new ClientboundLoginDisconnectPacket(ForgeSenderFactory.toNativeText(component))); + connection.disconnect(ForgeSenderFactory.toNativeText(component)); + } else { + // Schedule the message to be sent on the next tick. + this.plugin.getBootstrap().getServer().orElseThrow(IllegalStateException::new).execute(() -> { + Component component = TranslationManager.render(Message.LOADING_STATE_ERROR.build()); + connection.send(new ClientboundChatPacket(ForgeSenderFactory.toNativeText(component), ChatType.SYSTEM, Util.NIL_UUID)); + }); + } + + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(uniqueId, username, null); + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public void onPlayerLoadFromFile(PlayerEvent.LoadFromFile event) { + ServerPlayer player = (ServerPlayer) event.getPlayer(); + GameProfile profile = player.getGameProfile(); + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing post-login for " + profile.getId() + " - " + profile.getName()); + } + + User user = this.plugin.getUserManager().getIfLoaded(profile.getId()); + + if (user == null) { + if (!getUniqueConnections().contains(profile.getId())) { + this.plugin.getLogger().warn("User " + profile.getId() + " - " + profile.getName() + + " doesn't have data pre-loaded, they have never been processed during pre-login in this session."); + } else { + this.plugin.getLogger().warn("User " + profile.getId() + " - " + profile.getName() + + " doesn't currently have data pre-loaded, but they have been processed before in this session."); + } + + Component component = TranslationManager.render(Message.LOADING_STATE_ERROR.build(), player.getLanguage()); + if (this.plugin.getConfiguration().get(ConfigKeys.CANCEL_FAILED_LOGINS)) { + player.connection.disconnect(ForgeSenderFactory.toNativeText(component)); + } else { + player.sendMessage(ForgeSenderFactory.toNativeText(component), Util.NIL_UUID); + } + } + + // initialise capability + UserCapabilityImpl userCapability = UserCapabilityImpl.get(player); + userCapability.initialise(user, player, this.plugin.getContextManager()); + this.plugin.getContextManager().signalContextUpdate(player); + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) { + ServerPlayer player = (ServerPlayer) event.getPlayer(); + handleDisconnect(player.getGameProfile().getId()); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgePlatformListener.java b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgePlatformListener.java new file mode 100644 index 000000000..22fe6385c --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgePlatformListener.java @@ -0,0 +1,94 @@ +/* + * 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.forge.listeners; + +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.forge.LPForgePlugin; +import me.lucko.luckperms.forge.util.BrigadierInjector; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.server.players.ServerOpList; +import net.minecraftforge.event.AddReloadListenerEvent; +import net.minecraftforge.event.CommandEvent; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +import java.io.IOException; +import java.util.Locale; + +public class ForgePlatformListener { + private final LPForgePlugin plugin; + + public ForgePlatformListener(LPForgePlugin plugin) { + this.plugin = plugin; + } + + @SubscribeEvent + public void onCommand(CommandEvent event) { + CommandContextBuilder context = event.getParseResults().getContext(); + + if (!this.plugin.getConfiguration().get(ConfigKeys.OPS_ENABLED)) { + for (ParsedCommandNode node : context.getNodes()) { + if (!(node.getNode() instanceof LiteralCommandNode)) { + continue; + } + + String name = node.getNode().getName().toLowerCase(Locale.ROOT); + if (name.equals("op") || name.equals("deop")) { + Message.OP_DISABLED.send(this.plugin.getSenderFactory().wrap(context.getSource())); + event.setCanceled(true); + return; + } + } + } + } + + @SubscribeEvent + public void onAddReloadListener(AddReloadListenerEvent event) { + Commands commands = event.getServerResources().getCommands(); + BrigadierInjector.inject(this.plugin, commands.getDispatcher()); + } + + @SubscribeEvent + public void onServerStarted(ServerStartedEvent event) { + if (!this.plugin.getConfiguration().get(ConfigKeys.OPS_ENABLED)) { + ServerOpList ops = event.getServer().getPlayerList().getOps(); + ops.getEntries().clear(); + try { + ops.save(); + } catch (IOException ex) { + this.plugin.getLogger().severe("Encountered an error while saving ops", ex); + } + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/messaging/ForgeMessagingFactory.java b/forge/src/main/java/me/lucko/luckperms/forge/messaging/ForgeMessagingFactory.java new file mode 100644 index 000000000..a0c07d566 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/messaging/ForgeMessagingFactory.java @@ -0,0 +1,72 @@ +/* + * 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.forge.messaging; + +import me.lucko.luckperms.common.messaging.InternalMessagingService; +import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; +import me.lucko.luckperms.common.messaging.MessagingFactory; +import me.lucko.luckperms.forge.LPForgePlugin; + +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.luckperms.api.messenger.MessengerProvider; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public class ForgeMessagingFactory extends MessagingFactory { + public ForgeMessagingFactory(LPForgePlugin plugin) { + super(plugin); + } + + @Override + protected InternalMessagingService getServiceFor(String messagingType) { + if (messagingType.equals("pluginmsg") || messagingType.equals("bungee") || messagingType.equals("velocity")) { + try { + return new LuckPermsMessagingService(getPlugin(), new PluginMessageMessengerProvider()); + } catch (Exception e) { + getPlugin().getLogger().severe("Exception occurred whilst enabling messaging", e); + } + } + + return super.getServiceFor(messagingType); + } + + private class PluginMessageMessengerProvider implements MessengerProvider { + + @Override + public @NonNull String getName() { + return "PluginMessage"; + } + + @Override + public @NonNull Messenger obtain(@NonNull IncomingMessageConsumer incomingMessageConsumer) { + PluginMessageMessenger messenger = new PluginMessageMessenger(getPlugin(), incomingMessageConsumer); + messenger.init(); + return messenger; + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/messaging/PluginMessageMessenger.java b/forge/src/main/java/me/lucko/luckperms/forge/messaging/PluginMessageMessenger.java new file mode 100644 index 000000000..f3cda3635 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/messaging/PluginMessageMessenger.java @@ -0,0 +1,101 @@ +/* + * 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.forge.messaging; + +import com.google.common.collect.Iterables; + +import me.lucko.luckperms.common.messaging.pluginmsg.AbstractPluginMessageMessenger; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; +import me.lucko.luckperms.forge.LPForgePlugin; + +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraftforge.network.NetworkRegistry; +import net.minecraftforge.network.event.EventNetworkChannel; + +import io.netty.buffer.Unpooled; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public class PluginMessageMessenger extends AbstractPluginMessageMessenger implements Messenger { + private static final ResourceLocation CHANNEL = new ResourceLocation(AbstractPluginMessageMessenger.CHANNEL); + + private final LPForgePlugin plugin; + private EventNetworkChannel channel; + + public PluginMessageMessenger(LPForgePlugin plugin, IncomingMessageConsumer consumer) { + super(consumer); + this.plugin = plugin; + } + + public void init() { + this.channel = NetworkRegistry.newEventChannel(CHANNEL, () -> "1", predicate -> true, predicate -> true); + this.channel.addListener(event -> { + byte[] buf = new byte[event.getPayload().readableBytes()]; + event.getPayload().readBytes(buf); + + handleIncomingMessage(buf); + event.getSource().get().setPacketHandled(true); + }); + } + + @Override + protected void sendOutgoingMessage(byte[] buf) { + AtomicReference taskRef = new AtomicReference<>(); + SchedulerTask task = this.plugin.getBootstrap().getScheduler().asyncRepeating(() -> { + ServerPlayer player = this.plugin.getBootstrap().getServer() + .map(MinecraftServer::getPlayerList) + .map(PlayerList::getPlayers) + .map(players -> Iterables.getFirst(players, null)) + .orElse(null); + + if (player == null) { + return; + } + + FriendlyByteBuf byteBuf = new FriendlyByteBuf(Unpooled.buffer()); + byteBuf.writeBytes(buf); + Packet packet = new ClientboundCustomPayloadPacket(CHANNEL, byteBuf); + + player.connection.send(packet); + + SchedulerTask t = taskRef.getAndSet(null); + if (t != null) { + t.cancel(); + } + }, 10, TimeUnit.SECONDS); + taskRef.set(task); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandler.java b/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandler.java new file mode 100644 index 000000000..86ed3e20d --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandler.java @@ -0,0 +1,155 @@ +/* + * 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.forge.service; + +import me.lucko.luckperms.common.cacheddata.type.MetaCache; +import me.lucko.luckperms.common.cacheddata.type.PermissionCache; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.forge.LPForgeBootstrap; +import me.lucko.luckperms.forge.LPForgePlugin; +import me.lucko.luckperms.forge.capabilities.UserCapabilityImpl; + +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.query.QueryMode; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.server.permission.handler.IPermissionHandler; +import net.minecraftforge.server.permission.nodes.PermissionDynamicContext; +import net.minecraftforge.server.permission.nodes.PermissionNode; +import net.minecraftforge.server.permission.nodes.PermissionTypes; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class ForgePermissionHandler implements IPermissionHandler { + public static final ResourceLocation IDENTIFIER = new ResourceLocation(LPForgeBootstrap.ID, "permission_handler"); + + private final LPForgePlugin plugin; + private final Set> permissionNodes; + + public ForgePermissionHandler(LPForgePlugin plugin, Collection> permissionNodes) { + this.plugin = plugin; + this.permissionNodes = Collections.unmodifiableSet(new HashSet<>(permissionNodes)); + + for (PermissionNode node : this.permissionNodes) { + this.plugin.getPermissionRegistry().insert(node.getNodeName()); + } + } + + @Override + public ResourceLocation getIdentifier() { + return IDENTIFIER; + } + + @Override + public Set> getRegisteredNodes() { + return this.permissionNodes; + } + + @Override + public T getPermission(ServerPlayer player, PermissionNode node, PermissionDynamicContext... context) { + UserCapabilityImpl capability = UserCapabilityImpl.getNullable(player); + + if (capability != null) { + User user = capability.getUser(); + QueryOptions queryOptions = capability.getQueryOptionsCache().getQueryOptions(); + + T value = getPermissionValue(user, queryOptions, node, context); + if (value != null) { + return value; + } + } + + return node.getDefaultResolver().resolve(player, player.getUUID(), context); + } + + @Override + public T getOfflinePermission(UUID player, PermissionNode node, PermissionDynamicContext... context) { + User user = this.plugin.getUserManager().getIfLoaded(player); + + if (user != null) { + QueryOptions queryOptions = user.getQueryOptions(); + T value = getPermissionValue(user, queryOptions, node, context); + if (value != null) { + return value; + } + } + + return node.getDefaultResolver().resolve(null, player, context); + } + + @SuppressWarnings("unchecked") + private static T getPermissionValue(User user, QueryOptions queryOptions, PermissionNode node, PermissionDynamicContext... context) { + queryOptions = appendContextToQueryOptions(queryOptions, context); + + if (node.getType() == PermissionTypes.BOOLEAN) { + PermissionCache cache = user.getCachedData().getPermissionData(queryOptions); + Tristate value = cache.checkPermission(node.getNodeName(), CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + return (T) (Boolean) value.asBoolean(); + } + + if (node.getType() == PermissionTypes.INTEGER) { + MetaCache cache = user.getCachedData().getMetaData(queryOptions); + Integer value = cache.getMetaValue(node.getNodeName(), Integer::parseInt).orElse(null); + if (value != null) { + return (T) value; + } + } + + if (node.getType() == PermissionTypes.STRING) { + MetaCache cache = user.getCachedData().getMetaData(queryOptions); + String value = cache.getMetaValue(node.getNodeName()); + if (value != null) { + return (T) value; + } + } + + return null; + } + + private static QueryOptions appendContextToQueryOptions(QueryOptions queryOptions, PermissionDynamicContext... context) { + if (context.length == 0 || queryOptions.mode() != QueryMode.CONTEXTUAL) { + return queryOptions; + } + + ImmutableContextSet.Builder contextBuilder = new ImmutableContextSetImpl.BuilderImpl() + .addAll(queryOptions.context()); + + for (PermissionDynamicContext dynamicContext : context) { + contextBuilder.add(dynamicContext.getDynamic().name(), dynamicContext.getSerializedValue()); + } + + return queryOptions.toBuilder().context(contextBuilder.build()).build(); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandlerListener.java b/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandlerListener.java new file mode 100644 index 000000000..315cb9e43 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandlerListener.java @@ -0,0 +1,65 @@ +/* + * 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.forge.service; + +import me.lucko.luckperms.common.command.access.CommandPermission; +import me.lucko.luckperms.forge.LPForgePlugin; + +import net.minecraftforge.common.ForgeConfig; +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.server.permission.events.PermissionGatherEvent; +import net.minecraftforge.server.permission.handler.DefaultPermissionHandler; +import net.minecraftforge.server.permission.nodes.PermissionNode; +import net.minecraftforge.server.permission.nodes.PermissionTypes; + +public class ForgePermissionHandlerListener { + private final LPForgePlugin plugin; + + public ForgePermissionHandlerListener(LPForgePlugin plugin) { + this.plugin = plugin; + } + + @SubscribeEvent + public void onPermissionGatherHandler(PermissionGatherEvent.Handler event) { + // Override the default permission handler with LuckPerms + ForgeConfigSpec.ConfigValue permissionHandler = ForgeConfig.SERVER.permissionHandler; + if (permissionHandler.get().equals(DefaultPermissionHandler.IDENTIFIER.toString())) { + permissionHandler.set(ForgePermissionHandler.IDENTIFIER.toString()); + } + + event.addPermissionHandler(ForgePermissionHandler.IDENTIFIER, permissions -> new ForgePermissionHandler(this.plugin, permissions)); + } + + @SubscribeEvent + public void onPermissionGatherNodes(PermissionGatherEvent.Nodes event) { + // register luckperms nodes + for (CommandPermission permission : CommandPermission.values()) { + event.addNodes(new PermissionNode<>("luckperms", permission.getNode(), PermissionTypes.BOOLEAN, (player, uuid, context) -> false)); + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/util/BrigadierInjector.java b/forge/src/main/java/me/lucko/luckperms/forge/util/BrigadierInjector.java new file mode 100644 index 000000000..9f27e7af7 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/util/BrigadierInjector.java @@ -0,0 +1,186 @@ +/* + * 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.forge.util; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import me.lucko.luckperms.common.graph.Graph; +import me.lucko.luckperms.common.graph.TraversalAlgorithm; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.forge.capabilities.UserCapability; +import me.lucko.luckperms.forge.capabilities.UserCapabilityImpl; + +import net.luckperms.api.util.Tristate; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.level.ServerPlayer; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Locale; +import java.util.function.Predicate; + +/** + * Utility for injecting permission requirements into a Brigadier command tree. + */ +public final class BrigadierInjector { + private BrigadierInjector() {} + + private static final Field REQUIREMENT_FIELD; + + static { + Field requirementField; + try { + requirementField = CommandNode.class.getDeclaredField("requirement"); + requirementField.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new ExceptionInInitializerError(e); + } + REQUIREMENT_FIELD = requirementField; + } + + /** + * Inject permission requirements into the commands in the given dispatcher. + * + * @param plugin the plugin + * @param dispatcher the command dispatcher + */ + public static void inject(LuckPermsPlugin plugin, CommandDispatcher dispatcher) { + Iterable tree = CommandNodeGraph.INSTANCE.traverse( + TraversalAlgorithm.DEPTH_FIRST_PRE_ORDER, + new CommandNodeWithParent(null, dispatcher.getRoot()) + ); + + for (CommandNodeWithParent node : tree) { + Predicate requirement = node.node.getRequirement(); + + // already injected - skip + if (requirement instanceof InjectedPermissionRequirement) { + continue; + } + + String permission = buildPermissionNode(node); + if (permission == null) { + continue; + } + + plugin.getPermissionRegistry().insert(permission); + + InjectedPermissionRequirement newRequirement = new InjectedPermissionRequirement(permission, requirement); + try { + REQUIREMENT_FIELD.set(node.node, newRequirement); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + private static String buildPermissionNode(CommandNodeWithParent node) { + StringBuilder builder = new StringBuilder(); + + while (node != null) { + if (node.node instanceof LiteralCommandNode) { + if (builder.length() != 0) { + builder.insert(0, '.'); + } + + String name = node.node.getName().toLowerCase(Locale.ROOT); + builder.insert(0, name); + } + + node = node.parent; + } + + if (builder.length() == 0) { + return null; + } + + builder.insert(0, "command."); + return builder.toString(); + } + + /** + * Injected {@link CommandNode#getRequirement() requirement} that checks for a permission, before + * delegating to the existing requirement. + */ + private static final class InjectedPermissionRequirement implements Predicate { + private final String permission; + private final Predicate delegate; + + private InjectedPermissionRequirement(String permission, Predicate delegate) { + this.permission = permission; + this.delegate = delegate; + } + + @Override + public boolean test(CommandSourceStack source) { + if (source.getEntity() instanceof ServerPlayer) { + ServerPlayer player = (ServerPlayer) source.getEntity(); + + UserCapability user = UserCapabilityImpl.get(player); + Tristate state = user.checkPermission(this.permission); + + if (state != Tristate.UNDEFINED) { + return state.asBoolean() && this.delegate.test(source.withPermission(4)); + } + } + + return this.delegate.test(source); + } + } + + /** + * A {@link Graph} to represent the brigadier command node tree. + */ + private enum CommandNodeGraph implements Graph { + INSTANCE; + + @Override + public Iterable successors(CommandNodeWithParent ctx) { + CommandNode node = ctx.node; + Collection successors = new ArrayList<>(); + + for (CommandNode child : node.getChildren()) { + successors.add(new CommandNodeWithParent(ctx, child)); + } + + return successors; + } + } + + private static final class CommandNodeWithParent { + private final CommandNodeWithParent parent; + private final CommandNode node; + + private CommandNodeWithParent(CommandNodeWithParent parent, CommandNode node) { + this.parent = parent; + this.node = node; + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/util/ForgeEventBusFacade.java b/forge/src/main/java/me/lucko/luckperms/forge/util/ForgeEventBusFacade.java new file mode 100644 index 000000000..3c6ea378c --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/util/ForgeEventBusFacade.java @@ -0,0 +1,265 @@ +/* + * 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.forge.util; + +import me.lucko.luckperms.common.loader.JarInJarClassLoader; + +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.eventbus.api.GenericEvent; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.eventbus.api.IGenericEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.event.IModBusEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; + +import java.lang.invoke.CallSite; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * A utility for registering Forge listeners for methods in a jar-in-jar. + * + *

This differs from {@link IEventBus#register} as reflection is used for invoking the registered listeners + * instead of ASM, which is incompatible with {@link JarInJarClassLoader}

+ */ +public class ForgeEventBusFacade { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + private final List listeners = new ArrayList<>(); + + /** + * Register listeners for all methods annotated with {@link SubscribeEvent} on the target object. + */ + public void register(Object target) { + for (Method method : target.getClass().getMethods()) { + // Ignore static methods, Support for these could be added, but they are not used in LuckPerms + if (Modifier.isStatic(method.getModifiers())) { + continue; + } + + // Methods require a SubscribeEvent annotation in order to be registered + SubscribeEvent subscribeEvent = method.getAnnotation(SubscribeEvent.class); + if (subscribeEvent == null) { + continue; + } + + EventType type = determineListenerType(method); + Consumer invoker = createInvokerFunction(method, target, type); + + // Determine the 'IEventBus' that this eventType should be registered to. + IEventBus eventBus; + if (IModBusEvent.class.isAssignableFrom(type.eventType)) { + eventBus = FMLJavaModLoadingContext.get().getModEventBus(); + } else { + eventBus = MinecraftForge.EVENT_BUS; + } + + if (IGenericEvent.class.isAssignableFrom(type.eventType)) { + addGenericListener(eventBus, type.genericType, subscribeEvent, type.eventType, invoker); + } else { + addListener(eventBus, subscribeEvent, type.eventType, invoker); + } + + this.listeners.add(new ListenerRegistration(invoker, eventBus, target)); + } + } + + /** + * Unregister previously registered listeners on the target object. + * + * @param target the target listener + */ + public void unregister(Object target) { + this.listeners.removeIf(listener -> { + if (listener.target == target) { + listener.close(); + return true; + } else { + return false; + } + }); + } + + /** + * Unregister all listeners created through this interface. + */ + public void unregisterAll() { + for (ListenerRegistration listener : this.listeners) { + listener.close(); + } + this.listeners.clear(); + } + + /** + * A listener registration. + */ + private static final class ListenerRegistration implements AutoCloseable { + /** The lambda invoker function */ + private final Consumer invoker; + /** The event bus that the invoker was registered to */ + private final IEventBus eventBus; + /** The target listener class */ + private final Object target; + + private ListenerRegistration(Consumer invoker, IEventBus eventBus, Object target) { + this.invoker = invoker; + this.eventBus = eventBus; + this.target = target; + } + + @Override + public void close() { + this.eventBus.unregister(this.invoker); + } + } + + private static Consumer createInvokerFunction(Method method, Object target, EventType type) { + // Use the 'LambdaMetafactory' to generate a consumer which can be passed directly to an 'IEventBus' + // when registering a listener, this reduces the overhead involved when reflectively invoking methods. + try { + MethodHandle methodHandle = LOOKUP.unreflect(method); + CallSite callSite = LambdaMetafactory.metafactory( + LOOKUP, + "accept", + MethodType.methodType(Consumer.class, target.getClass()), + MethodType.methodType(void.class, Object.class), + methodHandle, + MethodType.methodType(void.class, type.eventType) + ); + + return (Consumer) callSite.getTarget().bindTo(target).invokeExact(); + } catch (Throwable t) { + throw new RuntimeException("Error whilst registering " + method, t); + } + } + + public static EventType determineListenerType(Method method) { + // Get the parameter types, this includes generic information which is required for GenericEvent + Type[] parameterTypes = method.getGenericParameterTypes(); + if (parameterTypes.length != 1) { + throw new IllegalArgumentException("" + + "Method " + method + " has @SubscribeEvent annotation. " + + "It has " + parameterTypes.length + " arguments, " + + "but event handler methods require a single argument only." + ); + } + + Type parameterType = parameterTypes[0]; + Class eventType; + Class genericType; + + if (parameterType instanceof Class) { // Non-generic event + eventType = (Class) parameterType; + genericType = null; + } else if (parameterType instanceof ParameterizedType) { // Generic event + ParameterizedType parameterizedType = (ParameterizedType) parameterType; + + // Get the event class + Type rawType = parameterizedType.getRawType(); + if (rawType instanceof Class) { + eventType = (Class) rawType; + } else { + throw new UnsupportedOperationException("Raw Type " + rawType.getClass() + " is not supported"); + } + + // Find the type of 'T' in 'GenericEvent' + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + if (typeArguments.length != 1) { + throw new IllegalArgumentException("" + + "Method " + method + " has @SubscribeEvent annotation. " + + "It has a " + eventType + " argument, " + + "but generic events require a single type argument only." + ); + } + + // Get the generic class + Type typeArgument = typeArguments[0]; + if (typeArgument instanceof Class) { + genericType = (Class) typeArgument; + } else { + throw new UnsupportedOperationException("Type Argument " + typeArgument.getClass() + " is not supported"); + } + } else { + throw new UnsupportedOperationException("Parameter Type " + parameterType.getClass() + " is not supported"); + } + + // Ensure 'genericType' is set if 'eventType' is a generic event + if (GenericEvent.class.isAssignableFrom(eventType) && genericType == null) { + throw new IllegalArgumentException("" + + "Method " + method + " has @SubscribeEvent annotation, " + + "but the generic argument type cannot be determined for " + + "for the GenericEvent subtype: " + eventType + ); + } + + // Ensure 'eventType' is a subclass of event + if (!Event.class.isAssignableFrom(eventType)) { + throw new IllegalArgumentException("" + + "Method " + method + " has @SubscribeEvent annotation, " + + "but takes an argument that is not an Event subtype: " + eventType + ); + } + + return new EventType(eventType, genericType); + } + + private static final class EventType { + private final Class eventType; + private final Class genericType; + + private EventType(Class eventType, Class genericType) { + this.eventType = eventType; + this.genericType = genericType; + } + } + + /** + * Handles casting generics for {@link IEventBus#addGenericListener}. + */ + @SuppressWarnings("unchecked") + private static , F> void addGenericListener(IEventBus eventBus, Class genericClassFilter, SubscribeEvent annotation, Class eventType, Consumer consumer) { + eventBus.addGenericListener((Class) genericClassFilter, annotation.priority(), annotation.receiveCanceled(), (Class) eventType, (Consumer) consumer); + } + + /** + * Handles casting generics for {@link IEventBus#addListener}. + */ + @SuppressWarnings("unchecked") + private static void addListener(IEventBus eventBus, SubscribeEvent annotation, Class eventType, Consumer consumer) { + eventBus.addListener(annotation.priority(), annotation.receiveCanceled(), (Class) eventType, (Consumer) consumer); + } + +} diff --git a/forge/src/main/resources/luckperms.conf b/forge/src/main/resources/luckperms.conf new file mode 100644 index 000000000..0093326c8 --- /dev/null +++ b/forge/src/main/resources/luckperms.conf @@ -0,0 +1,640 @@ +#################################################################################################### +# +----------------------------------------------------------------------------------------------+ # +# | __ __ ___ __ __ | # +# | | | | / ` |__/ |__) |__ |__) |\/| /__` | # +# | |___ \__/ \__, | \ | |___ | \ | | .__/ | # +# | | # +# | https://luckperms.net | # +# | | # +# | WIKI: https://luckperms.net/wiki | # +# | DISCORD: https://discord.gg/luckperms | # +# | BUG REPORTS: https://github.com/lucko/LuckPerms/issues | # +# | | # +# | Each option in this file is documented and explained here: | # +# | ==> https://luckperms.net/wiki/Configuration | # +# | | # +# | New options are not added to this file automatically. Default values are used if an | # +# | option cannot be found. The latest config versions can be obtained at the link above. | # +# +----------------------------------------------------------------------------------------------+ # +#################################################################################################### + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | ESSENTIAL SETTINGS | # +# | | # +# | Important settings that control how LuckPerms functions. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The name of the server, used for server specific permissions. +# +# - When set to "global" this setting is effectively ignored. +# - In all other cases, the value here is added to all players in a "server" context. +# - See: https://luckperms.net/wiki/Context +server = "global" + +# If the servers own UUID cache/lookup facility should be used when there is no record for a player +# already in LuckPerms. +# +# - When this is set to 'false', commands using a player's username will not work unless the player +# has joined since LuckPerms was first installed. +# - To get around this, you can use a player's uuid directly in the command, or enable this option. +# - When this is set to 'true', the server facility is used. This may use a number of methods, +# including checking the servers local cache, or making a request to the Mojang API. +use-server-uuid-cache = false + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | STORAGE SETTINGS | # +# | | # +# | Controls which storage method LuckPerms will use to store data. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# How the plugin should store data +# +# - The various options are explained in more detail on the wiki: +# https://luckperms.net/wiki/Storage-types +# +# - Possible options: +# +# | Remote databases - require connection information to be configured below +# |=> MySQL +# |=> MariaDB (preferred over MySQL) +# |=> PostgreSQL +# |=> MongoDB +# +# | Flatfile/local database - don't require any extra configuration +# |=> H2 (preferred over SQLite) +# |=> SQLite +# +# | Readable & editable text files - don't require any extra configuration +# |=> YAML (.yml files) +# |=> JSON (.json files) +# |=> HOCON (.conf files) +# |=> TOML (.toml files) +# | +# | By default, user, group and track data is separated into different files. Data can be combined +# | and all stored in the same file by switching to a combined storage variant. +# | Just add '-combined' to the end of the storage-method, e.g. 'yaml-combined' +# +# - A H2 database is the default option. +# - If you want to edit data manually in "traditional" storage files, we suggest using YAML. +storage-method = "h2" + +# The following block defines the settings for remote database storage methods. +# +# - You don't need to touch any of the settings here if you're using a local storage method! +# - The connection detail options are shared between all remote storage types. +data { + + # Define the address and port for the database. + # - The standard DB engine port is used by default + # (MySQL = 3306, PostgreSQL = 5432, MongoDB = 27017) + # - Specify as "host:port" if differs + address = "localhost" + + # The name of the database to store LuckPerms data in. + # - This must be created already. Don't worry about this setting if you're using MongoDB. + database = "minecraft" + + # Credentials for the database. + username = "root" + password = "" + + # These settings apply to the MySQL connection pool. + # - The default values will be suitable for the majority of users. + # - Do not change these settings unless you know what you're doing! + pool-settings { + + # Sets the maximum size of the MySQL connection pool. + # - Basically this value will determine the maximum number of actual + # connections to the database backend. + # - More information about determining the size of connection pools can be found here: + # https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing + maximum-pool-size = 10 + + # Sets the minimum number of idle connections that the pool will try to maintain. + # - For maximum performance and responsiveness to spike demands, it is recommended to not set + # this value and instead allow the pool to act as a fixed size connection pool. + # (set this value to the same as 'maximum-pool-size') + minimum-idle = 10 + + # This setting controls the maximum lifetime of a connection in the pool in milliseconds. + # - The value should be at least 30 seconds less than any database or infrastructure imposed + # connection time limit. + maximum-lifetime = 1800000 # 30 minutes + + # This setting controls how frequently the pool will 'ping' a connection in order to prevent it + # from being timed out by the database or network infrastructure, measured in milliseconds. + # - The value should be less than maximum-lifetime and greater than 30000 (30 seconds). + # - Setting the value to zero will disable the keepalive functionality. + keepalive-time = 0 + + # This setting controls the maximum number of milliseconds that the plugin will wait for a + # connection from the pool, before timing out. + connection-timeout = 5000 # 5 seconds + + # This setting allows you to define extra properties for connections. + # + # By default, the following options are set to enable utf8 encoding. (you may need to remove + # these if you are using PostgreSQL) + # useUnicode = true + # characterEncoding = "utf8" + # + # You can also use this section to disable SSL connections, by uncommenting the 'useSSL' and + # 'verifyServerCertificate' options below. + properties { + useUnicode = true + characterEncoding = "utf8" + #useSSL: false + #verifyServerCertificate: false + } + } + + # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). + # - Change this if you want to use different tables for different servers. + table-prefix = "luckperms_" + + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. + mongodb-collection-prefix = "" + + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ + mongodb-connection-uri = "" +} + +# Define settings for a "split" storage setup. +# +# - This allows you to define a storage method for each type of data. +# - The connection options above still have to be correct for each type here. +split-storage { + # Don't touch this if you don't want to use split storage! + enabled = false + methods { + # These options don't need to be modified if split storage isn't enabled. + user = "h2" + group = "h2" + track = "h2" + uuid = "h2" + log = "h2" + } +} + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | UPDATE PROPAGATION & MESSAGING SERVICE | # +# | | # +# | Controls the ways in which LuckPerms will sync data & notify other servers of changes. | # +# | These options are documented on greater detail on the wiki under "Instant Updates". | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# This option controls how frequently LuckPerms will perform a sync task. +# +# - A sync task will refresh all data from the storage, and ensure that the most up-to-date data is +# being used by the plugin. +# - This is disabled by default, as most users will not need it. However, if you're using a remote +# storage type without a messaging service setup, you may wish to set this to something like 3. +# - Set to -1 to disable the task completely. +sync-minutes = -1 + +# If the file watcher should be enabled. +# +# - When using a file-based storage type, LuckPerms can monitor the data files for changes, and +# automatically update when changes are detected. +# - If you don't want this feature to be active, set this option to false. +watch-files = true + +# Define which messaging service should be used by the plugin. +# +# - If enabled and configured, LuckPerms will use the messaging service to inform other connected +# servers of changes. +# - Use the command "/lp networksync" to manually push changes. +# - Data is NOT stored using this service. It is only used as a messaging platform. +# +# - If you decide to enable this feature, you should set "sync-minutes" to -1, as there is no need +# for LuckPerms to poll the database for changes. +# +# - Possible options: +# => sql Uses the SQL database to form a queue system for communication. Will only work when +# 'storage-method' is set to MySQL or MariaDB. This is chosen by default if the +# option is set to 'auto' and SQL storage is in use. Set to 'notsql' to disable this. +# => pluginmsg Uses the plugin messaging channels to communicate with the proxy. +# LuckPerms must be installed on your proxy & all connected servers backend servers. +# Won't work if you have more than one proxy. +# => redis Uses Redis pub-sub to push changes. Your server connection info must be configured +# below. +# => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. +# => auto Attempts to automatically setup a messaging service using redis or sql. +messaging-service = "auto" + +# If LuckPerms should automatically push updates after a change has been made with a command. +auto-push-updates = true + +# If LuckPerms should push logging entries to connected servers via the messaging service. +push-log-entries = true + +# If LuckPerms should broadcast received logging entries to players on this platform. +# +# - If you have LuckPerms installed on your backend servers as well as a BungeeCord proxy, you +# should set this option to false on either your backends or your proxies, to avoid players being +# messaged twice about log entries. +broadcast-received-log-entries = true + +# Settings for Redis. +# Port 6379 is used by default; set address to "host:port" if differs +redis { + enabled = false + address = "localhost" + username = "" + password = "" +} + +# Settings for RabbitMQ. +# Port 5672 is used by default; set address to "host:port" if differs +rabbitmq { + enabled = false + address = "localhost" + vhost = "/" + username = "guest" + password = "guest" +} + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | CUSTOMIZATION SETTINGS | # +# | | # +# | Settings that allow admins to customize the way LuckPerms operates. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# Controls how temporary permissions/parents/meta should be accumulated. +# +# - The default behaviour is "deny". +# - This behaviour can also be specified when the command is executed. See the command usage +# documentation for more info. +# +# - Possible options: +# => accumulate durations will be added to the existing expiry time +# => replace durations will be replaced if the new duration is later than the current +# expiration +# => deny the command will just fail if you try to add another node with the same expiry +temporary-add-behaviour = "deny" + +# Controls how LuckPerms will determine a users "primary" group. +# +# - The meaning and influence of "primary groups" are explained in detail on the wiki. +# - The preferred approach is to let LuckPerms automatically determine a users primary group +# based on the relative weight of their parent groups. +# +# - Possible options: +# => stored use the value stored against the users record in the file/database +# => parents-by-weight just use the users most highly weighted parent +# => all-parents-by-weight same as above, but calculates based upon all parents inherited from +# both directly and indirectly +primary-group-calculation = "parents-by-weight" + +# If the plugin should check for "extra" permissions with users run LP commands. +# +# - These extra permissions allow finer control over what users can do with each command, and who +# they have access to edit. +# - The nature of the checks are documented on the wiki under "Argument based command permissions". +# - Argument based permissions are *not* static, unlike the 'base' permissions, and will depend upon +# the arguments given within the command. +argument-based-command-permissions = false + +# If the plugin should check whether senders are a member of a given group before they're able to +# edit the groups data or add/remove other users to/from it. +# Note: these limitations do not apply to the web editor! +require-sender-group-membership-to-modify = false + +# If the plugin should send log notifications to users whenever permissions are modified. +# +# - Notifications are only sent to those with the appropriate permission to receive them +# - They can also be temporarily enabled/disabled on a per-user basis using +# '/lp log notify ' +log-notify = true + +# Defines a list of log entries which should not be sent as notifications to users. +# +# - Each entry in the list is a RegEx expression which is matched against the log entry description. +log-notify-filtered-descriptions = [ +# "parent add example" +] + +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations = true + +# Defines the options for prefix and suffix stacking. +# +# - The feature allows you to display multiple prefixes or suffixes alongside a players username in +# chat. +# - It is explained and documented in more detail on the wiki under "Prefix & Suffix Stacking". +# +# - The options are divided into separate sections for prefixes and suffixes. +# - The 'duplicates' setting refers to how duplicate elements are handled. Can be 'retain-all', +# 'first-only' or 'last-only'. +# - The value of 'start-spacer' is included at the start of the resultant prefix/suffix. +# - The value of 'end-spacer' is included at the end of the resultant prefix/suffix. +# - The value of 'middle-spacer' is included between each element in the resultant prefix/suffix. +# +# - Possible format options: +# => highest Selects the value with the highest weight, from all values +# held by or inherited by the player. +# +# => lowest Same as above, except takes the one with the lowest weight. +# +# => highest_own Selects the value with the highest weight, but will not +# accept any inherited values. +# +# => lowest_own Same as above, except takes the value with the lowest weight. +# +# => highest_inherited Selects the value with the highest weight, but will only +# accept inherited values. +# +# => lowest_inherited Same as above, except takes the value with the lowest weight. +# +# => highest_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group on the given track. +# +# => lowest_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group not on the given track. +# +# => lowest_not_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_from_group_ Selects the value with the highest weight, but only if the +# value was inherited from the given group. +# +# => lowest_from_group_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_from_group_ Selects the value with the highest weight, but only if the +# value was not inherited from the given group. +# +# => lowest_not_from_group_ Same as above, except takes the value with the lowest weight. +meta-formatting { + prefix { + format = [ + "highest" + ] + duplicates = "first-only" + start-spacer = "" + middle-spacer = " " + end-spacer = "" + } + suffix { + format = [ + "highest" + ] + duplicates = "first-only" + start-spacer = "" + middle-spacer = " " + end-spacer = "" + } +} + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | PERMISSION CALCULATION AND INHERITANCE | # +# | | # +# | Modify the way permission checks, meta lookups and inheritance resolutions are handled. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The algorithm LuckPerms should use when traversing the "inheritance tree". +# +# - Possible options: +# => breadth-first See: https://en.wikipedia.org/wiki/Breadth-first_search +# => depth-first-pre-order See: https://en.wikipedia.org/wiki/Depth-first_search +# => depth-first-post-order See: https://en.wikipedia.org/wiki/Depth-first_search +inheritance-traversal-algorithm = "depth-first-pre-order" + +# If a final sort according to "inheritance rules" should be performed after the traversal algorithm +# has resolved the inheritance tree. +# +# "Inheritance rules" refers to things such as group weightings, primary group status, and the +# natural contextual ordering of the group nodes. +# +# Setting this to 'true' will allow for the inheritance rules to take priority over the structure of +# the inheritance tree. +# +# Effectively when this setting is 'true': the tree is flattened, and rules applied afterwards, +# and when this setting is 'false':, the rules are just applied during each step of the traversal. +post-traversal-inheritance-sort = false + +# Defines the mode used to determine whether a set of contexts are satisfied. +# +# - Possible options: +# => at-least-one-value-per-key Set A will be satisfied by another set B, if at least one of the +# key-value entries per key in A are also in B. +# => all-values-per-key Set A will be satisfied by another set B, if all key-value +# entries in A are also in B. +context-satisfy-mode = "at-least-one-value-per-key" + +# LuckPerms has a number of built-in contexts. These can be disabled by adding the context key to +# the list below. +disabled-contexts = [ +# "world" +] + +# +----------------------------------------------------------------------------------------------+ # +# | Permission resolution settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If users on this server should have their global permissions applied. +# When set to false, only server specific permissions will apply for users on this server +include-global = true + +# If users on this server should have their global world permissions applied. +# When set to false, only world specific permissions will apply for users on this server +include-global-world = true + +# If users on this server should have global (non-server specific) groups applied +apply-global-groups = true + +# If users on this server should have global (non-world specific) groups applied +apply-global-world-groups = true + +# +----------------------------------------------------------------------------------------------+ # +# | Meta lookup settings | # +# +----------------------------------------------------------------------------------------------+ # + +# Defines how meta values should be selected. +# +# - Possible options: +# => inheritance Selects the meta value that was inherited first +# => highest-number Selects the highest numerical meta value +# => lowest-number Selects the lowest numerical meta value +meta-value-selection-default = "inheritance" + +# Defines how meta values should be selected per key. +meta-value-selection { + #max-homes = "highest-number" +} + +# +----------------------------------------------------------------------------------------------+ # +# | Inheritance settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If the plugin should apply wildcard permissions. +# +# - If set to true, LuckPerms will detect wildcard permissions, and resolve & apply all registered +# permissions matching the wildcard. +apply-wildcards = true + +# If LuckPerms should resolve and apply permissions according to the Sponge style implicit wildcard +# inheritance system. +# +# - That being: If a user has been granted "example", then the player should have also be +# automatically granted "example.function", "example.another", "example.deeper.nesting", +# and so on. +apply-sponge-implicit-wildcards = false + +# If the plugin should parse regex permissions. +# +# - If set to true, LuckPerms will detect regex permissions, marked with "r=" at the start of the +# node, and resolve & apply all registered permissions matching the regex. +apply-regex = true + +# If the plugin should complete and apply shorthand permissions. +# +# - If set to true, LuckPerms will detect and expand shorthand node patterns. +apply-shorthand = true + +# If the owner of an integrated server should bypass permission checks. +# +# - This setting only applies when LuckPerms is active on a single-player world. +# - The owner of an integrated server is the player whose client instance is running the server. +integrated-server-owner-bypasses-checks = true + +# +----------------------------------------------------------------------------------------------+ # +# | Extra settings | # +# +----------------------------------------------------------------------------------------------+ # + +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators = [] + +# Allows you to set "aliases" for the worlds sent forward for context calculation. +# +# - These aliases are provided in addition to the real world name. Applied recursively. +# - Remove the comment characters for the default aliases to apply. +world-rewrite { + #world_nether = "world" + #world_the_end = "world" +} + +# Define special group weights for this server. +# +# - Group weights can also be applied directly to group data, using the setweight command. +# - This section allows weights to be set on a per-server basis. +group-weight { + #admin = 10 +} + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | FINE TUNING OPTIONS | # +# | | # +# | A number of more niche settings for tweaking and changing behaviour. The section also | # +# | contains toggles for some more specialised features. It is only necessary to make changes to | # +# | these options if you want to fine-tune LuckPerms behaviour. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# +----------------------------------------------------------------------------------------------+ # +# | Server Operator (OP) settings | # +# +----------------------------------------------------------------------------------------------+ # + +# Controls whether server operators should exist at all. +# +# - When set to 'false', all players will be de-opped, and the /op and /deop commands will be +# disabled. Note that vanilla features like the spawn-protection require an operator on the +# server to work. +enable-ops = true + +# Enables or disables a special permission based system in LuckPerms for controlling OP status. +# +# - If set to true, any user with the permission "luckperms.autoop" will automatically be granted +# server operator status. This permission can be inherited, or set on specific servers/worlds, +# temporarily, etc. +# - Additionally, setting this to true will force the "enable-ops" option above to false. All users +# will be de-opped unless they have the permission node, and the op/deop commands will be +# disabled. +# - It is recommended that you use this option instead of assigning a single '*' permission. +# - However, on Forge this setting can be used as a "pseudo" root wildcard, as many mods support +# the operator system over permissions. +auto-op = false + +# +----------------------------------------------------------------------------------------------+ # +# | Miscellaneous (and rarely used) settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If LuckPerms should produce extra logging output when it handles logins. +# +# - Useful if you're having issues with UUID forwarding or data not being loaded. +debug-logins = false + +# If LuckPerms should allow usernames with non alphanumeric characters. +# +# - Note that due to the design of the storage implementation, usernames must still be 16 characters +# or less. +allow-invalid-usernames = false + +# If LuckPerms should not require users to confirm bulkupdate operations. +# +# - When set to true, operations will be executed immediately. +# - This is not recommended, as bulkupdate has the potential to irreversibly delete large amounts of +# data, and is not designed to be executed automatically. +# - If automation is needed, users should prefer using the LuckPerms API. +skip-bulkupdate-confirmation = false + +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate = false + +# If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. +# +# - When this happens, the plugin will set their primary group back to default. +prevent-primary-group-removal = false + +# If LuckPerms should update the list of commands sent to the client when permissions are changed. +update-client-command-list = true + +# If LuckPerms should attempt to resolve Vanilla command target selectors for LP commands. +# See here for more info: https://minecraft.gamepedia.com/Commands#Target_selectors +resolve-command-selectors = false diff --git a/gradle.properties b/gradle.properties index f12c912d6..2668b73c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,4 @@ # Fabric requires some more ram. -org.gradle.jvmargs=-Xmx1G \ No newline at end of file +org.gradle.jvmargs=-Xmx1G +# ForgeGradle is special. +org.gradle.daemon=false \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 17a0c9c99..06b95aad0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,6 +21,9 @@ include ( 'bungee', 'bungee:loader', 'fabric', + 'forge', + 'forge:loader', + 'forge:forge-api', 'nukkit', 'nukkit:loader', 'sponge',