diff --git a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/api/cmds/event/MinecordCommandEvents.java b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/api/cmds/event/MinecordCommandEvents.java index 5d60c3e..8770f25 100644 --- a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/api/cmds/event/MinecordCommandEvents.java +++ b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/api/cmds/event/MinecordCommandEvents.java @@ -5,11 +5,17 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; +import me.axieum.mcmod.minecord.api.cmds.command.MinecordCommand; + /** * A collection of callbacks for Minecord issued commands. */ @@ -18,67 +24,126 @@ public final class MinecordCommandEvents private MinecordCommandEvents() {} /** - * A collection of uptime command events. + * Called before executing a Minecord command. + */ + public static final Event BEFORE_EXECUTE = + EventFactory.createArrayBacked(BeforeExecute.class, callbacks -> (ctx, event, server) -> { + for (BeforeExecute callback : callbacks) { + if (!callback.onBeforeMinecordCommand(ctx, event, server)) + return false; + } + return true; + }); + + /** + * Called after executing a Minecord command. */ - public static final class Uptime + public static final Event AFTER_EXECUTE = + EventFactory.createArrayBacked(AfterExecute.class, callbacks -> (ctx, event, server) -> { + for (AfterExecute callback : callbacks) { + callback.onMinecordCommand(ctx, event, server); + } + }); + + @FunctionalInterface + public interface BeforeExecute + { + /** + * Called before executing a Minecord command. + * + * @param context Minecord command context + * @param event JDA slash command event to reply to + * @param server Minecraft server + * @return true if the Minecord command should execute + */ + boolean onBeforeMinecordCommand( + MinecordCommand context, + SlashCommandInteractionEvent event, + @Nullable MinecraftServer server + ); + } + + @FunctionalInterface + public interface AfterExecute { /** - * Called after executing an uptime command. + * Called after executing a Minecord command. + * + * @param context Minecord command context + * @param event JDA slash command event to reply to + * @param server Minecraft server */ - public static final Event AFTER_EXECUTE = - EventFactory.createArrayBacked(AfterExecuteUptimeCommand.class, callbacks -> (event, server, embed) -> { - EmbedBuilder builder = embed; - for (AfterExecuteUptimeCommand callback : callbacks) - builder = callback.onAfterExecuteUptime(event, server, embed); - return builder; + void onMinecordCommand( + MinecordCommand context, + SlashCommandInteractionEvent event, + @Nullable MinecraftServer server + ); + } + + /** + * A collection of built-in Minecord command events. + */ + public static final class Builtin + { + /** + * Called after executing the ticks-per-second (TPS) command. + */ + public static final Event TPS = + EventFactory.createArrayBacked(TPSCommand.class, callbacks -> (ctx, event, server, embed) -> { + for (TPSCommand callback : callbacks) { + embed = callback.onTPSCommand(ctx, event, server, embed); + } + return embed; + }); + + /** + * Called after executing the uptime command. + */ + public static final Event UPTIME = + EventFactory.createArrayBacked(UptimeCommand.class, callbacks -> (ctx, event, srv, embed) -> { + for (UptimeCommand callback : callbacks) { + embed = callback.onUptimeCommand(ctx, event, srv, embed); + } + return embed; }); @FunctionalInterface - public interface AfterExecuteUptimeCommand + public interface TPSCommand { /** - * Called after executing an uptime command. + * Called after executing the ticks-per-second (TPS) command. * + * @param context Minecord command context * @param event JDA slash command event to reply to - * @param server Minecraft server, if present + * @param server Minecraft server * @param embed builder used to build the embed that will be sent to Discord * @return embed builder used to build the embed that will be sent to Discord */ - @NotNull EmbedBuilder onAfterExecuteUptime( - SlashCommandInteractionEvent event, @Nullable MinecraftServer server, EmbedBuilder embed + @NotNull EmbedBuilder onTPSCommand( + MinecordCommand context, + SlashCommandInteractionEvent event, + @NotNull MinecraftServer server, + EmbedBuilder embed ); } - } - - /** - * A collection of ticks-per-second (TPS) command events. - */ - public static final class TPS - { - /** - * Called after executing a ticks-per-second (TPS) command. - */ - public static final Event AFTER_EXECUTE = - EventFactory.createArrayBacked(AfterExecuteTPSCommand.class, callbacks -> (event, server, embed) -> { - EmbedBuilder builder = embed; - for (AfterExecuteTPSCommand callback : callbacks) - builder = callback.onAfterExecuteTPS(event, server, embed); - return builder; - }); @FunctionalInterface - public interface AfterExecuteTPSCommand + public interface UptimeCommand { /** - * Called after executing a ticks-per-second (TPS) command. + * Called after executing the uptime command. * + * @param context Minecord command * @param event JDA slash command event to reply to - * @param server Minecraft server + * @param server Minecraft server, if present * @param embed builder used to build the embed that will be sent to Discord * @return embed builder used to build the embed that will be sent to Discord */ - @NotNull EmbedBuilder onAfterExecuteTPS( - SlashCommandInteractionEvent event, @NotNull MinecraftServer server, EmbedBuilder embed + @NotNull EmbedBuilder onUptimeCommand( + MinecordCommand context, + SlashCommandInteractionEvent event, + @Nullable MinecraftServer server, + EmbedBuilder embed ); } } @@ -88,64 +153,154 @@ public interface AfterExecuteTPSCommand */ public static final class Custom { + /** + * Called before allowing custom Minecraft-proxy command execution. + * + *

NB: If you cancel the execution by returning {@code null}, + * you are responsible for replying to the interaction. + */ + public static final Event ALLOW_EXECUTE = + EventFactory.createArrayBacked(AllowExecute.class, callbacks -> (ctx, event, server, cmd) -> { + for (AllowExecute callback : callbacks) { + cmd = callback.onAllowCustomCommand(ctx, event, server, cmd); + if (cmd == null) return null; + } + return cmd; + }); + /** * Called before executing a custom Minecraft-proxy command. */ - public static final Event BEFORE_EXECUTE = - EventFactory.createArrayBacked(BeforeExecuteCustomCommand.class, callbacks -> (command, event, server) -> { - for (BeforeExecuteCustomCommand callback : callbacks) - if (!callback.onBeforeExecuteCustom(command, event, server)) - return false; - return true; + public static final Event BEFORE_EXECUTE = + EventFactory.createArrayBacked(BeforeExecute.class, callbacks -> (ctx, event, server, cmd, source) -> { + for (BeforeExecute callback : callbacks) { + source = callback.onBeforeCustomCommand(ctx, event, server, cmd, source); + } + return source; + }); + + /** + * Called during execution of a custom Minecraft-proxy command when + * providing feedback to the executor. + * + *

NB: This is not called if no command feedback was provided! + */ + public static final Event FEEDBACK = + EventFactory.createArrayBacked(Feedback.class, callbacks -> (ctx, event, server, cmd, msg, s, embed) -> { + for (Feedback callback : callbacks) { + embed = callback.onCustomCommandFeedback(ctx, event, server, cmd, msg, s, embed); + if (embed == null) return null; + } + return embed; }); /** * Called after executing a custom Minecraft-proxy command. */ - public static final Event AFTER_EXECUTE = - EventFactory.createArrayBacked(AfterExecuteCustomCommand.class, callbacks -> (ev, se, co, re, su, em) -> { - EmbedBuilder builder = em; - for (AfterExecuteCustomCommand callback : callbacks) - builder = callback.onAfterExecuteCustom(ev, se, co, re, su, em); - return builder; + public static final Event AFTER_EXECUTE = + EventFactory.createArrayBacked(AfterExecute.class, callbacks -> (ctx, event, server, cmd, s, res, exc) -> { + for (AfterExecute callback : callbacks) { + callback.onCustomCommand(ctx, event, server, cmd, s, res, exc); + } }); @FunctionalInterface - public interface BeforeExecuteCustomCommand + public interface AllowExecute + { + /** + * Called before allowing custom Minecraft-proxy command execution. + * + *

NB: If you cancel the execution by returning {@code null}, + * you are responsible for replying to the interaction. + * + * @param context Minecord command context + * @param event JDA slash command event + * @param server Minecraft server + * @param command Minecraft command that will be executed (without leading '/') + * @return the command to be executed if the execution should go ahead, or {@code null} to cancel + */ + @Nullable String onAllowCustomCommand( + MinecordCommand context, + SlashCommandInteractionEvent event, + @NotNull MinecraftServer server, + String command + ); + } + + @FunctionalInterface + public interface BeforeExecute { /** * Called before executing a custom Minecraft-proxy command. * + * @param context Minecord command context * @param event JDA slash command event * @param server Minecraft server * @param command Minecraft command that will be executed (without leading '/') - * @return true if the execution should go ahead, or false to cancel + * @param source Minecraft command source + * @return Minecraft command source used to execute the command */ - boolean onBeforeExecuteCustom(String command, SlashCommandInteractionEvent event, MinecraftServer server); + @NotNull ServerCommandSource onBeforeCustomCommand( + MinecordCommand context, + SlashCommandInteractionEvent event, + @NotNull MinecraftServer server, + String command, + @NotNull ServerCommandSource source + ); } @FunctionalInterface - public interface AfterExecuteCustomCommand + public interface Feedback { /** - * Called after executing a custom Minecraft-proxy command. + * Called during execution of a custom Minecraft-proxy command when + * providing feedback to the executor. + * + *

NB: This is not called if no command feedback was provided! * + * @param context Minecord command context * @param event JDA slash command event to reply to * @param server Minecraft server * @param command Minecraft command that was executed (without leading '/') - * @param result Minecraft command execution feedback + * @param text Minecraft command execution feedback * @param success true if the command was a success * @param embed builder used to build the embed that will be sent to Discord - * @return embed builder used to build the embed that will be sent to Discord + * @return embed builder used to build the embed that will be sent to Discord, or {@code null} to cancel */ - @NotNull EmbedBuilder onAfterExecuteCustom( + @Nullable EmbedBuilder onCustomCommandFeedback( + MinecordCommand context, SlashCommandInteractionEvent event, - MinecraftServer server, + @NotNull MinecraftServer server, String command, - String result, + Text text, boolean success, EmbedBuilder embed ); } + + @FunctionalInterface + public interface AfterExecute + { + /** + * Called after executing a custom Minecraft-proxy command. + * + * @param context Minecord command context + * @param event JDA slash command event to reply to + * @param server Minecraft server + * @param command Minecraft command that was executed (without leading '/') + * @param success true if the command was a success + * @param result Minecraft command execution result + * @param exc Minecraft command syntax exception if present, else {@code null} + */ + void onCustomCommand( + MinecordCommand context, + SlashCommandInteractionEvent event, + @NotNull MinecraftServer server, + String command, + boolean success, + int result, + @Nullable CommandSyntaxException exc + ); + } } } diff --git a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/callback/DiscordCommandListener.java b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/callback/DiscordCommandListener.java index d29e397..aef275f 100644 --- a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/callback/DiscordCommandListener.java +++ b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/callback/DiscordCommandListener.java @@ -6,11 +6,13 @@ import net.dv8tion.jda.api.events.ReadyEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.Nullable; import net.minecraft.server.MinecraftServer; import me.axieum.mcmod.minecord.api.Minecord; import me.axieum.mcmod.minecord.api.cmds.MinecordCommands; +import me.axieum.mcmod.minecord.api.cmds.event.MinecordCommandEvents; import me.axieum.mcmod.minecord.api.util.StringTemplate; import static me.axieum.mcmod.minecord.impl.cmds.MinecordCommandsImpl.LOGGER; import static me.axieum.mcmod.minecord.impl.cmds.MinecordCommandsImpl.getConfig; @@ -44,7 +46,7 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) // Attempt to run the command try { // Check whether Minecraft is required, and hence whether the server has started - final MinecraftServer server = Minecord.getInstance().getMinecraft().orElse(null); + final @Nullable MinecraftServer server = Minecord.getInstance().getMinecraft().orElse(null); if (command.requiresMinecraft() && (server == null || server.getTickTime() == 0)) { LOGGER.warn("@{} used '{}' but the server is not yet ready!", username, raw); event.getHook().sendMessageEmbeds( @@ -74,9 +76,18 @@ public void onSlashCommandInteraction(SlashCommandInteractionEvent event) } } + // Fire an event to allow the command execution to be cancelled + if (!MinecordCommandEvents.BEFORE_EXECUTE.invoker().onBeforeMinecordCommand(command, event, server)) { + LOGGER.debug("@{} used '{}' but its execution was externally cancelled!", username, raw); + return; + } + // Attempt to cascade the event to the matched command LOGGER.info("@{} used '{}'", username, raw); command.execute(event, server); + + // Fire an event for the successful command execution + MinecordCommandEvents.AFTER_EXECUTE.invoker().onMinecordCommand(command, event, server); } catch (Exception e) { LOGGER.error("@{} failed to use '{}'", username, raw, e); event.getHook().sendMessageEmbeds( diff --git a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/CustomCommand.java b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/CustomCommand.java index faa7efc..fae2677 100644 --- a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/CustomCommand.java +++ b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/CustomCommand.java @@ -2,6 +2,8 @@ import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -25,6 +27,7 @@ import me.axieum.mcmod.minecord.api.util.StringTemplate; import me.axieum.mcmod.minecord.impl.cmds.config.CommandConfig; import static me.axieum.mcmod.minecord.impl.cmds.MinecordCommandsImpl.LOGGER; +import static me.axieum.mcmod.minecord.impl.cmds.MinecordCommandsImpl.getConfig; /** * Custom Minecraft proxy Minecord command. @@ -63,43 +66,71 @@ public boolean isEphemeral() @Override public void execute(@NotNull SlashCommandInteractionEvent event, @Nullable MinecraftServer server) throws Exception { - assert server != null; + assert server != null; // this.requiresMinecraft = true // Prepare the Minecraft command - final String mcCommand; + final String origCommand; try { - mcCommand = prepareCommand(config.command, event.getOptions()); + origCommand = prepareCommand(config.command, event.getOptions()); } catch (ParsingException | IllegalArgumentException e) { throw new Exception("Unable to prepare Minecraft command!", e); } - // Fire an event to allow the command execution to be cancelled - if (!MinecordCommandEvents.Custom.BEFORE_EXECUTE.invoker().onBeforeExecuteCustom(mcCommand, event, server)) { - return; - } + // Fire an event to allow the command execution to be mutated or cancelled + final String mcCommand = MinecordCommandEvents.Custom.ALLOW_EXECUTE.invoker().onAllowCustomCommand( + this, event, server, origCommand + ); + if (mcCommand == null || mcCommand.isEmpty()) return; // Create a temporary command source and hence output, to relay command feedback + final String tag = event.getUser().getAsTag(); final String username = event.getMember() != null ? event.getMember().getEffectiveName() : event.getUser().getName(); - final ServerCommandSource source = new ServerCommandSource( - new DiscordCommandOutput(event, server, mcCommand), - Vec3d.ZERO, Vec2f.ZERO, server.getOverworld(), - PERMISSION_LEVEL, username, Text.literal(username), - server, null + final DiscordCommandOutput output = new DiscordCommandOutput(event, server, mcCommand); + final ServerCommandSource origSource = new ServerCommandSource( + output, // command output + Vec3d.ZERO, Vec2f.ZERO, server.getOverworld(), // location & world + PERMISSION_LEVEL, tag, Text.literal(username), // permission & display name + server, null // server & entity + ); + + // Fire an event to allow the command source to be mutated + final ServerCommandSource source = MinecordCommandEvents.Custom.BEFORE_EXECUTE.invoker().onBeforeCustomCommand( + this, event, server, mcCommand, origSource ); // Attempt to proxy the Minecraft command + AtomicBoolean success = new AtomicBoolean(false); + AtomicInteger result = new AtomicInteger(0); + @Nullable CommandSyntaxException error = null; try { - LOGGER.info("@{} is running '/{}'", event.getUser().getAsTag(), mcCommand); + LOGGER.info("@{} is running '/{}'", tag, mcCommand); server.getCommandManager().getDispatcher().execute(mcCommand, source.withConsumer((c, s, r) -> { - // The command was proxied, however the actual feedback was sent to the source's command output - LOGGER.info("@{} ran '/{}' with result {}", event.getUser().getAsTag(), c.getInput(), r); + success.set(s); // if unsuccessful, it may choose to raise a command syntax exception + result.set(r); })); } catch (CommandSyntaxException e) { - // The command was indeed proxied, but the result was not a success - // e.g. trying to whitelist a player who is already whitelisted - reply(event, server, mcCommand, e.getMessage(), false); + error = e; + } finally { + LOGGER.info( + "@{} ran '/{}' with result {} ({})", tag, mcCommand, result.get(), success.get() ? "success" : "fail" + ); + } + + // Fire an event to broadcast the commands successful/failed execution + MinecordCommandEvents.Custom.AFTER_EXECUTE.invoker().onCustomCommand( + this, event, server, mcCommand, success.get(), result.get(), error + ); + + // Finally, if there was still no command feedback sent, let them know with a default message + // NB: This is to prevent the "The application did not respond" error in Discord, e.g. '/say' or '/tellraw' + if (output.prevMessage == null) { + if (error == null) { + source.sendFeedback(Text.literal(getConfig().messages.feedback), false); + } else { + source.sendError(Text.literal(error.getMessage())); + } } } @@ -135,35 +166,6 @@ private static String prepareCommand(@NotNull String command, List 0 && result.charAt(0) == '/' ? result.substring(1) : result; } - /** - * Replies with an embed containing the command execution result. - * - * @param event JDA slash command event to reply to - * @param server Minecraft server - * @param command Minecraft command that was executed (without leading '/') - * @param result Minecraft command execution feedback - * @param success true if the command was a success - */ - public void reply( - SlashCommandInteractionEvent event, MinecraftServer server, String command, String result, boolean success - ) - { - // Build an initial embed for the command feedback - EmbedBuilder embed = new EmbedBuilder() - // Set the colour to green for a success, and red for a failure - .setColor(success ? 0x00ff00 : 0xff0000) - // Set the message - .setDescription(result); - - // Fire an event to allow the command feedback to be mutated or cancelled - embed = MinecordCommandEvents.Custom.AFTER_EXECUTE.invoker().onAfterExecuteCustom( - event, server, command, result, success, embed - ); - - // Build and reply with the resulting embed - event.getHook().sendMessageEmbeds(embed.build()).queue(); - } - /** * A virtual Minecraft command output for use via Discord. */ @@ -172,6 +174,8 @@ private final class DiscordCommandOutput implements CommandOutput private final SlashCommandInteractionEvent event; private final MinecraftServer server; private final String mcCommand; + public boolean erroneous = false; + public @Nullable String prevMessage = null; /** * Constructs a new virtual command output for relaying feedback to Discord. @@ -190,7 +194,30 @@ private DiscordCommandOutput(SlashCommandInteractionEvent event, MinecraftServer @Override public void sendMessage(Text message) { - reply(event, server, mcCommand, message.getString(), true); + // Build an initial embed for the command feedback + final String text = prevMessage != null ? prevMessage + '\n' + message.getString() : message.getString(); + EmbedBuilder embed = new EmbedBuilder() + // Set the colour to green for a success, and red for a failure + .setColor(!erroneous ? 0x00ff00 : 0xff0000) + // Set the message + .setDescription(text); + + // Fire an event to allow the command feedback to be mutated or cancelled + embed = MinecordCommandEvents.Custom.FEEDBACK.invoker().onCustomCommandFeedback( + CustomCommand.this, event, server, mcCommand, message, !erroneous, embed + ); + + // Build and reply with the resulting embed + if (embed != null) { + if (prevMessage == null) { + // This is the first reply, send a new message + event.getHook().sendMessageEmbeds(embed.build()).queue(); + } else { + // There already exists a reply, edit the original message + event.getHook().editOriginalEmbeds(embed.build()).queue(); + } + prevMessage = text; + } } @Override @@ -202,7 +229,8 @@ public boolean shouldReceiveFeedback() @Override public boolean shouldTrackOutput() { - return false; + // This method appears to only be called during 'ServerCommandSource#sendError' + return erroneous = true; } @Override diff --git a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/TPSCommand.java b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/TPSCommand.java index 46b98ff..cb4b9da 100644 --- a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/TPSCommand.java +++ b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/TPSCommand.java @@ -50,8 +50,8 @@ public void execute(@NotNull SlashCommandInteractionEvent event, @Nullable Minec // Set the embed colour on a red to green scale (scale down to a 4-step gradient) .setColor(Color.HSBtoRGB(Math.round(meanTPS / 5d) / 4f / 3f, 1f, 1f)); - // Fire an event to allow the embed to be mutated or cancelled - embed = MinecordCommandEvents.TPS.AFTER_EXECUTE.invoker().onAfterExecuteTPS(event, server, embed); + // Fire an event to allow the embed to be mutated + embed = MinecordCommandEvents.Builtin.TPS.invoker().onTPSCommand(this, event, server, embed); // Build and reply with the resulting embed event.getHook().sendMessageEmbeds(embed.build()).queue(); diff --git a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/UptimeCommand.java b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/UptimeCommand.java index 53cc0df..8339d48 100644 --- a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/UptimeCommand.java +++ b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/command/discord/UptimeCommand.java @@ -46,8 +46,8 @@ public void execute(@NotNull SlashCommandInteractionEvent event, @Nullable Minec // Prepare an embed to be sent to the user EmbedBuilder embed = new EmbedBuilder().setDescription(TEMPLATE.format(getConfig().builtin.uptime.message)); - // Fire an event to allow the embed to be mutated or cancelled - embed = MinecordCommandEvents.Uptime.AFTER_EXECUTE.invoker().onAfterExecuteUptime(event, server, embed); + // Fire an event to allow the embed to be mutated + embed = MinecordCommandEvents.Builtin.UPTIME.invoker().onUptimeCommand(this, event, server, embed); // Build and reply with the resulting embed event.getHook().sendMessageEmbeds(embed.build()).queue(); diff --git a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/config/CommandConfig.java b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/config/CommandConfig.java index 709805a..966ac3c 100644 --- a/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/config/CommandConfig.java +++ b/minecord-cmds/src/main/java/me/axieum/mcmod/minecord/impl/cmds/config/CommandConfig.java @@ -43,6 +43,10 @@ public static class MessagesSchema The error message used when a user must wait before executing a command Usages: ${cooldown} and ${remaining}""") public String cooldown = "Please wait another ${remaining} before doing that! :alarm_clock:"; + + @Comment(""" + The default message used when a command does not provide any feedback of its own, e.g. '/say'""") + public String feedback = "Consider it done! :thumbsup:"; } @Category("Built-in Commands")