From 99a87382bdc3a30a3e0e6127aa6bfc0703ac219b Mon Sep 17 00:00:00 2001 From: Vankka Date: Sun, 20 Mar 2022 13:46:06 +0200 Subject: [PATCH] In-game commands --- .../com/discordsrv/api/DiscordSRVApi.java | 25 +- ...ent.java => DiscordSRVConnectedEvent.java} | 14 +- .../lifecycle/DiscordSRVReadyEvent.java | 3 +- .../lifecycle/DiscordSRVReloadedEvent.java | 49 ++ build.gradle | 12 +- bukkit/build.gradle | 40 +- bukkit/loader/src/main/resources/plugin.yml | 11 + .../discordsrv/bukkit/BukkitDiscordSRV.java | 14 +- .../bukkit/DiscordSRVBukkitBootstrap.java | 18 +- .../AbstractBukkitCommandExecutor.java | 49 ++ .../handler/AbstractBukkitCommandHandler.java | 91 ++++ .../handler/BukkitBasicCommandHandler.java | 63 +++ .../game/handler/CommodoreHandler.java | 73 +++ .../game/sender/BukkitCommandSender.java | 55 ++ .../bukkit/console/BukkitConsole.java | 25 +- .../bukkit/player/BukkitOfflinePlayer.java | 1 - .../bukkit/player/BukkitPlayer.java | 32 +- .../bukkit/player/BukkitPlayerProvider.java | 6 +- bungee/build.gradle | 18 +- .../discordsrv/bungee/BungeeDiscordSRV.java | 10 + .../game/handler/BungeeCommandHandler.java | 80 +++ .../game/sender/BungeeCommandSender.java | 55 ++ .../bungee/console/BungeeConsole.java | 24 +- .../bungee/player/BungeePlayer.java | 25 +- common/build.gradle | 3 + .../discordsrv/common/AbstractDiscordSRV.java | 201 +++++--- .../com/discordsrv/common/DiscordSRV.java | 6 +- .../common/channel/ChannelConfigHelper.java | 11 +- .../common/channel/ChannelUpdaterModule.java | 1 - .../command/game/GameCommandModule.java | 77 +++ .../command/game/abstraction/GameCommand.java | 473 ++++++++++++++++++ .../abstraction/GameCommandArguments.java | 29 ++ .../game/abstraction/GameCommandExecutor.java | 27 + .../abstraction/GameCommandSuggester.java | 30 ++ .../game/command/DiscordSRVCommand.java | 64 +++ .../game/command/subcommand/LinkCommand.java | 51 ++ .../command/subcommand/ReloadCommand.java | 148 ++++++ .../command/subcommand/VersionCommand.java | 58 +++ .../game/handler/BasicCommandHandler.java | 237 +++++++++ .../command/game/handler/ICommandHandler.java | 26 + .../game/handler/util/BrigadierUtil.java | 136 +++++ .../common/config/main/CommandConfig.java | 35 ++ .../config/main/DiscordInviteConfig.java | 38 ++ .../common/config/main/MainConfig.java | 6 + .../config/main/channels/MirroringConfig.java | 2 +- .../dependency/InitialDependencyLoader.java | 3 +- .../common/groupsync/GroupSyncModule.java | 6 +- .../common/invite/DiscordInviteModule.java | 98 ++++ .../common/linking/impl/StorageLinker.java | 1 - .../common/module/ModuleManager.java | 82 ++- .../common/module/type/AbstractModule.java | 10 +- .../common/module/type/ModuleDelegate.java | 59 +++ .../com/discordsrv/common/MockDiscordSRV.java | 7 + .../com/discordsrv/config/MockDiscordSRV.java | 7 + sponge/build.gradle | 12 +- .../sponge/DiscordSRVSpongeBootstrap.java | 7 +- .../discordsrv/sponge/SpongeDiscordSRV.java | 18 +- .../game/handler/SpongeCommandHandler.java | 145 ++++++ .../game/sender/SpongeCommandSender.java | 59 +++ .../sponge/console/SpongeConsole.java | 27 +- .../sponge/player/SpongePlayer.java | 31 +- .../sponge/player/SpongePlayerProvider.java | 3 +- velocity/build.gradle | 12 +- .../velocity/VelocityDiscordSRV.java | 9 + .../game/handler/VelocityCommandHandler.java | 56 +++ .../game/sender/VelocityCommandSender.java | 51 ++ .../velocity/console/VelocityConsole.java | 24 +- .../velocity/player/VelocityPlayer.java | 22 +- 68 files changed, 2874 insertions(+), 327 deletions(-) rename api/src/main/java/com/discordsrv/api/event/events/lifecycle/{DiscordSRVReloadEvent.java => DiscordSRVConnectedEvent.java} (85%) create mode 100644 api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReloadedEvent.java create mode 100644 bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/AbstractBukkitCommandExecutor.java create mode 100644 bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/AbstractBukkitCommandHandler.java create mode 100644 bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/BukkitBasicCommandHandler.java create mode 100644 bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/CommodoreHandler.java create mode 100644 bukkit/src/main/java/com/discordsrv/bukkit/command/game/sender/BukkitCommandSender.java create mode 100644 bungee/src/main/java/com/discordsrv/bungee/command/game/handler/BungeeCommandHandler.java create mode 100644 bungee/src/main/java/com/discordsrv/bungee/command/game/sender/BungeeCommandSender.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommand.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandArguments.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandExecutor.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandSuggester.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/command/DiscordSRVCommand.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/command/subcommand/LinkCommand.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ReloadCommand.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/command/subcommand/VersionCommand.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/handler/BasicCommandHandler.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/handler/ICommandHandler.java create mode 100644 common/src/main/java/com/discordsrv/common/command/game/handler/util/BrigadierUtil.java create mode 100644 common/src/main/java/com/discordsrv/common/config/main/CommandConfig.java create mode 100644 common/src/main/java/com/discordsrv/common/config/main/DiscordInviteConfig.java create mode 100644 common/src/main/java/com/discordsrv/common/invite/DiscordInviteModule.java create mode 100644 common/src/main/java/com/discordsrv/common/module/type/ModuleDelegate.java create mode 100644 sponge/src/main/java/com/discordsrv/sponge/command/game/handler/SpongeCommandHandler.java create mode 100644 sponge/src/main/java/com/discordsrv/sponge/command/game/sender/SpongeCommandSender.java create mode 100644 velocity/src/main/java/com/discordsrv/velocity/command/game/handler/VelocityCommandHandler.java create mode 100644 velocity/src/main/java/com/discordsrv/velocity/command/game/sender/VelocityCommandSender.java diff --git a/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java b/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java index 81e16873..0c78bcbf 100644 --- a/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java +++ b/api/src/main/java/com/discordsrv/api/DiscordSRVApi.java @@ -35,7 +35,7 @@ import net.dv8tion.jda.api.JDA; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -import java.util.Optional; +import java.util.*; /** * The DiscordSRV API. @@ -215,4 +215,27 @@ public interface DiscordSRVApi { } } + + enum ReloadFlag { + CONFIG(false), + LINKED_ACCOUNT_PROVIDER(false), + STORAGE(true), + DISCORD_CONNECTION(true), + MODULES(false), + + ; + + public static final Set ALL = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(values()))); + public static final Set DEFAULT_FLAGS = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(CONFIG, MODULES))); + + private final boolean requiresConfirm; + + ReloadFlag(boolean requiresConfirm) { + this.requiresConfirm = requiresConfirm; + } + + public boolean requiresConfirm() { + return requiresConfirm; + } + } } diff --git a/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReloadEvent.java b/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVConnectedEvent.java similarity index 85% rename from api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReloadEvent.java rename to api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVConnectedEvent.java index 45a87dd8..d6fb392e 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReloadEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVConnectedEvent.java @@ -25,15 +25,9 @@ package com.discordsrv.api.event.events.lifecycle; import com.discordsrv.api.event.events.Event; -public class DiscordSRVReloadEvent implements Event { +/** + * Indicates that DiscordSRV's Discord connection has been (re-)established. This may run more than once. + */ +public class DiscordSRVConnectedEvent implements Event { - private final boolean config; - - public DiscordSRVReloadEvent(boolean config) { - this.config = config; - } - - public boolean isConfig() { - return config; - } } diff --git a/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReadyEvent.java b/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReadyEvent.java index 4d78445c..047046fa 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReadyEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReadyEvent.java @@ -26,8 +26,7 @@ package com.discordsrv.api.event.events.lifecycle; import com.discordsrv.api.event.events.Event; /** - * Indicates that DiscordSRV's systems (including Discord connection) are ready. + * Indicates that DiscordSRV is ready (including Discord connection), this only runs once when DiscordSRV enables. */ public class DiscordSRVReadyEvent implements Event { - } diff --git a/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReloadedEvent.java b/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReloadedEvent.java new file mode 100644 index 00000000..0616a7c4 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/event/events/lifecycle/DiscordSRVReloadedEvent.java @@ -0,0 +1,49 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV 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 com.discordsrv.api.event.events.lifecycle; + +import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.api.event.events.Event; + +import java.util.Set; + +/** + * An event for when DiscordSRV successfully reloads partially or completely. + */ +public class DiscordSRVReloadedEvent implements Event { + + private final Set flags; + + public DiscordSRVReloadedEvent(Set flags) { + this.flags = flags; + } + + /** + * Set of DiscordSRV systems that were reloaded. + * @return an unmodifiable set of {@link com.discordsrv.api.DiscordSRVApi.ReloadFlag}s + */ + public Set flags() { + return flags; + } +} diff --git a/build.gradle b/build.gradle index cb0f8c3f..d55b70d1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'com.github.johnrengelman.shadow' version '7.1.1' apply false - id 'org.cadixdev.licenser' version '0.6.0' apply false + id 'org.cadixdev.licenser' version '0.6.1' apply false id 'net.kyori.blossom' version '1.2.0' apply false id 'dev.vankka.dependencydownload.plugin' version '1.1.5-SNAPSHOT' apply false } @@ -17,8 +17,8 @@ ext { // Configurate configurateVersion = '4.1.2' // Adventure & Adventure Platform - adventureVersion = '4.9.1' - adventurePlatformVersion = '4.0.0' + adventureVersion = '4.10.0' + adventurePlatformVersion = '4.1.0' } allprojects { @@ -59,6 +59,12 @@ allprojects { includeGroup 'me.scarsz' } } + maven { + url 'https://libraries.minecraft.net' + content { + includeGroup 'com.mojang' + } + } // Get dependencies from central last, everything else should be filtered mavenCentral() diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 0b943e23..e5c462aa 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -1,5 +1,30 @@ +import dev.vankka.dependencydownload.task.GenerateDependencyDownloadResourceTask + apply from: rootProject.file('buildscript/runtime.gradle') +configurations { + commodore + compileOnly.extendsFrom commodore +} + +task generateResourceForCommodore(type: GenerateDependencyDownloadResourceTask) { + var conf = configurations.commodore + configuration = conf + file = 'dependencies/' + conf.name + '.txt' +} + +shadowJar { + archiveFileName = 'bukkit.jarinjar' + + [ + 'net.kyori', + 'me.lucko.commodore' + ].each { + relocate it, 'com.discordsrv.dependencies.' + it + } + // More relocations in buildscript/relocations.gradle +} + allprojects { repositories { exclusiveContent { @@ -41,13 +66,18 @@ dependencies { // Adventure runtimeDownloadApi 'net.kyori:adventure-platform-bukkit:' + rootProject.adventurePlatformVersion + // Commodore + commodore('me.lucko:commodore:1.10') { + // We only use commodore when it's included in the server, so we don't want to download it + exclude module: 'brigadier' + // We don't use the commodore file format + exclude module: 'commodore-file' + } + // Integrations compileOnly 'net.milkbowl.vault:VaultAPI:1.7' } -shadowJar { - archiveFileName = 'bukkit.jarinjar' - - relocate 'net.kyori', 'com.discordsrv.dependencies.net.kyori' - // More relocations in buildscript/relocations.gradle +processResources { + dependsOn(generateResourceForCommodore) } diff --git a/bukkit/loader/src/main/resources/plugin.yml b/bukkit/loader/src/main/resources/plugin.yml index 9945f703..fe16d639 100644 --- a/bukkit/loader/src/main/resources/plugin.yml +++ b/bukkit/loader/src/main/resources/plugin.yml @@ -5,3 +5,14 @@ description: "" authors: [Scarsz, Vankka] load: STARTUP api-version: 1.13 + +softdepend: [ + # Permission + group providers + Vault, LuckPerms, + # Adventure + ViaVersion +] + +commands: + discordsrv: + description: "DiscordSRV's primary command" diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java index 356b7928..e1595b52 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java @@ -19,6 +19,7 @@ package com.discordsrv.bukkit; import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.bukkit.command.game.handler.AbstractBukkitCommandHandler; import com.discordsrv.bukkit.config.connection.BukkitConnectionConfig; import com.discordsrv.bukkit.config.main.BukkitConfig; import com.discordsrv.bukkit.config.manager.BukkitConfigManager; @@ -32,6 +33,7 @@ import com.discordsrv.bukkit.listener.BukkitStatusMessageListener; import com.discordsrv.bukkit.player.BukkitPlayerProvider; import com.discordsrv.bukkit.plugin.BukkitPluginManager; import com.discordsrv.bukkit.scheduler.BukkitScheduler; +import com.discordsrv.common.command.game.handler.ICommandHandler; import com.discordsrv.common.config.manager.ConnectionConfigManager; import com.discordsrv.common.config.manager.MainConfigManager; import com.discordsrv.common.debug.data.OnlineMode; @@ -60,6 +62,7 @@ public class BukkitDiscordSRV extends ServerDiscordSRV connectionConfigManager() { return connectionConfigManager; @@ -176,15 +184,19 @@ public class BukkitDiscordSRV extends ServerDiscordSRV resources = new ArrayList<>(); + resources.add("dependencies/runtimeDownload-bukkit.txt"); + + try { + Class.forName("com.mojang.brigadier.CommandDispatcher"); + resources.add("dependencies/commodore.txt"); + } catch (ClassNotFoundException ignored) { + // CommandDispatches not present, don't need to bother downloading commodore + } + + return resources.toArray(new String[0]); + } + @Override public void onEnable() { dependencies.loadAndEnable(() -> this.discordSRV = new BukkitDiscordSRV(this, logger)); diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/AbstractBukkitCommandExecutor.java b/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/AbstractBukkitCommandExecutor.java new file mode 100644 index 00000000..a51b39e0 --- /dev/null +++ b/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/AbstractBukkitCommandExecutor.java @@ -0,0 +1,49 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.bukkit.command.game.handler; + +import com.discordsrv.bukkit.BukkitDiscordSRV; +import com.discordsrv.common.command.game.handler.BasicCommandHandler; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; + +public abstract class AbstractBukkitCommandExecutor extends AbstractBukkitCommandHandler implements CommandExecutor { + + protected final BasicCommandHandler handler = new BasicCommandHandler(); + + public AbstractBukkitCommandExecutor(BukkitDiscordSRV discordSRV) { + super(discordSRV); + } + + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ) { + handler.execute(sender(sender), label, Arrays.asList(args)); + return true; + } + +} diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/AbstractBukkitCommandHandler.java b/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/AbstractBukkitCommandHandler.java new file mode 100644 index 00000000..fd5e6245 --- /dev/null +++ b/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/AbstractBukkitCommandHandler.java @@ -0,0 +1,91 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.bukkit.command.game.handler; + +import com.discordsrv.bukkit.BukkitDiscordSRV; +import com.discordsrv.bukkit.command.game.sender.BukkitCommandSender; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.handler.ICommandHandler; +import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.common.logging.Logger; +import com.discordsrv.common.logging.NamedLogger; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Constructor; +import java.util.Locale; + +public abstract class AbstractBukkitCommandHandler implements ICommandHandler { + + public static AbstractBukkitCommandHandler get(BukkitDiscordSRV discordSRV) { + try { + Class.forName("me.lucko.commodore.Commodore"); + return new CommodoreHandler(discordSRV); + } catch (Throwable e) { + BukkitBasicCommandHandler handler = new BukkitBasicCommandHandler(discordSRV); + if (e instanceof ClassNotFoundException) { + handler.logger.debug("Brigadier classes not present, not using commodore"); + } else { + handler.logger.debug("Failed to initialize Commodore", e); + } + return handler; + } + } + + protected final BukkitDiscordSRV discordSRV; + protected final Logger logger; + + public AbstractBukkitCommandHandler(BukkitDiscordSRV discordSRV) { + this.discordSRV = discordSRV; + this.logger = new NamedLogger(discordSRV, "COMMAND_HANDLER"); + } + + protected ICommandSender sender(CommandSender commandSender) { + if (commandSender instanceof Player) { + return discordSRV.playerProvider().player((Player) commandSender); + } else if (commandSender instanceof ConsoleCommandSender) { + return discordSRV.console(); + } else { + return new BukkitCommandSender(discordSRV, commandSender, () -> discordSRV.audiences().sender(commandSender)); + } + } + + protected PluginCommand command(GameCommand gameCommand) { + String label = gameCommand.getLabel(); + PluginCommand pluginCommand = discordSRV.plugin().getCommand(label); + if (pluginCommand != null) { + return pluginCommand; + } + + PluginCommand command = null; + try { + Constructor constructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class); + if (!constructor.isAccessible()) { + constructor.setAccessible(true); + } + command = (PluginCommand) constructor.newInstance(label, discordSRV.plugin()); + discordSRV.server().getCommandMap().register(label, discordSRV.plugin().getName().toLowerCase(Locale.ROOT), command); + } catch (ReflectiveOperationException ignored) {} + + return command; + } +} diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/BukkitBasicCommandHandler.java b/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/BukkitBasicCommandHandler.java new file mode 100644 index 00000000..bd287934 --- /dev/null +++ b/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/BukkitBasicCommandHandler.java @@ -0,0 +1,63 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.bukkit.command.game.handler; + +import com.discordsrv.bukkit.BukkitDiscordSRV; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.command.TabCompleter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; + +public class BukkitBasicCommandHandler extends AbstractBukkitCommandExecutor implements TabCompleter { + + public BukkitBasicCommandHandler(BukkitDiscordSRV discordSRV) { + super(discordSRV); + } + + @Override + public @Nullable List onTabComplete( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String alias, + @NotNull String[] args + ) { + return handler.suggest(sender(sender), alias, Arrays.asList(args)); + } + + @Override + public void registerCommand(GameCommand command) { + PluginCommand pluginCommand = command(command); + if (pluginCommand == null) { + logger.error("Failed to create command " + command.getLabel()); + return; + } + + logger.debug("Registering command " + command.getLabel() + " with basic handler"); + + handler.registerCommand(command); + pluginCommand.setExecutor(this); + pluginCommand.setTabCompleter(this); + } +} diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/CommodoreHandler.java b/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/CommodoreHandler.java new file mode 100644 index 00000000..772a22b4 --- /dev/null +++ b/bukkit/src/main/java/com/discordsrv/bukkit/command/game/handler/CommodoreHandler.java @@ -0,0 +1,73 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.bukkit.command.game.handler; + +import com.discordsrv.bukkit.BukkitDiscordSRV; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.handler.util.BrigadierUtil; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.lucko.commodore.Commodore; +import me.lucko.commodore.CommodoreProvider; +import org.bukkit.command.PluginCommand; + +/** + * No avoiding basic handler on bukkit. Unfortunately it isn't possible to use brigadier for executing. + */ +public class CommodoreHandler extends AbstractBukkitCommandExecutor { + + private final Commodore commodore; + + public CommodoreHandler(BukkitDiscordSRV discordSRV) { + super(discordSRV); + this.commodore = CommodoreProvider.getCommodore(discordSRV.plugin()); + } + + @Override + public void registerCommand(GameCommand command) { + logger.debug("Registering command " + command.getLabel() + " with commodore"); + + LiteralCommandNode argumentBuilder = BrigadierUtil.convertToBrigadier( + command, + wrapper -> sender(commodore.getBukkitSender(wrapper)) + ); + CommandNode redirection = argumentBuilder.getRedirect(); + if (redirection != null) { + // Commodore handles the label being different fine + argumentBuilder = (LiteralCommandNode) redirection; + } + + PluginCommand pluginCommand = command(command); + if (pluginCommand == null) { + logger.error("Failed to create command " + command.getLabel()); + return; + } + + handler.registerCommand(command); + pluginCommand.setExecutor(this); + + String requiredPermission = command.getRequiredPermission(); + LiteralCommandNode finalArgumentBuilder = argumentBuilder; + discordSRV.scheduler().runOnMainThread(() -> commodore.register( + pluginCommand, + finalArgumentBuilder, + player -> requiredPermission == null || player.hasPermission(requiredPermission) + )); + } +} diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/command/game/sender/BukkitCommandSender.java b/bukkit/src/main/java/com/discordsrv/bukkit/command/game/sender/BukkitCommandSender.java new file mode 100644 index 00000000..c3195d48 --- /dev/null +++ b/bukkit/src/main/java/com/discordsrv/bukkit/command/game/sender/BukkitCommandSender.java @@ -0,0 +1,55 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.bukkit.command.game.sender; + +import com.discordsrv.bukkit.BukkitDiscordSRV; +import com.discordsrv.common.command.game.sender.ICommandSender; +import net.kyori.adventure.audience.Audience; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Supplier; + +public class BukkitCommandSender implements ICommandSender { + + protected final BukkitDiscordSRV discordSRV; + protected final CommandSender commandSender; + protected final Supplier audienceSupplier; + + public BukkitCommandSender(BukkitDiscordSRV discordSRV, CommandSender commandSender, Supplier audienceSupplier) { + this.discordSRV = discordSRV; + this.commandSender = commandSender; + this.audienceSupplier = audienceSupplier; + } + + @Override + public boolean hasPermission(String permission) { + return commandSender.hasPermission(permission); + } + + @Override + public void runCommand(String command) { + discordSRV.scheduler().runOnMainThread(() -> discordSRV.server().dispatchCommand(commandSender, command)); + } + + @Override + public @NotNull Audience audience() { + return audienceSupplier.get(); + } +} diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/console/BukkitConsole.java b/bukkit/src/main/java/com/discordsrv/bukkit/console/BukkitConsole.java index fc2fbf21..e62909ce 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/console/BukkitConsole.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/console/BukkitConsole.java @@ -19,21 +19,19 @@ package com.discordsrv.bukkit.console; import com.discordsrv.bukkit.BukkitDiscordSRV; +import com.discordsrv.bukkit.command.game.sender.BukkitCommandSender; import com.discordsrv.common.console.Console; import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.logging.backend.LoggingBackend; import com.discordsrv.common.logging.backend.impl.JavaLoggerImpl; import com.discordsrv.common.logging.backend.impl.Log4JLoggerImpl; -import net.kyori.adventure.audience.Audience; -import org.jetbrains.annotations.NotNull; -public class BukkitConsole implements Console { +public class BukkitConsole extends BukkitCommandSender implements Console { - private final BukkitDiscordSRV discordSRV; private final LoggingBackend loggingBackend; public BukkitConsole(BukkitDiscordSRV discordSRV) { - this.discordSRV = discordSRV; + super(discordSRV, discordSRV.server().getConsoleSender(), () -> discordSRV.audiences().console()); LoggingBackend logging; try { @@ -49,25 +47,8 @@ public class BukkitConsole implements Console { this.loggingBackend = logging; } - @Override - public boolean hasPermission(String permission) { - return discordSRV.server().getConsoleSender().hasPermission(permission); - } - - @Override - public void runCommand(String command) { - discordSRV.scheduler().runOnMainThread(() -> - discordSRV.server().dispatchCommand( - discordSRV.server().getConsoleSender(), command)); - } - @Override public LoggingBackend loggingBackend() { return loggingBackend; } - - @Override - public @NotNull Audience audience() { - return discordSRV.audiences().console(); - } } diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java index 17003c4f..815626dc 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitOfflinePlayer.java @@ -42,7 +42,6 @@ public class BukkitOfflinePlayer implements IOfflinePlayer { return discordSRV; } - @SuppressWarnings("NullabilityProblems") @Override public String username() { return offlinePlayer.getName(); diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java index 30398f15..f9f8d1fa 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayer.java @@ -19,36 +19,25 @@ package com.discordsrv.bukkit.player; import com.discordsrv.bukkit.BukkitDiscordSRV; +import com.discordsrv.bukkit.command.game.sender.BukkitCommandSender; import com.discordsrv.bukkit.component.util.PaperComponentUtil; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.component.util.ComponentUtil; import com.discordsrv.common.player.IPlayer; -import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -@SuppressWarnings("NullableProblems") // BukkitOfflinePlayer nullability -public class BukkitPlayer extends BukkitOfflinePlayer implements IPlayer { +public class BukkitPlayer extends BukkitCommandSender implements IPlayer { private final Player player; - private final Audience audience; + private final Identity identity; public BukkitPlayer(BukkitDiscordSRV discordSRV, Player player) { - super(discordSRV, player); + super(discordSRV, player, () -> discordSRV.audiences().player(player)); this.player = player; - this.audience = discordSRV.audiences().player(player); - } - - @Override - public boolean hasPermission(String permission) { - return player.hasPermission(permission); - } - - @Override - public void runCommand(String command) { - discordSRV.scheduler().runOnMainThread(() -> - discordSRV.server().dispatchCommand(player, command)); + this.identity = Identity.identity(player.getUniqueId()); } @Override @@ -56,6 +45,11 @@ public class BukkitPlayer extends BukkitOfflinePlayer implements IPlayer { return discordSRV; } + @Override + public @NotNull String username() { + return player.getName(); + } + @SuppressWarnings("deprecation") // Paper @Override public @NotNull Component displayName() { @@ -65,7 +59,7 @@ public class BukkitPlayer extends BukkitOfflinePlayer implements IPlayer { } @Override - public @NotNull Audience audience() { - return audience; + public @NotNull Identity identity() { + return identity; } } diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java index b1b2822e..a59d3cfe 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/player/BukkitPlayerProvider.java @@ -26,7 +26,7 @@ import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerQuitEvent; import java.util.Optional; @@ -52,8 +52,8 @@ public class BukkitPlayerProvider extends ServerPlayerProvider connectionConfigManager() { return null; @@ -135,6 +143,8 @@ public class BungeeDiscordSRV extends ProxyDiscordSRV. + */ + +package com.discordsrv.bungee.command.game.handler; + +import com.discordsrv.bungee.BungeeDiscordSRV; +import com.discordsrv.bungee.command.game.sender.BungeeCommandSender; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.handler.BasicCommandHandler; +import com.discordsrv.common.command.game.sender.ICommandSender; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.TabExecutor; + +import java.util.Arrays; + +/** + * BungeeCord has not concept of Brigadier. + */ +public class BungeeCommandHandler extends BasicCommandHandler { + + private final BungeeDiscordSRV discordSRV; + + public BungeeCommandHandler(BungeeDiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + + @Override + public void registerCommand(GameCommand command) { + super.registerCommand(command); + + discordSRV.proxy().getPluginManager().registerCommand(discordSRV.plugin(), new BungeeCommand(command)); + } + + public ICommandSender getSender(CommandSender sender) { + if (sender instanceof ProxiedPlayer) { + return discordSRV.playerProvider().player((ProxiedPlayer) sender); + } else if (sender == discordSRV.proxy().getConsole()) { + return discordSRV.console(); + } else { + return new BungeeCommandSender(discordSRV, sender, () -> discordSRV.audiences().sender(sender)); + } + } + + public class BungeeCommand extends Command implements TabExecutor { + + private final GameCommand command; + + public BungeeCommand(GameCommand command) { + super(command.getLabel(), command.getRequiredPermission()); + this.command = command; + } + + @Override + public void execute(CommandSender sender, String[] args) { + BungeeCommandHandler.this.execute(getSender(sender), command.getLabel(), Arrays.asList(args)); + } + + @Override + public Iterable onTabComplete(CommandSender sender, String[] args) { + return BungeeCommandHandler.this.suggest(getSender(sender), command.getLabel(), Arrays.asList(args)); + } + } +} diff --git a/bungee/src/main/java/com/discordsrv/bungee/command/game/sender/BungeeCommandSender.java b/bungee/src/main/java/com/discordsrv/bungee/command/game/sender/BungeeCommandSender.java new file mode 100644 index 00000000..824cccb0 --- /dev/null +++ b/bungee/src/main/java/com/discordsrv/bungee/command/game/sender/BungeeCommandSender.java @@ -0,0 +1,55 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.bungee.command.game.sender; + +import com.discordsrv.bungee.BungeeDiscordSRV; +import com.discordsrv.common.command.game.sender.ICommandSender; +import net.kyori.adventure.audience.Audience; +import net.md_5.bungee.api.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Supplier; + +public class BungeeCommandSender implements ICommandSender { + + protected final BungeeDiscordSRV discordSRV; + protected final CommandSender commandSender; + protected final Supplier audience; + + public BungeeCommandSender(BungeeDiscordSRV discordSRV, CommandSender commandSender, Supplier audience) { + this.discordSRV = discordSRV; + this.commandSender = commandSender; + this.audience = audience; + } + + @Override + public boolean hasPermission(String permission) { + return commandSender.hasPermission(permission); + } + + @Override + public void runCommand(String command) { + discordSRV.proxy().getPluginManager().dispatchCommand(commandSender, command); + } + + @Override + public @NotNull Audience audience() { + return audience.get(); + } +} diff --git a/bungee/src/main/java/com/discordsrv/bungee/console/BungeeConsole.java b/bungee/src/main/java/com/discordsrv/bungee/console/BungeeConsole.java index 285ffe06..90b21cf5 100644 --- a/bungee/src/main/java/com/discordsrv/bungee/console/BungeeConsole.java +++ b/bungee/src/main/java/com/discordsrv/bungee/console/BungeeConsole.java @@ -19,40 +19,22 @@ package com.discordsrv.bungee.console; import com.discordsrv.bungee.BungeeDiscordSRV; +import com.discordsrv.bungee.command.game.sender.BungeeCommandSender; import com.discordsrv.common.console.Console; import com.discordsrv.common.logging.backend.LoggingBackend; import com.discordsrv.common.logging.backend.impl.JavaLoggerImpl; -import net.kyori.adventure.audience.Audience; -import org.jetbrains.annotations.NotNull; -public class BungeeConsole implements Console { +public class BungeeConsole extends BungeeCommandSender implements Console { - private final BungeeDiscordSRV discordSRV; private final LoggingBackend loggingBackend; public BungeeConsole(BungeeDiscordSRV discordSRV) { - this.discordSRV = discordSRV; + super(discordSRV, discordSRV.proxy().getConsole(), () -> discordSRV.audiences().console()); this.loggingBackend = JavaLoggerImpl.getRoot(); } - @Override - public boolean hasPermission(String permission) { - return discordSRV.proxy().getConsole().hasPermission(permission); - } - - @Override - public void runCommand(String command) { - discordSRV.proxy().getPluginManager().dispatchCommand( - discordSRV.proxy().getConsole(), command); - } - @Override public LoggingBackend loggingBackend() { return loggingBackend; } - - @Override - public @NotNull Audience audience() { - return discordSRV.audiences().console(); - } } diff --git a/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java b/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java index 7fb64c4a..171d4019 100644 --- a/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java +++ b/bungee/src/main/java/com/discordsrv/bungee/player/BungeePlayer.java @@ -19,37 +19,24 @@ package com.discordsrv.bungee.player; import com.discordsrv.bungee.BungeeDiscordSRV; +import com.discordsrv.bungee.command.game.sender.BungeeCommandSender; import com.discordsrv.bungee.component.util.BungeeComponentUtil; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.player.IPlayer; -import net.kyori.adventure.audience.Audience; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.md_5.bungee.api.connection.ProxiedPlayer; import org.jetbrains.annotations.NotNull; -public class BungeePlayer implements IPlayer { +public class BungeePlayer extends BungeeCommandSender implements IPlayer { - private final BungeeDiscordSRV discordSRV; private final ProxiedPlayer player; private final Identity identity; - private final Audience audience; public BungeePlayer(BungeeDiscordSRV discordSRV, ProxiedPlayer player) { - this.discordSRV = discordSRV; + super(discordSRV, player, () -> discordSRV.audiences().player(player)); this.player = player; this.identity = Identity.identity(player.getUniqueId()); - this.audience = discordSRV.audiences().player(player); - } - - @Override - public boolean hasPermission(String permission) { - return player.hasPermission(permission); - } - - @Override - public void runCommand(String command) { - discordSRV.proxy().getPluginManager().dispatchCommand(player, command); } @Override @@ -59,7 +46,7 @@ public class BungeePlayer implements IPlayer { @Override public @NotNull String username() { - return player.getName(); + return commandSender.getName(); } @Override @@ -72,8 +59,4 @@ public class BungeePlayer implements IPlayer { return BungeeComponentUtil.fromLegacy(player.getDisplayName()); } - @Override - public @NotNull Audience audience() { - return audience; - } } diff --git a/common/build.gradle b/common/build.gradle index 78e3769c..34f9c968 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -64,6 +64,9 @@ dependencies { runtimeDownloadApi 'dev.vankka:mcdiscordreserializer:4.2.4-SNAPSHOT' runtimeDownloadApi 'dev.vankka:enhancedlegacytext:1.0.0-SNAPSHOT' + // Brigadier + compileOnlyApi 'com.mojang:brigadier:1.0.18' + // Database hikari('com.zaxxer:HikariCP:4.0.3') { exclude group: 'org.slf4j' } h2Driver 'com.h2database:h2:2.1.210' diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index f4fa7f40..27d69cae 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -20,13 +20,16 @@ package com.discordsrv.common; import com.discordsrv.api.discord.connection.DiscordConnectionDetails; import com.discordsrv.api.event.bus.EventBus; -import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadEvent; +import com.discordsrv.api.event.events.lifecycle.DiscordSRVConnectedEvent; +import com.discordsrv.api.event.events.lifecycle.DiscordSRVReadyEvent; +import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadedEvent; import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; import com.discordsrv.api.module.type.Module; import com.discordsrv.common.api.util.ApiInstanceUtil; import com.discordsrv.common.channel.ChannelConfigHelper; import com.discordsrv.common.channel.ChannelUpdaterModule; import com.discordsrv.common.channel.GlobalChannelLookupModule; +import com.discordsrv.common.command.game.GameCommandModule; import com.discordsrv.common.component.ComponentFactory; import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.main.LinkedAccountConfig; @@ -45,6 +48,7 @@ import com.discordsrv.common.function.CheckedFunction; import com.discordsrv.common.function.CheckedRunnable; import com.discordsrv.common.groupsync.GroupSyncModule; import com.discordsrv.common.integration.LuckPermsIntegration; +import com.discordsrv.common.invite.DiscordInviteModule; import com.discordsrv.common.linking.LinkProvider; import com.discordsrv.common.linking.impl.MemoryLinker; import com.discordsrv.common.linking.impl.StorageLinker; @@ -69,6 +73,7 @@ import org.jetbrains.annotations.NotNull; import javax.annotation.OverridingMethodsMustInvokeSuper; import java.util.Locale; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -96,11 +101,12 @@ public abstract class AbstractDiscordSRV void registerModule(CheckedFunction> function) { - try { - registerModule(function.apply((T) this)); - } catch (Throwable ignored) {} + moduleManager.registerModule((T) this, function); } @Override @@ -249,6 +254,9 @@ public abstract class AbstractDiscordSRV invokeEnable() { - return invokeLifecycle(this::enable, "Failed to enable", true); + return invokeLifecycle(() -> { + this.enable(); + waitForStatus(Status.CONNECTED); + eventBus().publish(new DiscordSRVReadyEvent()); + }, "Failed to enable", true); } @Override @@ -308,8 +320,8 @@ public abstract class AbstractDiscordSRV invokeReload() { - return invoke(this::reload, "Failed to reload", false); + public final CompletableFuture invokeReload(Set flags, boolean silent) { + return invoke(() -> reload(flags, silent), "Failed to reload", false); } @OverridingMethodsMustInvokeSuper @@ -323,76 +335,16 @@ public abstract class AbstractDiscordSRV flags, boolean initial) throws Throwable { + if (!initial) { + logger().info("Reloading DiscordSRV..."); + } + if (flags.contains(ReloadFlag.CONFIG)) { + try { + connectionConfigManager().load(); + configManager().load(); + } catch (Throwable t) { + setStatus(Status.FAILED_TO_LOAD_CONFIG); + throw t; + } + + channelConfig().reload(); + } + + if (flags.contains(ReloadFlag.LINKED_ACCOUNT_PROVIDER)) { + LinkedAccountConfig linkedAccountConfig = config().linkedAccounts; + if (linkedAccountConfig != null && linkedAccountConfig.enabled) { + String provider = linkedAccountConfig.provider; + switch (provider) { + case "auto": + case "storage": + linkProvider = new StorageLinker(this); + logger().info("Using storage for linked accounts"); + break; + case "memory": { + linkProvider = new MemoryLinker(); + logger().warning("Using memory for linked accounts"); + logger().warning("Linked accounts will be lost upon restart"); + break; + } + default: { + linkProvider = null; + logger().error("Unknown linked account provider: \"" + provider + "\", linked accounts will not be used"); + break; + } + } + } else { + linkProvider = null; + logger().info("Linked accounts are disabled"); + } + } + + if (flags.contains(ReloadFlag.STORAGE)) { + if (storage != null) { + storage.close(); + } + + try { + try { + StorageType storageType = getStorageType(); + logger().info("Using " + storageType.prettyName() + " as storage"); + if (storageType.hikari() && !hikariLoaded) { + hikariLoaded = true; + DependencyLoader.hikari(this).process(classpathAppender()).get(); + } + storage = storageType.storageFunction().apply(this); + storage.initialize(); + logger().info("Storage connection successfully established"); + } catch (ExecutionException e) { + throw new StorageException(e.getCause()); + } catch (StorageException e) { + throw e; + } catch (Throwable t) { + throw new StorageException(t); + } + } catch (StorageException e) { + e.log(this); + logger().error("Failed to connect to storage"); + setStatus(Status.FAILED_TO_START); + return; + } + } + + if (flags.contains(ReloadFlag.DISCORD_CONNECTION)) { + try { + if (discordConnectionManager.instance() != null) { + discordConnectionManager.reconnect().get(); + } else { + discordConnectionManager.connect().get(); + } + waitForStatus(Status.CONNECTED, 20, TimeUnit.SECONDS); + } catch (ExecutionException e) { + throw e.getCause(); + } + } + + if (flags.contains(ReloadFlag.MODULES)) { + moduleManager.reload(); + } + + if (!initial) { + eventBus().publish(new DiscordSRVReloadedEvent(flags)); + logger().info("Reload complete."); + } } } diff --git a/common/src/main/java/com/discordsrv/common/DiscordSRV.java b/common/src/main/java/com/discordsrv/common/DiscordSRV.java index 2d7d5895..bca84add 100644 --- a/common/src/main/java/com/discordsrv/common/DiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/DiscordSRV.java @@ -21,6 +21,7 @@ package com.discordsrv.common; import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.module.type.Module; import com.discordsrv.common.channel.ChannelConfigHelper; +import com.discordsrv.common.command.game.handler.ICommandHandler; import com.discordsrv.common.component.ComponentFactory; import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.main.MainConfig; @@ -47,7 +48,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.nio.file.Path; -import java.util.Locale; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -62,6 +63,7 @@ public interface DiscordSRV extends DiscordSRVApi { PluginManager pluginManager(); OnlineMode onlineMode(); ClasspathAppender classpathAppender(); + ICommandHandler commandHandler(); @NotNull AbstractPlayerProvider playerProvider(); // DiscordSRVApi @@ -126,6 +128,6 @@ public interface DiscordSRV extends DiscordSRVApi { // Lifecycle CompletableFuture invokeEnable(); CompletableFuture invokeDisable(); - CompletableFuture invokeReload(); + CompletableFuture invokeReload(Set flags, boolean silent); } diff --git a/common/src/main/java/com/discordsrv/common/channel/ChannelConfigHelper.java b/common/src/main/java/com/discordsrv/common/channel/ChannelConfigHelper.java index c3c55d90..d6002218 100644 --- a/common/src/main/java/com/discordsrv/common/channel/ChannelConfigHelper.java +++ b/common/src/main/java/com/discordsrv/common/channel/ChannelConfigHelper.java @@ -22,9 +22,7 @@ import com.discordsrv.api.channel.GameChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordMessageChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordTextChannel; import com.discordsrv.api.discord.api.entity.channel.DiscordThreadChannel; -import com.discordsrv.api.event.bus.Subscribe; import com.discordsrv.api.event.events.channel.GameChannelLookupEvent; -import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadEvent; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.config.main.channels.base.ChannelConfig; @@ -94,16 +92,9 @@ public class ChannelConfigHelper { } return map; }); - - discordSRV.eventBus().subscribe(this); } - @Subscribe - public void onReload(DiscordSRVReloadEvent event) { - if (!event.isConfig()) { - return; - } - + public void reload() { Map> newMap = new HashMap<>(); for (Map.Entry entry : channels().entrySet()) { String channelName = entry.getKey(); diff --git a/common/src/main/java/com/discordsrv/common/channel/ChannelUpdaterModule.java b/common/src/main/java/com/discordsrv/common/channel/ChannelUpdaterModule.java index ad6b9556..1797f294 100644 --- a/common/src/main/java/com/discordsrv/common/channel/ChannelUpdaterModule.java +++ b/common/src/main/java/com/discordsrv/common/channel/ChannelUpdaterModule.java @@ -115,5 +115,4 @@ public class ChannelUpdaterModule extends AbstractModule { } } - } diff --git a/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java b/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java new file mode 100644 index 00000000..baed3f6f --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/GameCommandModule.java @@ -0,0 +1,77 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.command.DiscordSRVCommand; +import com.discordsrv.common.command.game.command.subcommand.LinkCommand; +import com.discordsrv.common.command.game.handler.ICommandHandler; +import com.discordsrv.common.config.main.CommandConfig; +import com.discordsrv.common.module.type.AbstractModule; + +import java.util.HashSet; +import java.util.Set; + +public class GameCommandModule extends AbstractModule { + + private final Set commands = new HashSet<>(); + + private final GameCommand primaryCommand; + private final GameCommand discordAlias; + private final GameCommand linkCommand; + + public GameCommandModule(DiscordSRV discordSRV) { + super(discordSRV); + this.primaryCommand = DiscordSRVCommand.get(discordSRV); + this.discordAlias = GameCommand.literal("discord").redirect(primaryCommand); + this.linkCommand = LinkCommand.get(discordSRV); + + registerCommand(primaryCommand); + } + + @Override + public void reload() { + CommandConfig config = discordSRV.config().command; + if (config == null) { + return; + } + + registerCommand(primaryCommand); + if (config.useDiscordCommand) { + registerCommand(discordAlias); + } + if (config.useLinkAlias) { + registerCommand(linkCommand); + } + } + + private void registerCommand(GameCommand command) { + ICommandHandler handler = discordSRV.commandHandler(); + if (handler == null) { + return; + } + + if (!commands.add(command)) { + return; + } + + handler.registerCommand(command); + } +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommand.java b/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommand.java new file mode 100644 index 00000000..3d00feb3 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommand.java @@ -0,0 +1,473 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.abstraction; + +import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.common.function.CheckedFunction; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class GameCommand { + + private static final GameCommandSuggester BOOLEANS_SUGGESTER = (player, previous, current) -> { + if (current.isEmpty()) { + return Arrays.asList("true", "false"); + } else if ("true".startsWith(current)) { + return Collections.singletonList("true"); + } else if ("false".startsWith(current)) { + return Collections.singletonList("false"); + } else { + return Collections.emptyList(); + } + }; + + public static GameCommand literal(String label) { + return new GameCommand(label, ArgumentType.LITERAL); + } + + public static GameCommand booleanArgument(String label) { + return new GameCommand(label, ArgumentType.BOOLEAN).suggester(BOOLEANS_SUGGESTER); + } + + public static GameCommand doubleArgument(String label) { + return new GameCommand(label, ArgumentType.DOUBLE); + } + + public static GameCommand floatArgument(String label) { + return new GameCommand(label, ArgumentType.FLOAT); + } + + public static GameCommand integerArgument(String label) { + return new GameCommand(label, ArgumentType.INTEGER); + } + + public static GameCommand longArgument(String label) { + return new GameCommand(label, ArgumentType.LONG); + } + + public static GameCommand string(String label) { + return new GameCommand(label, ArgumentType.STRING); + } + + public static GameCommand stringWord(String label) { + return new GameCommand(label, ArgumentType.STRING_WORD); + } + + public static GameCommand stringGreedy(String label) { + return new GameCommand(label, ArgumentType.STRING_GREEDY); + } + + private final ExecutorProxy executorProxy = new ExecutorProxy(); + private final SuggesterProxy suggesterProxy = new SuggesterProxy(); + + // Command info + private GameCommand parent = null; + private final String label; + private final ArgumentType argumentType; + private final List children; + private GameCommand redirection = null; + + // Permission + private String requiredPermission; + private Component noPermissionMessage = null; + + // Executor & suggestor + private GameCommandExecutor commandExecutor = null; + private GameCommandSuggester commandSuggester = null; + + // Argument type bounds + private double maxValue = Double.MAX_VALUE; + private double minValue = Double.MIN_VALUE; + + private GameCommand(String label, ArgumentType argumentType) { + this.label = label; + this.argumentType = argumentType; + this.children = new ArrayList<>(); + } + + private GameCommand(GameCommand original) { + this.parent = original.parent; + this.label = original.label; + this.argumentType = original.argumentType; + this.children = original.children; + this.redirection = original.redirection; + this.requiredPermission = original.requiredPermission; + this.noPermissionMessage = original.noPermissionMessage; + this.commandExecutor = original.commandExecutor; + this.commandSuggester = original.commandSuggester; + this.maxValue = original.maxValue; + this.minValue = original.minValue; + } + + public String getLabel() { + return label; + } + + public ArgumentType getArgumentType() { + return argumentType; + } + + /** + * Adds a sub command. You can only have multiple literals (with unique labels) or a single non-literal one. + */ + public GameCommand then(GameCommand child) { + if (redirection != null) { + throw new IllegalStateException("Cannot add children to a redirected node"); + } + for (GameCommand builder : children) { + if (builder.getArgumentType() == ArgumentType.LITERAL + && child.getArgumentType() == ArgumentType.LITERAL + && builder.getLabel().equals(child.getLabel())) { + throw new IllegalArgumentException("Duplicate literal with label \"" + child.label + "\""); + } + if (child.getArgumentType() == ArgumentType.LITERAL && builder.getArgumentType() != ArgumentType.LITERAL) { + throw new IllegalStateException("A non-literal is already present, cannot add literal"); + } + if (child.getArgumentType() != ArgumentType.LITERAL) { + throw new IllegalStateException("Cannot add non-literal when another child is already present"); + } + } + if (child.getNoPermissionMessage() == null && noPermissionMessage != null) { + child.noPermissionMessage(noPermissionMessage); + } + child.parent = this; + this.children.add(child); + return this; + } + + public List getChildren() { + return children; + } + + public GameCommand redirect(GameCommand redirection) { + if (!children.isEmpty()) { + throw new IllegalStateException("Cannot redirect a node with children"); + } + if (requiredPermission != null) { + throw new IllegalStateException("Cannot redirect a node with a required permission"); + } + this.redirection = redirection; + return this; + } + + public GameCommand getRedirection() { + return redirection; + } + + public GameCommand requiredPermission(String permission) { + if (redirection != null) { + throw new IllegalStateException("Cannot required permissions on a node with a redirection"); + } + this.requiredPermission = permission; + return this; + } + + public String getRequiredPermission() { + if (redirection != null) { + return redirection.getRequiredPermission(); + } + return requiredPermission; + } + + public GameCommand noPermissionMessage(Component noPermissionMessage) { + this.noPermissionMessage = noPermissionMessage; + return this; + } + + public Component getNoPermissionMessage() { + return noPermissionMessage; + } + + public GameCommand executor(GameCommandExecutor executor) { + this.commandExecutor = executor; + return this; + } + + public GameCommandExecutor getExecutor() { + return executorProxy; + } + + /** + * Cannot be used with {@link #literal(String)}. + */ + public GameCommand suggester(GameCommandSuggester suggester) { + if (argumentType == ArgumentType.LITERAL) { + throw new IllegalArgumentException("Cannot use on argument type literal"); + } + this.commandSuggester = suggester; + return this; + } + + public GameCommandSuggester getSuggester() { + return suggesterProxy; + } + + /** + * Can only be used on number argument types. + */ + public GameCommand minValue(double minValue) { + mustBeNumber(); + this.minValue = minValue; + return this; + } + + public double getMinValue() { + return minValue; + } + + /** + * Can only be used on number argument types. + */ + public GameCommand maxValue(double maxValue) { + mustBeNumber(); + this.maxValue = maxValue; + return this; + } + + public double getMaxValue() { + return maxValue; + } + + private void mustBeNumber() { + if (!argumentType.number()) { + throw new IllegalArgumentException("Cannot be used on this argument type"); + } + } + + public boolean hasPermission(ICommandSender sender) { + String requiredPermission = getRequiredPermission(); + return requiredPermission == null || sender.hasPermission(requiredPermission); + } + + public void sendNoPermission(ICommandSender sender) { + sender.sendMessage(noPermissionMessage != null + ? noPermissionMessage + : Component.text("No permission", NamedTextColor.RED)); + } + + public ArgumentResult checkArgument(String argument) { + switch (getArgumentType()) { + case LITERAL: return ArgumentResult.fromBoolean(argument.equals(getLabel())); + case STRING: { + if (argument.startsWith("\"")) { + boolean single = argument.length() == 1; + if (argument.endsWith("\"") && !single) { + return new ArgumentResult(argument.substring(1, argument.length() - 1), MatchResult.MATCHES); + } + return new ArgumentResult(single ? "" : argument.substring(1), single ? MatchResult.END : MatchResult.CONTINUE); + } + if (argument.endsWith("\"")) { + return new ArgumentResult(argument.substring(0, argument.length() - 1), MatchResult.END); + } + return new ArgumentResult(argument, MatchResult.MATCHES); + } + case STRING_GREEDY: return new ArgumentResult(argument, GameCommand.MatchResult.CONTINUE); + default: { + try { + return new ArgumentResult(getArgumentType().function().apply(argument), MatchResult.MATCHES); + } catch (Throwable ignored) { + return ArgumentResult.NO_MATCH; + } + } + } + } + + public String getArgumentLabel() { + if (argumentType == ArgumentType.LITERAL) { + return label; + } else { + return "<" + label + ">"; + } + } + + public Component describe() { + StringBuilder stringBuilder = new StringBuilder(); + GameCommand current = this; + while (current != null) { + stringBuilder.insert(0, current.getArgumentLabel() + " "); + current = current.parent; + } + + String command = "/" + stringBuilder.substring(1); + return Component.text(command, NamedTextColor.AQUA); + } + + public void sendCommandInstructions(ICommandSender sender) { + if (children.isEmpty()) { + throw new IllegalStateException("No children"); + } + + TextComponent.Builder builder = Component.text(); + builder.append(describe().color(NamedTextColor.GRAY).append(Component.text(" available subcommands:"))); + boolean anyAvailable = false; + for (GameCommand child : children) { + if (!child.hasPermission(sender)) { + continue; + } + anyAvailable = true; + builder.append(Component.newline()).append(child.describe()); + } + if (!anyAvailable) { + builder.append(Component.newline()) + .append(Component.text("No available subcommands", NamedTextColor.RED)); + } + sender.sendMessage(builder.build()); + } + + @SuppressWarnings({"CloneDoesntDeclareCloneNotSupportedException", "MethodDoesntCallSuperMethod"}) + @Override + protected GameCommand clone() { + return new GameCommand(this); + } + + public static class ArgumentResult { + + public static final ArgumentResult EMPTY_MATCH = new ArgumentResult(MatchResult.MATCHES); + public static final ArgumentResult NO_MATCH = new ArgumentResult(MatchResult.NO_MATCH); + + public static ArgumentResult fromBoolean(boolean value) { + return value ? EMPTY_MATCH : NO_MATCH; + } + + private final Object value; + private final MatchResult result; + + private ArgumentResult(MatchResult result) { + this(null, result); + } + + public ArgumentResult(Object value, MatchResult result) { + this.value = value; + this.result = result; + } + + public Object value() { + return value; + } + + public MatchResult result() { + return result; + } + } + + public enum MatchResult { + MATCHES, + CONTINUE, + END, + NO_MATCH + } + + public enum ArgumentType { + + LITERAL(), + BOOLEAN(false, input -> { + boolean value; + if ((value = input.equals("true")) || input.equals("false")) { + return value; + } + throw new IllegalArgumentException(); + }), + DOUBLE(true, Double::parseDouble), + FLOAT(true, Float::parseFloat), + INTEGER(true, Integer::parseInt), + LONG(true, Long::parseLong), + STRING_WORD(false, input -> input), + STRING(false, true), + STRING_GREEDY(false, true); + + private final boolean number; + private final boolean multi; + private final CheckedFunction function; + + ArgumentType() { + this(false, null); + } + + ArgumentType(boolean number, CheckedFunction function) { + this(number, false, function); + } + + ArgumentType(boolean number, boolean multi) { + this(number, multi, null); + } + + ArgumentType(boolean number, boolean multi, CheckedFunction function) { + this.number = number; + this.multi = multi; + this.function = function; + } + + public boolean number() { + return number; + } + + public boolean multi() { + return multi; + } + + public CheckedFunction function() { + return function; + } + } + + private class ExecutorProxy implements GameCommandExecutor { + + @Override + public void execute(ICommandSender sender, GameCommandArguments arguments) { + if (!hasPermission(sender)) { + sendNoPermission(sender); + return; + } + + if (commandExecutor != null) { + commandExecutor.execute(sender, arguments); + } else if (!children.isEmpty()) { + sendCommandInstructions(sender); + } else { + throw new IllegalStateException("Command (" + GameCommand.this + ") doesn't have children and has no executor"); + } + } + } + + private class SuggesterProxy implements GameCommandSuggester { + + @Override + public List suggestValues( + ICommandSender sender, + GameCommandArguments previousArguments, + String currentInput + ) { + if (!hasPermission(sender)) { + return Collections.emptyList(); + } + + if (commandSuggester != null) { + return commandSuggester.suggestValues(sender, previousArguments, currentInput); + } else { + return Collections.emptyList(); + } + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandArguments.java b/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandArguments.java new file mode 100644 index 00000000..7b74316c --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandArguments.java @@ -0,0 +1,29 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.abstraction; + +@FunctionalInterface +public interface GameCommandArguments { + + T get(String label, Class type); + + default Integer getInt(String label) { + return get(label, Integer.class); + } +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandExecutor.java b/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandExecutor.java new file mode 100644 index 00000000..45df77ec --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandExecutor.java @@ -0,0 +1,27 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.abstraction; + +import com.discordsrv.common.command.game.sender.ICommandSender; + +@FunctionalInterface +public interface GameCommandExecutor { + + void execute(ICommandSender sender, GameCommandArguments arguments); +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandSuggester.java b/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandSuggester.java new file mode 100644 index 00000000..a6400936 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/abstraction/GameCommandSuggester.java @@ -0,0 +1,30 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.abstraction; + +import com.discordsrv.common.command.game.sender.ICommandSender; + +import java.util.List; + +@FunctionalInterface +public interface GameCommandSuggester { + + List suggestValues(ICommandSender sender, GameCommandArguments previousArguments, String currentInput); + +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/command/DiscordSRVCommand.java b/common/src/main/java/com/discordsrv/common/command/game/command/DiscordSRVCommand.java new file mode 100644 index 00000000..2e562e7b --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/command/DiscordSRVCommand.java @@ -0,0 +1,64 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.command; + +import com.discordsrv.api.component.MinecraftComponent; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.abstraction.GameCommandArguments; +import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; +import com.discordsrv.common.command.game.command.subcommand.LinkCommand; +import com.discordsrv.common.command.game.command.subcommand.ReloadCommand; +import com.discordsrv.common.command.game.command.subcommand.VersionCommand; +import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.common.component.util.ComponentUtil; + +public class DiscordSRVCommand implements GameCommandExecutor { + + private static GameCommand INSTANCE; + + public static GameCommand get(DiscordSRV discordSRV) { + if (INSTANCE == null) { + INSTANCE = GameCommand.literal("discordsrv") + .requiredPermission("discordsrv.player.command") + .executor(new DiscordSRVCommand(discordSRV)) + .then(LinkCommand.get(discordSRV)) + .then(ReloadCommand.get(discordSRV)) + .then(VersionCommand.get(discordSRV)); + } + return INSTANCE; + } + + private final DiscordSRV discordSRV; + + public DiscordSRVCommand(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + + @Override + public void execute(ICommandSender sender, GameCommandArguments arguments) { + MinecraftComponent component = discordSRV.componentFactory() + .enhancedBuilder(discordSRV.config().command.discordFormat) + .addContext(sender) + .applyPlaceholderService() + .build(); + + sender.sendMessage(ComponentUtil.fromAPI(component)); + } +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/LinkCommand.java b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/LinkCommand.java new file mode 100644 index 00000000..c085efa1 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/LinkCommand.java @@ -0,0 +1,51 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.command.subcommand; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.abstraction.GameCommandArguments; +import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; +import com.discordsrv.common.command.game.sender.ICommandSender; + +public class LinkCommand implements GameCommandExecutor { + + private static GameCommand INSTANCE; + + public static GameCommand get(DiscordSRV discordSRV) { + if (INSTANCE == null) { + INSTANCE = GameCommand.literal("link") + .requiredPermission("discordsrv.player.link") + .executor(new LinkCommand(discordSRV)); + } + + return INSTANCE; + } + + private final DiscordSRV discordSRV; + + public LinkCommand(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + + @Override + public void execute(ICommandSender sender, GameCommandArguments arguments) { + + } +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ReloadCommand.java b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ReloadCommand.java new file mode 100644 index 00000000..9fdb41ae --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/ReloadCommand.java @@ -0,0 +1,148 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.command.subcommand; + +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.abstraction.GameCommandArguments; +import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; +import com.discordsrv.common.command.game.abstraction.GameCommandSuggester; +import com.discordsrv.common.command.game.sender.ICommandSender; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +public class ReloadCommand implements GameCommandExecutor, GameCommandSuggester { + + private static GameCommand INSTANCE; + + public static GameCommand get(DiscordSRV discordSRV) { + if (INSTANCE == null) { + ReloadCommand cmd = new ReloadCommand(discordSRV); + INSTANCE = GameCommand.literal("reload") + .requiredPermission("discordsrv.admin.reload") + .executor(cmd) + .then( + GameCommand.stringGreedy("flags") + .executor(cmd).suggester(cmd) + ); + } + + return INSTANCE; + } + + private final DiscordSRV discordSRV; + + public ReloadCommand(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + + @Override + public void execute(ICommandSender sender, GameCommandArguments arguments) { + AtomicBoolean dangerousFlags = new AtomicBoolean(false); + Set flags = getFlagsFromArguments(sender, arguments, dangerousFlags); + if (flags == null) { + flags = DiscordSRV.ReloadFlag.DEFAULT_FLAGS; + } + + if (dangerousFlags.get()) { + sender.sendMessage(Component.text("You can add -confirm to the end of the command if you wish to proceed anyway", NamedTextColor.DARK_RED)); + return; + } + + if (flags.isEmpty()) { + sender.sendMessage(Component.text("Please specify at least one valid flag", NamedTextColor.RED)); + return; + } + + discordSRV.invokeReload(flags, false).whenComplete((v, t) -> { + if (t != null) { + discordSRV.logger().error("Failed to reload", t); + sender.sendMessage( + Component.text() + .append(Component.text("Reload failed.", NamedTextColor.DARK_RED, TextDecoration.BOLD)) + .append(Component.text("Please check the server console/log for more details.")) + ); + } else { + sender.sendMessage(Component.text("Reload successful", NamedTextColor.GRAY)); + } + }); + } + + private Set getFlagsFromArguments(ICommandSender sender, GameCommandArguments arguments, AtomicBoolean dangerousFlags) { + String argument = null; + try { + argument = arguments.get("flags", String.class); + } catch (IllegalArgumentException ignored) {} + + if (argument == null) { + return null; + } + + List parts = new ArrayList<>(Arrays.asList(argument.split(" "))); + boolean confirm = parts.remove("-confirm"); + + Set flags = new LinkedHashSet<>(); + for (String part : parts) { + try { + DiscordSRV.ReloadFlag flag = DiscordSRV.ReloadFlag.valueOf(part.toUpperCase(Locale.ROOT)); + if (flag.requiresConfirm() && !confirm) { + dangerousFlags.set(true); + sender.sendMessage( + Component.text("Reloading ", NamedTextColor.RED) + .append(Component.text(part, NamedTextColor.GRAY)) + .append(Component.text(" might cause DiscordSRV to end up in a unrecoverable state", NamedTextColor.RED)) + ); + continue; + } + flags.add(flag); + } catch (IllegalArgumentException ignored) { + sender.sendMessage(Component.text("Flag ", NamedTextColor.RED) + .append(Component.text(part, NamedTextColor.GRAY)) + .append(Component.text(" is not known", NamedTextColor.RED))); + } + } + return flags; + } + + @Override + public List suggestValues( + ICommandSender sender, + GameCommandArguments previousArguments, + String currentInput + ) { + int lastSpace = currentInput.lastIndexOf(' ') + 1; + String last = currentInput.substring(lastSpace); + String beforeLastSpace = currentInput.substring(0, lastSpace); + + List options = DiscordSRV.ReloadFlag.ALL.stream() + .map(flag -> flag.name().toLowerCase(Locale.ROOT)) + .filter(flag -> flag.startsWith(last)) + .collect(Collectors.toList()); + for (String part : currentInput.split(" ")) { + options.remove(part); + } + + return options.stream().map(flag -> beforeLastSpace + flag).collect(Collectors.toList()); + } +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/VersionCommand.java b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/VersionCommand.java new file mode 100644 index 00000000..5f77998c --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/command/subcommand/VersionCommand.java @@ -0,0 +1,58 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.command.subcommand; + +import com.discordsrv.api.color.Color; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.command.game.abstraction.GameCommandArguments; +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; +import com.discordsrv.common.command.game.sender.ICommandSender; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; + +public class VersionCommand implements GameCommandExecutor { + + private static GameCommand INSTANCE; + + public static GameCommand get(DiscordSRV discordSRV) { + if (INSTANCE == null) { + INSTANCE = GameCommand.literal("version") + .requiredPermission("discordsrv.admin.version") + .executor(new VersionCommand(discordSRV)); + } + + return INSTANCE; + } + + private final DiscordSRV discordSRV; + + public VersionCommand(DiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + + @Override + public void execute(ICommandSender sender, GameCommandArguments arguments) { + sender.sendMessage( + Component.text().content("Running DiscordSRV ").color(TextColor.color(Color.BLURPLE.rgb())) + .append(Component.text("v" + discordSRV.version(), NamedTextColor.GRAY)) + ); + } +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/handler/BasicCommandHandler.java b/common/src/main/java/com/discordsrv/common/command/game/handler/BasicCommandHandler.java new file mode 100644 index 00000000..7cbd7bcd --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/handler/BasicCommandHandler.java @@ -0,0 +1,237 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.handler; + +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.abstraction.GameCommandArguments; +import com.discordsrv.common.command.game.sender.ICommandSender; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +public class BasicCommandHandler implements ICommandHandler { + + private final Map commands = new HashMap<>(); + + @Override + public void registerCommand(GameCommand command) { + commands.put(command.getLabel(), command); + } + + private GameCommand processCommand( + GameCommand command, + List arguments, + Arguments argumentValues, + BiConsumer tooManyArguments, + BiConsumer unclosedQuote, + List literalOptions + ) { + GameCommand redirection = command.getRedirection(); + if (redirection != null) { + command = redirection; + } + + if (!arguments.isEmpty()) { + List currentOptions = new ArrayList<>(); + for (GameCommand child : command.getChildren()) { + if (child.getArgumentType() == GameCommand.ArgumentType.LITERAL) { + currentOptions.add(child.getLabel()); + } + + GameCommand.ArgumentResult result = child.checkArgument(arguments.get(0)); + GameCommand.MatchResult matchResult = result.result(); + if (matchResult == GameCommand.MatchResult.NO_MATCH) { + continue; + } + arguments.remove(0); + + Object value; + if (child.getArgumentType().multi()) { + StringBuilder content = new StringBuilder((String) result.value()); + if (matchResult == GameCommand.MatchResult.END) { + // Premature end + if (unclosedQuote != null) { + unclosedQuote.accept(command, argumentValues); + } + return unclosedQuote == null ? command : null; + } + + boolean stringStart = matchResult == GameCommand.MatchResult.CONTINUE; + while (stringStart && !arguments.isEmpty()) { + result = child.checkArgument(arguments.remove(0)); + Object resultValue = result.value(); + content.append(' ').append(resultValue != null ? resultValue : ""); + matchResult = result.result(); + if (matchResult == GameCommand.MatchResult.END) { + stringStart = false; + } + } + if (stringStart && child.getArgumentType() == GameCommand.ArgumentType.STRING) { + // Unclosed quote + if (unclosedQuote != null) { + unclosedQuote.accept(command, argumentValues); + } else { + argumentValues.put(child.getLabel(), "\"" + content); + } + return unclosedQuote == null ? child : null; + } + + value = content.toString(); + } else { + value = result.value(); + } + + argumentValues.put(child.getLabel(), value); + return processCommand(child, arguments, argumentValues, tooManyArguments, unclosedQuote, literalOptions); + } + + if (literalOptions != null) { + literalOptions.addAll(currentOptions); + return command; + } + } + + if (!arguments.isEmpty() && tooManyArguments != null) { + tooManyArguments.accept(command, argumentValues); + return null; + } + + return command; + } + + private T useCommand( + String command, + List arguments, + BiConsumer tooManyArguments, + BiConsumer unclosedQuote, + BiFunction function, + List literalOptions + ) { + command = command.substring(command.lastIndexOf(':') + 1); + GameCommand commandBuilder = commands.get(command); + if (commandBuilder == null) { + return null; + } + + Arguments args = new Arguments(); + GameCommand subCommand = processCommand(commandBuilder, new ArrayList<>(arguments), args, tooManyArguments, unclosedQuote, literalOptions); + if (subCommand == null) { + return null; + } + + return function.apply(subCommand, args); + } + + public void execute(ICommandSender sender, String command, List arguments) { + useCommand( + command, + arguments, + (cmd, args) -> { + if (!cmd.hasPermission(sender)) { + cmd.sendNoPermission(sender); + return; + } + + error(sender, command, arguments, args, "Incorrect argument for command"); + }, + (cmd, args) -> { + if (!cmd.hasPermission(sender)) { + cmd.sendNoPermission(sender); + return; + } + + error(sender, command, arguments, args, "Unclosed quoted string"); + }, + (cmd, args) -> { + cmd.getExecutor().execute(sender, args); + return null; + }, + null); + } + + private void error(ICommandSender sender, String command, List arguments, Arguments args, String title) { + // Mimic brigadier behaviour (en-US) + StringBuilder builder = new StringBuilder(command); + for (Object value : args.getValues().values()) { + builder.append(" ").append(value); + } + int length = builder.length(); + String pre = length >= 10 ? "..." + builder.substring(length - 9, length) : builder.toString(); + + sender.sendMessage( + Component.text() + .append(Component.text(title, NamedTextColor.RED)) + .append(Component.newline()) + .append(Component.text(pre + " ", NamedTextColor.GRAY)) + .append(Component.text(String.join(" ", arguments), NamedTextColor.RED, TextDecoration.UNDERLINED)) + .append(Component.text("<--[HERE]", NamedTextColor.RED, TextDecoration.ITALIC)) + ); + } + + public List suggest(ICommandSender sender, String command, List arguments) { + List suggestions = new ArrayList<>(); + return useCommand( + command, + arguments, + null, + null, + (cmd, args) -> { + suggestions.addAll( + cmd.getSuggester().suggestValues(sender, args, String.valueOf(args.get(cmd.getLabel(), Object.class))) + .stream() + .map(value -> value.substring(value.lastIndexOf(' ') + 1)) + .collect(Collectors.toList()) + ); + return suggestions; + }, + suggestions + ); + } + + private static class Arguments implements GameCommandArguments { + + private final Map values = new LinkedHashMap<>(); + + @SuppressWarnings("unchecked") + @Override + public T get(String label, Class type) { + return (T) values.get(label); + } + + public void put(String label, Object value) { + values.put(label, value); + } + + public Map getValues() { + return values; + } + + @Override + public String toString() { + return "Arguments{" + + "values=" + values + + '}'; + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/handler/ICommandHandler.java b/common/src/main/java/com/discordsrv/common/command/game/handler/ICommandHandler.java new file mode 100644 index 00000000..401f567b --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/handler/ICommandHandler.java @@ -0,0 +1,26 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.handler; + +import com.discordsrv.common.command.game.abstraction.GameCommand; + +public interface ICommandHandler { + + void registerCommand(GameCommand command); +} diff --git a/common/src/main/java/com/discordsrv/common/command/game/handler/util/BrigadierUtil.java b/common/src/main/java/com/discordsrv/common/command/game/handler/util/BrigadierUtil.java new file mode 100644 index 00000000..01cd4c16 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/command/game/handler/util/BrigadierUtil.java @@ -0,0 +1,136 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.command.game.handler.util; + +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; +import com.discordsrv.common.command.game.abstraction.GameCommandSuggester; +import com.discordsrv.common.command.game.sender.ICommandSender; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.*; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * Helper class to convert DiscordSRV's abstract command tree into a brigadier one. + */ +public final class BrigadierUtil { + + private static final Map> CACHE = new ConcurrentHashMap<>(); + + private BrigadierUtil() {} + + public static LiteralCommandNode convertToBrigadier(GameCommand command, Function commandSenderMapper) { + return (LiteralCommandNode) convert(command, commandSenderMapper); + } + + @SuppressWarnings("unchecked") + private static CommandNode convert(GameCommand commandBuilder, Function commandSenderMapper) { + CommandNode alreadyConverted = (CommandNode) CACHE.get(commandBuilder); + if (alreadyConverted != null) { + return alreadyConverted; + } + + GameCommand.ArgumentType type = commandBuilder.getArgumentType(); + String label = commandBuilder.getLabel(); + GameCommandExecutor executor = commandBuilder.getExecutor(); + GameCommandSuggester suggester = commandBuilder.getSuggester(); + GameCommand redirection = commandBuilder.getRedirection(); + String requiredPermission = commandBuilder.getRequiredPermission(); + + ArgumentBuilder argumentBuilder; + if (type == GameCommand.ArgumentType.LITERAL) { + argumentBuilder = LiteralArgumentBuilder.literal(label); + } else { + argumentBuilder = RequiredArgumentBuilder.argument(label, convertType(commandBuilder)); + } + + for (GameCommand child : commandBuilder.getChildren()) { + argumentBuilder.then(convert(child, commandSenderMapper)); + } + if (redirection != null) { + CommandNode redirectNode = (CommandNode) CACHE.get(redirection); + if (redirectNode == null) { + redirectNode = convert(redirection, commandSenderMapper); + } + argumentBuilder.redirect(redirectNode); + } + + if (requiredPermission != null) { + argumentBuilder.requires(sender -> { + ICommandSender commandSender = commandSenderMapper.apply(sender); + return commandSender.hasPermission(requiredPermission); + }); + } + if (executor != null) { + argumentBuilder.executes(context -> { + executor.execute( + commandSenderMapper.apply(context.getSource()), + context::getArgument + ); + return Command.SINGLE_SUCCESS; + }); + } + if (suggester != null && argumentBuilder instanceof RequiredArgumentBuilder) { + ((RequiredArgumentBuilder) argumentBuilder).suggests((context, builder) -> { + try { + List suggestions = suggester.suggestValues( + commandSenderMapper.apply(context.getSource()), + context::getArgument, + builder.getRemaining() + ); + suggestions.forEach(suggestion -> builder.suggest(suggestion.toString())); + } catch (Throwable t) { + t.printStackTrace(); + } + return CompletableFuture.completedFuture(builder.build()); + }); + } + + CommandNode node = argumentBuilder.build(); + CACHE.put(commandBuilder, node); + return node; + } + + private static ArgumentType convertType(GameCommand builder) { + GameCommand.ArgumentType argumentType = builder.getArgumentType(); + double min = builder.getMinValue(); + double max = builder.getMaxValue(); + switch (argumentType) { + case LONG: return LongArgumentType.longArg((long) min, (long) max); + case FLOAT: return FloatArgumentType.floatArg((float) min, (float) max); + case DOUBLE: return DoubleArgumentType.doubleArg(min, max); + case STRING: return StringArgumentType.string(); + case STRING_WORD: return StringArgumentType.word(); + case STRING_GREEDY: return StringArgumentType.greedyString(); + case BOOLEAN: return BoolArgumentType.bool(); + case INTEGER: return IntegerArgumentType.integer((int) min, (int) max); + } + throw new IllegalStateException(); + } +} diff --git a/common/src/main/java/com/discordsrv/common/config/main/CommandConfig.java b/common/src/main/java/com/discordsrv/common/config/main/CommandConfig.java new file mode 100644 index 00000000..e82788b1 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/main/CommandConfig.java @@ -0,0 +1,35 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.config.main; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class CommandConfig { + + @Comment("If the /discord command should be set by DiscordSRV") + public boolean useDiscordCommand = true; + + @Comment("Use /link as a alias for /discord link") + public boolean useLinkAlias = false; + + @Comment("Discord command format, player placeholders may be used") + public String discordFormat = "[click:open_url:%discord_invite%]&b&lClick here &r&ato join our Discord server!"; +} diff --git a/common/src/main/java/com/discordsrv/common/config/main/DiscordInviteConfig.java b/common/src/main/java/com/discordsrv/common/config/main/DiscordInviteConfig.java new file mode 100644 index 00000000..47b4444a --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/main/DiscordInviteConfig.java @@ -0,0 +1,38 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.config.main; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class DiscordInviteConfig { + + @Comment("Manually enter a invite url here, if this isn't set this is ignored and the options below will take effect") + public String inviteUrl = ""; + + @Comment("If the bot is only in one Discord server, it will attempt to get it's vanity url") + public boolean attemptToUseVanityUrl = true; + + @Comment("If the bot is only in one Discord server, it will attempt to automatically create a invite for it.\n" + + "The bot will only attempt to do so if it has permission to \"Create Invite\"\n" + + "The server must also have a rules channel (available for community servers) or default channel (automatically determined by Discord)") + public boolean autoCreateInvite = true; + +} diff --git a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java index eba30d41..1281fcf1 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java @@ -49,4 +49,10 @@ public class MainConfig implements Config { @Comment("Configuration options for group-role synchronization") public GroupSyncConfig groupSync = new GroupSyncConfig(); + + @Comment("Command configuration") + public CommandConfig command = new CommandConfig(); + + @Comment("The %discord_invite% placeholder configuration. The below options will be attempted in the order they are in") + public DiscordInviteConfig invite = new DiscordInviteConfig(); } diff --git a/common/src/main/java/com/discordsrv/common/config/main/channels/MirroringConfig.java b/common/src/main/java/com/discordsrv/common/config/main/channels/MirroringConfig.java index ce7328f4..b86a454f 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/channels/MirroringConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/channels/MirroringConfig.java @@ -33,5 +33,5 @@ public class MirroringConfig { @Comment("The format of the username of mirrored messages\n" + "It's recommended to include some special character if in-game messages use webhooks,\n" + "in order to prevent Discord users and in-game players being grouped together") - public String usernameFormat = "%user_effective_name% [M]"; + public String usernameFormat = "%user_effective_name% \uD83D\uDD03"; } diff --git a/common/src/main/java/com/discordsrv/common/dependency/InitialDependencyLoader.java b/common/src/main/java/com/discordsrv/common/dependency/InitialDependencyLoader.java index e25d86de..8f98aae2 100644 --- a/common/src/main/java/com/discordsrv/common/dependency/InitialDependencyLoader.java +++ b/common/src/main/java/com/discordsrv/common/dependency/InitialDependencyLoader.java @@ -18,6 +18,7 @@ package com.discordsrv.common.dependency; +import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.logging.Logger; import com.discordsrv.common.scheduler.threadfactory.CountingForkJoinWorkerThreadFactory; @@ -91,7 +92,7 @@ public class InitialDependencyLoader { if (discordSRV == null) { return; } - discordSRV.invokeReload(); + discordSRV.invokeReload(DiscordSRVApi.ReloadFlag.DEFAULT_FLAGS, false); } public void disable(DiscordSRV discordSRV) { diff --git a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java index f064f578..286b7e43 100644 --- a/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java +++ b/common/src/main/java/com/discordsrv/common/groupsync/GroupSyncModule.java @@ -69,7 +69,11 @@ public class GroupSyncModule extends AbstractModule { @Override public void reload() { synchronized (pairs) { - pairs.values().forEach(future -> future.cancel(false)); + pairs.values().forEach(future -> { + if (future != null) { + future.cancel(false); + } + }); pairs.clear(); groupsToPairs.clear(); rolesToPairs.clear(); diff --git a/common/src/main/java/com/discordsrv/common/invite/DiscordInviteModule.java b/common/src/main/java/com/discordsrv/common/invite/DiscordInviteModule.java new file mode 100644 index 00000000..977ee4ff --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/invite/DiscordInviteModule.java @@ -0,0 +1,98 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.invite; + +import com.discordsrv.api.placeholder.annotation.Placeholder; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.config.main.DiscordInviteConfig; +import com.discordsrv.common.module.type.AbstractModule; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.TextChannel; + +import java.util.List; + +public class DiscordInviteModule extends AbstractModule { + + private static final String UNKNOWN_INVITE = "https://discord.gg/#Could_not_get_invite,_please_check_your_configuration"; + private String invite; + + public DiscordInviteModule(DiscordSRV discordSRV) { + super(discordSRV); + discordSRV.placeholderService().addGlobalContext(this); + } + + @Override + public void reload() { + JDA jda = discordSRV.jda().orElse(null); + if (jda == null) { + return; + } + + DiscordInviteConfig config = discordSRV.config().invite; + + // Manual + String invite = config.inviteUrl; + if (invite != null && !invite.isEmpty()) { + this.invite = invite; + return; + } + + List guilds = jda.getGuilds(); + if (guilds.size() != 1) { + return; + } + + Guild guild = guilds.get(0); + + // Vanity url + if (config.attemptToUseVanityUrl) { + String vanityUrl = guild.getVanityUrl(); + if (vanityUrl != null) { + this.invite = vanityUrl; + return; + } + } + + // Auto create + if (config.autoCreateInvite) { + Member selfMember = guild.getSelfMember(); + if (!selfMember.hasPermission(Permission.CREATE_INSTANT_INVITE)) { + return; + } + + TextChannel channel = guild.getRulesChannel(); + if (channel == null) { + channel = guild.getDefaultChannel(); + } + if (channel == null) { + return; + } + + channel.createInvite().setMaxAge(0).setUnique(true).queue(inv -> this.invite = inv.getUrl()); + } + } + + @Placeholder("discord_invite") + public String getInvite() { + return invite != null ? invite : UNKNOWN_INVITE; + } +} diff --git a/common/src/main/java/com/discordsrv/common/linking/impl/StorageLinker.java b/common/src/main/java/com/discordsrv/common/linking/impl/StorageLinker.java index 4c5b8cec..3d524c42 100644 --- a/common/src/main/java/com/discordsrv/common/linking/impl/StorageLinker.java +++ b/common/src/main/java/com/discordsrv/common/linking/impl/StorageLinker.java @@ -31,7 +31,6 @@ public class StorageLinker extends CachedLinkProvider implements LinkProvider, L public StorageLinker(DiscordSRV discordSRV) { super(discordSRV); - discordSRV.logger().info("Using storage for linked accounts"); } @Override diff --git a/common/src/main/java/com/discordsrv/common/module/ModuleManager.java b/common/src/main/java/com/discordsrv/common/module/ModuleManager.java index 7f636962..1c07d770 100644 --- a/common/src/main/java/com/discordsrv/common/module/ModuleManager.java +++ b/common/src/main/java/com/discordsrv/common/module/ModuleManager.java @@ -20,13 +20,16 @@ package com.discordsrv.common.module; import com.discordsrv.api.event.bus.EventPriority; import com.discordsrv.api.event.bus.Subscribe; -import com.discordsrv.api.event.events.lifecycle.DiscordSRVReloadEvent; import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; import com.discordsrv.api.module.type.Module; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.debug.DebugGenerateEvent; import com.discordsrv.common.debug.file.TextDebugFile; +import com.discordsrv.common.function.CheckedFunction; +import com.discordsrv.common.logging.Logger; +import com.discordsrv.common.logging.NamedLogger; import com.discordsrv.common.module.type.AbstractModule; +import com.discordsrv.common.module.type.ModuleDelegate; import java.util.ArrayList; import java.util.List; @@ -39,13 +42,20 @@ public class ModuleManager { private final Set modules = new CopyOnWriteArraySet<>(); private final Map moduleLookupTable = new ConcurrentHashMap<>(); + private final Map> delegates = new ConcurrentHashMap<>(); private final DiscordSRV discordSRV; + private final Logger logger; public ModuleManager(DiscordSRV discordSRV) { this.discordSRV = discordSRV; + this.logger = new NamedLogger(discordSRV, "MODULE_MANAGER"); discordSRV.eventBus().subscribe(this); } + public Logger logger() { + return logger; + } + @SuppressWarnings("unchecked") public T getModule(Class moduleType) { return (T) moduleLookupTable.computeIfAbsent(moduleType.getName(), key -> { @@ -62,37 +72,66 @@ public class ModuleManager { }); } - public void register(AbstractModule module) { + private AbstractModule getAbstract(Module module) { + return module instanceof AbstractModule + ? (AbstractModule) module + : delegates.computeIfAbsent(module, mod -> new ModuleDelegate(discordSRV, mod)); + } + + public
void registerModule(DT discordSRV, CheckedFunction> function) { + try { + register(function.apply(discordSRV)); + } catch (Throwable t) { + logger.debug("Module initialization failed", t); + } + } + + public void register(Module module) { + if (module instanceof ModuleDelegate) { + throw new IllegalArgumentException("Cannot register a delegate"); + } + + AbstractModule abstractModule = getAbstract(module); + + logger.debug(module + " registered"); this.modules.add(module); this.moduleLookupTable.put(module.getClass().getName(), module); - enable(module, true); + if (discordSRV.config() != null) { + // Check if config is ready, if it is already we'll enable the module + enable(abstractModule); + } } - private void enable(Module module, boolean enableNonAbstract) { + private void enable(AbstractModule module) { try { - if (module instanceof AbstractModule) { - ((AbstractModule) module).enableModule(); - } else if (enableNonAbstract) { - module.enable(); + if (module.enableModule()) { + logger.debug(module + " enabled"); } } catch (Throwable t) { - discordSRV.logger().error("Failed to enable " + module.getClass().getSimpleName(), t); + discordSRV.logger().error("Failed to enable " + module.toString(), t); } } public void unregister(Module module) { - this.modules.remove(module); - this.moduleLookupTable.values().removeIf(mod -> mod == module); + if (module instanceof ModuleDelegate) { + throw new IllegalArgumentException("Cannot unregister a delegate"); + } disable(module); + + this.modules.remove(module); + this.moduleLookupTable.values().removeIf(mod -> mod == module); + this.delegates.remove(module); } private void disable(Module module) { + AbstractModule abstractModule = getAbstract(module); try { - module.disable(); + logger.debug(module + " disabling"); + abstractModule.disable(); } catch (Throwable t) { - discordSRV.logger().error("Failed to disable " + module.getClass().getSimpleName(), t); + discordSRV.logger().error("Failed to disable " + abstractModule.toString(), t); } } @@ -103,14 +142,15 @@ public class ModuleManager { } } - @Subscribe(priority = EventPriority.EARLY) - public void onReload(DiscordSRVReloadEvent event) { + public void reload() { for (Module module : modules) { + AbstractModule abstractModule = getAbstract(module); + // Check if the module needs to be enabled due to reload - enable(module, false); + enable(abstractModule); try { - module.reload(); + abstractModule.reload(); } catch (Throwable t) { discordSRV.logger().error("Failed to reload " + module.getClass().getSimpleName(), t); } @@ -124,11 +164,13 @@ public class ModuleManager { builder.append("Enabled modules:"); List disabled = new ArrayList<>(); for (Module module : modules) { - if (!module.isEnabled()) { - disabled.add(module); + AbstractModule abstractModule = getAbstract(module); + + if (!abstractModule.isEnabled()) { + disabled.add(abstractModule); continue; } - appendModule(builder, module); + appendModule(builder, abstractModule); } builder.append("\n\nDisabled modules:"); diff --git a/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java b/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java index 76b60b6d..16768474 100644 --- a/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java +++ b/common/src/main/java/com/discordsrv/common/module/type/AbstractModule.java @@ -44,9 +44,9 @@ public abstract class AbstractModule
implements Module { return logger; } - public final void enableModule() { + public final boolean enableModule() { if (hasBeenEnabled || !isEnabled()) { - return; + return false; } hasBeenEnabled = true; @@ -56,6 +56,12 @@ public abstract class AbstractModule
implements Module { discordSRV.eventBus().subscribe(this); // Ignore not having listener methods exception } catch (IllegalArgumentException ignored) {} + return true; + } + + @Override + public String toString() { + return getClass().getName() + "{enabled=" + isEnabled() + "}"; } // Utility diff --git a/common/src/main/java/com/discordsrv/common/module/type/ModuleDelegate.java b/common/src/main/java/com/discordsrv/common/module/type/ModuleDelegate.java new file mode 100644 index 00000000..e8b62fb6 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/module/type/ModuleDelegate.java @@ -0,0 +1,59 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.module.type; + +import com.discordsrv.api.module.type.Module; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.logging.NamedLogger; + +public class ModuleDelegate extends AbstractModule { + + private final Module module; + + public ModuleDelegate(DiscordSRV discordSRV, Module module) { + super(discordSRV, new NamedLogger(discordSRV, module.getClass().getName())); + this.module = module; + } + + @Override + public boolean isEnabled() { + return module.isEnabled(); + } + + @Override + public void enable() { + module.enable(); + } + + @Override + public void reload() { + module.reload(); + } + + @Override + public void disable() { + module.disable(); + } + + @Override + public String toString() { + String original = super.toString(); + return original.substring(0, original.length() - 1) + ",module=" + module.getClass().getName() + "(" + module + ")}"; + } +} diff --git a/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java b/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java index 06a2369d..9fa7b2ce 100644 --- a/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java +++ b/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java @@ -18,6 +18,7 @@ package com.discordsrv.common; +import com.discordsrv.common.command.game.handler.ICommandHandler; import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.main.MainConfig; import com.discordsrv.common.config.manager.ConnectionConfigManager; @@ -38,6 +39,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +@SuppressWarnings("ConstantConditions") public class MockDiscordSRV extends AbstractDiscordSRV { public static final MockDiscordSRV INSTANCE = new MockDiscordSRV(); @@ -102,6 +104,11 @@ public class MockDiscordSRV extends AbstractDiscordSRV playerProvider() { return null; diff --git a/i18n/src/main/java/com/discordsrv/config/MockDiscordSRV.java b/i18n/src/main/java/com/discordsrv/config/MockDiscordSRV.java index 589d7af6..ef3a4957 100644 --- a/i18n/src/main/java/com/discordsrv/config/MockDiscordSRV.java +++ b/i18n/src/main/java/com/discordsrv/config/MockDiscordSRV.java @@ -19,6 +19,7 @@ package com.discordsrv.config; import com.discordsrv.common.AbstractDiscordSRV; +import com.discordsrv.common.command.game.handler.ICommandHandler; import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.main.MainConfig; import com.discordsrv.common.config.manager.ConnectionConfigManager; @@ -35,6 +36,7 @@ import org.jetbrains.annotations.NotNull; import java.nio.file.Path; import java.nio.file.Paths; +@SuppressWarnings("ConstantConditions") public class MockDiscordSRV extends AbstractDiscordSRV { @Override @@ -77,6 +79,11 @@ public class MockDiscordSRV extends AbstractDiscordSRV playerProvider() { return null; diff --git a/sponge/build.gradle b/sponge/build.gradle index a4fd9efe..d1c49d8e 100644 --- a/sponge/build.gradle +++ b/sponge/build.gradle @@ -1,5 +1,11 @@ apply from: rootProject.file('buildscript/runtime.gradle') +shadowJar { + archiveFileName = 'sponge.jarinjar' + + // Relocations in buildscript/relocations.gradle +} + var spongeVersion = '8.0.0' allprojects { repositories { @@ -31,9 +37,3 @@ dependencies { implementation 'dev.vankka:dependencydownload-jarinjar-bootstrap:' + rootProject.ddVersion compileOnlyApi project(':sponge:sponge-loader') } - -shadowJar { - archiveFileName = 'sponge.jarinjar' - - // Relocations in buildscript/relocations.gradle -} diff --git a/sponge/src/main/java/com/discordsrv/sponge/DiscordSRVSpongeBootstrap.java b/sponge/src/main/java/com/discordsrv/sponge/DiscordSRVSpongeBootstrap.java index 6b8a71a5..5c84d6f1 100644 --- a/sponge/src/main/java/com/discordsrv/sponge/DiscordSRVSpongeBootstrap.java +++ b/sponge/src/main/java/com/discordsrv/sponge/DiscordSRVSpongeBootstrap.java @@ -22,6 +22,7 @@ import com.discordsrv.common.dependency.InitialDependencyLoader; import com.discordsrv.common.logging.Logger; import com.discordsrv.common.logging.backend.impl.Log4JLoggerImpl; import com.discordsrv.sponge.bootstrap.ISpongeBootstrap; +import com.discordsrv.sponge.command.game.handler.SpongeCommandHandler; import dev.vankka.dependencydownload.classpath.ClasspathAppender; import dev.vankka.mcdependencydownload.bootstrap.AbstractBootstrap; import dev.vankka.mcdependencydownload.bootstrap.classpath.JarInJarClasspathAppender; @@ -39,6 +40,7 @@ public class DiscordSRVSpongeBootstrap extends AbstractBootstrap implements ISpo private final ClasspathAppender classpathAppender; private final InitialDependencyLoader dependencies; private SpongeDiscordSRV discordSRV; + private SpongeCommandHandler commandHandler; private final PluginContainer pluginContainer; private final Game game; @@ -63,11 +65,14 @@ public class DiscordSRVSpongeBootstrap extends AbstractBootstrap implements ISpo @Override public void onConstruct() { dependencies.load(); + + this.commandHandler = new SpongeCommandHandler(() -> discordSRV, pluginContainer); + game.eventManager().registerListeners(pluginContainer, commandHandler); } @Override public void onStarted() { - dependencies.enable(() -> this.discordSRV = new SpongeDiscordSRV(logger, classpathAppender, dataDirectory, pluginContainer, game)); + dependencies.enable(() -> this.discordSRV = new SpongeDiscordSRV(logger, classpathAppender, dataDirectory, pluginContainer, game, commandHandler)); if (discordSRV != null) { discordSRV.invokeServerStarted(); } diff --git a/sponge/src/main/java/com/discordsrv/sponge/SpongeDiscordSRV.java b/sponge/src/main/java/com/discordsrv/sponge/SpongeDiscordSRV.java index 8a5de485..b0b11b9d 100644 --- a/sponge/src/main/java/com/discordsrv/sponge/SpongeDiscordSRV.java +++ b/sponge/src/main/java/com/discordsrv/sponge/SpongeDiscordSRV.java @@ -19,6 +19,7 @@ package com.discordsrv.sponge; import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.common.command.game.handler.ICommandHandler; import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.main.MainConfig; import com.discordsrv.common.config.manager.ConnectionConfigManager; @@ -27,6 +28,7 @@ import com.discordsrv.common.debug.data.OnlineMode; import com.discordsrv.common.logging.Logger; import com.discordsrv.common.plugin.PluginManager; import com.discordsrv.common.server.ServerDiscordSRV; +import com.discordsrv.sponge.command.game.handler.SpongeCommandHandler; import com.discordsrv.sponge.console.SpongeConsole; import com.discordsrv.sponge.player.SpongePlayerProvider; import com.discordsrv.sponge.plugin.SpongePluginManager; @@ -52,8 +54,16 @@ public class SpongeDiscordSRV extends ServerDiscordSRV connectionConfigManager() { return null; diff --git a/sponge/src/main/java/com/discordsrv/sponge/command/game/handler/SpongeCommandHandler.java b/sponge/src/main/java/com/discordsrv/sponge/command/game/handler/SpongeCommandHandler.java new file mode 100644 index 00000000..6e022ac2 --- /dev/null +++ b/sponge/src/main/java/com/discordsrv/sponge/command/game/handler/SpongeCommandHandler.java @@ -0,0 +1,145 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.sponge.command.game.handler; + +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.abstraction.GameCommandArguments; +import com.discordsrv.common.command.game.abstraction.GameCommandExecutor; +import com.discordsrv.common.command.game.handler.ICommandHandler; +import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.sponge.SpongeDiscordSRV; +import com.discordsrv.sponge.command.game.sender.SpongeCommandSender; +import net.kyori.adventure.audience.Audience; +import org.spongepowered.api.SystemSubject; +import org.spongepowered.api.command.Command; +import org.spongepowered.api.command.CommandResult; +import org.spongepowered.api.command.parameter.CommandContext; +import org.spongepowered.api.command.parameter.Parameter; +import org.spongepowered.api.command.parameter.managed.Flag; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.lifecycle.RegisterCommandEvent; +import org.spongepowered.api.service.permission.Subject; +import org.spongepowered.plugin.PluginContainer; + +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Sponge has its own api for interacting with Brigadier... + */ +public class SpongeCommandHandler implements ICommandHandler { + + private final Map commands = new LinkedHashMap<>(); + private final Supplier discordSRV; + private final PluginContainer container; + + public SpongeCommandHandler(Supplier discordSRV, PluginContainer container) { + this.discordSRV = discordSRV; + this.container = container; + } + + @Override + public void registerCommand(GameCommand command) { + commands.put(command.getLabel(), remap(command)); + } + + private ICommandSender getSender(Subject subject, Audience audience) { + SpongeDiscordSRV discordSRV = this.discordSRV.get(); + if (discordSRV != null) { + if (subject instanceof ServerPlayer) { + return discordSRV.playerProvider().player((ServerPlayer) subject); + } else if (subject instanceof SystemSubject) { + return discordSRV.console(); + } + } + + return new SpongeCommandSender(discordSRV, () -> subject, () -> audience); + } + + private ICommandSender getSender(CommandContext context) { + return getSender(context.subject(), context.cause().audience()); + } + + private Command.Parameterized remap(GameCommand command) { + GameCommand redirection = command.getRedirection(); + if (redirection != null) { + command = redirection; + } + + //GameCommandSuggester suggester = command.getSuggester(); + GameCommandExecutor executor = command.getExecutor(); + String permission = command.getRequiredPermission(); + + Command.Builder builder = Command.builder(); + builder.addFlag(Flag.builder().alias(command.getLabel()).build()); + + if (permission != null) { + builder.permission(permission); + } + for (GameCommand child : command.getChildren()) { + builder.addChild(remap(child)); + } + + if (executor != null) { + builder.executor(context -> { + executor.execute(getSender(context), new GameCommandArguments() { + @Override + public T get(String label, Class type) { + return context.one(new Parameter.Key() { + @Override + public String key() { + return label; + } + + @Override + public Type type() { + return type; + } + + @Override + public boolean isInstance(Object value) { + return type.isInstance(value); + } + + @SuppressWarnings("unchecked") + @Override + public T cast(Object value) { + return (T) value; + } + }).orElse(null); + } + }); + + return CommandResult.success(); + }); + } + + return builder.build(); + } + + @Listener + public void onRegister(RegisterCommandEvent event) { + for (Map.Entry entry : commands.entrySet()) { + event.register(container, entry.getValue(), entry.getKey()); + } + } +} diff --git a/sponge/src/main/java/com/discordsrv/sponge/command/game/sender/SpongeCommandSender.java b/sponge/src/main/java/com/discordsrv/sponge/command/game/sender/SpongeCommandSender.java new file mode 100644 index 00000000..cbcd5762 --- /dev/null +++ b/sponge/src/main/java/com/discordsrv/sponge/command/game/sender/SpongeCommandSender.java @@ -0,0 +1,59 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.sponge.command.game.sender; + +import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.sponge.SpongeDiscordSRV; +import net.kyori.adventure.audience.Audience; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.api.command.exception.CommandException; +import org.spongepowered.api.service.permission.Subject; + +import java.util.function.Supplier; + +public class SpongeCommandSender implements ICommandSender { + + protected final SpongeDiscordSRV discordSRV; + protected final Supplier subjectSupplier; + protected final Supplier audienceSupplier; + + public SpongeCommandSender(SpongeDiscordSRV discordSRV, Supplier subjectSupplier, Supplier audienceSupplier) { + this.discordSRV = discordSRV; + this.subjectSupplier = subjectSupplier; + this.audienceSupplier = audienceSupplier; + } + + @Override + public boolean hasPermission(String permission) { + return subjectSupplier.get().hasPermission(permission); + } + + @Override + public void runCommand(String command) { + try { + Subject subject = subjectSupplier.get(); + discordSRV.game().server().commandManager().process((Subject & Audience) subject, command); + } catch (CommandException ignored) {} + } + + @Override + public @NotNull Audience audience() { + return audienceSupplier.get(); + } +} diff --git a/sponge/src/main/java/com/discordsrv/sponge/console/SpongeConsole.java b/sponge/src/main/java/com/discordsrv/sponge/console/SpongeConsole.java index 880f8d68..039cd57a 100644 --- a/sponge/src/main/java/com/discordsrv/sponge/console/SpongeConsole.java +++ b/sponge/src/main/java/com/discordsrv/sponge/console/SpongeConsole.java @@ -22,40 +22,19 @@ import com.discordsrv.common.console.Console; import com.discordsrv.common.logging.backend.LoggingBackend; import com.discordsrv.common.logging.backend.impl.Log4JLoggerImpl; import com.discordsrv.sponge.SpongeDiscordSRV; -import net.kyori.adventure.audience.Audience; -import org.jetbrains.annotations.NotNull; -import org.spongepowered.api.command.exception.CommandException; +import com.discordsrv.sponge.command.game.sender.SpongeCommandSender; -public class SpongeConsole implements Console { +public class SpongeConsole extends SpongeCommandSender implements Console { - private final SpongeDiscordSRV discordSRV; private final LoggingBackend loggingBackend; public SpongeConsole(SpongeDiscordSRV discordSRV) { - this.discordSRV = discordSRV; + super(discordSRV, () -> discordSRV.game().systemSubject(), () -> discordSRV.game().systemSubject()); this.loggingBackend = Log4JLoggerImpl.getRoot(); } - @Override - public boolean hasPermission(String permission) { - return discordSRV.game().systemSubject().hasPermission(permission); - } - - @Override - public void runCommand(String command) { - try { - discordSRV.game().server().commandManager().process( - discordSRV.game().systemSubject(), command); - } catch (CommandException ignored) {} - } - @Override public LoggingBackend loggingBackend() { return loggingBackend; } - - @Override - public @NotNull Audience audience() { - return discordSRV.game().systemSubject(); - } } diff --git a/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java b/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java index ef25d5f8..51210975 100644 --- a/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java +++ b/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayer.java @@ -21,31 +21,21 @@ package com.discordsrv.sponge.player; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.player.IPlayer; import com.discordsrv.sponge.SpongeDiscordSRV; -import net.kyori.adventure.audience.Audience; +import com.discordsrv.sponge.command.game.sender.SpongeCommandSender; +import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; -import org.spongepowered.api.command.exception.CommandException; import org.spongepowered.api.entity.living.player.server.ServerPlayer; -public class SpongePlayer extends SpongeOfflinePlayer implements IPlayer { +public class SpongePlayer extends SpongeCommandSender implements IPlayer { private final ServerPlayer player; + private final Identity identity; public SpongePlayer(SpongeDiscordSRV discordSRV, ServerPlayer player) { - super(discordSRV, player.user()); + super(discordSRV, () -> player, () -> player); this.player = player; - } - - @Override - public boolean hasPermission(String permission) { - return player.hasPermission(permission); - } - - @Override - public void runCommand(String command) { - try { - discordSRV.game().server().commandManager().process(player, command); - } catch (CommandException ignored) {} + this.identity = Identity.identity(player.uniqueId()); } @Override @@ -53,13 +43,18 @@ public class SpongePlayer extends SpongeOfflinePlayer implements IPlayer { return discordSRV; } + @Override + public @NotNull String username() { + return player.name(); + } + @Override public @NotNull Component displayName() { return player.displayName().get(); } @Override - public @NotNull Audience audience() { - return player; + public @NotNull Identity identity() { + return identity; } } diff --git a/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayerProvider.java b/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayerProvider.java index 2d84ff80..c6c07819 100644 --- a/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayerProvider.java +++ b/sponge/src/main/java/com/discordsrv/sponge/player/SpongePlayerProvider.java @@ -21,7 +21,6 @@ package com.discordsrv.sponge.player; import com.discordsrv.common.player.IOfflinePlayer; import com.discordsrv.common.server.player.ServerPlayerProvider; import com.discordsrv.sponge.SpongeDiscordSRV; -import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.entity.living.player.User; import org.spongepowered.api.entity.living.player.server.ServerPlayer; import org.spongepowered.api.event.Listener; @@ -64,7 +63,7 @@ public class SpongePlayerProvider extends ServerPlayerProvider new IllegalStateException("Player not available")); } diff --git a/velocity/build.gradle b/velocity/build.gradle index 0880f39d..381df80b 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -1,6 +1,12 @@ apply from: rootProject.file('buildscript/standalone.gradle') apply plugin: 'net.kyori.blossom' +shadowJar { + archiveBaseName = 'DiscordSRV-Velocity' + + // Relocations in buildscript/relocations.gradle +} + repositories { exclusiveContent { forRepository { @@ -33,12 +39,6 @@ jar { dependsOn blossomSourceReplacementJava } -shadowJar { - archiveBaseName = 'DiscordSRV-Velocity' - - // Relocations in buildscript/relocations.gradle -} - blossom { var mainClass = 'src/main/java/com/discordsrv/velocity/DiscordSRVVelocityBootstrap.java' replaceToken('@VERSION@', project.version, mainClass) diff --git a/velocity/src/main/java/com/discordsrv/velocity/VelocityDiscordSRV.java b/velocity/src/main/java/com/discordsrv/velocity/VelocityDiscordSRV.java index a8cb5177..df6d3604 100644 --- a/velocity/src/main/java/com/discordsrv/velocity/VelocityDiscordSRV.java +++ b/velocity/src/main/java/com/discordsrv/velocity/VelocityDiscordSRV.java @@ -18,6 +18,7 @@ package com.discordsrv.velocity; +import com.discordsrv.common.command.game.handler.ICommandHandler; import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.main.MainConfig; import com.discordsrv.common.config.manager.ConnectionConfigManager; @@ -27,6 +28,7 @@ import com.discordsrv.common.logging.Logger; import com.discordsrv.common.plugin.PluginManager; import com.discordsrv.common.scheduler.StandardScheduler; import com.discordsrv.proxy.ProxyDiscordSRV; +import com.discordsrv.velocity.command.game.handler.VelocityCommandHandler; import com.discordsrv.velocity.console.VelocityConsole; import com.discordsrv.velocity.player.VelocityPlayerProvider; import com.discordsrv.velocity.plugin.VelocityPluginManager; @@ -50,6 +52,7 @@ public class VelocityDiscordSRV extends ProxyDiscordSRV connectionConfigManager() { return null; diff --git a/velocity/src/main/java/com/discordsrv/velocity/command/game/handler/VelocityCommandHandler.java b/velocity/src/main/java/com/discordsrv/velocity/command/game/handler/VelocityCommandHandler.java new file mode 100644 index 00000000..8fc69cec --- /dev/null +++ b/velocity/src/main/java/com/discordsrv/velocity/command/game/handler/VelocityCommandHandler.java @@ -0,0 +1,56 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.velocity.command.game.handler; + +import com.discordsrv.common.command.game.abstraction.GameCommand; +import com.discordsrv.common.command.game.handler.ICommandHandler; +import com.discordsrv.common.command.game.handler.util.BrigadierUtil; +import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.velocity.VelocityDiscordSRV; +import com.discordsrv.velocity.command.game.sender.VelocityCommandSender; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.velocitypowered.api.command.BrigadierCommand; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.ConsoleCommandSource; +import com.velocitypowered.api.proxy.Player; + +public class VelocityCommandHandler implements ICommandHandler { + + private final VelocityDiscordSRV discordSRV; + + public VelocityCommandHandler(VelocityDiscordSRV discordSRV) { + this.discordSRV = discordSRV; + } + + private ICommandSender getSender(CommandSource source) { + if (source instanceof Player) { + return discordSRV.playerProvider().player((Player) source); + } else if (source instanceof ConsoleCommandSource) { + return discordSRV.console(); + } else { + return new VelocityCommandSender(discordSRV, source); + } + } + + @Override + public void registerCommand(GameCommand command) { + LiteralCommandNode node = BrigadierUtil.convertToBrigadier(command, this::getSender); + discordSRV.proxy().getCommandManager().register(new BrigadierCommand(node)); + } +} diff --git a/velocity/src/main/java/com/discordsrv/velocity/command/game/sender/VelocityCommandSender.java b/velocity/src/main/java/com/discordsrv/velocity/command/game/sender/VelocityCommandSender.java new file mode 100644 index 00000000..2fa9c399 --- /dev/null +++ b/velocity/src/main/java/com/discordsrv/velocity/command/game/sender/VelocityCommandSender.java @@ -0,0 +1,51 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.velocity.command.game.sender; + +import com.discordsrv.common.command.game.sender.ICommandSender; +import com.discordsrv.velocity.VelocityDiscordSRV; +import com.velocitypowered.api.command.CommandSource; +import net.kyori.adventure.audience.Audience; +import org.jetbrains.annotations.NotNull; + +public class VelocityCommandSender implements ICommandSender { + + protected final VelocityDiscordSRV discordSRV; + protected final CommandSource commandSource; + + public VelocityCommandSender(VelocityDiscordSRV discordSRV, CommandSource commandSource) { + this.discordSRV = discordSRV; + this.commandSource = commandSource; + } + + @Override + public boolean hasPermission(String permission) { + return commandSource.hasPermission(permission); + } + + @Override + public void runCommand(String command) { + discordSRV.proxy().getCommandManager().executeAsync(commandSource, command); + } + + @Override + public @NotNull Audience audience() { + return commandSource; + } +} diff --git a/velocity/src/main/java/com/discordsrv/velocity/console/VelocityConsole.java b/velocity/src/main/java/com/discordsrv/velocity/console/VelocityConsole.java index 186b176c..297a98d4 100644 --- a/velocity/src/main/java/com/discordsrv/velocity/console/VelocityConsole.java +++ b/velocity/src/main/java/com/discordsrv/velocity/console/VelocityConsole.java @@ -22,37 +22,19 @@ import com.discordsrv.common.console.Console; import com.discordsrv.common.logging.backend.LoggingBackend; import com.discordsrv.common.logging.backend.impl.Log4JLoggerImpl; import com.discordsrv.velocity.VelocityDiscordSRV; -import net.kyori.adventure.audience.Audience; -import org.jetbrains.annotations.NotNull; +import com.discordsrv.velocity.command.game.sender.VelocityCommandSender; -public class VelocityConsole implements Console { +public class VelocityConsole extends VelocityCommandSender implements Console { - private final VelocityDiscordSRV discordSRV; private final LoggingBackend loggingBackend; public VelocityConsole(VelocityDiscordSRV discordSRV) { - this.discordSRV = discordSRV; + super(discordSRV, discordSRV.proxy().getConsoleCommandSource()); this.loggingBackend = Log4JLoggerImpl.getRoot(); } - @Override - public boolean hasPermission(String permission) { - return discordSRV.proxy().getConsoleCommandSource().hasPermission(permission); - } - - @Override - public void runCommand(String command) { - discordSRV.proxy().getCommandManager().executeAsync( - discordSRV.proxy().getConsoleCommandSource(), command); - } - @Override public LoggingBackend loggingBackend() { return loggingBackend; } - - @Override - public @NotNull Audience audience() { - return discordSRV.proxy().getConsoleCommandSource(); - } } diff --git a/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java b/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java index 8ae1b74c..c0c75942 100644 --- a/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java +++ b/velocity/src/main/java/com/discordsrv/velocity/player/VelocityPlayer.java @@ -21,32 +21,21 @@ package com.discordsrv.velocity.player; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.player.IPlayer; import com.discordsrv.velocity.VelocityDiscordSRV; +import com.discordsrv.velocity.command.game.sender.VelocityCommandSender; import com.velocitypowered.api.proxy.Player; -import net.kyori.adventure.audience.Audience; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import org.jetbrains.annotations.NotNull; -public class VelocityPlayer implements IPlayer { +public class VelocityPlayer extends VelocityCommandSender implements IPlayer { - private final VelocityDiscordSRV discordSRV; private final Player player; public VelocityPlayer(VelocityDiscordSRV discordSRV, Player player) { - this.discordSRV = discordSRV; + super(discordSRV, player); this.player = player; } - @Override - public boolean hasPermission(String permission) { - return player.hasPermission(permission); - } - - @Override - public void runCommand(String command) { - discordSRV.proxy().getCommandManager().executeAsync(player, command); - } - @Override public DiscordSRV discordSRV() { return discordSRV; @@ -70,9 +59,4 @@ public class VelocityPlayer implements IPlayer { () -> Component.text(player.getUsername()) ); } - - @Override - public @NotNull Audience audience() { - return player; - } }