579 lines
13 KiB
Java
579 lines
13 KiB
Java
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);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|