diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..022b84144 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf 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 bc31a34a8..32b727316 100644 --- a/api/src/main/java/net/luckperms/api/platform/Platform.java +++ b/api/src/main/java/net/luckperms/api/platform/Platform.java @@ -76,6 +76,7 @@ public interface Platform { NUKKIT("Nukkit"), VELOCITY("Velocity"), FABRIC("Fabric"), + NEOFORGE("NeoForge"), FORGE("Forge"), STANDALONE("Standalone"); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d28dee09..f9d402340 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] shadow = "8.1.7" blossom = "1.3.1" +moddevgradle = "1.0.17" forgegradle = "[6.0,6.2)" loom = "1.6-SNAPSHOT" licenser = "0.6.1" @@ -8,6 +9,7 @@ licenser = "0.6.1" [plugins] blossom = { id = "net.kyori.blossom", version.ref = "blossom" } shadow = { id = "io.github.goooler.shadow", version.ref = "shadow" } +moddevgradle = { id = "net.neoforged.moddev", version.ref = "moddevgradle" } forgegradle = { id = "net.minecraftforge.gradle", version.ref = "forgegradle" } loom = { id = "fabric-loom", version.ref = "loom" } licenser = { id = "org.cadixdev.licenser", version.ref = "licenser" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f..2c3521197 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4..09523c0e5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426..f5feea6d6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30dbd..9d21a2183 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/neoforge/build.gradle b/neoforge/build.gradle new file mode 100644 index 000000000..8e1adb360 --- /dev/null +++ b/neoforge/build.gradle @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.blossom) + alias(libs.plugins.shadow) + alias(libs.plugins.moddevgradle) +} + +sourceCompatibility = 17 +targetCompatibility = 21 + +Configuration shade = configurations.create('shade') +configurations.implementation { + extendsFrom configurations.shade +} + +blossom { + replaceTokenIn 'src/main/java/me/lucko/luckperms/neoforge/LPNeoForgeBootstrap.java' + replaceToken '@version@', project.ext.fullVersion +} + +neoForge { + version = project.neoForgeVersion + + validateAccessTransformers = true +} + +dependencies { + add('shade', project(':common')) + compileOnly project(':common:loader-utils') + compileOnly project(':neoforge:neoforge-api') +} + +shadowJar { + archiveFileName = "luckperms-neoforge.jarinjar" + configurations = [shade] + + dependencies { + include(dependency('me.lucko.luckperms:.*')) + } + + relocate 'net.kyori.adventure', 'me.lucko.luckperms.lib.adventure' + relocate 'net.kyori.event', 'me.lucko.luckperms.lib.eventbus' + relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine' + relocate 'okio', 'me.lucko.luckperms.lib.okio' + relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3' + relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy' + relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' + relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' + relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' + relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' + relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' + relocate 'org.bson', 'me.lucko.luckperms.lib.bson' + relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' + relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' + relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' + relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' + relocate 'org.yaml.snakeyaml', 'me.lucko.luckperms.lib.yaml' +} + +artifacts { + archives shadowJar +} diff --git a/neoforge/gradle.properties b/neoforge/gradle.properties new file mode 100644 index 000000000..912a267b6 --- /dev/null +++ b/neoforge/gradle.properties @@ -0,0 +1,2 @@ +minecraftVersion=1.21 +neoForgeVersion=21.0.161 \ No newline at end of file diff --git a/neoforge/loader/build.gradle b/neoforge/loader/build.gradle new file mode 100644 index 000000000..38dbed6b0 --- /dev/null +++ b/neoforge/loader/build.gradle @@ -0,0 +1,93 @@ +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import net.neoforged.moddevgradle.internal.RunGameTask + +plugins { + alias(libs.plugins.shadow) + alias(libs.plugins.moddevgradle) + id("java-library") +} + +sourceCompatibility = 17 +targetCompatibility = 21 + +neoForge { + version = project.neoForgeVersion + + validateAccessTransformers = true + + runs { + client { + client() + mods.set(new HashSet()) // Work around classpath issues by using the production jar for dev runs + } + server { + server() + mods.set(new HashSet()) // Work around classpath issues by using the production jar for dev runs + } + } +} + +// Work around classpath issues by using the production jar for dev runs +tasks.withType(RunGameTask).configureEach { + dependsOn(tasks.shadowJar) + doFirst { + File jar = file("run/mods/main.jar") + jar.parentFile.mkdirs() + Files.copy(tasks.shadowJar.archiveFile.get().asFile.toPath(), jar.toPath(), StandardCopyOption.REPLACE_EXISTING) + } +} + +Configuration shade = configurations.create('shade') +configurations.implementation { + extendsFrom configurations.shade +} + +dependencies { + add('shade', project(':api')) + add('shade', project(':common:loader-utils')) + add('shade', project(':neoforge:neoforge-api')) +} + +build { + dependsOn(":neoforge:build") + dependsOn(":neoforge:neoforge-api:build") +} + +jar { + manifest { + attributes( + 'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), + 'Implementation-Title': 'LuckPerms', + 'Implementation-Vendor': 'LuckPerms', + 'Implementation-Version': project.ext.fullVersion, + 'Specification-Title': 'luckperms', + 'Specification-Vendor': 'LuckPerms', + 'Specification-Version': '1' + ) + } +} + +processResources { + filesMatching('META-INF/neoforge.mods.toml') { + expand 'version': project.ext.fullVersion + } +} + +shadowJar { + archiveFileName = "LuckPerms-NeoForge-${project.ext.fullVersion}.jar" + configurations = [shade] + + from { + project(':neoforge').tasks.shadowJar.archiveFile + } + + dependencies { + include(dependency('net.luckperms:.*')) + include(dependency('me.lucko.luckperms:.*')) + } +} + +artifacts { + archives shadowJar +} diff --git a/neoforge/loader/src/main/java/me/lucko/luckperms/neoforge/loader/NeoForgeLoaderPlugin.java b/neoforge/loader/src/main/java/me/lucko/luckperms/neoforge/loader/NeoForgeLoaderPlugin.java new file mode 100644 index 000000000..bd838191a --- /dev/null +++ b/neoforge/loader/src/main/java/me/lucko/luckperms/neoforge/loader/NeoForgeLoaderPlugin.java @@ -0,0 +1,74 @@ +/* + * 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.neoforge.loader; + +import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.fml.loading.FMLEnvironment; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.function.Supplier; + +@Mod(value = "luckperms") +public class NeoForgeLoaderPlugin implements Supplier { + private static final Logger LOGGER = LogManager.getLogger("luckperms"); + + private static final String JAR_NAME = "luckperms-neoforge.jarinjar"; + private static final String BOOTSTRAP_CLASS = "me.lucko.luckperms.neoforge.LPNeoForgeBootstrap"; + + private final ModContainer container; + + private JarInJarClassLoader loader; + private LoaderBootstrap plugin; + + public NeoForgeLoaderPlugin(final ModContainer modContainer, final IEventBus modBus) { + this.container = modContainer; + + if (FMLEnvironment.dist.isClient()) { + LOGGER.info("Skipping LuckPerms init (not supported on the client!)"); + return; + } + + this.loader = new JarInJarClassLoader(getClass().getClassLoader(), JAR_NAME); + modBus.addListener(this::onCommonSetup); + } + + @Override + public ModContainer get() { + return this.container; + } + + public void onCommonSetup(FMLCommonSetupEvent event) { + this.plugin = this.loader.instantiatePlugin(BOOTSTRAP_CLASS, Supplier.class, this); + this.plugin.onLoad(); + } + +} diff --git a/neoforge/loader/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/loader/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 000000000..4314d2cc7 --- /dev/null +++ b/neoforge/loader/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,16 @@ +modLoader="javafml" +loaderVersion="[1,)" +license="MIT" +issueTrackerURL="https://github.com/LuckPerms/LuckPerms/issues" +showAsResourcePack=false + +[[mods]] + modId="luckperms" + version="${version}" + displayName="LuckPerms" + displayURL="https://luckperms.net/" + logoFile="luckperms.png" + credits="Luck" + authors="Luck" + description="A permissions plugin for Minecraft servers." + displayTest="IGNORE_ALL_VERSION" \ No newline at end of file diff --git a/neoforge/loader/src/main/resources/luckperms.png b/neoforge/loader/src/main/resources/luckperms.png new file mode 100644 index 000000000..2e0ea669a Binary files /dev/null and b/neoforge/loader/src/main/resources/luckperms.png differ diff --git a/neoforge/loader/src/main/resources/pack.mcmeta b/neoforge/loader/src/main/resources/pack.mcmeta new file mode 100644 index 000000000..f6749d3ff --- /dev/null +++ b/neoforge/loader/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "LuckPerms resources", + "pack_format": 9 + } +} \ No newline at end of file diff --git a/neoforge/neoforge-api/build.gradle b/neoforge/neoforge-api/build.gradle new file mode 100644 index 000000000..b231d62e4 --- /dev/null +++ b/neoforge/neoforge-api/build.gradle @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.moddevgradle) +} + +sourceCompatibility = 17 +targetCompatibility = 21 + +neoForge { + version = project.neoForgeVersion + + validateAccessTransformers = true +} + +dependencies { + implementation project(':api') +} diff --git a/neoforge/neoforge-api/src/main/java/me/lucko/luckperms/neoforge/capabilities/UserCapability.java b/neoforge/neoforge-api/src/main/java/me/lucko/luckperms/neoforge/capabilities/UserCapability.java new file mode 100644 index 000000000..5fbdb85b2 --- /dev/null +++ b/neoforge/neoforge-api/src/main/java/me/lucko/luckperms/neoforge/capabilities/UserCapability.java @@ -0,0 +1,83 @@ +/* + * 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.neoforge.capabilities; + +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.capabilities.EntityCapability; + +/** + * A NeoForge {@link EntityCapability} that attaches LuckPerms functionality onto {@link ServerPlayer}s. + */ +public interface UserCapability { + + /** + * The identifier used for the capability + */ + ResourceLocation IDENTIFIER = ResourceLocation.fromNamespaceAndPath("luckperms", "user"); + + /** + * The capability instance. + */ + EntityCapability CAPABILITY = EntityCapability.createVoid(IDENTIFIER, UserCapability.class); + + /** + * Checks for a permission. + * + * @param permission the permission + * @return the result + */ + default boolean hasPermission(String permission) { + return checkPermission(permission).asBoolean(); + } + + /** + * Runs a permission check. + * + * @param permission the permission + * @return the result + */ + Tristate checkPermission(String permission); + + /** + * Runs a permission check. + * + * @param permission the permission + * @param queryOptions the query options + * @return the result + */ + Tristate checkPermission(String permission, QueryOptions queryOptions); + + /** + * Gets the user's currently query options. + * + * @return the current query options for the user + */ + QueryOptions getQueryOptions(); + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgeBootstrap.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgeBootstrap.java new file mode 100644 index 000000000..f1001cf0e --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgeBootstrap.java @@ -0,0 +1,287 @@ +/* + * 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.neoforge; + +import com.mojang.authlib.GameProfile; +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; +import me.lucko.luckperms.common.plugin.classpath.JarInJarClassPathAppender; +import me.lucko.luckperms.common.plugin.logging.Log4jPluginLogger; +import me.lucko.luckperms.common.plugin.logging.PluginLogger; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.neoforge.util.NeoForgeEventBusFacade; +import net.luckperms.api.platform.Platform; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import net.neoforged.neoforgespi.language.IModInfo; +import org.apache.logging.log4j.LogManager; +import org.apache.maven.artifact.versioning.ArtifactVersion; + +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.function.Supplier; + +/** + * Bootstrap plugin for LuckPerms running on Forge. + */ +public final class LPNeoForgeBootstrap implements LuckPermsBootstrap, LoaderBootstrap, BootstrappedWithLoader { + public static final String ID = "luckperms"; + + /** + * The plugin loader + */ + private final Supplier loader; + + /** + * The plugin logger + */ + private final PluginLogger logger; + + /** + * A scheduler adapter for the platform + */ + private final SchedulerAdapter schedulerAdapter; + + /** + * The plugin class path appender + */ + private final ClassPathAppender classPathAppender; + + /** + * A facade for the forge event bus, compatible with LP's jar-in-jar packaging + */ + private final NeoForgeEventBusFacade forgeEventBus; + + /** + * The plugin instance + */ + private final LPNeoForgePlugin 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 LPNeoForgeBootstrap(Supplier loader) { + this.loader = loader; + this.logger = new Log4jPluginLogger(LogManager.getLogger(LPNeoForgeBootstrap.ID)); + this.schedulerAdapter = new NeoForgeSchedulerAdapter(this); + this.classPathAppender = new JarInJarClassPathAppender(getClass().getClassLoader()); + this.forgeEventBus = new NeoForgeEventBusFacade(); + this.plugin = new LPNeoForgePlugin(this); + } + + // provide adapters + + @Override + public Object getLoader() { + return this.loader; + } + + @Override + public PluginLogger getPluginLogger() { + return this.logger; + } + + @Override + public SchedulerAdapter getScheduler() { + return this.schedulerAdapter; + } + + @Override + public ClassPathAppender getClassPathAppender() { + return this.classPathAppender; + } + + public void registerListeners(Object target) { + this.forgeEventBus.register(target); + } + + // lifecycle + + @Override + public void onLoad() { // called by the loader on FMLCommonSetupEvent + this.startTime = Instant.now(); + try { + this.plugin.load(); + } finally { + this.loadLatch.countDown(); + } + + this.forgeEventBus.register(this); + this.plugin.registerEarlyListeners(); + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public void onServerAboutToStart(ServerAboutToStartEvent event) { + this.server = event.getServer(); + try { + this.plugin.enable(); + } finally { + this.enableLatch.countDown(); + } + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onServerStopping(ServerStoppingEvent event) { + this.plugin.disable(); + this.forgeEventBus.unregisterAll(); + this.server = null; + } + + @Override + public CountDownLatch getLoadLatch() { + return this.loadLatch; + } + + @Override + public CountDownLatch getEnableLatch() { + return this.enableLatch; + } + + // MinecraftServer singleton getter + + public Optional getServer() { + return Optional.ofNullable(this.server); + } + + // provide information about the plugin + + @Override + public String getVersion() { + return "@version@"; + } + + @Override + public Instant getStartupTime() { + return this.startTime; + } + + // provide information about the platform + + @Override + public Platform.Type getType() { + return Platform.Type.NEOFORGE; + } + + @Override + public String getServerBrand() { + return ModList.get().getModContainerById("neoforge") + .map(ModContainer::getModInfo) + .map(IModInfo::getDisplayName) + .orElse("null"); + } + + @Override + public String getServerVersion() { + String forgeVersion = ModList.get().getModContainerById("neoforge") + .map(ModContainer::getModInfo) + .map(IModInfo::getVersion) + .map(ArtifactVersion::toString) + .orElse("null"); + + return getServer().map(MinecraftServer::getServerVersion).orElse("null") + "-" + forgeVersion; + } + + @Override + public Path getDataDirectory() { + return FMLPaths.CONFIGDIR.get().resolve(LPNeoForgeBootstrap.ID).toAbsolutePath(); + } + + @Override + public Optional getPlayer(UUID uniqueId) { + return getServer().map(MinecraftServer::getPlayerList).map(playerList -> playerList.getPlayer(uniqueId)); + } + + @Override + public Optional lookupUniqueId(String username) { + return getServer().map(MinecraftServer::getProfileCache).flatMap(profileCache -> profileCache.get(username)).map(GameProfile::getId); + } + + @Override + public Optional lookupUsername(UUID uniqueId) { + return getServer().map(MinecraftServer::getProfileCache).flatMap(profileCache -> profileCache.get(uniqueId)).map(GameProfile::getName); + } + + @Override + public int getPlayerCount() { + return getServer().map(MinecraftServer::getPlayerCount).orElse(0); + } + + @Override + public Collection getPlayerList() { + return getServer().map(MinecraftServer::getPlayerList).map(PlayerList::getPlayers).map(players -> { + List list = new ArrayList<>(players.size()); + for (ServerPlayer player : players) { + list.add(player.getGameProfile().getName()); + } + return list; + }).orElse(Collections.emptyList()); + } + + @Override + public Collection getOnlinePlayers() { + return getServer().map(MinecraftServer::getPlayerList).map(PlayerList::getPlayers).map(players -> { + List list = new ArrayList<>(players.size()); + for (ServerPlayer player : players) { + list.add(player.getGameProfile().getId()); + } + return list; + }).orElse(Collections.emptyList()); + } + + @Override + public boolean isPlayerOnline(UUID uniqueId) { + return getServer().map(MinecraftServer::getPlayerList).map(playerList -> playerList.getPlayer(uniqueId)).isPresent(); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgePlugin.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgePlugin.java new file mode 100644 index 000000000..453614480 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgePlugin.java @@ -0,0 +1,251 @@ +/* + * 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.neoforge; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.calculator.CalculatorFactory; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import me.lucko.luckperms.common.dependencies.Dependency; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.messaging.MessagingFactory; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.model.manager.track.StandardTrackManager; +import me.lucko.luckperms.common.model.manager.user.StandardUserManager; +import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; +import me.lucko.luckperms.common.sender.DummyConsoleSender; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.neoforge.calculator.NeoForgeCalculatorFactory; +import me.lucko.luckperms.neoforge.capabilities.UserCapabilityListener; +import me.lucko.luckperms.neoforge.context.NeoForgeContextManager; +import me.lucko.luckperms.neoforge.context.NeoForgePlayerCalculator; +import me.lucko.luckperms.neoforge.listeners.NeoForgeAutoOpListener; +import me.lucko.luckperms.neoforge.listeners.NeoForgeCommandListUpdater; +import me.lucko.luckperms.neoforge.listeners.NeoForgeConnectionListener; +import me.lucko.luckperms.neoforge.listeners.NeoForgePlatformListener; +import me.lucko.luckperms.neoforge.messaging.NeoForgeMessagingFactory; +import me.lucko.luckperms.neoforge.messaging.PluginMessageMessenger; +import me.lucko.luckperms.neoforge.service.NeoForgePermissionHandlerListener; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.PlayerList; +import net.neoforged.fml.ModContainer; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * LuckPerms implementation for Forge. + */ +public class LPNeoForgePlugin extends AbstractLuckPermsPlugin { + private final LPNeoForgeBootstrap bootstrap; + + private NeoForgeSenderFactory senderFactory; + private NeoForgeConnectionListener connectionListener; + private NeoForgeCommandExecutor commandManager; + private StandardUserManager userManager; + private StandardGroupManager groupManager; + private StandardTrackManager trackManager; + private NeoForgeContextManager contextManager; + + public LPNeoForgePlugin(LPNeoForgeBootstrap bootstrap) { + this.bootstrap = bootstrap; + } + + @Override + public LPNeoForgeBootstrap getBootstrap() { + return this.bootstrap; + } + + protected void registerEarlyListeners() { + this.connectionListener = new NeoForgeConnectionListener(this); + this.bootstrap.registerListeners(this.connectionListener); + + NeoForgePlatformListener platformListener = new NeoForgePlatformListener(this); + this.bootstrap.registerListeners(platformListener); + + UserCapabilityListener userCapabilityListener = new UserCapabilityListener(); + this.bootstrap.registerListeners(userCapabilityListener); + + NeoForgePermissionHandlerListener permissionHandlerListener = new NeoForgePermissionHandlerListener(this); + this.bootstrap.registerListeners(permissionHandlerListener); + + this.commandManager = new NeoForgeCommandExecutor(this); + this.bootstrap.registerListeners(this.commandManager); + + PluginMessageMessenger.registerChannel(); + } + + @Override + protected void setupSenderFactory() { + this.senderFactory = new NeoForgeSenderFactory(this); + } + + @Override + protected Set getGlobalDependencies() { + Set dependencies = super.getGlobalDependencies(); + dependencies.add(Dependency.CONFIGURATE_CORE); + dependencies.add(Dependency.CONFIGURATE_HOCON); + dependencies.add(Dependency.HOCON_CONFIG); + return dependencies; + } + + @Override + protected ConfigurationAdapter provideConfigurationAdapter() { + return new NeoForgeConfigAdapter(this, resolveConfig("luckperms.conf")); + } + + @Override + protected void registerPlatformListeners() { + // Too late for Forge, registered in #registerEarlyListeners + } + + @Override + protected MessagingFactory provideMessagingFactory() { + return new NeoForgeMessagingFactory(this); + } + + @Override + protected void registerCommands() { + // Too late for Forge, registered in #registerEarlyListeners + } + + @Override + protected void setupManagers() { + this.userManager = new StandardUserManager(this); + this.groupManager = new StandardGroupManager(this); + this.trackManager = new StandardTrackManager(this); + } + + @Override + protected CalculatorFactory provideCalculatorFactory() { + return new NeoForgeCalculatorFactory(this); + } + + @Override + protected void setupContextManager() { + this.contextManager = new NeoForgeContextManager(this); + + NeoForgePlayerCalculator playerCalculator = new NeoForgePlayerCalculator(this, getConfiguration().get(ConfigKeys.DISABLED_CONTEXTS)); + this.bootstrap.registerListeners(playerCalculator); + this.contextManager.registerCalculator(playerCalculator); + } + + @Override + protected void setupPlatformHooks() { + } + + @Override + protected AbstractEventBus provideEventBus(LuckPermsApiProvider provider) { + return new NeoForgeEventBus(this, provider); + } + + @Override + protected void registerApiOnPlatform(LuckPerms api) { + } + + @Override + protected void performFinalSetup() { + // register autoop listener + if (getConfiguration().get(ConfigKeys.AUTO_OP)) { + getApiProvider().getEventBus().subscribe(new NeoForgeAutoOpListener(this)); + } + + // register forge command list updater + if (getConfiguration().get(ConfigKeys.UPDATE_CLIENT_COMMAND_LIST)) { + getApiProvider().getEventBus().subscribe(new NeoForgeCommandListUpdater(this)); + } + } + + @Override + public Optional getQueryOptionsForUser(User user) { + return this.bootstrap.getPlayer(user.getUniqueId()).map(player -> this.contextManager.getQueryOptions(player)); + } + + @Override + public Stream getOnlineSenders() { + return Stream.concat( + Stream.of(getConsoleSender()), + this.bootstrap.getServer() + .map(MinecraftServer::getPlayerList) + .map(PlayerList::getPlayers) + .map(players -> players.stream().map(player -> this.senderFactory.wrap(player.createCommandSourceStack()))).orElseGet(Stream::empty) + ); + } + + @Override + public Sender getConsoleSender() { + return this.bootstrap.getServer() + .map(server -> this.senderFactory.wrap(server.createCommandSourceStack())) + .orElseGet(() -> new DummyConsoleSender(this) { + @Override + public void sendMessage(Component message) { + LPNeoForgePlugin.this.bootstrap.getPluginLogger().info(PlainTextComponentSerializer.plainText().serialize(TranslationManager.render(message))); + } + }); + } + + public NeoForgeSenderFactory getSenderFactory() { + return this.senderFactory; + } + + @Override + public NeoForgeConnectionListener getConnectionListener() { + return this.connectionListener; + } + + @Override + public NeoForgeCommandExecutor getCommandManager() { + return this.commandManager; + } + + @Override + public StandardUserManager getUserManager() { + return this.userManager; + } + + @Override + public StandardGroupManager getGroupManager() { + return this.groupManager; + } + + @Override + public StandardTrackManager getTrackManager() { + return this.trackManager; + } + + @Override + public NeoForgeContextManager getContextManager() { + return this.contextManager; + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeCommandExecutor.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeCommandExecutor.java new file mode 100644 index 000000000..0a5ef3767 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeCommandExecutor.java @@ -0,0 +1,108 @@ +/* + * 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.neoforge; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.lucko.luckperms.common.command.BrigadierCommandExecutor; +import me.lucko.luckperms.common.sender.Sender; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; +import java.util.ListIterator; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; + +public class NeoForgeCommandExecutor extends BrigadierCommandExecutor { + + private final LPNeoForgePlugin plugin; + + public NeoForgeCommandExecutor(LPNeoForgePlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + @SubscribeEvent + public void onRegisterCommands(RegisterCommandsEvent event) { + for (String alias : COMMAND_ALIASES) { + LiteralCommandNode command = Commands.literal(alias).executes(this).build(); + ArgumentCommandNode argument = Commands.argument("args", StringArgumentType.greedyString()) + .suggests(this) + .executes(this) + .build(); + + command.addChild(argument); + event.getDispatcher().getRoot().addChild(command); + } + } + + @Override + public Sender getSender(CommandSourceStack source) { + return this.plugin.getSenderFactory().wrap(source); + } + + @Override + public List resolveSelectors(CommandSourceStack source, List args) { + // usage of @ selectors requires at least level 2 permission + CommandSourceStack atAllowedSource = source.hasPermission(2) ? source : source.withPermission(2); + for (ListIterator it = args.listIterator(); it.hasNext(); ) { + String arg = it.next(); + if (arg.isEmpty() || arg.charAt(0) != '@') { + continue; + } + + List matchedPlayers; + try { + matchedPlayers = EntityArgument.entities().parse(new StringReader(arg)).findPlayers(atAllowedSource); + } catch (CommandSyntaxException e) { + this.plugin.getLogger().warn("Error parsing selector '" + arg + "' for " + source + " executing " + args, e); + continue; + } + + if (matchedPlayers.isEmpty()) { + continue; + } + + if (matchedPlayers.size() > 1) { + this.plugin.getLogger().warn("Error parsing selector '" + arg + "' for " + source + " executing " + args + + ": ambiguous result (more than one player matched) - " + matchedPlayers); + continue; + } + + ServerPlayer player = matchedPlayers.get(0); + it.set(player.getStringUUID()); + } + + return args; + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeConfigAdapter.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeConfigAdapter.java new file mode 100644 index 000000000..5cb671840 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeConfigAdapter.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.neoforge; + +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 NeoForgeConfigAdapter extends ConfigurateConfigAdapter { + public NeoForgeConfigAdapter(LuckPermsPlugin plugin, Path path) { + super(plugin, path); + } + + @Override + protected ConfigurationLoader createLoader(Path path) { + return HoconConfigurationLoader.builder().setPath(path).build(); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeEventBus.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeEventBus.java new file mode 100644 index 000000000..9936ac8fc --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeEventBus.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.neoforge; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import net.neoforged.fml.ModContainer; + +public class NeoForgeEventBus extends AbstractEventBus { + public NeoForgeEventBus(LuckPermsPlugin plugin, LuckPermsApiProvider apiProvider) { + super(plugin, apiProvider); + } + + @Override + protected ModContainer checkPlugin(Object modContainer) throws IllegalArgumentException { + if (modContainer instanceof ModContainer container) { + return container; + } + + throw new IllegalArgumentException("Object " + modContainer + " (" + modContainer.getClass().getName() + ") is not a ModContainer."); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeSchedulerAdapter.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeSchedulerAdapter.java new file mode 100644 index 000000000..4a257f700 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeSchedulerAdapter.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.neoforge; + +import me.lucko.luckperms.common.plugin.scheduler.AbstractJavaScheduler; + +import java.util.concurrent.Executor; + +public class NeoForgeSchedulerAdapter extends AbstractJavaScheduler { + private final Executor sync; + + public NeoForgeSchedulerAdapter(LPNeoForgeBootstrap bootstrap) { + super(bootstrap); + this.sync = r -> bootstrap.getServer().orElseThrow(() -> new IllegalStateException("Server not ready")).executeBlocking(r); + } + + @Override + public Executor sync() { + return this.sync; + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeSenderFactory.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeSenderFactory.java new file mode 100644 index 000000000..25049df2e --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeSenderFactory.java @@ -0,0 +1,124 @@ +/* + * 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.neoforge; + +import com.mojang.brigadier.ParseResults; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.sender.SenderFactory; +import me.lucko.luckperms.common.verbose.VerboseCheckTarget; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.neoforge.capabilities.UserCapability; +import me.lucko.luckperms.neoforge.capabilities.UserCapabilityImpl; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.luckperms.api.util.Tristate; +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.chat.Component.Serializer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.rcon.RconConsoleSource; +import net.minecraft.world.entity.player.Player; + +import java.util.Locale; +import java.util.UUID; + +public class NeoForgeSenderFactory extends SenderFactory { + public NeoForgeSenderFactory(LPNeoForgePlugin plugin) { + super(plugin); + } + + @Override + protected UUID getUniqueId(CommandSourceStack commandSource) { + if (commandSource.getEntity() instanceof Player) { + return commandSource.getEntity().getUUID(); + } + return Sender.CONSOLE_UUID; + } + + @Override + protected String getName(CommandSourceStack commandSource) { + if (commandSource.getEntity() instanceof Player) { + return commandSource.getTextName(); + } + return Sender.CONSOLE_NAME; + } + + @Override + protected void sendMessage(CommandSourceStack sender, Component message) { + Locale locale; + if (sender.getEntity() instanceof ServerPlayer) { + ServerPlayer player = (ServerPlayer) sender.getEntity(); + UserCapabilityImpl user = UserCapabilityImpl.get(player); + locale = user.getLocale(player); + } else { + locale = null; + } + + sender.sendSuccess(() -> toNativeText(TranslationManager.render(message, locale)), false); + } + + @Override + protected Tristate getPermissionValue(CommandSourceStack commandSource, String node) { + if (commandSource.getEntity() instanceof ServerPlayer) { + ServerPlayer player = (ServerPlayer) commandSource.getEntity(); + UserCapability user = UserCapabilityImpl.get(player); + return user.checkPermission(node); + } + + VerboseCheckTarget target = VerboseCheckTarget.internal(commandSource.getTextName()); + getPlugin().getVerboseHandler().offerPermissionCheckEvent(CheckOrigin.PLATFORM_API_HAS_PERMISSION, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, node, TristateResult.UNDEFINED); + getPlugin().getPermissionRegistry().offer(node); + return Tristate.UNDEFINED; + } + + @Override + protected boolean hasPermission(CommandSourceStack commandSource, String node) { + return getPermissionValue(commandSource, node).asBoolean(); + } + + @Override + protected void performCommand(CommandSourceStack sender, String command) { + ParseResults results = sender.getServer().getCommands().getDispatcher().parse(command, sender); + sender.getServer().getCommands().performCommand(results, command); + } + + @Override + protected boolean isConsole(CommandSourceStack sender) { + CommandSource output = sender.source; + return output == sender.getServer() || // Console + output.getClass() == RconConsoleSource.class || // Rcon + (output == CommandSource.NULL && sender.getTextName().equals("")); // Functions + } + + public static net.minecraft.network.chat.Component toNativeText(Component component) { + return Serializer.fromJson(GsonComponentSerializer.gson().serializeToTree(component), RegistryAccess.EMPTY); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/calculator/NeoForgeCalculatorFactory.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/calculator/NeoForgeCalculatorFactory.java new file mode 100644 index 000000000..afa733b30 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/calculator/NeoForgeCalculatorFactory.java @@ -0,0 +1,77 @@ +/* + * 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.neoforge.calculator; + +import me.lucko.luckperms.common.cacheddata.CacheMetadata; +import me.lucko.luckperms.common.calculator.CalculatorFactory; +import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.processor.DirectProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.RegexProcessor; +import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import me.lucko.luckperms.neoforge.context.NeoForgeContextManager; +import net.luckperms.api.query.QueryOptions; + +import java.util.ArrayList; +import java.util.List; + +public class NeoForgeCalculatorFactory implements CalculatorFactory { + private final LPNeoForgePlugin plugin; + + public NeoForgeCalculatorFactory(LPNeoForgePlugin plugin) { + this.plugin = plugin; + } + + @Override + public PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata) { + List processors = new ArrayList<>(5); + + processors.add(new DirectProcessor()); + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) { + processors.add(new RegexProcessor()); + } + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS)) { + processors.add(new WildcardProcessor()); + } + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS_SPONGE)) { + processors.add(new SpongeWildcardProcessor()); + } + + boolean integratedOwner = queryOptions.option(NeoForgeContextManager.INTEGRATED_SERVER_OWNER).orElse(false); + if (integratedOwner && this.plugin.getConfiguration().get(ConfigKeys.INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS)) { + processors.add(ServerOwnerProcessor.INSTANCE); + } + + return new PermissionCalculator(this.plugin, metadata, processors); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/calculator/ServerOwnerProcessor.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/calculator/ServerOwnerProcessor.java new file mode 100644 index 000000000..0773068c7 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/calculator/ServerOwnerProcessor.java @@ -0,0 +1,51 @@ +/* + * 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.neoforge.calculator; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import net.luckperms.api.util.Tristate; + +/** + * Permission processor which is added to the owner of an Integrated server to + * simply return true if no other processors match. + */ +public class ServerOwnerProcessor extends AbstractPermissionProcessor implements PermissionProcessor { + private static final TristateResult TRUE_RESULT = new TristateResult.Factory(ServerOwnerProcessor.class).result(Tristate.TRUE); + + public static final ServerOwnerProcessor INSTANCE = new ServerOwnerProcessor(); + + private ServerOwnerProcessor() { + + } + + @Override + public TristateResult hasPermission(String permission) { + return TRUE_RESULT; + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/capabilities/UserCapabilityImpl.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/capabilities/UserCapabilityImpl.java new file mode 100644 index 000000000..eec08754f --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/capabilities/UserCapabilityImpl.java @@ -0,0 +1,163 @@ +/* + * 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.neoforge.capabilities; + +import java.util.Optional; +import me.lucko.luckperms.common.cacheddata.type.PermissionCache; +import me.lucko.luckperms.common.context.manager.QueryOptionsCache; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.neoforge.context.NeoForgeContextManager; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +public class UserCapabilityImpl implements UserCapability { + + private static Optional getCapability(Player player) { + return Optional.ofNullable(player.getCapability(CAPABILITY)); + } + + /** + * Gets a {@link UserCapability} for a given {@link ServerPlayer}. + * + * @param player the player + * @return the capability + */ + public static @NotNull UserCapabilityImpl get(@NotNull Player player) { + return (UserCapabilityImpl) getCapability(player).orElseThrow(() -> new IllegalStateException("Capability missing for " + player.getUUID())); + } + + /** + * Gets a {@link UserCapability} for a given {@link ServerPlayer}. + * + * @param player the player + * @return the capability, or null + */ + public static @Nullable UserCapabilityImpl getNullable(@NotNull Player player) { + return (UserCapabilityImpl) getCapability(player).orElse(null); + } + + private boolean initialised = false; + private boolean invalidated = false; + + private User user; + private QueryOptionsCache queryOptionsCache; + private String language; + private Locale locale; + + public UserCapabilityImpl() { + + } + + public void initialise(UserCapabilityImpl previous) { + this.user = previous.user; + this.queryOptionsCache = previous.queryOptionsCache; + this.language = previous.language; + this.locale = previous.locale; + this.initialised = true; + } + + public void initialise(User user, ServerPlayer player, NeoForgeContextManager contextManager) { + this.user = user; + this.queryOptionsCache = new QueryOptionsCache<>(player, contextManager); + this.initialised = true; + } + + private void assertInitialised() { + if (!this.initialised) { + throw new IllegalStateException("Capability has not been initialised"); + } + if (this.invalidated) { + throw new IllegalStateException("Capability has been invalidated"); + } + } + + public void invalidate() { + this.invalidated = false; + this.user = null; + this.queryOptionsCache = null; + this.language = null; + this.locale = null; + } + + @Override + public Tristate checkPermission(String permission) { + assertInitialised(); + + if (permission == null) { + throw new NullPointerException("permission"); + } + + return checkPermission(permission, this.queryOptionsCache.getQueryOptions()); + } + + @Override + public Tristate checkPermission(String permission, QueryOptions queryOptions) { + assertInitialised(); + + if (permission == null) { + throw new NullPointerException("permission"); + } + + if (queryOptions == null) { + throw new NullPointerException("queryOptions"); + } + + PermissionCache cache = this.user.getCachedData().getPermissionData(queryOptions); + return cache.checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + } + + public User getUser() { + assertInitialised(); + return this.user; + } + + @Override + public QueryOptions getQueryOptions() { + return getQueryOptionsCache().getQueryOptions(); + } + + public QueryOptionsCache getQueryOptionsCache() { + assertInitialised(); + return this.queryOptionsCache; + } + + public Locale getLocale(ServerPlayer player) { + if (this.language == null || !this.language.equals(player.getLanguage())) { + this.language = player.getLanguage(); + this.locale = TranslationManager.parseLocale(this.language); + } + + return this.locale; + } +} \ No newline at end of file diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/capabilities/UserCapabilityListener.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/capabilities/UserCapabilityListener.java new file mode 100644 index 000000000..c34f66473 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/capabilities/UserCapabilityListener.java @@ -0,0 +1,69 @@ +/* + * 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.neoforge.capabilities; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Player; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; + +public class UserCapabilityListener { + + @SubscribeEvent + public void onRegisterCapabilities(RegisterCapabilitiesEvent event) { + event.registerEntity( + UserCapability.CAPABILITY, + EntityType.PLAYER, + (player, ctx) -> { + if (!(player instanceof ServerPlayer)) { + // Don't attach to LocalPlayer + return null; + } + return new UserCapabilityImpl(); + } + ); + } + + @SubscribeEvent + public void onPlayerClone(PlayerEvent.Clone event) { + Player previousPlayer = event.getOriginal(); + Player currentPlayer = event.getEntity(); + + try { + UserCapabilityImpl previous = UserCapabilityImpl.get(previousPlayer); + UserCapabilityImpl current = UserCapabilityImpl.get(currentPlayer); + + current.initialise(previous); + previous.invalidate(); + current.getQueryOptionsCache().invalidate(); + } catch (IllegalStateException e) { + // continue on if we cannot copy original data + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgeContextManager.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgeContextManager.java new file mode 100644 index 000000000..8aca67312 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgeContextManager.java @@ -0,0 +1,79 @@ +/* + * 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.neoforge.context; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.context.manager.ContextManager; +import me.lucko.luckperms.common.context.manager.QueryOptionsCache; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import me.lucko.luckperms.neoforge.capabilities.UserCapabilityImpl; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.query.OptionKey; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.level.ServerPlayer; + +import java.util.UUID; + +public class NeoForgeContextManager extends ContextManager { + public static final OptionKey INTEGRATED_SERVER_OWNER = OptionKey.of("integrated_server_owner", Boolean.class); + + public NeoForgeContextManager(LPNeoForgePlugin plugin) { + super(plugin, ServerPlayer.class, ServerPlayer.class); + } + + @Override + public UUID getUniqueId(ServerPlayer player) { + return player.getUUID(); + } + + @Override + public QueryOptionsCache getCacheFor(ServerPlayer subject) { + if (subject == null) { + throw new NullPointerException("subject"); + } + + return UserCapabilityImpl.get(subject).getQueryOptionsCache(); + } + + @Override + public QueryOptions formQueryOptions(ServerPlayer subject, ImmutableContextSet contextSet) { + QueryOptions.Builder builder = this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder(); + if (subject.getServer() != null && subject.getServer().isSingleplayerOwner(subject.getGameProfile())) { + builder.option(INTEGRATED_SERVER_OWNER, true); + } + + return builder.context(contextSet).build(); + } + + @Override + public void invalidateCache(ServerPlayer subject) { + UserCapabilityImpl capability = UserCapabilityImpl.getNullable(subject); + if (capability != null) { + capability.getQueryOptionsCache().invalidate(); + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgePlayerCalculator.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgePlayerCalculator.java new file mode 100644 index 000000000..c8595e605 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgePlayerCalculator.java @@ -0,0 +1,147 @@ +/* + * 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.neoforge.context; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.luckperms.api.context.Context; +import net.luckperms.api.context.ContextCalculator; +import net.luckperms.api.context.ContextConsumer; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.DefaultContextKeys; +import net.luckperms.api.context.ImmutableContextSet; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.storage.ServerLevelData; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Set; + +public class NeoForgePlayerCalculator implements ContextCalculator { + /** + * GameType.NOT_SET(-1, "") was removed in 1.17 + */ + private static final int GAME_MODE_NOT_SET = -1; + + private final LPNeoForgePlugin plugin; + + private final boolean gamemode; + private final boolean world; + private final boolean dimensionType; + + public NeoForgePlayerCalculator(LPNeoForgePlugin plugin, Set disabled) { + this.plugin = plugin; + this.gamemode = !disabled.contains(DefaultContextKeys.GAMEMODE_KEY); + this.world = !disabled.contains(DefaultContextKeys.WORLD_KEY); + this.dimensionType = !disabled.contains(DefaultContextKeys.DIMENSION_TYPE_KEY); + } + + @Override + public void calculate(@NonNull ServerPlayer target, @NonNull ContextConsumer consumer) { + ServerLevel level = target.serverLevel(); + if (this.dimensionType) { + consumer.accept(DefaultContextKeys.DIMENSION_TYPE_KEY, getContextKey(level.dimension().location())); + } + + ServerLevelData levelData = (ServerLevelData) level.getLevelData(); + if (this.world) { + this.plugin.getConfiguration().get(ConfigKeys.WORLD_REWRITES).rewriteAndSubmit(levelData.getLevelName(), consumer); + } + + GameType gameMode = target.gameMode.getGameModeForPlayer(); + if (this.gamemode && gameMode.getId() != GAME_MODE_NOT_SET) { + consumer.accept(DefaultContextKeys.GAMEMODE_KEY, gameMode.getName()); + } + } + + @Override + public @NonNull ContextSet estimatePotentialContexts() { + ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); + + if (this.gamemode) { + for (GameType gameType : GameType.values()) { + if (gameType.getId() == GAME_MODE_NOT_SET) { + continue; + } + + builder.add(DefaultContextKeys.GAMEMODE_KEY, gameType.getName()); + } + } + + MinecraftServer server = this.plugin.getBootstrap().getServer().orElse(null); + if (this.dimensionType && server != null) { + server.registryAccess().registry(Registries.DIMENSION_TYPE).ifPresent(registry -> { + for (ResourceLocation resourceLocation : registry.keySet()) { + builder.add(DefaultContextKeys.DIMENSION_TYPE_KEY, getContextKey(resourceLocation)); + } + }); + } + + if (this.world && server != null) { + for (ServerLevel level : server.getAllLevels()) { + ServerLevelData levelData = (ServerLevelData) level.getLevelData(); + if (Context.isValidValue(levelData.getLevelName())) { + builder.add(DefaultContextKeys.WORLD_KEY, levelData.getLevelName()); + } + } + } + + return builder.build(); + } + + private static String getContextKey(ResourceLocation key) { + if (key.getNamespace().equals("minecraft")) { + return key.getPath(); + } + return key.toString(); + } + + @SubscribeEvent + public void onPlayerChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) { + if (!(this.world || this.dimensionType)) { + return; + } + + this.plugin.getContextManager().signalContextUpdate((ServerPlayer) event.getEntity()); + } + + @SubscribeEvent + public void onPlayerChangeGameMode(PlayerEvent.PlayerChangeGameModeEvent event) { + if (!this.gamemode || event.getNewGameMode().getId() == GAME_MODE_NOT_SET) { + return; + } + + this.plugin.getContextManager().signalContextUpdate((ServerPlayer) event.getEntity()); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeAutoOpListener.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeAutoOpListener.java new file mode 100644 index 000000000..d40031f61 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeAutoOpListener.java @@ -0,0 +1,97 @@ +/* + * 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.neoforge.listeners; + +import me.lucko.luckperms.common.api.implementation.ApiUser; +import me.lucko.luckperms.common.event.LuckPermsEventListener; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.luckperms.api.event.EventBus; +import net.luckperms.api.event.context.ContextUpdateEvent; +import net.luckperms.api.event.user.UserDataRecalculateEvent; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Map; + +public class NeoForgeAutoOpListener implements LuckPermsEventListener { + private static final String NODE = "luckperms.autoop"; + + private final LPNeoForgePlugin plugin; + + public NeoForgeAutoOpListener(LPNeoForgePlugin plugin) { + this.plugin = plugin; + } + + @Override + public void bind(EventBus bus) { + bus.subscribe(ContextUpdateEvent.class, this::onContextUpdate); + bus.subscribe(UserDataRecalculateEvent.class, this::onUserDataRecalculate); + } + + private void onContextUpdate(ContextUpdateEvent event) { + event.getSubject(ServerPlayer.class).ifPresent(player -> refreshAutoOp(player, true)); + } + + private void onUserDataRecalculate(UserDataRecalculateEvent event) { + User user = ApiUser.cast(event.getUser()); + this.plugin.getBootstrap().getPlayer(user.getUniqueId()).ifPresent(player -> refreshAutoOp(player, false)); + } + + private void refreshAutoOp(ServerPlayer player, boolean callerIsSync) { + if (!callerIsSync && !this.plugin.getBootstrap().getServer().isPresent()) { + return; + } + + User user = this.plugin.getUserManager().getIfLoaded(player.getUUID()); + + boolean value; + if (user != null) { + QueryOptions queryOptions = this.plugin.getContextManager().getQueryOptions(player); + Map permData = user.getCachedData().getPermissionData(queryOptions).getPermissionMap(); + value = permData.getOrDefault(NODE, false); + } else { + value = false; + } + + if (callerIsSync) { + setOp(player, value); + } else { + this.plugin.getBootstrap().getScheduler().executeSync(() -> setOp(player, value)); + } + } + + private void setOp(ServerPlayer player, boolean value) { + this.plugin.getBootstrap().getServer().ifPresent(server -> { + if (value) { + server.getPlayerList().op(player.getGameProfile()); + } else { + server.getPlayerList().deop(player.getGameProfile()); + } + }); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeCommandListUpdater.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeCommandListUpdater.java new file mode 100644 index 000000000..8de108643 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeCommandListUpdater.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.neoforge.listeners; + +import com.github.benmanes.caffeine.cache.LoadingCache; +import me.lucko.luckperms.common.api.implementation.ApiGroup; +import me.lucko.luckperms.common.cache.BufferedRequest; +import me.lucko.luckperms.common.event.LuckPermsEventListener; +import me.lucko.luckperms.common.util.CaffeineFactory; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.luckperms.api.event.EventBus; +import net.luckperms.api.event.context.ContextUpdateEvent; +import net.luckperms.api.event.group.GroupDataRecalculateEvent; +import net.luckperms.api.event.user.UserDataRecalculateEvent; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Calls {@link net.minecraft.server.players.PlayerList#sendPlayerPermissionLevel(ServerPlayer)} when a players permissions change. + */ +public class NeoForgeCommandListUpdater implements LuckPermsEventListener { + private final LPNeoForgePlugin plugin; + private final LoadingCache sendingBuffers = CaffeineFactory.newBuilder() + .expireAfterAccess(10, TimeUnit.SECONDS) + .build(SendBuffer::new); + + public NeoForgeCommandListUpdater(LPNeoForgePlugin plugin) { + this.plugin = plugin; + } + + @Override + public void bind(EventBus bus) { + bus.subscribe(UserDataRecalculateEvent.class, this::onUserDataRecalculate); + bus.subscribe(GroupDataRecalculateEvent.class, this::onGroupDataRecalculate); + bus.subscribe(ContextUpdateEvent.class, this::onContextUpdate); + } + + private void onUserDataRecalculate(UserDataRecalculateEvent e) { + requestUpdate(e.getUser().getUniqueId()); + } + + private void onGroupDataRecalculate(GroupDataRecalculateEvent e) { + plugin.getUserManager().getAll().values().forEach(user -> { + if (user.resolveInheritanceTree(user.getQueryOptions()).contains(ApiGroup.cast(e.getGroup()))) { + requestUpdate(user.getUniqueId()); + } + }); + } + + private void onContextUpdate(ContextUpdateEvent e) { + e.getSubject(ServerPlayer.class).ifPresent(p -> requestUpdate(p.getUUID())); + } + + private void requestUpdate(UUID uniqueId) { + if (!this.plugin.getBootstrap().isPlayerOnline(uniqueId)) { + return; + } + + // Buffer the request to send a commands update. + SendBuffer sendBuffer = this.sendingBuffers.get(uniqueId); + if (sendBuffer != null) { + sendBuffer.request(); + } + } + + // Called when the buffer times out. + private void sendUpdate(UUID uniqueId) { + this.plugin.getBootstrap().getScheduler().sync().execute(() -> { + this.plugin.getBootstrap().getPlayer(uniqueId).ifPresent(player -> { + MinecraftServer server = player.getServer(); + if (server != null) { + server.getPlayerList().sendPlayerPermissionLevel(player); + } + }); + }); + } + + private final class SendBuffer extends BufferedRequest { + private final UUID uniqueId; + + SendBuffer(UUID uniqueId) { + super(500, TimeUnit.MILLISECONDS, NeoForgeCommandListUpdater.this.plugin.getBootstrap().getScheduler()); + this.uniqueId = uniqueId; + } + + @Override + protected Void perform() { + sendUpdate(this.uniqueId); + return null; + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeConnectionListener.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeConnectionListener.java new file mode 100644 index 000000000..c4a91e165 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeConnectionListener.java @@ -0,0 +1,158 @@ +/* + * 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.neoforge.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.neoforge.NeoForgeSenderFactory; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import me.lucko.luckperms.neoforge.capabilities.UserCapabilityImpl; +import me.lucko.luckperms.neoforge.util.AsyncConfigurationTask; +import net.kyori.adventure.text.Component; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketListener; +import net.minecraft.network.protocol.login.ClientboundLoginDisconnectPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ConfigurationTask; +import net.minecraft.server.network.ServerConfigurationPacketListenerImpl; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.network.event.RegisterConfigurationTasksEvent; + +public class NeoForgeConnectionListener extends AbstractConnectionListener { + private static final ConfigurationTask.Type USER_LOGIN_TASK_TYPE = new ConfigurationTask.Type("luckperms:user_login"); + + private final LPNeoForgePlugin plugin; + + public NeoForgeConnectionListener(LPNeoForgePlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + @SubscribeEvent + public void onGatherLoginConfigurationTasks(RegisterConfigurationTasksEvent event) { + PacketListener packetListener = event.getListener(); + if (!(packetListener instanceof ServerConfigurationPacketListenerImpl)) { + return; + } + + GameProfile gameProfile = ((ServerConfigurationPacketListenerImpl) packetListener).getOwner(); + if (gameProfile == null) { + return; + } + + String username = gameProfile.getName(); + UUID uniqueId = gameProfile.getId(); + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing pre-login (sync phase) for " + uniqueId + " - " + username); + } + + event.register(new AsyncConfigurationTask(this.plugin, USER_LOGIN_TASK_TYPE, () -> CompletableFuture.runAsync(() -> { + onPlayerNegotiationAsync(event.getListener().getConnection(), uniqueId, username); + }, this.plugin.getBootstrap().getScheduler().async()))); + } + + private void onPlayerNegotiationAsync(Connection connection, UUID uniqueId, String username) { + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing pre-login (async phase) for " + uniqueId + " - " + username); + } + + /* Actually process the login for the connection. + We do this here to delay the login until the data is ready. + If the login gets cancelled later on, then this will be cleaned up. + + This includes: + - loading uuid data + - loading permissions + - creating a user instance in the UserManager for this connection. + - setting up cached data. */ + try { + User user = loadUser(uniqueId, username); + recordConnection(uniqueId); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(uniqueId, username, user); + } catch (Exception ex) { + this.plugin.getLogger().severe("Exception occurred whilst loading data for " + uniqueId + " - " + username, ex); + + if (this.plugin.getConfiguration().get(ConfigKeys.CANCEL_FAILED_LOGINS)) { + Component component = TranslationManager.render(Message.LOADING_DATABASE_ERROR.build()); + connection.send(new ClientboundLoginDisconnectPacket(NeoForgeSenderFactory.toNativeText(component))); + connection.disconnect(NeoForgeSenderFactory.toNativeText(component)); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(uniqueId, username, null); + } + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + ServerPlayer player = (ServerPlayer) event.getEntity(); + GameProfile profile = player.getGameProfile(); + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing post-login for " + profile.getId() + " - " + profile.getName()); + } + + User user = this.plugin.getUserManager().getIfLoaded(profile.getId()); + + if (user == null) { + if (!getUniqueConnections().contains(profile.getId())) { + this.plugin.getLogger().warn("User " + profile.getId() + " - " + profile.getName() + + " doesn't have data pre-loaded, they have never been processed during pre-login in this session."); + } else { + this.plugin.getLogger().warn("User " + profile.getId() + " - " + profile.getName() + + " doesn't currently have data pre-loaded, but they have been processed before in this session."); + } + + Component component = TranslationManager.render(Message.LOADING_STATE_ERROR.build(), player.getLanguage()); + if (this.plugin.getConfiguration().get(ConfigKeys.CANCEL_FAILED_LOGINS)) { + player.connection.disconnect(NeoForgeSenderFactory.toNativeText(component)); + return; + } else { + player.sendSystemMessage(NeoForgeSenderFactory.toNativeText(component)); + } + } + + // initialise capability + UserCapabilityImpl userCapability = UserCapabilityImpl.get(player); + userCapability.initialise(user, player, this.plugin.getContextManager()); + this.plugin.getContextManager().signalContextUpdate(player); + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) { + ServerPlayer player = (ServerPlayer) event.getEntity(); + handleDisconnect(player.getGameProfile().getId()); + } + +} \ No newline at end of file diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgePlatformListener.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgePlatformListener.java new file mode 100644 index 000000000..1d04d8ca1 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgePlatformListener.java @@ -0,0 +1,92 @@ +/* + * 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.neoforge.listeners; + +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import me.lucko.luckperms.neoforge.util.BrigadierInjector; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.server.players.ServerOpList; + +import java.io.IOException; +import java.util.Locale; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.AddReloadListenerEvent; +import net.neoforged.neoforge.event.CommandEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; + +public class NeoForgePlatformListener { + private final LPNeoForgePlugin plugin; + + public NeoForgePlatformListener(LPNeoForgePlugin plugin) { + this.plugin = plugin; + } + + @SubscribeEvent + public void onCommand(CommandEvent event) { + CommandContextBuilder context = event.getParseResults().getContext(); + + if (!this.plugin.getConfiguration().get(ConfigKeys.OPS_ENABLED)) { + for (ParsedCommandNode node : context.getNodes()) { + if (!(node.getNode() instanceof LiteralCommandNode)) { + continue; + } + + String name = node.getNode().getName().toLowerCase(Locale.ROOT); + if (name.equals("op") || name.equals("deop")) { + Message.OP_DISABLED.send(this.plugin.getSenderFactory().wrap(context.getSource())); + event.setCanceled(true); + return; + } + } + } + } + + @SubscribeEvent + public void onAddReloadListener(AddReloadListenerEvent event) { + Commands commands = event.getServerResources().getCommands(); + BrigadierInjector.inject(this.plugin, commands.getDispatcher()); + } + + @SubscribeEvent + public void onServerStarted(ServerStartedEvent event) { + if (!this.plugin.getConfiguration().get(ConfigKeys.OPS_ENABLED)) { + ServerOpList ops = event.getServer().getPlayerList().getOps(); + ops.getEntries().clear(); + try { + ops.save(); + } catch (IOException ex) { + this.plugin.getLogger().severe("Encountered an error while saving ops", ex); + } + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/NeoForgeMessagingFactory.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/NeoForgeMessagingFactory.java new file mode 100644 index 000000000..26f35e71d --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/NeoForgeMessagingFactory.java @@ -0,0 +1,70 @@ +/* + * 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.neoforge.messaging; + +import me.lucko.luckperms.common.messaging.InternalMessagingService; +import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; +import me.lucko.luckperms.common.messaging.MessagingFactory; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.luckperms.api.messenger.MessengerProvider; +import org.checkerframework.checker.nullness.qual.NonNull; + +public class NeoForgeMessagingFactory extends MessagingFactory { + public NeoForgeMessagingFactory(LPNeoForgePlugin plugin) { + super(plugin); + } + + @Override + protected InternalMessagingService getServiceFor(String messagingType) { + if (messagingType.equals("pluginmsg") || messagingType.equals("bungee") || messagingType.equals("velocity")) { + try { + return new LuckPermsMessagingService(getPlugin(), new PluginMessageMessengerProvider()); + } catch (Exception e) { + getPlugin().getLogger().severe("Exception occurred whilst enabling messaging", e); + } + } + + return super.getServiceFor(messagingType); + } + + private class PluginMessageMessengerProvider implements MessengerProvider { + + @Override + public @NonNull String getName() { + return "PluginMessage"; + } + + @Override + public @NonNull Messenger obtain(@NonNull IncomingMessageConsumer incomingMessageConsumer) { + PluginMessageMessenger messenger = new PluginMessageMessenger(getPlugin(), incomingMessageConsumer); + messenger.init(); + return messenger; + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/PluginMessageMessenger.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/PluginMessageMessenger.java new file mode 100644 index 000000000..6bfbcbd69 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/PluginMessageMessenger.java @@ -0,0 +1,114 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.neoforge.messaging; + +import com.google.common.collect.Iterables; +import me.lucko.luckperms.common.messaging.pluginmsg.AbstractPluginMessageMessenger; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.HandlerThread; + +public class PluginMessageMessenger extends AbstractPluginMessageMessenger implements Messenger { + private static final ResourceLocation CHANNEL_ID = ResourceLocation.parse(AbstractPluginMessageMessenger.CHANNEL); + private static final CustomPacketPayload.Type PAYLOAD_TYPE = new CustomPacketPayload.Type<>(CHANNEL_ID); + + private final LPNeoForgePlugin plugin; + + public PluginMessageMessenger(LPNeoForgePlugin plugin, IncomingMessageConsumer consumer) { + super(consumer); + this.plugin = plugin; + } + + @SubscribeEvent + private void register(final RegisterPayloadHandlersEvent event) { + event.registrar("1").executesOn(HandlerThread.NETWORK).commonBidirectional( + PAYLOAD_TYPE, + StreamCodec.of( + (bytebuf, wrapper) -> bytebuf.writeBytes(wrapper.bytes), + buf -> { + byte[] bytes = new byte[buf.readableBytes()]; + return new MessageWrapper(bytes); + } + ), + (payload, context) -> handleIncomingMessage(payload.bytes()) + ); + } + + public void init() { + this.plugin.getBootstrap().registerListeners(this); + } + + @Override + protected void sendOutgoingMessage(byte[] buf) { + AtomicReference taskRef = new AtomicReference<>(); + SchedulerTask task = this.plugin.getBootstrap().getScheduler().asyncRepeating(() -> { + ServerPlayer player = this.plugin.getBootstrap().getServer() + .map(MinecraftServer::getPlayerList) + .map(PlayerList::getPlayers) + .map(players -> Iterables.getFirst(players, null)) + .orElse(null); + + if (player == null) { + return; + } + + PacketDistributor.sendToPlayer(player, new MessageWrapper(buf)); + + SchedulerTask t = taskRef.getAndSet(null); + if (t != null) { + t.cancel(); + } + }, 10, TimeUnit.SECONDS); + taskRef.set(task); + } + + public static void registerChannel() { + // do nothing - the channels are registered in the static initializer, we just + // need to make sure that is called (which it will be if this method runs) + } + + public record MessageWrapper(byte[] bytes) implements CustomPacketPayload { + @Override + public Type type() { + return PAYLOAD_TYPE; + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandler.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandler.java new file mode 100644 index 000000000..85966a01c --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandler.java @@ -0,0 +1,166 @@ +/* + * 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.neoforge.service; + +import me.lucko.luckperms.common.cacheddata.type.MetaCache; +import me.lucko.luckperms.common.cacheddata.type.PermissionCache; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.neoforge.LPNeoForgeBootstrap; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import me.lucko.luckperms.neoforge.capabilities.UserCapabilityImpl; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.query.QueryMode; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import net.neoforged.neoforge.server.permission.handler.IPermissionHandler; +import net.neoforged.neoforge.server.permission.nodes.PermissionDynamicContext; +import net.neoforged.neoforge.server.permission.nodes.PermissionNode; +import net.neoforged.neoforge.server.permission.nodes.PermissionType; +import net.neoforged.neoforge.server.permission.nodes.PermissionTypes; + +public class NeoForgePermissionHandler implements IPermissionHandler { + public static final ResourceLocation IDENTIFIER = ResourceLocation.fromNamespaceAndPath(LPNeoForgeBootstrap.ID, "permission_handler"); + + private final LPNeoForgePlugin plugin; + private final Set> permissionNodes; + + public NeoForgePermissionHandler(LPNeoForgePlugin plugin, Collection> permissionNodes) { + this.plugin = plugin; + this.permissionNodes = Collections.unmodifiableSet(new HashSet<>(permissionNodes)); + + for (PermissionNode node : this.permissionNodes) { + this.plugin.getPermissionRegistry().insert(node.getNodeName()); + } + } + + @Override + public ResourceLocation getIdentifier() { + return IDENTIFIER; + } + + @Override + public Set> getRegisteredNodes() { + return this.permissionNodes; + } + + @Override + public T getPermission(ServerPlayer player, PermissionNode node, PermissionDynamicContext... context) { + UserCapabilityImpl capability = UserCapabilityImpl.getNullable(player); + + if (capability != null) { + User user = capability.getUser(); + QueryOptions queryOptions = capability.getQueryOptionsCache().getQueryOptions(); + + T value = getPermissionValue(user, queryOptions, node, context); + if (value != null) { + return value; + } + } + + return node.getDefaultResolver().resolve(player, player.getUUID(), context); + } + + @Override + public T getOfflinePermission(UUID player, PermissionNode node, PermissionDynamicContext... context) { + User user = this.plugin.getUserManager().getIfLoaded(player); + + if (user != null) { + QueryOptions queryOptions = user.getQueryOptions(); + T value = getPermissionValue(user, queryOptions, node, context); + if (value != null) { + return value; + } + } + + return node.getDefaultResolver().resolve(null, player, context); + } + + @SuppressWarnings("unchecked") + private static T getPermissionValue(User user, QueryOptions queryOptions, PermissionNode node, PermissionDynamicContext... context) { + queryOptions = appendContextToQueryOptions(queryOptions, context); + String key = node.getNodeName(); + PermissionType type = node.getType(); + + // permission check + if (type == PermissionTypes.BOOLEAN) { + PermissionCache cache = user.getCachedData().getPermissionData(queryOptions); + Tristate value = cache.checkPermission(key, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + if (value != Tristate.UNDEFINED) { + return (T) (Boolean) value.asBoolean(); + } + } + + // meta lookup + if (node.getType() == PermissionTypes.STRING) { + MetaCache cache = user.getCachedData().getMetaData(queryOptions); + String value = cache.getMetaOrChatMetaValue(node.getNodeName(), CheckOrigin.PLATFORM_API); + if (value != null) { + return (T) value; + } + } + + // meta lookup (integer) + if (node.getType() == PermissionTypes.INTEGER) { + MetaCache cache = user.getCachedData().getMetaData(queryOptions); + String value = cache.getMetaOrChatMetaValue(node.getNodeName(), CheckOrigin.PLATFORM_API); + if (value != null) { + try { + return (T) Integer.valueOf(Integer.parseInt(value)); + } catch (IllegalArgumentException e) { + // ignore + } + } + } + + return null; + } + + private static QueryOptions appendContextToQueryOptions(QueryOptions queryOptions, PermissionDynamicContext... context) { + if (context.length == 0 || queryOptions.mode() != QueryMode.CONTEXTUAL) { + return queryOptions; + } + + ImmutableContextSet.Builder contextBuilder = new ImmutableContextSetImpl.BuilderImpl() + .addAll(queryOptions.context()); + + for (PermissionDynamicContext dynamicContext : context) { + contextBuilder.add(dynamicContext.getDynamic().name(), dynamicContext.getSerializedValue()); + } + + return queryOptions.toBuilder().context(contextBuilder.build()).build(); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandlerListener.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandlerListener.java new file mode 100644 index 000000000..96bbe13e4 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandlerListener.java @@ -0,0 +1,64 @@ +/* + * 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.neoforge.service; + +import me.lucko.luckperms.common.command.access.CommandPermission; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.common.ModConfigSpec; +import net.neoforged.neoforge.common.NeoForgeConfig; +import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; +import net.neoforged.neoforge.server.permission.handler.DefaultPermissionHandler; +import net.neoforged.neoforge.server.permission.nodes.PermissionNode; +import net.neoforged.neoforge.server.permission.nodes.PermissionTypes; + +public class NeoForgePermissionHandlerListener { + private final LPNeoForgePlugin plugin; + + public NeoForgePermissionHandlerListener(LPNeoForgePlugin plugin) { + this.plugin = plugin; + } + + @SubscribeEvent + public void onPermissionGatherHandler(PermissionGatherEvent.Handler event) { + // Override the default permission handler with LuckPerms + ModConfigSpec.ConfigValue permissionHandler = NeoForgeConfig.SERVER.permissionHandler; + if (permissionHandler.get().equals(DefaultPermissionHandler.IDENTIFIER.toString())) { + permissionHandler.set(NeoForgePermissionHandler.IDENTIFIER.toString()); + } + + event.addPermissionHandler(NeoForgePermissionHandler.IDENTIFIER, permissions -> new NeoForgePermissionHandler(this.plugin, permissions)); + } + + @SubscribeEvent + public void onPermissionGatherNodes(PermissionGatherEvent.Nodes event) { + // register luckperms nodes + for (CommandPermission permission : CommandPermission.values()) { + event.addNodes(new PermissionNode<>("luckperms", permission.getNode(), PermissionTypes.BOOLEAN, (player, uuid, context) -> false)); + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/AsyncConfigurationTask.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/AsyncConfigurationTask.java new file mode 100644 index 000000000..748ba9fe2 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/AsyncConfigurationTask.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.neoforge.util; + +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.minecraft.network.protocol.Packet; +import net.minecraft.server.network.ConfigurationTask; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class AsyncConfigurationTask implements ConfigurationTask { + private final LPNeoForgePlugin plugin; + private final Type type; + private final Supplier> task; + + public AsyncConfigurationTask(LPNeoForgePlugin plugin, Type type, Supplier> task) { + this.plugin = plugin; + this.type = type; + this.task = task; + } + + @Override + public void start(Consumer> send) { + CompletableFuture future = this.task.get(); + future.whenCompleteAsync((o, e) -> { + if (e != null) { + this.plugin.getLogger().warn("Configuration task threw an exception", e); + } + }).join(); + } + + @Override + public Type type() { + return this.type; + } +} \ No newline at end of file diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/BrigadierInjector.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/BrigadierInjector.java new file mode 100644 index 000000000..7cd65d6df --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/BrigadierInjector.java @@ -0,0 +1,197 @@ +/* + * 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.neoforge.util; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.lucko.luckperms.common.graph.Graph; +import me.lucko.luckperms.common.graph.TraversalAlgorithm; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import me.lucko.luckperms.neoforge.capabilities.UserCapability; +import me.lucko.luckperms.neoforge.capabilities.UserCapabilityImpl; +import net.luckperms.api.util.Tristate; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.level.ServerPlayer; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Locale; +import java.util.function.Predicate; + +/** + * Utility for injecting permission requirements into a Brigadier command tree. + */ +public final class BrigadierInjector { + private BrigadierInjector() {} + + private static final Field REQUIREMENT_FIELD; + + static { + Field requirementField; + try { + requirementField = CommandNode.class.getDeclaredField("requirement"); + requirementField.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new ExceptionInInitializerError(e); + } + REQUIREMENT_FIELD = requirementField; + } + + /** + * Inject permission requirements into the commands in the given dispatcher. + * + * @param plugin the plugin + * @param dispatcher the command dispatcher + */ + public static void inject(LPNeoForgePlugin plugin, CommandDispatcher dispatcher) { + Iterable tree = CommandNodeGraph.INSTANCE.traverse( + TraversalAlgorithm.DEPTH_FIRST_PRE_ORDER, + new CommandNodeWithParent(null, dispatcher.getRoot()) + ); + + for (CommandNodeWithParent node : tree) { + Predicate requirement = node.node.getRequirement(); + + // already injected - skip + if (requirement instanceof InjectedPermissionRequirement) { + continue; + } + + String permission = buildPermissionNode(node); + if (permission == null) { + continue; + } + + plugin.getPermissionRegistry().insert(permission); + + InjectedPermissionRequirement newRequirement = new InjectedPermissionRequirement(plugin, permission, requirement); + try { + REQUIREMENT_FIELD.set(node.node, newRequirement); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + private static String buildPermissionNode(CommandNodeWithParent node) { + StringBuilder builder = new StringBuilder(); + + while (node != null) { + if (node.node instanceof LiteralCommandNode) { + if (builder.length() != 0) { + builder.insert(0, '.'); + } + + String name = node.node.getName().toLowerCase(Locale.ROOT); + builder.insert(0, name); + } + + node = node.parent; + } + + if (builder.length() == 0) { + return null; + } + + builder.insert(0, "command."); + return builder.toString(); + } + + /** + * Injected {@link CommandNode#getRequirement() requirement} that checks for a permission, before + * delegating to the existing requirement. + */ + private static final class InjectedPermissionRequirement implements Predicate { + private final LPNeoForgePlugin plugin; + private final String permission; + private final Predicate delegate; + + private InjectedPermissionRequirement(LPNeoForgePlugin plugin, String permission, Predicate delegate) { + this.plugin = plugin; + this.permission = permission; + this.delegate = delegate; + } + + @Override + public boolean test(CommandSourceStack source) { + if (source.getEntity() instanceof ServerPlayer) { + ServerPlayer player = (ServerPlayer) source.getEntity(); + Tristate state = Tristate.UNDEFINED; + // If player is still connecting and has not been added to world then check LP user directly + if (!player.isAddedToLevel()) { + User user = this.plugin.getUserManager().getIfLoaded(player.getUUID()); + if (user == null) { + // Should never happen but just in case... + return false; + } + state = user.getCachedData().getPermissionData().checkPermission(permission); + } else { + UserCapability user = UserCapabilityImpl.get(player); + state = user.checkPermission(this.permission); + } + + if (state != Tristate.UNDEFINED) { + return state.asBoolean() && this.delegate.test(source.withPermission(4)); + } + } + + return this.delegate.test(source); + } + } + + /** + * A {@link Graph} to represent the brigadier command node tree. + */ + private enum CommandNodeGraph implements Graph { + INSTANCE; + + @Override + public Iterable successors(CommandNodeWithParent ctx) { + CommandNode node = ctx.node; + Collection successors = new ArrayList<>(); + + for (CommandNode child : node.getChildren()) { + successors.add(new CommandNodeWithParent(ctx, child)); + } + + return successors; + } + } + + private static final class CommandNodeWithParent { + private final CommandNodeWithParent parent; + private final CommandNode node; + + private CommandNodeWithParent(CommandNodeWithParent parent, CommandNode node) { + this.parent = parent; + this.node = node; + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/NeoForgeEventBusFacade.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/NeoForgeEventBusFacade.java new file mode 100644 index 000000000..e2cdc129f --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/NeoForgeEventBusFacade.java @@ -0,0 +1,241 @@ +/* + * 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.neoforge.util; + +import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import net.neoforged.bus.api.Event; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModLoadingContext; +import net.neoforged.fml.event.IModBusEvent; + +import java.lang.invoke.CallSite; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.neoforged.neoforge.common.NeoForge; + +/** + * A utility for registering Forge listeners for methods in a jar-in-jar. + * + *

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

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