feat: blat furnace and chat helper
This commit is contained in:
@@ -0,0 +1,578 @@
|
||||
package com.example.essentialsplus.helpers;
|
||||
|
||||
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||
import java.awt.Color;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.runelite.api.ChatLineBuffer;
|
||||
import net.runelite.api.ChatMessageType;
|
||||
import net.runelite.api.Client;
|
||||
import net.runelite.api.GameState;
|
||||
import net.runelite.api.MenuAction;
|
||||
import net.runelite.api.MessageNode;
|
||||
import net.runelite.api.Player;
|
||||
import net.runelite.api.events.ChatMessage;
|
||||
import net.runelite.api.events.GameStateChanged;
|
||||
import net.runelite.api.events.GameTick;
|
||||
import net.runelite.api.events.MenuEntryAdded;
|
||||
import net.runelite.api.events.ScriptCallbackEvent;
|
||||
import net.runelite.api.gameval.InterfaceID;
|
||||
import net.runelite.api.widgets.ComponentID;
|
||||
import net.runelite.api.widgets.Widget;
|
||||
import net.runelite.api.widgets.WidgetUtil;
|
||||
import net.runelite.client.callback.ClientThread;
|
||||
import net.runelite.client.eventbus.Subscribe;
|
||||
import net.runelite.client.util.ColorUtil;
|
||||
import net.runelite.client.util.Text;
|
||||
|
||||
/**
|
||||
* Chat Helper:
|
||||
* - Auto-clears messages after a configurable duration (e.g. 1 minute) to keep the chat tidy.
|
||||
* - Collapses consecutive duplicate messages (x2, x3...).
|
||||
* - Filters repetitive game noise and spam messages.
|
||||
* - Highlights mentions of the player's name in speech chats.
|
||||
* - Adds right-click "Copy message" and "Clear chat" options.
|
||||
*/
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class ChatHelper
|
||||
{
|
||||
private static final Set<String> NOISE_MESSAGES = Set.of(
|
||||
"Nothing interesting happens.",
|
||||
"You can't reach that.",
|
||||
"You cannot reach that.",
|
||||
"I can't reach that!",
|
||||
"You can't do that here.",
|
||||
"You cannot do that here.",
|
||||
"You can't do that while moving.",
|
||||
"You cannot do that while moving.",
|
||||
"You can't do that right now.",
|
||||
"You cannot do that right now.",
|
||||
"You can't log out right now.",
|
||||
"You don't have enough inventory space.",
|
||||
"You do not have enough inventory space.",
|
||||
"There is no player by that name.",
|
||||
"That player is currently offline.",
|
||||
"You are not currently in a clan.",
|
||||
"You are not currently in a friends chat."
|
||||
);
|
||||
|
||||
private static final Pattern NOISE_PREFIX_PATTERN = Pattern.compile(
|
||||
"^(Your inventory is too full to hold any more|You need a higher level to|You need to be in a member's world to).*",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
private static final Pattern PUBLIC_SPAM_PATTERN = Pattern.compile(
|
||||
"(?i).*(giveaway|free gp|quitting.*search|search on yt|search yt|sub to my yt|2b for|swap cc|dicing cc|roll 55|autoclicker|osrsgold|buy gp).*"
|
||||
);
|
||||
|
||||
private static class Duplicate
|
||||
{
|
||||
int messageId = -1;
|
||||
int count = 1;
|
||||
}
|
||||
|
||||
private final Client client;
|
||||
private final EssentialsPlusConfig config;
|
||||
private final ClientThread clientThread;
|
||||
|
||||
private final LinkedHashMap<String, Duplicate> duplicateChatCache = new LinkedHashMap<String, Duplicate>(100, 0.75f, true)
|
||||
{
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<String, Duplicate> eldest)
|
||||
{
|
||||
return size() > 100;
|
||||
}
|
||||
};
|
||||
|
||||
private String lastConsecutiveKey = null;
|
||||
private int lastCheckEpoch = 0;
|
||||
|
||||
@Inject
|
||||
public ChatHelper(Client client, EssentialsPlusConfig config, ClientThread clientThread)
|
||||
{
|
||||
this.client = client;
|
||||
this.config = config;
|
||||
this.clientThread = clientThread;
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
duplicateChatCache.clear();
|
||||
lastConsecutiveKey = null;
|
||||
lastCheckEpoch = 0;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onGameStateChanged(GameStateChanged event)
|
||||
{
|
||||
if (event.getGameState() == GameState.LOGIN_SCREEN || event.getGameState() == GameState.HOPPING)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-clear expired chat messages based on configured retention time.
|
||||
*/
|
||||
@Subscribe
|
||||
public void onGameTick(GameTick event)
|
||||
{
|
||||
if (!config.chatHelper() || !config.chatAutoClear())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int timeoutSeconds = config.chatAutoClearTime();
|
||||
if (timeoutSeconds <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int now = (int) Instant.now().getEpochSecond();
|
||||
if (now - lastCheckEpoch < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
lastCheckEpoch = now;
|
||||
|
||||
Map<Integer, ChatLineBuffer> chatLineMap = client.getChatLineMap();
|
||||
if (chatLineMap == null || chatLineMap.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
boolean removedAny = false;
|
||||
for (ChatLineBuffer buffer : chatLineMap.values())
|
||||
{
|
||||
if (buffer == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MessageNode[] lines = buffer.getLines();
|
||||
if (lines == null || lines.length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MessageNode[] copy = lines.clone();
|
||||
for (MessageNode node : copy)
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int ts = node.getTimestamp();
|
||||
if (ts > 0 && (now - ts) >= timeoutSeconds && shouldClearType(node.getType()))
|
||||
{
|
||||
buffer.removeMessageNode(node);
|
||||
removedAny = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removedAny)
|
||||
{
|
||||
client.runScript(83);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldClearType(ChatMessageType type)
|
||||
{
|
||||
if (type == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case GAMEMESSAGE:
|
||||
case ENGINE:
|
||||
case ITEM_EXAMINE:
|
||||
case NPC_EXAMINE:
|
||||
case OBJECT_EXAMINE:
|
||||
case SPAM:
|
||||
case BROADCAST:
|
||||
case CONSOLE:
|
||||
return config.chatClearGame();
|
||||
|
||||
case PUBLICCHAT:
|
||||
case MODCHAT:
|
||||
case AUTOTYPER:
|
||||
case TRADEREQ:
|
||||
case TRADE:
|
||||
return config.chatClearPublic();
|
||||
|
||||
case PRIVATECHAT:
|
||||
case PRIVATECHATOUT:
|
||||
case MODPRIVATECHAT:
|
||||
return config.chatClearPrivate();
|
||||
|
||||
case CLAN_CHAT:
|
||||
case CLAN_GUEST_CHAT:
|
||||
case FRIENDSCHAT:
|
||||
case CLAN_MESSAGE:
|
||||
case CLAN_GIM_CHAT:
|
||||
case CLAN_GIM_MESSAGE:
|
||||
return config.chatClearClan();
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks incoming chat messages for duplicate collapsing.
|
||||
*/
|
||||
@Subscribe
|
||||
public void onChatMessage(ChatMessage event)
|
||||
{
|
||||
if (!config.chatHelper() || !config.chatCollapseDuplicates())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMessageType type = event.getType();
|
||||
if (!isCollapsible(type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
String name = event.getName() != null ? event.getName() : "";
|
||||
String message = event.getMessage();
|
||||
String key = name + ":" + type.ordinal() + ":" + message;
|
||||
|
||||
if (!key.equals(lastConsecutiveKey))
|
||||
{
|
||||
lastConsecutiveKey = key;
|
||||
duplicateChatCache.remove(key);
|
||||
}
|
||||
|
||||
Duplicate dup = duplicateChatCache.get(key);
|
||||
if (dup == null)
|
||||
{
|
||||
dup = new Duplicate();
|
||||
dup.count = 1;
|
||||
dup.messageId = event.getMessageNode() != null ? event.getMessageNode().getId() : -1;
|
||||
duplicateChatCache.put(key, dup);
|
||||
}
|
||||
else
|
||||
{
|
||||
dup.count++;
|
||||
dup.messageId = event.getMessageNode() != null ? event.getMessageNode().getId() : -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts chat line rendering to filter spam, collapse duplicates, and highlight names.
|
||||
*/
|
||||
@Subscribe
|
||||
public void onScriptCallbackEvent(ScriptCallbackEvent event)
|
||||
{
|
||||
if (!"chatFilterCheck".equals(event.getEventName()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.chatHelper())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int[] intStack = client.getIntStack();
|
||||
int intStackSize = client.getIntStackSize();
|
||||
Object[] objectStack = client.getObjectStack();
|
||||
int objectStackSize = client.getObjectStackSize();
|
||||
|
||||
if (intStackSize < 3 || objectStackSize < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int messageTypeVal = intStack[intStackSize - 2];
|
||||
int messageId = intStack[intStackSize - 1];
|
||||
String message = (String) objectStack[objectStackSize - 1];
|
||||
if (message == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Spam & Noise filtering
|
||||
if (config.chatFilterSpam() && isSpamOrNoise(messageTypeVal, message))
|
||||
{
|
||||
intStack[intStackSize - 3] = 0; // hide message
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Duplicate collapsing
|
||||
if (config.chatCollapseDuplicates())
|
||||
{
|
||||
ChatMessageType type = ChatMessageType.of(messageTypeVal);
|
||||
if (isCollapsible(type))
|
||||
{
|
||||
MessageNode node = null;
|
||||
if (client.getMessages() != null)
|
||||
{
|
||||
node = (MessageNode) client.getMessages().get((long) messageId);
|
||||
}
|
||||
String name = node != null && node.getName() != null ? node.getName() : "";
|
||||
|
||||
String key = name + ":" + (type != null ? type.ordinal() : messageTypeVal) + ":" + message;
|
||||
Duplicate dup = duplicateChatCache.get(key);
|
||||
if (dup != null && dup.messageId != -1)
|
||||
{
|
||||
if (dup.messageId == messageId)
|
||||
{
|
||||
if (dup.count > 1)
|
||||
{
|
||||
message = message + " <col=888888>(x" + dup.count + ")</col>";
|
||||
objectStack[objectStackSize - 1] = message;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hide earlier duplicate instances
|
||||
intStack[intStackSize - 3] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Highlight own name
|
||||
if (config.chatHighlightName() && isPlayerSpeechType(messageTypeVal))
|
||||
{
|
||||
String highlighted = highlightPlayerName(message);
|
||||
if (highlighted != null)
|
||||
{
|
||||
objectStack[objectStackSize - 1] = highlighted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCollapsible(ChatMessageType type)
|
||||
{
|
||||
if (type == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case GAMEMESSAGE:
|
||||
case ENGINE:
|
||||
case SPAM:
|
||||
case PUBLICCHAT:
|
||||
case MODCHAT:
|
||||
case AUTOTYPER:
|
||||
case CLAN_CHAT:
|
||||
case FRIENDSCHAT:
|
||||
case ITEM_EXAMINE:
|
||||
case OBJECT_EXAMINE:
|
||||
case NPC_EXAMINE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPlayerSpeechType(int messageTypeVal)
|
||||
{
|
||||
ChatMessageType type = ChatMessageType.of(messageTypeVal);
|
||||
if (type == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case PUBLICCHAT:
|
||||
case MODCHAT:
|
||||
case FRIENDSCHAT:
|
||||
case CLAN_CHAT:
|
||||
case CLAN_GUEST_CHAT:
|
||||
case CLAN_GIM_CHAT:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSpamOrNoise(int messageTypeVal, String message)
|
||||
{
|
||||
ChatMessageType type = ChatMessageType.of(messageTypeVal);
|
||||
if (type == null || message == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
String clean = Text.removeTags(message).trim();
|
||||
|
||||
if (type == ChatMessageType.GAMEMESSAGE || type == ChatMessageType.ENGINE || type == ChatMessageType.SPAM)
|
||||
{
|
||||
if (NOISE_MESSAGES.contains(clean) || NOISE_PREFIX_PATTERN.matcher(clean).matches())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == ChatMessageType.PUBLICCHAT || type == ChatMessageType.MODCHAT || type == ChatMessageType.AUTOTYPER)
|
||||
{
|
||||
if (PUBLIC_SPAM_PATTERN.matcher(clean).matches())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private String highlightPlayerName(String message)
|
||||
{
|
||||
Player local = client.getLocalPlayer();
|
||||
if (local == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
String name = local.getName();
|
||||
if (name == null || name.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
String lower = message.toLowerCase();
|
||||
String lowerName = name.toLowerCase();
|
||||
if (!lower.contains(lowerName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Color highlightColor = config.chatHighlightColor();
|
||||
String hex = ColorUtil.colorToHexCode(highlightColor != null ? highlightColor : new Color(0xFFD700));
|
||||
|
||||
Pattern pattern = Pattern.compile("(?i)(?<![a-zA-Z0-9_])" + Pattern.quote(name) + "(?![a-zA-Z0-9_])");
|
||||
return pattern.matcher(message).replaceAll("<col=" + hex + "><b>" + name + "</b></col>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds "Copy message" and "Clear chat" to right-click menus.
|
||||
*/
|
||||
@Subscribe
|
||||
public void onMenuEntryAdded(MenuEntryAdded event)
|
||||
{
|
||||
if (!config.chatHelper())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int type = event.getType();
|
||||
if (type != MenuAction.CC_OP.getId() && type != MenuAction.CC_OP_LOW_PRIORITY.getId())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int param1 = event.getActionParam1();
|
||||
int interfaceId = WidgetUtil.componentToInterface(param1);
|
||||
int childId = WidgetUtil.componentToId(param1);
|
||||
|
||||
if (interfaceId != InterfaceID.CHATBOX)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Copy message
|
||||
if (config.chatCopyToClipboard() && "Report".equals(event.getOption()))
|
||||
{
|
||||
Widget lineWidget = client.getWidget(interfaceId, childId);
|
||||
if (lineWidget != null && lineWidget.getParent() != null && lineWidget.getParent().getId() == ComponentID.CHATBOX_MESSAGE_LINES)
|
||||
{
|
||||
int lineIndex = (childId - WidgetUtil.componentToId(ComponentID.CHATBOX_FRAME)) * 4 + 1;
|
||||
Widget textWidget = lineWidget.getParent().getChild(lineIndex);
|
||||
if (textWidget != null && textWidget.getText() != null)
|
||||
{
|
||||
String fullText = textWidget.getText();
|
||||
client.createMenuEntry(1)
|
||||
.setOption("Copy message")
|
||||
.setTarget(event.getTarget())
|
||||
.setType(MenuAction.RUNELITE)
|
||||
.onClick(e -> copyToClipboard(fullText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Clear chat on chat tab buttons
|
||||
String option = event.getOption();
|
||||
if (option != null && (option.startsWith("Switch tab") || option.startsWith("Filter")))
|
||||
{
|
||||
client.createMenuEntry(-2)
|
||||
.setOption("Clear chat")
|
||||
.setType(MenuAction.RUNELITE_HIGH_PRIORITY)
|
||||
.onClick(e -> clearAllChat());
|
||||
}
|
||||
}
|
||||
|
||||
private void copyToClipboard(String text)
|
||||
{
|
||||
try
|
||||
{
|
||||
String cleanText = Text.removeTags(text);
|
||||
StringSelection selection = new StringSelection(cleanText);
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("Failed to copy chat text to clipboard", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually empties all chat line buffers and rebuilds the chatbox widget.
|
||||
*/
|
||||
public void clearAllChat()
|
||||
{
|
||||
clientThread.invokeLater(() ->
|
||||
{
|
||||
Map<Integer, ChatLineBuffer> chatLineMap = client.getChatLineMap();
|
||||
if (chatLineMap == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (ChatLineBuffer buffer : chatLineMap.values())
|
||||
{
|
||||
if (buffer == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MessageNode[] lines = buffer.getLines();
|
||||
if (lines == null || lines.length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MessageNode[] copy = lines.clone();
|
||||
for (MessageNode node : copy)
|
||||
{
|
||||
if (node != null)
|
||||
{
|
||||
buffer.removeMessageNode(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.runScript(83);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user