From 21abb9479640582e89db099f482b0cf538272893 Mon Sep 17 00:00:00 2001 From: i509VCB Date: Mon, 4 Jan 2021 09:23:54 -0600 Subject: [PATCH] Fabric (#2029) --- CONTRIBUTING.md | 2 +- .../net/luckperms/api/platform/Platform.java | 3 +- common/build.gradle | 5 +- .../luckperms/common/config/ConfigKeys.java | 5 + .../plugin/bootstrap/LuckPermsBootstrap.java | 1 + .../plugin/logging/Log4jPluginLogger.java | 61 ++ fabric/build.gradle | 94 +++ .../fabric/FabricCalculatorFactory.java | 76 +++ .../luckperms/fabric/FabricClassLoader.java | 49 ++ .../fabric/FabricCommandExecutor.java | 118 ++++ .../luckperms/fabric/FabricConfigAdapter.java | 46 ++ .../luckperms/fabric/FabricEventBus.java | 47 ++ .../fabric/FabricSchedulerAdapter.java | 43 ++ .../luckperms/fabric/FabricSenderFactory.java | 110 ++++ .../luckperms/fabric/LPFabricBootstrap.java | 280 +++++++++ .../luckperms/fabric/LPFabricPlugin.java | 248 ++++++++ .../calculator/ServerOwnerProcessor.java | 44 ++ .../fabric/context/FabricContextManager.java | 81 +++ .../context/FabricPlayerCalculator.java | 119 ++++ .../event/PlayerChangeWorldCallback.java | 42 ++ .../fabric/event/RespawnPlayerCallback.java | 42 ++ .../listeners/FabricConnectionListener.java | 133 +++++ .../listeners/PermissionCheckListener.java | 89 +++ .../ClientSettingsC2SPacketAccessor.java | 39 ++ .../fabric/mixin/PlayerManagerMixin.java | 49 ++ .../ServerLoginNetworkHandlerAccessor.java | 45 ++ .../fabric/mixin/ServerPlayerEntityMixin.java | 142 +++++ .../luckperms/fabric/model/MixinUser.java | 67 +++ .../main/resources/assets/luckperms/icon.png | Bin 0 -> 25938 bytes fabric/src/main/resources/fabric.mod.json | 43 ++ fabric/src/main/resources/luckperms.conf | 556 ++++++++++++++++++ .../src/main/resources/mixins.luckperms.json | 16 + gradle.properties | 2 + settings.gradle | 14 +- .../luckperms/sponge/LPSpongePlugin.java | 3 +- 35 files changed, 2709 insertions(+), 5 deletions(-) create mode 100644 common/src/main/java/me/lucko/luckperms/common/plugin/logging/Log4jPluginLogger.java create mode 100644 fabric/build.gradle create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/FabricClassLoader.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/FabricConfigAdapter.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/FabricEventBus.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/FabricSchedulerAdapter.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricBootstrap.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricPlugin.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/calculator/ServerOwnerProcessor.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricContextManager.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricPlayerCalculator.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/event/PlayerChangeWorldCallback.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/event/RespawnPlayerCallback.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/listeners/PermissionCheckListener.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ClientSettingsC2SPacketAccessor.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/mixin/PlayerManagerMixin.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerLoginNetworkHandlerAccessor.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java create mode 100644 fabric/src/main/resources/assets/luckperms/icon.png create mode 100644 fabric/src/main/resources/fabric.mod.json create mode 100644 fabric/src/main/resources/luckperms.conf create mode 100644 fabric/src/main/resources/mixins.luckperms.json create mode 100644 gradle.properties diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e0dfad55..30037813a 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** - Each use the common module to implement plugins on the respective server platforms. +* **Bukkit, BungeeCord, Sponge, Nukkit, Velocity & Fabric** - Each use the common module to implement plugins on the respective server platforms. 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 271a6e6ca..316ee956f 100644 --- a/api/src/main/java/net/luckperms/api/platform/Platform.java +++ b/api/src/main/java/net/luckperms/api/platform/Platform.java @@ -73,7 +73,8 @@ public interface Platform { BUNGEECORD("BungeeCord"), SPONGE("Sponge"), NUKKIT("Nukkit"), - VELOCITY("Velocity"); + VELOCITY("Velocity"), + FABRIC("Fabric"); private final String friendlyName; diff --git a/common/build.gradle b/common/build.gradle index 68fc40308..3f66ed81f 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -9,7 +9,10 @@ dependencies { testCompile 'org.junit.jupiter:junit-jupiter-engine:5.7.0' compile project(':api') - compile 'org.checkerframework:checker-qual:2.5.5' + compile 'org.checkerframework:checker-qual:3.8.0' + + compileOnly 'org.slf4j:slf4j-api:1.7.30' + compileOnly 'org.apache.logging.log4j:log4j-api:2.14.0' // This is a special re-packaged version of 'net.kyori:adventure-*' for our own use. // Don't use it in other projects, you want the net.kyori version instead. 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 66110fbd0..b9ffc5caf 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 @@ -483,6 +483,11 @@ 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. + */ + public static final ConfigKey FABRIC_INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS = booleanKey("integrated-server-owner-bypasses-checks", true); + /** * The world rewrites map */ 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 4d18a1528..77dc2d69a 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 @@ -138,6 +138,7 @@ public interface LuckPermsBootstrap { *

Bukkit: /root/plugins/LuckPerms

*

Bungee: /root/plugins/LuckPerms

*

Sponge: /root/luckperms/

+ *

Fabric: /root/mods/LuckPerms

* * @return the platforms data folder */ diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/logging/Log4jPluginLogger.java b/common/src/main/java/me/lucko/luckperms/common/plugin/logging/Log4jPluginLogger.java new file mode 100644 index 000000000..ff3338a28 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/logging/Log4jPluginLogger.java @@ -0,0 +1,61 @@ +/* + * 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.plugin.logging; + +import org.apache.logging.log4j.Logger; + +public class Log4jPluginLogger implements PluginLogger { + private final Logger logger; + + public Log4jPluginLogger(Logger logger) { + this.logger = logger; + } + + @Override + public void info(String s) { + this.logger.info(s); + } + + @Override + public void warn(String s) { + this.logger.warn(s); + } + + @Override + public void warn(String s, Throwable t) { + this.logger.warn(s, t); + } + + @Override + public void severe(String s) { + this.logger.error(s); + } + + @Override + public void severe(String s, Throwable t) { + this.logger.error(s, t); + } +} diff --git a/fabric/build.gradle b/fabric/build.gradle new file mode 100644 index 000000000..2d804e24f --- /dev/null +++ b/fabric/build.gradle @@ -0,0 +1,94 @@ +import net.fabricmc.loom.task.RemapJarTask + +plugins { + id 'com.github.johnrengelman.shadow' version '4.0.1' + id 'fabric-loom' version '0.5-SNAPSHOT' +} + +repositories { + maven { url 'https://maven.fabricmc.net/' } + mavenLocal() +} + +def minecraftVersion = '1.16.4' +def yarnBuild = 7 +def loaderVersion = '0.10.8' +def fabricApiVersion = '0.28.4+1.16' + +dependencies { + // Fabric Stuff, We don't specifically target only a single version but yarn mappings require a version to be specified. + minecraft "com.mojang:minecraft:${minecraftVersion}" + mappings "net.fabricmc:yarn:${minecraftVersion}+build.${yarnBuild}:v2" + modImplementation "net.fabricmc:fabric-loader:${loaderVersion}" + + Set apiModules = [ + 'fabric-api-base', + 'fabric-command-api-v1', + 'fabric-lifecycle-events-v1', + 'fabric-networking-api-v1' + ] + + apiModules.forEach { + modImplementation(fabricApi.module(it, fabricApiVersion)) + } + + include(modImplementation('me.lucko:fabric-permissions-api:0.1-SNAPSHOT')) + + compile project(':common') +} + +processResources { + inputs.property 'version', project.ext.fullVersion + + from(sourceSets.main.resources.srcDirs) { + include 'fabric.mod.json' + expand 'version': project.ext.fullVersion + } + + from(sourceSets.main.resources.srcDirs) { + exclude 'fabric.mod.json' + } +} + +shadowJar { + archiveName = "LuckPerms-Fabric-${project.ext.fullVersion}-dev.jar" + + dependencies { + exclude('net.fabricmc:.*') + include(dependency('net.luckperms:.*')) + include(dependency('me.lucko.luckperms:.*')) + // We don't want to include the mappings in the jar do we? + exclude '/mappings/*' + } + + 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 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' + relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' +} + +task remappedShadowJar(type: RemapJarTask) { + dependsOn tasks.shadowJar + input = tasks.shadowJar.archivePath + addNestedDependencies = true + archiveName = "LuckPerms-Fabric-${project.ext.fullVersion}.jar" +} + +tasks.assemble.dependsOn tasks.remappedShadowJar + +artifacts { + archives remappedShadowJar + shadow shadowJar +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java new file mode 100644 index 000000000..47cf4c831 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java @@ -0,0 +1,76 @@ +/* + * 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.fabric; + +import com.google.common.collect.ImmutableList; + +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.MapProcessor; +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.fabric.calculator.ServerOwnerProcessor; +import me.lucko.luckperms.fabric.context.FabricContextManager; + +import net.luckperms.api.query.QueryOptions; + +public class FabricCalculatorFactory implements CalculatorFactory { + private final LPFabricPlugin plugin; + + public FabricCalculatorFactory(LPFabricPlugin plugin) { + this.plugin = plugin; + } + + @Override + public PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata) { + ImmutableList.Builder processors = ImmutableList.builder(); + + processors.add(new MapProcessor()); + + 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(FabricContextManager.INTEGRATED_SERVER_OWNER).orElse(false); + if (integratedOwner && this.plugin.getConfiguration().get(ConfigKeys.FABRIC_INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS)) { + processors.add(new ServerOwnerProcessor()); + } + + return new PermissionCalculator(this.plugin, metadata, processors.build()); + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricClassLoader.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricClassLoader.java new file mode 100644 index 000000000..819250dc3 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricClassLoader.java @@ -0,0 +1,49 @@ +/* + * 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.fabric; + +import me.lucko.luckperms.common.dependencies.classloader.PluginClassLoader; + +import net.fabricmc.loader.launch.common.FabricLauncherBase; + +import java.net.MalformedURLException; +import java.nio.file.Path; + +public class FabricClassLoader implements PluginClassLoader { + + @Override + public void addJarToClasspath(Path file) { + try { + // Fabric abstracts class loading away to the FabricLauncher. + // TODO(i509VCB): Work on API for Fabric Loader which does not touch internals. + // Player wants to use project jigsaw in the future. + FabricLauncherBase.getLauncher().propose(file.toUri().toURL()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java new file mode 100644 index 000000000..8086bc3f6 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java @@ -0,0 +1,118 @@ +/* + * 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.fabric; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +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.sender.Sender; + +import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; +import net.minecraft.server.command.ServerCommandSource; + +import java.util.List; +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"}; + + private final LPFabricPlugin plugin; + + public FabricCommandExecutor(LPFabricPlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + public void register() { + CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> { + for (String alias : COMMAND_ALIASES) { + LiteralCommandNode cmd = literal(alias) + .executes(this) + .build(); + + ArgumentCommandNode args = argument("args", greedyString()) + .suggests(this) + .executes(this) + .build(); + + cmd.addChild(args); + dispatcher.getRoot().addChild(cmd); + } + }); + } + + @Override + public int run(CommandContext ctx) { + Sender wrapped = this.plugin.getSenderFactory().wrap(ctx.getSource()); + + int start = ctx.getRange().getStart(); + List arguments = 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; + } + + @Override + public CompletableFuture getSuggestions(CommandContext ctx, SuggestionsBuilder builder) { + Sender wrapped = this.plugin.getSenderFactory().wrap(ctx.getSource()); + + int idx = builder.getStart(); + + String buffer = ctx.getInput().substring(idx); + idx += buffer.length(); + + List arguments = 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(); + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricConfigAdapter.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricConfigAdapter.java new file mode 100644 index 000000000..fbc07eb2c --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricConfigAdapter.java @@ -0,0 +1,46 @@ +/* + * 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.fabric; + +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 FabricConfigAdapter extends ConfigurateConfigAdapter { + public FabricConfigAdapter(LuckPermsPlugin plugin, Path path) { + super(plugin, path); + } + + @Override + protected ConfigurationLoader createLoader(Path path) { + return HoconConfigurationLoader.builder().setPath(path).build(); + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricEventBus.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricEventBus.java new file mode 100644 index 000000000..59a7d41b6 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricEventBus.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.fabric; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; + +import net.fabricmc.loader.api.ModContainer; + +public class FabricEventBus extends AbstractEventBus { + public FabricEventBus(LuckPermsPlugin plugin, LuckPermsApiProvider apiProvider) { + super(plugin, apiProvider); + } + + @Override + protected ModContainer checkPlugin(Object mod) throws IllegalArgumentException { + if (mod instanceof ModContainer) { + return (ModContainer) mod; + } + + throw new IllegalArgumentException("Object " + mod + " (" + mod.getClass().getName() + ") is not a ModContainer."); + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSchedulerAdapter.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSchedulerAdapter.java new file mode 100644 index 000000000..30fc6315d --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSchedulerAdapter.java @@ -0,0 +1,43 @@ +/* + * 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.fabric; + +import me.lucko.luckperms.common.plugin.scheduler.AbstractJavaScheduler; + +import java.util.concurrent.Executor; + +public class FabricSchedulerAdapter extends AbstractJavaScheduler { + private final Executor sync; + + public FabricSchedulerAdapter(LPFabricBootstrap bootstrap) { + this.sync = r -> bootstrap.getServer().orElseThrow(() -> new IllegalStateException("Server not ready")).submitAndJoin(r); + } + + @Override + public Executor sync() { + return this.sync; + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java new file mode 100644 index 000000000..5bcc9aee5 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.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.fabric; + +import me.lucko.fabric.api.permissions.v0.Permissions; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.sender.SenderFactory; +import me.lucko.luckperms.fabric.model.MixinUser; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.luckperms.api.util.Tristate; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +import java.util.Locale; +import java.util.UUID; + +public class FabricSenderFactory extends SenderFactory { + private final LPFabricPlugin plugin; + + public FabricSenderFactory(LPFabricPlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + @Override + protected LPFabricPlugin getPlugin() { + return this.plugin; + } + + @Override + protected UUID getUniqueId(ServerCommandSource commandSource) { + if (commandSource.getEntity() != null) { + return commandSource.getEntity().getUuid(); + } + return Sender.CONSOLE_UUID; + } + + @Override + protected String getName(ServerCommandSource commandSource) { + String name = commandSource.getName(); + if (commandSource.getEntity() != null && name.equals("Server")) { + return Sender.CONSOLE_NAME; + } + return name; + } + + @Override + protected void sendMessage(ServerCommandSource sender, Component message) { + Locale locale = null; + if (sender.getEntity() instanceof ServerPlayerEntity) { + locale = ((MixinUser) sender.getEntity()).getCachedLocale(); + } + sender.sendFeedback(toNativeText(TranslationManager.render(message, locale)), false); + } + + @Override + protected Tristate getPermissionValue(ServerCommandSource commandSource, String node) { + switch (Permissions.getPermissionValue(commandSource, node)) { + case TRUE: + return Tristate.TRUE; + case FALSE: + return Tristate.FALSE; + case DEFAULT: + return Tristate.UNDEFINED; + default: + throw new AssertionError(); + } + } + + @Override + protected boolean hasPermission(ServerCommandSource commandSource, String node) { + return getPermissionValue(commandSource, node).asBoolean(); + } + + @Override + protected void performCommand(ServerCommandSource sender, String command) { + sender.getMinecraftServer().getCommandManager().execute(sender, command); + } + + public static Text toNativeText(Component component) { + return Text.Serializer.fromJson(GsonComponentSerializer.gson().serialize(component)); + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricBootstrap.java b/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricBootstrap.java new file mode 100644 index 000000000..2a34f5433 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricBootstrap.java @@ -0,0 +1,280 @@ +/* + * 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.fabric; + +import com.mojang.authlib.GameProfile; + +import me.lucko.luckperms.common.dependencies.classloader.PluginClassLoader; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +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 net.fabricmc.api.DedicatedServerModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.luckperms.api.platform.Platform; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; + +import org.apache.logging.log4j.LogManager; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +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; + +/** + * Bootstrap plugin for LuckPerms running on Fabric. + */ +public final class LPFabricBootstrap implements LuckPermsBootstrap, DedicatedServerModInitializer { + + private static final String MODID = "luckperms"; + private static final ModContainer MOD_CONTAINER = FabricLoader.getInstance().getModContainer(MODID) + .orElseThrow(() -> new RuntimeException("Could not get the LuckPerms mod container.")); + + /** + * The plugin logger + */ + private final PluginLogger logger; + + /** + * A scheduler adapter for the platform + */ + private final SchedulerAdapter schedulerAdapter; + + /** + * The plugin class loader. + */ + private final PluginClassLoader classLoader; + + /** + * The plugin instance + */ + private LPFabricPlugin 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 LPFabricBootstrap() { + this.logger = new Log4jPluginLogger(LogManager.getLogger(MODID)); + this.schedulerAdapter = new FabricSchedulerAdapter(this); + this.classLoader = new FabricClassLoader(); + this.plugin = new LPFabricPlugin(this); + } + + // provide adapters + + @Override + public PluginLogger getPluginLogger() { + return this.logger; + } + + @Override + public SchedulerAdapter getScheduler() { + return this.schedulerAdapter; + } + + @Override + public PluginClassLoader getPluginClassLoader() { + return this.classLoader; + } + + // lifecycle + + @Override + public void onInitializeServer() { + this.plugin = new LPFabricPlugin(this); + try { + this.plugin.load(); + } finally { + this.loadLatch.countDown(); + } + + // Register the Server startup/shutdown events now + ServerLifecycleEvents.SERVER_STARTING.register(this::onServerStarting); + ServerLifecycleEvents.SERVER_STOPPING.register(this::onServerStopping); + this.plugin.registerFabricListeners(); + } + + private void onServerStarting(MinecraftServer server) { + this.server = server; + this.startTime = Instant.now(); + this.plugin.enable(); + } + + private void onServerStopping(MinecraftServer server) { + this.plugin.disable(); + 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 MOD_CONTAINER.getMetadata().getVersion().getFriendlyString(); + } + + @Override + public Instant getStartupTime() { + return this.startTime; + } + + // provide information about the platform + + @Override + public Platform.Type getType() { + return Platform.Type.FABRIC; + } + + @Override + public String getServerBrand() { + String fabricVersion = FabricLoader.getInstance().getModContainer("fabric") + .map(c -> c.getMetadata().getVersion().getFriendlyString()) + .orElse("unknown"); + + return "fabric@" + fabricVersion; + } + + @Override + public String getServerVersion() { + String fabricApiVersion = FabricLoader.getInstance().getModContainer("fabric-api-base") + .map(c -> c.getMetadata().getVersion().getFriendlyString()) + .orElse("unknown"); + + return getServer().map(MinecraftServer::getVersion).orElse("null") + " - fabric-api@" + fabricApiVersion; + } + + @Override + public Path getDataDirectory() { + return FabricLoader.getInstance().getGameDir().resolve("mods").resolve(MODID); + } + + @Override + public Path getConfigDirectory() { + return FabricLoader.getInstance().getConfigDir().resolve(MODID); + } + + @Override + public InputStream getResourceStream(String path) { + try { + return Files.newInputStream(LPFabricBootstrap.MOD_CONTAINER.getPath(path)); + } catch (IOException e) { + return null; + } + } + + @Override + public Optional getPlayer(UUID uniqueId) { + return getServer().map(MinecraftServer::getPlayerManager).map(s -> s.getPlayer(uniqueId)); + } + + @Override + public Optional lookupUniqueId(String username) { + return getServer().map(MinecraftServer::getUserCache).map(c -> c.findByName(username)).map(GameProfile::getId); + + } + + @Override + public Optional lookupUsername(UUID uniqueId) { + return getServer().map(MinecraftServer::getUserCache).map(c -> c.getByUuid(uniqueId)).map(GameProfile::getName); + } + + @Override + public int getPlayerCount() { + return getServer().map(MinecraftServer::getCurrentPlayerCount).orElse(0); + } + + @Override + public Collection getPlayerList() { + return getServer().map(MinecraftServer::getPlayerManager) + .map(server -> { + List players = server.getPlayerList(); + List list = new ArrayList<>(players.size()); + for (ServerPlayerEntity player : players) { + list.add(player.getGameProfile().getName()); + } + return list; + }) + .orElse(Collections.emptyList()); + } + + @Override + public Collection getOnlinePlayers() { + return getServer().map(MinecraftServer::getPlayerManager) + .map(server -> { + List players = server.getPlayerList(); + List list = new ArrayList<>(players.size()); + for (ServerPlayerEntity player : players) { + list.add(player.getGameProfile().getId()); + } + return list; + }) + .orElse(Collections.emptyList()); + } + + @Override + public boolean isPlayerOnline(UUID uniqueId) { + return getServer().map(MinecraftServer::getPlayerManager).map(s -> s.getPlayer(uniqueId) != null).orElse(false); + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricPlugin.java b/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricPlugin.java new file mode 100644 index 000000000..991d7078d --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricPlugin.java @@ -0,0 +1,248 @@ +/* + * 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.fabric; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.calculator.CalculatorFactory; +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.DummySender; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.tasks.CacheHousekeepingTask; +import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; +import me.lucko.luckperms.common.util.MoreFiles; +import me.lucko.luckperms.fabric.context.FabricContextManager; +import me.lucko.luckperms.fabric.context.FabricPlayerCalculator; +import me.lucko.luckperms.fabric.listeners.FabricConnectionListener; +import me.lucko.luckperms.fabric.listeners.PermissionCheckListener; + +import net.fabricmc.loader.api.ModContainer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.MinecraftServer; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +public class LPFabricPlugin extends AbstractLuckPermsPlugin { + private final LPFabricBootstrap bootstrap; + + private FabricConnectionListener connectionListener; + private FabricCommandExecutor commandManager; + private FabricSenderFactory senderFactory; + private FabricContextManager contextManager; + private StandardUserManager userManager; + private StandardGroupManager groupManager; + private StandardTrackManager trackManager; + + public LPFabricPlugin(LPFabricBootstrap bootstrap) { + this.bootstrap = bootstrap; + } + + @Override + public LPFabricBootstrap getBootstrap() { + return this.bootstrap; + } + + protected void registerFabricListeners() { + // Events are registered very early on, and persist between game states + this.connectionListener = new FabricConnectionListener(this); + this.connectionListener.registerListeners(); + + new PermissionCheckListener(this).registerListeners(); + + // Command registration also need to occur early, and will persist across game states as well. + this.commandManager = new FabricCommandExecutor(this); + this.commandManager.register(); + } + + @Override + protected void setupSenderFactory() { + this.senderFactory = new FabricSenderFactory(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() { + Path configPath = this.getBootstrap().getConfigDirectory().resolve("luckperms.conf"); + + if (!Files.exists(configPath)) { + try { + MoreFiles.createDirectoriesIfNotExists(this.bootstrap.getConfigDirectory()); + try (InputStream is = this.getBootstrap().getResourceStream("luckperms.conf")) { + Files.copy(is, configPath); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + return new FabricConfigAdapter(this, configPath); + } + + @Override + protected void registerPlatformListeners() { + // Too late for Fabric, registered in #registerFabricListeners + } + + @Override + protected MessagingFactory provideMessagingFactory() { + return new MessagingFactory<>(this); + } + + @Override + protected void registerCommands() { + // Too late for Fabric, registered in #registerFabricListeners + } + + @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 FabricCalculatorFactory(this); + } + + @Override + protected void setupContextManager() { + this.contextManager = new FabricContextManager(this); + + FabricPlayerCalculator playerCalculator = new FabricPlayerCalculator(this); + playerCalculator.registerListeners(); + this.contextManager.registerCalculator(playerCalculator); + } + + @Override + protected void setupPlatformHooks() { + } + + @Override + protected AbstractEventBus provideEventBus(LuckPermsApiProvider provider) { + return new FabricEventBus(this, provider); + } + + @Override + protected void registerApiOnPlatform(LuckPerms api) { + } + + @Override + protected void registerHousekeepingTasks() { + this.bootstrap.getScheduler().asyncRepeating(new ExpireTemporaryTask(this), 3, TimeUnit.SECONDS); + this.bootstrap.getScheduler().asyncRepeating(new CacheHousekeepingTask(this), 2, TimeUnit.MINUTES); + } + + @Override + protected void performFinalSetup() { + } + + public FabricSenderFactory getSenderFactory() { + return this.senderFactory; + } + + @Override + public FabricConnectionListener getConnectionListener() { + return this.connectionListener; + } + + @Override + public FabricCommandExecutor getCommandManager() { + return this.commandManager; + } + + @Override + public FabricContextManager getContextManager() { + return this.contextManager; + } + + @Override + public StandardUserManager getUserManager() { + return this.userManager; + } + + @Override + public StandardGroupManager getGroupManager() { + return this.groupManager; + } + + @Override + public StandardTrackManager getTrackManager() { + return this.trackManager; + } + + @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::getPlayerManager).map(s -> s.getPlayerList().stream().map(p -> this.senderFactory.wrap(p.getCommandSource()))).orElseGet(Stream::empty) + ); + } + + @Override + public Sender getConsoleSender() { + return this.bootstrap.getServer() + .map(s -> this.senderFactory.wrap(s.getCommandSource())) + .orElseGet(() -> new DummySender(this, Sender.CONSOLE_UUID, Sender.CONSOLE_NAME) { + @Override + public void sendMessage(Component message) { + LPFabricPlugin.this.bootstrap.getPluginLogger().info(PlainComponentSerializer.plain().serialize(TranslationManager.render(message))); + } + }); + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/calculator/ServerOwnerProcessor.java b/fabric/src/main/java/me/lucko/luckperms/fabric/calculator/ServerOwnerProcessor.java new file mode 100644 index 000000000..61519020b --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/calculator/ServerOwnerProcessor.java @@ -0,0 +1,44 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.fabric.calculator; + +import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; +import me.lucko.luckperms.common.calculator.result.TristateResult; + +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 { + private static final TristateResult TRUE_RESULT = new TristateResult.Factory(ServerOwnerProcessor.class).result(Tristate.TRUE); + + @Override + public TristateResult hasPermission(String permission) { + return TRUE_RESULT; + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricContextManager.java b/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricContextManager.java new file mode 100644 index 000000000..eac3eddfc --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricContextManager.java @@ -0,0 +1,81 @@ +/* + * 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.fabric.context; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.context.ContextManager; +import me.lucko.luckperms.common.context.QueryOptionsCache; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.fabric.model.MixinUser; + +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.query.OptionKey; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.network.ServerPlayerEntity; + +import java.util.UUID; + +public class FabricContextManager extends ContextManager { + public static final OptionKey INTEGRATED_SERVER_OWNER = OptionKey.of("integrated_server_owner", Boolean.class); + + public FabricContextManager(LuckPermsPlugin plugin) { + super(plugin, ServerPlayerEntity.class, ServerPlayerEntity.class); + } + + @Override + public UUID getUniqueId(ServerPlayerEntity player) { + return player.getUuid(); + } + + public QueryOptionsCache newQueryOptionsCache(ServerPlayerEntity player) { + return new QueryOptionsCache<>(player, this); + } + + @Override + public QueryOptionsCache getCacheFor(ServerPlayerEntity subject) { + if (subject == null) { + throw new NullPointerException("subject"); + } + + return ((MixinUser) subject).getQueryOptionsCache(this); + } + + @Override + public void invalidateCache(ServerPlayerEntity subject) { + getCacheFor(subject).invalidate(); + } + + @Override + public QueryOptions formQueryOptions(ServerPlayerEntity subject, ImmutableContextSet contextSet) { + QueryOptions.Builder queryOptions = this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder(); + if (subject.getServer().isHost(subject.getGameProfile())) { + queryOptions.option(INTEGRATED_SERVER_OWNER, true); + } + + return queryOptions.context(contextSet).build(); + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricPlayerCalculator.java b/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricPlayerCalculator.java new file mode 100644 index 000000000..ac42b8be2 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricPlayerCalculator.java @@ -0,0 +1,119 @@ +/* + * 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.fabric.context; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.util.EnumNamer; +import me.lucko.luckperms.fabric.LPFabricPlugin; +import me.lucko.luckperms.fabric.event.PlayerChangeWorldCallback; +import me.lucko.luckperms.fabric.event.RespawnPlayerCallback; + +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.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.world.GameMode; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Optional; + +public class FabricPlayerCalculator implements ContextCalculator { + private static final EnumNamer GAMEMODE_NAMER = new EnumNamer<>( + GameMode.class, + EnumNamer.LOWER_CASE_NAME + ); + + private final LPFabricPlugin plugin; + + public FabricPlayerCalculator(LPFabricPlugin plugin) { + this.plugin = plugin; + } + + public void registerListeners() { + PlayerChangeWorldCallback.EVENT.register(this::onWorldChange); + RespawnPlayerCallback.EVENT.register(this::onPlayerRespawn); + } + + @Override + public void calculate(@NonNull ServerPlayerEntity target, @NonNull ContextConsumer consumer) { + GameMode mode = target.interactionManager.getGameMode(); + if (mode != null && mode != GameMode.NOT_SET) { + consumer.accept(DefaultContextKeys.GAMEMODE_KEY, GAMEMODE_NAMER.name(mode)); + } + + // TODO: figure out dimension type context too + ServerWorld world = target.getServerWorld(); + this.plugin.getConfiguration().get(ConfigKeys.WORLD_REWRITES).rewriteAndSubmit(getContextKey(world.getRegistryKey().getValue()), consumer); + } + + @Override + public ContextSet estimatePotentialContexts() { + ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); + + for (GameMode mode : GameMode.values()) { + builder.add(DefaultContextKeys.GAMEMODE_KEY, GAMEMODE_NAMER.name(mode)); + } + + // TODO: dimension type + + Optional server = this.plugin.getBootstrap().getServer(); + if (server.isPresent()) { + Iterable worlds = server.get().getWorlds(); + for (ServerWorld world : worlds) { + String worldName = getContextKey(world.getRegistryKey().getValue()); + if (Context.isValidValue(worldName)) { + builder.add(DefaultContextKeys.WORLD_KEY, worldName); + } + } + } + + return builder.build(); + } + + private static String getContextKey(Identifier key) { + if (key.getNamespace().equals("minecraft")) { + return key.getPath(); + } + return key.toString(); + } + + private void onWorldChange(ServerWorld origin, ServerWorld destination, ServerPlayerEntity player) { + this.plugin.getContextManager().invalidateCache(player); + } + + private void onPlayerRespawn(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer, boolean alive) { + this.plugin.getContextManager().invalidateCache(oldPlayer); + this.plugin.getContextManager().invalidateCache(newPlayer); + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/event/PlayerChangeWorldCallback.java b/fabric/src/main/java/me/lucko/luckperms/fabric/event/PlayerChangeWorldCallback.java new file mode 100644 index 000000000..555dfb3a4 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/event/PlayerChangeWorldCallback.java @@ -0,0 +1,42 @@ +/* + * 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.fabric.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; + +// TODO: Use Fabric API alternative when merged. +public interface PlayerChangeWorldCallback { + Event EVENT = EventFactory.createArrayBacked(PlayerChangeWorldCallback.class, (callbacks) -> (originalWorld, destination, player) -> { + for (PlayerChangeWorldCallback callback : callbacks) { + callback.onChangeWorld(originalWorld, destination, player); + } + }); + + void onChangeWorld(ServerWorld originalWorld, ServerWorld destination, ServerPlayerEntity player); +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/event/RespawnPlayerCallback.java b/fabric/src/main/java/me/lucko/luckperms/fabric/event/RespawnPlayerCallback.java new file mode 100644 index 000000000..0309ea928 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/event/RespawnPlayerCallback.java @@ -0,0 +1,42 @@ +/* + * 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.fabric.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.network.ServerPlayerEntity; + +// TODO: Use Fabric API alternative when merged. +// https://github.com/FabricMC/fabric/pull/957 +public interface RespawnPlayerCallback { + Event EVENT = EventFactory.createArrayBacked(RespawnPlayerCallback.class, (callbacks) -> (newPlayer, oldPlayer, alive) -> { + for (RespawnPlayerCallback callback : callbacks) { + callback.onRespawn(newPlayer, oldPlayer, alive); + } + }); + + void onRespawn(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer, boolean alive); +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java new file mode 100644 index 000000000..6353f1224 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java @@ -0,0 +1,133 @@ +/* + * 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.fabric.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.fabric.FabricSenderFactory; +import me.lucko.luckperms.fabric.LPFabricPlugin; +import me.lucko.luckperms.fabric.mixin.ServerLoginNetworkHandlerAccessor; +import me.lucko.luckperms.fabric.model.MixinUser; + +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.ServerLoginConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerLoginNetworking.LoginSynchronizer; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.kyori.adventure.text.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerLoginNetworkHandler; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; + +import java.util.concurrent.CompletableFuture; + +public class FabricConnectionListener extends AbstractConnectionListener { + private final LPFabricPlugin plugin; + + public FabricConnectionListener(LPFabricPlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + public void registerListeners() { + ServerLoginConnectionEvents.QUERY_START.register(this::onPreLogin); + ServerPlayConnectionEvents.JOIN.register(this::onLogin); + ServerPlayConnectionEvents.DISCONNECT.register(this::onDisconnect); + } + + private void onPreLogin(ServerLoginNetworkHandler netHandler, MinecraftServer server, PacketSender packetSender, LoginSynchronizer sync) { + /* Called when the player first attempts a connection with the server. */ + + // Get their profile from the net handler - it should have been initialised by now. + GameProfile profile = ((ServerLoginNetworkHandlerAccessor) netHandler).getGameProfile(); + + + // Register with the LoginSynchronizer that we want to perform a task before the login proceeds. + sync.waitFor(CompletableFuture.runAsync(() -> onPreLoginAsync(netHandler, profile), this.plugin.getBootstrap().getScheduler().async())); + } + + private void onPreLoginAsync(ServerLoginNetworkHandler netHandler, GameProfile e) { + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing pre-login for " + e.getId() + " - " + e.getName()); + } + + /* 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(e.getId(), e.getName()); + recordConnection(e.getId()); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(e.getId(), e.getName(), user); + } catch (Exception ex) { + this.plugin.getLogger().severe("Exception occurred whilst loading data for " + e.getId() + " - " + e.getName(), ex); + + // deny the connection + Component reason = TranslationManager.render(Message.LOADING_DATABASE_ERROR.build()); + netHandler.disconnect(FabricSenderFactory.toNativeText(reason)); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(e.getId(), e.getName(), null); + } + } + + private void onLogin(ServerPlayNetworkHandler netHandler, PacketSender packetSender, MinecraftServer server) { + final ServerPlayerEntity player = netHandler.player; + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing login for " + player.getUuid() + " - " + player.getName()); + } + + final User user = this.plugin.getUserManager().getIfLoaded(player.getUuid()); + + /* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */ + if (user == null) { + this.plugin.getLogger().warn("User " + player.getUuid() + " - " + player.getName() + + " doesn't currently have data pre-loaded - denying login."); + Component reason = TranslationManager.render(Message.LOADING_STATE_ERROR.build()); + netHandler.disconnect(FabricSenderFactory.toNativeText(reason)); + return; + } + + // init permissions handler + ((MixinUser) player).initializePermissions(user); + + this.plugin.getContextManager().signalContextUpdate(player); + } + + private void onDisconnect(ServerPlayNetworkHandler netHandler, MinecraftServer server) { + handleDisconnect(netHandler.player.getUuid()); + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/PermissionCheckListener.java b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/PermissionCheckListener.java new file mode 100644 index 000000000..adaab827f --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/PermissionCheckListener.java @@ -0,0 +1,89 @@ +/* + * 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.fabric.listeners; + +import me.lucko.fabric.api.permissions.v0.PermissionCheckEvent; +import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import me.lucko.luckperms.fabric.LPFabricPlugin; +import me.lucko.luckperms.fabric.model.MixinUser; + +import net.fabricmc.fabric.api.util.TriState; +import net.minecraft.command.CommandSource; +import net.minecraft.entity.Entity; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Listener to route permission checks made via fabric-permissions-api to LuckPerms. + */ +public class PermissionCheckListener { + private final LPFabricPlugin plugin; + + public PermissionCheckListener(LPFabricPlugin plugin) { + this.plugin = plugin; + } + + public void registerListeners() { + PermissionCheckEvent.EVENT.register(this::onPermissionCheck); + } + + private @NonNull TriState onPermissionCheck(CommandSource source, String permission) { + if (source instanceof ServerCommandSource) { + Entity entity = ((ServerCommandSource) source).getEntity(); + if (entity instanceof ServerPlayerEntity) { + return onPlayerPermissionCheck((ServerPlayerEntity) entity, permission); + } + } + return onOtherPermissionCheck(source, permission); + } + + private TriState onPlayerPermissionCheck(ServerPlayerEntity player, String permission) { + switch (((MixinUser) player).hasPermission(permission)) { + case TRUE: + return TriState.TRUE; + case FALSE: + return TriState.FALSE; + case UNDEFINED: + return TriState.DEFAULT; + default: + throw new AssertionError(); + } + } + + private TriState onOtherPermissionCheck(CommandSource source, String permission) { + if (source instanceof ServerCommandSource) { + String name = ((ServerCommandSource) source).getName(); + this.plugin.getVerboseHandler().offerPermissionCheckEvent(me.lucko.luckperms.common.verbose.event.PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK, name, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.UNDEFINED); + this.plugin.getPermissionRegistry().offer(permission); + } + + return TriState.DEFAULT; + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ClientSettingsC2SPacketAccessor.java b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ClientSettingsC2SPacketAccessor.java new file mode 100644 index 000000000..929006fca --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ClientSettingsC2SPacketAccessor.java @@ -0,0 +1,39 @@ +/* + * 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.fabric.mixin; + +import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ClientSettingsC2SPacket.class) +public interface ClientSettingsC2SPacketAccessor { + + @Accessor("language") + String getLanguage(); + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/PlayerManagerMixin.java b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/PlayerManagerMixin.java new file mode 100644 index 000000000..11b72ac2d --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/PlayerManagerMixin.java @@ -0,0 +1,49 @@ +/* + * 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.fabric.mixin; + +import me.lucko.luckperms.fabric.event.RespawnPlayerCallback; + +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +@Mixin(PlayerManager.class) +public abstract class PlayerManagerMixin { + + // Implement the callback for RespawnPlayerCallback + // We'll switch to Fabric's event when FabricMC/fabric#957 is merged. + @Inject(at = @At("TAIL"), method = "respawnPlayer", locals = LocalCapture.CAPTURE_FAILEXCEPTION) + private void luckperms_onRespawnPlayer(ServerPlayerEntity player, boolean alive, CallbackInfoReturnable cir) { + RespawnPlayerCallback.EVENT.invoker().onRespawn(player, cir.getReturnValue(), alive); // Transfer the old caches to the new player. + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerLoginNetworkHandlerAccessor.java b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerLoginNetworkHandlerAccessor.java new file mode 100644 index 000000000..9d0db0c74 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerLoginNetworkHandlerAccessor.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.fabric.mixin; + +import com.mojang.authlib.GameProfile; + +import net.minecraft.server.network.ServerLoginNetworkHandler; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * Accessor mixin to provide access to the underlying {@link GameProfile} during the server + * login handling. + */ +@Mixin(ServerLoginNetworkHandler.class) +public interface ServerLoginNetworkHandlerAccessor { + + @Accessor("profile") + GameProfile getGameProfile(); + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java new file mode 100644 index 000000000..9bf69edcd --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java @@ -0,0 +1,142 @@ +/* + * 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.fabric.mixin; + +import me.lucko.luckperms.common.cacheddata.type.PermissionCache; +import me.lucko.luckperms.common.context.QueryOptionsCache; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; +import me.lucko.luckperms.fabric.context.FabricContextManager; +import me.lucko.luckperms.fabric.event.PlayerChangeWorldCallback; +import me.lucko.luckperms.fabric.model.MixinUser; + +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.Locale; + +/** + * Mixin into {@link ServerPlayerEntity} to store LP caches and implement {@link MixinUser}. + * + *

This mixin is also temporarily used to implement our internal PlayerChangeWorldCallback, + * until a similar event is added to Fabric itself.

+ */ +@Mixin(ServerPlayerEntity.class) +public abstract class ServerPlayerEntityMixin implements MixinUser { + + /** Cache a reference to the LP {@link User} instance loaded for this player */ + private User luckperms$user; + + /** + * Hold a QueryOptionsCache instance on the player itself, so we can just cast instead of + * having to maintain a map of Player->Cache. + */ + private QueryOptionsCache luckperms$queryOptions; + + // Cache player locale + private Locale luckperms$locale; + + // Used by PlayerChangeWorldCallback hook below. + @Shadow public abstract ServerWorld getServerWorld(); + + @Override + public QueryOptionsCache getQueryOptionsCache(FabricContextManager contextManager) { + if (this.luckperms$queryOptions == null) { + this.luckperms$queryOptions = contextManager.newQueryOptionsCache(((ServerPlayerEntity) (Object) this)); + } + return this.luckperms$queryOptions; + } + + @Override + public Locale getCachedLocale() { + return this.luckperms$locale; + } + + @Override + public void initializePermissions(User user) { + this.luckperms$user = user; + + // ensure query options cache is initialised too. + if (this.luckperms$queryOptions == null) { + this.getQueryOptionsCache((FabricContextManager) user.getPlugin().getContextManager()); + } + } + + @Override + public Tristate hasPermission(String permission) { + if (permission == null) { + throw new NullPointerException("permission"); + } + return hasPermission(permission, this.luckperms$queryOptions.getQueryOptions()); + } + + @Override + public Tristate hasPermission(String permission, QueryOptions queryOptions) { + if (permission == null) { + throw new NullPointerException("permission"); + } + if (queryOptions == null) { + throw new NullPointerException("queryOptions"); + } + + final User user = this.luckperms$user; + if (user == null) { + throw new IllegalStateException("Permissions have not been initialised for this player yet."); + } + + PermissionCache data = user.getCachedData().getPermissionData(queryOptions); + return data.checkPermission(permission, PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK).result(); + } + + @Inject( + at = @At("HEAD"), + method = "setClientSettings" + ) + private void luckperms_setClientSettings(ClientSettingsC2SPacket information, CallbackInfo ci) { + String language = ((ClientSettingsC2SPacketAccessor) information).getLanguage(); + this.luckperms$locale = TranslationManager.parseLocale(language); + } + + @Inject( + at = @At("TAIL"), + method = "worldChanged", + locals = LocalCapture.CAPTURE_FAILEXCEPTION + ) + private void luckperms_onChangeDimension(ServerWorld targetWorld, CallbackInfo ci) { + PlayerChangeWorldCallback.EVENT.invoker().onChangeWorld(this.getServerWorld(), targetWorld, (ServerPlayerEntity) (Object) this); + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java b/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java new file mode 100644 index 000000000..801f300f0 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java @@ -0,0 +1,67 @@ +/* + * 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.fabric.model; + +import me.lucko.luckperms.common.context.QueryOptionsCache; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.fabric.context.FabricContextManager; + +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.server.network.ServerPlayerEntity; + +import java.util.Locale; + +/** + * Mixin interface for {@link ServerPlayerEntity} implementing {@link User} related + * caches and functions. + */ +public interface MixinUser { + + /** + * Gets (or creates using the manager) the objects {@link QueryOptionsCache}. + * + * @param contextManager the contextManager + * @return the cache + */ + QueryOptionsCache getQueryOptionsCache(FabricContextManager contextManager); + + Locale getCachedLocale(); + + /** + * Initialises permissions for this player using the given {@link User}. + * + * @param user the user + */ + void initializePermissions(User user); + + // methods to perform permission checks using the User instance initialised on login + + Tristate hasPermission(String permission); + + Tristate hasPermission(String permission, QueryOptions queryOptions); + +} diff --git a/fabric/src/main/resources/assets/luckperms/icon.png b/fabric/src/main/resources/assets/luckperms/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ca72e468521344b8c53ace586e21d8c7a06dc831 GIT binary patch literal 25938 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≦~@PVl)n-cbaB0TS!b!0`$e}ZBm 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%0AZFeM|3biMRPgI zRdp6T^vVD_6sQ=O@_utnK40!P-Kz*;#kssF?P={YwFBjEgKKMRL!vQ1$Zjhjk+R~! zI2OL{g4NYDK}2R$!7QS)3~o&;U*zfuwoYcB%2(f6+D%KLk@{|@d6T8A>l2z0Lx8HL zdT)2%mMTJK&YR8-jF1OxAunDolz_XU&JMFpDe%{s9#>*z-x>+GzTvGlM`DDLHM*L{ zj53D<4C(&#VK_v$6$Itq%2g1$eO$Jqihl^kmCX)3%2cmsbzbadfm$8DTEMyW;f{Rj z9ZmN<4$no(y<4Z@zP&iHzB$e1BH6ej>{aCh7@ONTlkH*P8y=XRdI!f*NZ=*X(*92n zz|fLAB*i2q_b&fLzV z&f3oCk`Yd=*K=n~XGVb9T=RN}=x(q5^kKy0=#=F5DWxGyKG&4zJLLYL0Svx8h5ihL zs3GtE-VNXKet69CuTkj*HhhEc{ka}NFoZy!FbU$?!pMjA;ma8r84LRBWUk!$u&L1e z{Cv?h1teJuWq^{BQXU`3r!Xwp4I-tE=(D zb`59rq{ONmIHSyA&}cGS;$`RP_aq^4?~#iYnP(9jxLuf;B^l7hQgoqRZg{;QV_ord zL1}WS*pp$>#SZ1+yUtn+e%v8={v>M-on!fYq~oGvBlQ%OEotZrlmBKobDX_*-E0Y& z7;bYNE-kBk5XNq5@PWUIO0bS^5v@cQFRciX9(B+wO0r70jtDOEd;@}b1 znNelQj*o?^oJlMckJVgAa_$ErRr)q0XQ1R8)F`1)+%&X9C|8}vVc@eck9R-T$iQlb zW9u)&1S_gsdnelB9iQlMiJ2dr3DS>{`}s!P$;gkH*bvcAp+C@F;cr4dL1|4t9cVq; zt!lISnW6_-#Di^%cDueBT)|HW#QLlKD6?(JXQzUvOmg%8E#uvKGSo5@{->Kuu>mTt2Zz94AtF6XO=>*-QulQT=v6x!KSx;tqD-R$4eg(-1=?IDzo3|kskz!@t z6X9 z<9i{V?lEmg5{$)f4gOB5z!*1>R%1a-jYSqzHN8&THl+sq2$q$4j4n>~&}|PzTn#9Y z5&ZbXa^D!q?K9@&_m)ypH%JF$a0B`y$z9T}Wwa*PXC8BZW1rw)Wo0$elgLP`pd`Oc z{W=gk5St+M@QQ0BS-Y)odzr7bF^Rv+wF|;({>xRSTWCC`uh5PI%Rg~ulV0+v!MD)o zh~a5YR@U^Aa_8oXHfDPAtrRmwfd>ru(fgZD4NOs|GSy}T+OK&F( z4xfaFe_BM}6KPY}b31*$DMA;&Xsxs%;@xnrKe&2P$GX=4)1o4&O@OOJ3i4MK;$tW> z=lu1SxM8i1hmjKvxo@h>CgrAji%Mkz!-lbUeI{E#oqFBnsrfH1Nf~Jh^nIIqe=zS Gp#KA9M(QU3 literal 0 HcmV?d00001 diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 000000000..81913b107 --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,43 @@ +{ + "schemaVersion": 1, + "id": "luckperms", + "version": "${version}", + "name": "LuckPerms", + "icon": "assets/luckperms/icon.png", + "description": "A permissions plugin (mod)", + "authors": [ + { + "name": "Luck", + "contact": { + "sources": "https://github.com/lucko", + "homepage": "https://lucko.me/" + } + } + ], + "license": "MIT", + "contact": { + "homepage": "https://luckperms.net", + "source": "https://github.com/lucko/LuckPerms", + "issues": "https://github.com/lucko/LuckPerms/issues" + }, + "environment": "server", + "entrypoints": { + "server": [ + "me.lucko.luckperms.fabric.LPFabricBootstrap" + ] + }, + "mixins": [ + "mixins.luckperms.json" + ], + "depends": { + "fabricloader": ">=0.9.0", + "fabric-api-base": "*", + "fabric-command-api-v1": "*", + "fabric-lifecycle-events-v1": "*", + "fabric-networking-v0": "*", + "fabric-permissions-api-v0": "*" + }, + "custom": { + "modmenu:api": true + } +} diff --git a/fabric/src/main/resources/luckperms.conf b/fabric/src/main/resources/luckperms.conf new file mode 100644 index 000000000..19e9ab882 --- /dev/null +++ b/fabric/src/main/resources/luckperms.conf @@ -0,0 +1,556 @@ +#################################################################################################### +# +----------------------------------------------------------------------------------------------+ # +# | __ __ ___ __ __ | # +# | | | | / ` |__/ |__) |__ |__) |\/| /__` | # +# | |___ \__/ \__, | \ | |___ | \ | | .__/ | # +# | | # +# | 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 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. + # - Change this if you want to use different tables for different servers. + table-prefix = "luckperms_" + + # The prefix to use for all LuckPerms collections. Change this if you want to use different + # collections for different servers. The default is no prefix. + mongodb-collection-prefix = "" + + # MongoDB ClientConnectionURI for use with replica sets and custom connection options + # - 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. +# => 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" + password = "" +} + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | 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" + +# +----------------------------------------------------------------------------------------------+ # +# | 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=true + +# 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 | # +# +----------------------------------------------------------------------------------------------+ # + +# 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. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# +----------------------------------------------------------------------------------------------+ # +# | 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 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 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/fabric/src/main/resources/mixins.luckperms.json b/fabric/src/main/resources/mixins.luckperms.json new file mode 100644 index 000000000..a2d619141 --- /dev/null +++ b/fabric/src/main/resources/mixins.luckperms.json @@ -0,0 +1,16 @@ +{ + "required": true, + "package": "me.lucko.luckperms.fabric.mixin", + "compatibilityLevel": "JAVA_8", + "mixins": [ + "ClientSettingsC2SPacketAccessor", + "PlayerManagerMixin", + "ServerLoginNetworkHandlerAccessor", + "ServerPlayerEntityMixin" + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..f12c912d6 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +# Fabric requires some more ram. +org.gradle.jvmargs=-Xmx1G \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index e5de4f935..01e87906c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,14 @@ +// Fabric Needs this +pluginManagement { + repositories { + jcenter() + maven { + url 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} + rootProject.name = 'luckperms' include ( 'api', @@ -5,7 +16,8 @@ include ( 'bukkit', 'bukkit-legacy', 'bungee', - 'sponge', 'sponge:sponge-service', 'sponge:sponge-service-api6', 'sponge:sponge-service-api7', + 'fabric', 'nukkit', + 'sponge', 'sponge:sponge-service', 'sponge:sponge-service-api6', 'sponge:sponge-service-api7', 'velocity' ) diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java index 38df1acf1..9fe5bd141 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java @@ -32,6 +32,7 @@ import me.lucko.luckperms.common.command.access.CommandPermission; 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.track.StandardTrackManager; @@ -275,7 +276,7 @@ public class LPSpongePlugin extends AbstractLuckPermsPlugin { return new DummySender(this, Sender.CONSOLE_UUID, Sender.CONSOLE_NAME) { @Override public void sendMessage(Component message) { - LPSpongePlugin.this.bootstrap.getPluginLogger().info(LegacyComponentSerializer.legacySection().serialize(message)); + LPSpongePlugin.this.bootstrap.getPluginLogger().info(LegacyComponentSerializer.legacySection().serialize(TranslationManager.render(message))); } }; }