diff --git a/minecord-chat/src/main/java/me/axieum/mcmod/minecord/api/chat/event/ChatPlaceholderEvents.java b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/api/chat/event/ChatPlaceholderEvents.java index 42c0423..47c0293 100644 --- a/minecord-chat/src/main/java/me/axieum/mcmod/minecord/api/chat/event/ChatPlaceholderEvents.java +++ b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/api/chat/event/ChatPlaceholderEvents.java @@ -15,9 +15,11 @@ import net.minecraft.network.message.MessageType; import net.minecraft.network.message.SignedMessage; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.filter.FilteredMessage; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; import net.minecraft.util.crash.CrashReport; import net.minecraft.util.registry.RegistryKey; @@ -250,6 +252,36 @@ public static final class Minecraft } }); + /** + * Called when a player sent an in-game message via the {@code /me} command. + */ + public static final Event EMOTE_COMMAND = + EventFactory.createArrayBacked(EmoteCommand.class, callbacks -> (st, source, action) -> { + for (EmoteCommand callback : callbacks) { + callback.onEmoteCommandPlaceholder(st, source, action); + } + }); + + /** + * Called when an admin broadcast an in-game message via the {@code /say} command. + */ + public static final Event SAY_COMMAND = + EventFactory.createArrayBacked(SayCommand.class, callbacks -> (st, source, action) -> { + for (SayCommand callback : callbacks) { + callback.onSayCommandPlaceholder(st, source, action); + } + }); + + /** + * Called when an admin broadcast an in-game message to all players via the {@code /tellraw @a} command. + */ + public static final Event TELLRAW_COMMAND = + EventFactory.createArrayBacked(TellRawCommand.class, callbacks -> (st, source, action) -> { + for (TellRawCommand callback : callbacks) { + callback.onTellRawCommandPlaceholder(st, source, action); + } + }); + @FunctionalInterface public interface ServerStarting { @@ -402,5 +434,60 @@ public interface PlayerDeath */ void onPlayerDeathPlaceholder(StringTemplate template, ServerPlayerEntity player, DamageSource source); } + + @FunctionalInterface + public interface EmoteCommand + { + /** + * Called when a player sent an in-game message via the {@code /me} + * command. + * + * @param template mutable string template + * @param source source of the command, e.g. a player + * @param action received message contents + * @see net.minecraft.network.message.MessageType#EMOTE_COMMAND + * @see net.fabricmc.fabric.api.message.v1.ServerMessageEvents#COMMAND_MESSAGE + */ + void onEmoteCommandPlaceholder( + StringTemplate template, + ServerCommandSource source, + FilteredMessage action + ); + } + + @FunctionalInterface + public interface SayCommand + { + /** + * Called when an admin broadcast an in-game message via the + * {@code /say} command. + * + * @param template mutable string template + * @param source source of the message, e.g. a player + * @param action received message contents + * @see net.minecraft.network.message.MessageType#SAY_COMMAND + * @see net.fabricmc.fabric.api.message.v1.ServerMessageEvents#COMMAND_MESSAGE + */ + void onSayCommandPlaceholder( + StringTemplate template, + ServerCommandSource source, + FilteredMessage action + ); + } + + @FunctionalInterface + public interface TellRawCommand + { + /** + * Called when an admin broadcast an in-game message to *all* + * players via the {@code /tellraw @a} command. + * + * @param template mutable string template + * @param source source of the message, e.g. a player + * @param message received message contents + * @see net.minecraft.network.message.MessageType#TELLRAW_COMMAND + */ + void onTellRawCommandPlaceholder(StringTemplate template, ServerCommandSource source, Text message); + } } } diff --git a/minecord-chat/src/main/java/me/axieum/mcmod/minecord/api/chat/event/minecraft/TellRawMessageCallback.java b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/api/chat/event/minecraft/TellRawMessageCallback.java new file mode 100644 index 0000000..0e4a18e --- /dev/null +++ b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/api/chat/event/minecraft/TellRawMessageCallback.java @@ -0,0 +1,30 @@ +package me.axieum.mcmod.minecord.api.chat.event.minecraft; + +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; + +public interface TellRawMessageCallback +{ + /** + * Called when a server (or player) broadcasts a {@code /tellraw} command + * message to *all* players. + */ + Event EVENT = + EventFactory.createArrayBacked(TellRawMessageCallback.class, callbacks -> (message, source) -> { + for (TellRawMessageCallback callback : callbacks) { + callback.onTellRawCommandMessage(message, source); + } + }); + + /** + * Called when a server (or player) broadcasts a {@code /tellraw} command + * message to *all* players. + * + * @param message broadcast message with message decorators applied if applicable + * @param source command source that sent the message + */ + void onTellRawCommandMessage(Text message, ServerCommandSource source); +} diff --git a/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/MinecordChat.java b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/MinecordChat.java index f1051b2..e50373c 100644 --- a/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/MinecordChat.java +++ b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/MinecordChat.java @@ -14,6 +14,7 @@ import me.axieum.mcmod.minecord.api.addon.MinecordAddon; import me.axieum.mcmod.minecord.api.chat.event.minecraft.EntityDeathEvents; import me.axieum.mcmod.minecord.api.chat.event.minecraft.GrantCriterionCallback; +import me.axieum.mcmod.minecord.api.chat.event.minecraft.TellRawMessageCallback; import me.axieum.mcmod.minecord.api.event.ServerShutdownCallback; import me.axieum.mcmod.minecord.impl.chat.callback.discord.MessageReactionListener; import me.axieum.mcmod.minecord.impl.chat.callback.discord.MessageReceivedListener; @@ -97,6 +98,11 @@ public void onInitializeServer() // A named animal/monster (with name tag) died EntityDeathEvents.ANIMAL_MONSTER.register(new EntityDeathCallback()); + // A player sent an in-game message via the '/me' command + // An admin broadcast an in-game message via the '/say' command + ServerMessageEvents.COMMAND_MESSAGE.register(new ServerMessageCallback()); + // An admin broadcast an in-game message to all players via the '/tellraw' command + TellRawMessageCallback.EVENT.register(new ServerMessageCallback()); } /** diff --git a/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/callback/minecraft/ServerMessageCallback.java b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/callback/minecraft/ServerMessageCallback.java index 66e5342..2338c95 100644 --- a/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/callback/minecraft/ServerMessageCallback.java +++ b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/callback/minecraft/ServerMessageCallback.java @@ -1,20 +1,26 @@ package me.axieum.mcmod.minecord.impl.chat.callback.minecraft; +import org.jetbrains.annotations.Nullable; + import net.minecraft.network.message.MessageType; import net.minecraft.network.message.SignedMessage; +import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.filter.FilteredMessage; import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; import net.minecraft.util.registry.RegistryKey; import net.fabricmc.fabric.api.message.v1.ServerMessageEvents.ChatMessage; +import net.fabricmc.fabric.api.message.v1.ServerMessageEvents.CommandMessage; import me.axieum.mcmod.minecord.api.Minecord; import me.axieum.mcmod.minecord.api.chat.event.ChatPlaceholderEvents; +import me.axieum.mcmod.minecord.api.chat.event.minecraft.TellRawMessageCallback; import me.axieum.mcmod.minecord.api.util.StringTemplate; import me.axieum.mcmod.minecord.api.util.StringUtils; import me.axieum.mcmod.minecord.impl.chat.util.DiscordDispatcher; -public class ServerMessageCallback implements ChatMessage +public class ServerMessageCallback implements ChatMessage, CommandMessage, TellRawMessageCallback { @Override public void onChatMessage( @@ -51,4 +57,127 @@ public void onChatMessage( entry -> entry.discord.chat != null && entry.hasWorld(player.world)); }); } + + @Override + public void onCommandMessage( + FilteredMessage message, ServerCommandSource source, RegistryKey typeKey + ) + { + if (MessageType.EMOTE_COMMAND.equals(typeKey)) { + // '/me ' + onEmoteCommandMessage(message, source); + } else if (MessageType.SAY_COMMAND.equals(typeKey)) { + // '/say ' + onSayCommandMessage(message, source); + } + } + + /** + * Called when a player broadcasts a {@code /me} command message to all + * players. + * + * @param message broadcast message with message decorators applied if + * applicable + * @param source command source that sent the message + */ + public void onEmoteCommandMessage(FilteredMessage message, ServerCommandSource source) + { + Minecord.getInstance().getJDA().ifPresent(jda -> { + final @Nullable ServerPlayerEntity player = source.getPlayer(); + + /* + * Prepare a message template. + */ + + final StringTemplate st = new StringTemplate(); + + // The player's username + st.add("username", player != null ? player.getName().getString() : null); + // The player's display name + st.add("player", player != null ? player.getDisplayName().getString() : null); + // The name of the world the player logged into + st.add("world", player != null ? StringUtils.getWorldName(source.getWorld()) : null); + // The formatted message contents + st.add("action", StringUtils.minecraftToDiscord( + message.filteredOrElse(message.raw()).getContent().getString() + )); + + ChatPlaceholderEvents.Minecraft.EMOTE_COMMAND.invoker().onEmoteCommandPlaceholder(st, source, message); + + /* + * Dispatch the message. + */ + + DiscordDispatcher.dispatch((embed, entry) -> + embed.setContent(st.format(entry.discord.emote)), + entry -> entry.discord.emote != null && (player == null || entry.hasWorld(source.getWorld()))); + }); + } + + /** + * Called when a server (or player) broadcasts a {@code /say} command + * message to all players. + * + * @param message broadcast message with message decorators applied if + * applicable + * @param source command source that sent the message + */ + public void onSayCommandMessage(FilteredMessage message, ServerCommandSource source) + { + Minecord.getInstance().getJDA().ifPresent(jda -> { + final @Nullable ServerPlayerEntity player = source.getPlayer(); + + /* + * Prepare a message template. + */ + + final StringTemplate st = new StringTemplate(); + + // The player's username + st.add("username", player != null ? player.getName().getString() : null); + // The player's display name + st.add("player", player != null ? player.getDisplayName().getString() : null); + // The name of the world the player logged into + st.add("world", player != null ? StringUtils.getWorldName(source.getWorld()) : null); + // The formatted message contents + st.add("message", StringUtils.minecraftToDiscord( + message.filteredOrElse(message.raw()).getContent().getString() + )); + + ChatPlaceholderEvents.Minecraft.SAY_COMMAND.invoker().onSayCommandPlaceholder(st, source, message); + + /* + * Dispatch the message. + */ + + DiscordDispatcher.dispatch((embed, entry) -> + embed.setContent(st.format(entry.discord.say)), + entry -> entry.discord.say != null && (player == null || entry.hasWorld(source.getWorld()))); + }); + } + + @Override + public void onTellRawCommandMessage(Text message, ServerCommandSource source) + { + Minecord.getInstance().getJDA().ifPresent(jda -> { + /* + * Prepare a message template. + */ + + final StringTemplate st = new StringTemplate(); + + // The formatted message contents + st.add("message", StringUtils.minecraftToDiscord(message.getString())); + + ChatPlaceholderEvents.Minecraft.TELLRAW_COMMAND.invoker().onTellRawCommandPlaceholder(st, source, message); + + /* + * Dispatch the message. + */ + + DiscordDispatcher.dispatch((embed, entry) -> + embed.setContent(st.format(entry.discord.tellraw)), + entry -> entry.discord.tellraw != null); + }); + } } diff --git a/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/config/ChatConfig.java b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/config/ChatConfig.java index a2ea4cb..0a24558 100644 --- a/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/config/ChatConfig.java +++ b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/impl/chat/config/ChatConfig.java @@ -40,6 +40,7 @@ public static class ChatEntrySchema /** * Discord events configuration schema. */ + @SuppressWarnings("checkstyle:linelength") public static class DiscordSchema { @Comment(""" @@ -47,6 +48,23 @@ public static class DiscordSchema Usages: ${username}, ${player}, ${message} and ${world}""") public String chat = "`${world}` **${player}** > ${message}"; + @Comment(""" + A player sent an in-game message via the '/me' command + Note: there is no player or world if sent from a command block or console! + Usages: ${username}, ${player}, ${action} and ${world}""") + public String emote = "`${world:-∞}` **${player:-Server}** _${action}_"; + + @Comment(""" + An admin broadcast an in-game message via the '/say' command + Note: there is no player or world if sent from a command block or console! + Usages: ${username}, ${player}, ${message} and ${world}""") + public String say = "**[${player:-Server}]** ${message}"; + + @Comment(""" + An admin broadcast an in-game message to all players via the '/tellraw @a' command + Usages: ${message}""") + public String tellraw = "${message}"; + @Comment(""" A player had died Usages: ${username}, ${player}, ${cause}, ${world}, ${x}, ${y}, ${z}, ${score} and ${exp}""") @@ -64,8 +82,8 @@ public static class DiscordSchema @Comment(""" A player teleported to another dimension - Usages: ${username}, ${player}, ${origin} and ${destination}""") - public String teleport = "**${player}** entered ${destination}. :cyclone:"; + Usages: ${username}, ${player}, ${world}, ${x}, ${y}, ${z}, ${origin}, ${origin_x}, ${origin_y} and ${origin_z}""") + public String teleport = "**${player}** entered ${world}. :cyclone:"; @Comment(""" A player joined the game diff --git a/minecord-chat/src/main/java/me/axieum/mcmod/minecord/mixin/chat/TellRawCommandMixin.java b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/mixin/chat/TellRawCommandMixin.java new file mode 100644 index 0000000..da24478 --- /dev/null +++ b/minecord-chat/src/main/java/me/axieum/mcmod/minecord/mixin/chat/TellRawCommandMixin.java @@ -0,0 +1,95 @@ +package me.axieum.mcmod.minecord.mixin.chat; + +import java.util.Collection; +import java.util.Collections; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import net.minecraft.command.EntitySelectorReader; +import net.minecraft.command.argument.EntityArgumentType; +import net.minecraft.command.argument.TextArgumentType; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.command.TellRawCommand; +import net.minecraft.server.network.ServerPlayerEntity; + +import me.axieum.mcmod.minecord.api.chat.event.minecraft.TellRawMessageCallback; + +/** + * Injects into, and broadcasts any '/tellraw' command invocations. + */ +@Mixin(TellRawCommand.class) +public abstract class TellRawCommandMixin +{ + /** + * Broadcasts any {@code /tellraw @a} command invocations that target + * all players. + * + * @param context command context + * @param cir mixin callback info + */ + @Inject(method = "method_13777", at = @At(value = "TAIL"), remap = false) + private static void execute(CommandContext context, CallbackInfoReturnable cir) + { + // If the message was sent to *all* players, then also include Discord in the discussion + if (targetsAllPlayers(context)) { + TellRawMessageCallback.EVENT.invoker().onTellRawCommandMessage( + TextArgumentType.getTextArgument(context, "message"), context.getSource() + ); + } + } + + /** + * Intercepts the player argument resolution during the {@code /tellraw} + * command to prevent the "No player was found" error when no players are + * online. This allows the message to still propagate through to Discord. + * + * @param context command context + * @param name player argument name, i.e. {@code targets} + * @return collection of target players to send the message to + */ + @Redirect( + method = "method_13777", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/command/argument/EntityArgumentType;getPlayers(" + + "Lcom/mojang/brigadier/context/CommandContext;Ljava/lang/String;)Ljava/util/Collection;" + ), + remap = false + ) + private static Collection getPlayers( + CommandContext context, String name + ) throws CommandSyntaxException + { + try { + // Continue with execution as normal + return EntityArgumentType.getPlayers(context, name); + } catch (CommandSyntaxException e) { + // If the command targets all players, but no players are online, return an empty list instead of failing + if (targetsAllPlayers(context) && e.getType() == EntityArgumentType.PLAYER_NOT_FOUND_EXCEPTION) { + return Collections.emptyList(); + } + // Else, continue by throwing the error as normal + throw e; + } + } + + /** + * Returns true if the {@code /tellraw} command targets all players. + * + * @param context command context + * @return true if the {@code /tellraw @a} command was executed + */ + private static boolean targetsAllPlayers(CommandContext context) + { + return context.getNodes().size() > 1 && context.getNodes().get(1).getRange().get(context.getInput()).equals( + "" + EntitySelectorReader.SELECTOR_PREFIX + 'a' // see private `EntitySelectorReader#ALL_PLAYERS` for 'a' + ); + } +} diff --git a/minecord-chat/src/main/resources/minecord-chat.mixins.json b/minecord-chat/src/main/resources/minecord-chat.mixins.json index 20fc5c8..5f4a23a 100644 --- a/minecord-chat/src/main/resources/minecord-chat.mixins.json +++ b/minecord-chat/src/main/resources/minecord-chat.mixins.json @@ -7,7 +7,8 @@ "server": [ "LivingEntityMixin", "PlayerAdvancementTrackerMixin", - "ServerPlayerEntityMixin" + "ServerPlayerEntityMixin", + "TellRawCommandMixin" ], "client": [], "injectors": {