From 80489715adfb76f17ec8cf17d38154b271cdd21c Mon Sep 17 00:00:00 2001 From: mini-bomba <55105495+mini-bomba@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:12:07 +0100 Subject: [PATCH] Add an option to render Twitch emotes in all messages. Use `/twitch emotes renderEverywhere` to toggle this. This closes #8. Also, TwitchMessageHandler.processEmotes() now preserves whitespace. --- .../streamchatmod/StreamChatMod.java | 2 + .../streamchatmod/StreamConfig.java | 2 + .../streamchatmod/StreamEmotes.java | 2 +- .../streamchatmod/StreamEvents.java | 49 ++++++++++++++++++- .../subcommands/TwitchEmotesSubcommand.java | 22 +++++++-- .../runnables/TwitchMessageHandler.java | 20 ++++++-- 6 files changed, 86 insertions(+), 11 deletions(-) diff --git a/src/main/java/me/mini_bomba/streamchatmod/StreamChatMod.java b/src/main/java/me/mini_bomba/streamchatmod/StreamChatMod.java index f52b854..d275f7e 100644 --- a/src/main/java/me/mini_bomba/streamchatmod/StreamChatMod.java +++ b/src/main/java/me/mini_bomba/streamchatmod/StreamChatMod.java @@ -12,6 +12,7 @@ import com.github.twitch4j.helix.domain.*; import com.github.twitch4j.tmi.domain.Chatters; import com.sun.net.httpserver.HttpServer; +import lombok.Getter; import me.mini_bomba.streamchatmod.asm.hooks.FontRendererHook; import me.mini_bomba.streamchatmod.commands.TwitchChatCommand; import me.mini_bomba.streamchatmod.commands.TwitchCommand; @@ -73,6 +74,7 @@ public class StreamChatMod { @Nullable private CredentialManager twitchCredentialManager = null; @Nullable + @Getter private String twitchUsername = null; @Nullable private List twitchScopes = null; diff --git a/src/main/java/me/mini_bomba/streamchatmod/StreamConfig.java b/src/main/java/me/mini_bomba/streamchatmod/StreamConfig.java index 19f6c82..3b9e327 100644 --- a/src/main/java/me/mini_bomba/streamchatmod/StreamConfig.java +++ b/src/main/java/me/mini_bomba/streamchatmod/StreamConfig.java @@ -22,6 +22,7 @@ public class StreamConfig { public final Property subOnlyFormatting; public final Property minecraftChatPrefix; public final Property allowMessageDeletion; + public final Property showEmotesEverywhere; // tokens protected final Property twitchToken; // twitch @@ -62,6 +63,7 @@ public StreamConfig(File configFile) { subOnlyFormatting = config.get("common", "subOnlyFormatting", false); minecraftChatPrefix = config.get("common", "minecraftChatPrefix", "!!"); allowMessageDeletion = config.get("common", "allowMessageDeletion", true); + showEmotesEverywhere = config.get("common", "showEmotesEverywhere", false); // tokens twitchToken = config.get("tokens", "twitch", ""); // twitch diff --git a/src/main/java/me/mini_bomba/streamchatmod/StreamEmotes.java b/src/main/java/me/mini_bomba/streamchatmod/StreamEmotes.java index 88988e4..9287da5 100644 --- a/src/main/java/me/mini_bomba/streamchatmod/StreamEmotes.java +++ b/src/main/java/me/mini_bomba/streamchatmod/StreamEmotes.java @@ -65,6 +65,7 @@ public boolean isGlobalEmote(String name) { } public boolean isChannelEmote(String channelId, String name) { + if (channelId == null) return false; return channelEmotes.containsKey(channelId) && channelEmotes.get(channelId).containsKey(name); } @@ -691,7 +692,6 @@ private TwitchBadgeGlobal(ChatBadgeSet set, ChatBadge badge) { this.id = TwitchGlobalBadge.getBadgeId(badge); } } - private static class TwitchBadgeChannel { public final ChatBadgeSet set; public final ChatBadge badge; diff --git a/src/main/java/me/mini_bomba/streamchatmod/StreamEvents.java b/src/main/java/me/mini_bomba/streamchatmod/StreamEvents.java index ed2eb75..c9d491c 100644 --- a/src/main/java/me/mini_bomba/streamchatmod/StreamEvents.java +++ b/src/main/java/me/mini_bomba/streamchatmod/StreamEvents.java @@ -1,11 +1,18 @@ package me.mini_bomba.streamchatmod; +import com.github.twitch4j.helix.domain.User; import me.mini_bomba.streamchatmod.commands.IDrawsChatOutline; import me.mini_bomba.streamchatmod.events.LocalMessageEvent; +import me.mini_bomba.streamchatmod.runnables.TwitchMessageHandler; import me.mini_bomba.streamchatmod.tweaker.TransformerField; +import me.mini_bomba.streamchatmod.utils.ChatComponentStreamEmote; import net.minecraft.client.gui.GuiChat; import net.minecraft.client.gui.GuiTextField; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.IChatComponent; +import net.minecraftforge.client.event.ClientChatReceivedEvent; import net.minecraftforge.client.event.GuiScreenEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; @@ -15,6 +22,7 @@ import java.lang.reflect.Field; import java.util.Arrays; +import java.util.List; public class StreamEvents { @@ -96,13 +104,50 @@ public void onLocalMinecraftMessage(LocalMessageEvent event) { } event.setCanceled(true); if (mod.twitch == null || mod.twitchSender == null || !mod.config.twitchEnabled.getBoolean() || mod.twitchSender.getChat() == null) { - StreamUtils.addMessage(EnumChatFormatting.RED+"The message was not sent anywhere: Chat mode is set to 'Redirect to Twitch', but Twitch chat (or part of it) is disabled!"); + StreamUtils.addMessage(EnumChatFormatting.RED + "The message was not sent anywhere: Chat mode is set to 'Redirect to Twitch', but Twitch chat (or part of it) is disabled!"); return; } if (mod.config.twitchSelectedChannel.getString().length() == 0) { - StreamUtils.addMessage(EnumChatFormatting.RED+"The message was not sent anywhere: Chat mode is set to 'Redirect to Twitch', but no channel is selected!"); + StreamUtils.addMessage(EnumChatFormatting.RED + "The message was not sent anywhere: Chat mode is set to 'Redirect to Twitch', but no channel is selected!"); return; } mod.twitchSender.getChat().sendMessage(mod.config.twitchSelectedChannel.getString(), event.message); } + + @SubscribeEvent + public void onMessage(ClientChatReceivedEvent event) { + String channel = mod.config.twitchSelectedChannel.getString(); + if (channel.length() == 0) channel = mod.getTwitchUsername(); + if (mod.config.showEmotesEverywhere.getBoolean()) { + User user = channel == null ? null : mod.getTwitchUserByName(channel); + event.message = transformComponent(event.message, user == null ? null : user.getId()); + } + } + + public IChatComponent transformComponent(IChatComponent component, String channelId) { + IChatComponent newComponent; + if (component instanceof ChatComponentText) { + List components = TwitchMessageHandler.processEmotes(mod, component.getUnformattedTextForChat(), channelId); + components.stream().filter(c -> !(c instanceof ChatComponentStreamEmote)).forEach(c -> c.setChatStyle(component.getChatStyle().createShallowCopy())); + if (components.size() > 0) { + newComponent = components.get(0); + components.remove(0); + for (IChatComponent c : components) newComponent.appendSibling(c); + } else { + newComponent = new ChatComponentText(""); + newComponent.setChatStyle(component.getChatStyle().createShallowCopy()); + } + } else if (component instanceof ChatComponentTranslation) { + ChatComponentTranslation castedComponent = (ChatComponentTranslation) component; + newComponent = new ChatComponentTranslation(castedComponent.getKey(), Arrays.stream(castedComponent.getFormatArgs()).map(c -> c instanceof IChatComponent ? transformComponent((IChatComponent) c, channelId) : c).toArray()); + newComponent.setChatStyle(castedComponent.getChatStyle().createShallowCopy()); + } else { + newComponent = component.createCopy(); + newComponent.getSiblings().clear(); + } + for (IChatComponent sibling : component.getSiblings()) { + newComponent.appendSibling(transformComponent(sibling, channelId)); + } + return newComponent; + } } diff --git a/src/main/java/me/mini_bomba/streamchatmod/commands/subcommands/TwitchEmotesSubcommand.java b/src/main/java/me/mini_bomba/streamchatmod/commands/subcommands/TwitchEmotesSubcommand.java index 717e007..68e4901 100644 --- a/src/main/java/me/mini_bomba/streamchatmod/commands/subcommands/TwitchEmotesSubcommand.java +++ b/src/main/java/me/mini_bomba/streamchatmod/commands/subcommands/TwitchEmotesSubcommand.java @@ -67,12 +67,14 @@ public void processSubcommand(ICommandSender sender, String[] args) throws Comma .setChatStyle(new ChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/twitch emotes " + type.name().toLowerCase())))).collect(Collectors.toList())); components.add(new ChatComponentText(EnumChatFormatting.AQUA + "Animated emotes: " + (mod.config.allowAnimatedEmotes.getBoolean() ? EnumChatFormatting.GREEN + "Enabled" : EnumChatFormatting.RED + "Disabled")) .setChatStyle(new ChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/twitch emotes animated")))); + components.add(new ChatComponentText(EnumChatFormatting.AQUA + "Render emotes everywhere: " + (mod.config.showEmotesEverywhere.getBoolean() ? EnumChatFormatting.GREEN + "Enabled" : EnumChatFormatting.RED + "Disabled")) + .setChatStyle(new ChatStyle().setChatClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/twitch emotes renderEverywhere")))); components.add(new ChatComponentText(EnumChatFormatting.GRAY + "Used internal emote slots: " + EnumChatFormatting.AQUA + StreamEmote.getEmoteCount() + EnumChatFormatting.GRAY + "/2048")); StreamUtils.addMessages(sender, components.toArray(new IChatComponent[0])); } else { if (args[0].equalsIgnoreCase("animated")) { if (args.length == 1) - StreamUtils.addMessage(EnumChatFormatting.AQUA + "Animated emotes are currently " + (mod.config.allowAnimatedEmotes.getBoolean() ? EnumChatFormatting.GREEN + "enabled" : EnumChatFormatting.RED + "eisabled")); + StreamUtils.addMessage(EnumChatFormatting.AQUA + "Animated emotes are currently " + (mod.config.allowAnimatedEmotes.getBoolean() ? EnumChatFormatting.GREEN + "enabled" : EnumChatFormatting.RED + "disabled")); else { Boolean newState = StreamUtils.readStringAsBoolean(args[1]); if (newState == null) @@ -80,7 +82,21 @@ public void processSubcommand(ICommandSender sender, String[] args) throws Comma else { mod.config.allowAnimatedEmotes.set(newState); FontRendererHook.setAllowAnimated(newState); - StreamUtils.addMessage(EnumChatFormatting.GREEN + "Animated emotes have been " + (newState ? "enabled" : "eisabled")); + StreamUtils.addMessage(EnumChatFormatting.GREEN + "Animated emotes have been " + (newState ? "enabled" : "disabled")); + } + } + return; + } + if (args[0].equalsIgnoreCase("renderEverywhere")) { + if (args.length == 1) + StreamUtils.addMessage(EnumChatFormatting.AQUA + "Twitch emotes are currently rendered " + (mod.config.showEmotesEverywhere.getBoolean() ? EnumChatFormatting.GREEN + "everywhere" : EnumChatFormatting.LIGHT_PURPLE + "only in Twitch chat")); + else { + Boolean newState = StreamUtils.readStringAsBoolean(args[1]); + if (newState == null) + throw new CommandException("Invalid boolean value: " + args[1]); + else { + mod.config.showEmotesEverywhere.set(newState); + StreamUtils.addMessage(EnumChatFormatting.GREEN + "Twitch emotes are now rendered " + (newState ? "everywhere" : "only in Twitch chat")); } } return; @@ -108,7 +124,7 @@ public void processSubcommand(ICommandSender sender, String[] args) throws Comma @Override public List getAutocompletions(String[] args) { if (args.length == 1) { - List matchingTypes = Stream.concat(Arrays.stream(StreamEmote.Type.values()).map(type -> type.name().toLowerCase()), Stream.of("animated")).filter(name -> name.startsWith(args[0].toLowerCase())).collect(Collectors.toList()); + List matchingTypes = Stream.concat(Arrays.stream(StreamEmote.Type.values()).map(type -> type.name().toLowerCase()), Stream.of("animated", "rendereverywhere")).filter(name -> name.startsWith(args[0].toLowerCase())).collect(Collectors.toList()); if (matchingTypes.size() == 1 && matchingTypes.get(0).equals(args[0])) matchingTypes = StreamUtils.singletonModifiableList(matchingTypes.get(0) + " "); return matchingTypes; diff --git a/src/main/java/me/mini_bomba/streamchatmod/runnables/TwitchMessageHandler.java b/src/main/java/me/mini_bomba/streamchatmod/runnables/TwitchMessageHandler.java index 1aa9dad..8f2d0a0 100644 --- a/src/main/java/me/mini_bomba/streamchatmod/runnables/TwitchMessageHandler.java +++ b/src/main/java/me/mini_bomba/streamchatmod/runnables/TwitchMessageHandler.java @@ -29,6 +29,7 @@ public class TwitchMessageHandler implements Runnable { private static final char formatChar = '\u00a7'; private static final String validFormats = "0123456789abcdefklmnorABCDEFKLMNORzZ"; public static final Pattern urlPattern = Pattern.compile("https?://[^.\\s/]+(?:\\.[^.\\s/]+)+\\S*"); + public static final Pattern whitespacePattern = Pattern.compile("\\s+"); private static final Pattern formatCodePattern = Pattern.compile(formatChar + "[0-9a-fA-Fk-rK-RzZ]"); private static final String clipsDomain = "https://clips.twitch.tv/"; @@ -52,21 +53,25 @@ private String processColorCodes(String message, boolean allowFormatting) { return message; } - private List processEmotes(String message) { + public static List processEmotes(StreamChatMod mod, String message, String channelId) { List result = new LinkedList<>(); List nextComponent = new LinkedList<>(); char color = 0; char format = 0; char nextColor = 0; char nextFormat = 0; + Matcher whitespace = whitespacePattern.matcher(message); + if (message.length() > 0 && whitespacePattern.matcher(message.substring(0, 1)).find() && whitespace.find()) + nextComponent.add(whitespace.group()); for (String word : StringUtils.split(message, " \n\t")) { - if (mod.emotes.isEmote(event.getChannel().getId(), word)) { + if (mod.emotes.isEmote(channelId, word)) { if (nextComponent.size() > 0) - result.add(new ChatComponentText((color != 0 ? "" + formatChar + color : "") + (format != 0 ? "" + formatChar + format : "") + (result.size() == 0 ? "" : " ") + String.join(" ", nextComponent) + " ")); + result.add(new ChatComponentText((color != 0 ? "" + formatChar + color : "") + (format != 0 ? "" + formatChar + format : "") + String.join("", nextComponent))); nextComponent.clear(); + if (whitespace.find()) nextComponent.add(whitespace.group()); color = nextColor; format = nextFormat; - result.add(new ChatComponentStreamEmote(mod, mod.emotes.getEmote(event.getChannel().getId(), word))); + result.add(new ChatComponentStreamEmote(mod, mod.emotes.getEmote(channelId, word))); } else { Matcher formatMatcher = formatCodePattern.matcher(word); while (formatMatcher.find()) { @@ -82,13 +87,18 @@ private List processEmotes(String message) { } } nextComponent.add(word); + if (whitespace.find()) nextComponent.add(whitespace.group()); } } if (nextComponent.size() > 0) - result.add(new ChatComponentText((color != 0 ? "" + formatChar + color : "") + (format != 0 ? "" + formatChar + format : "") + (result.size() == 0 ? "" : " ") + String.join(" ", nextComponent))); + result.add(new ChatComponentText((color != 0 ? "" + formatChar + color : "") + (format != 0 ? "" + formatChar + format : "") + String.join("", nextComponent))); return result; } + private List processEmotes(String message) { + return processEmotes(mod, message, event.getChannel().getId()); + } + @Override public void run() { boolean showChannel = mod.config.forceShowChannelName.getBoolean() || (mod.twitch != null && mod.twitch.getChat().getChannels().size() > 1);