feat: blat furnace and chat helper

This commit is contained in:
Hikyu
2026-09-18 19:04:09 +02:00
parent 15463c2e3c
commit ddcbd2303c
13 changed files with 3151 additions and 7 deletions
+2 -1
View File
@@ -6,4 +6,5 @@ build
.classpath
nbactions.xml
nb-configuration.xml
nbproject/
nbproject/
.aider*
+15
View File
@@ -38,3 +38,18 @@ Bei **JEDEM** Update / Build müssen immer folgende Schritte ausgeführt werden:
- **Wealth & Currency Store**: Vermögen, GE-Buy-Limits und Minigame-Währungen.
- **Loot & Combat Tracker**: Drop Tables, Sessions, DPS-Rechner.
- **Bank Highlighter**: Farbhervorhebung in Bank-Tabs.
- **Blast Furnace Helper** (`com.example.essentialsplus.helpers.BlastFurnace*`):
- Markiert strikt sequenziell immer nur genau ein Ziel ("niemals mehrere Dinge gleichzeitig"):
- Bei geöffneter Bank: Barren einlagern -> leere Vials einlagern -> bei <= 30 Energie Stamina Potion trinken (Priorität: 1-Dose Extended > 1-Dose Normal > 2-4 Dose Extended > 2-4 Dose Normal; angebrochene Tränke werden vor dem Erz-Abheben zurück in die Bank gelegt) -> Coal-Bag füllen im Inventar & Ziel-Erz/Kohle in der Bank abheben werden gleichzeitig markiert (sodass man Coal Bag füllen und direkt das Erz/Kohle anklicken kann; Mindestbestand im Ofen mit +1 Puffer, z. B. 4x Erz-Slots = 108 Kohle bei Runite mit Coal Bag; Priorität: Runite > Adamantite > Mithril > Gold > Iron/Steel > Silver > Bronze nach Smithing-Level & Bank-Bestand; Erze mit < 27 Stück in der Bank werden ignoriert, um halbe Touren zu verhindern; wenn alle Erze < 27 sind, ist der Helper fertig und markiert nichts mehr).
- Sobald das Inventar voll ist, wird das Förderband ("Put ore on belt" / "Put coal on belt") sofort in der 3D-Welt markiert – auch bei noch geöffneter Bank, sodass man mit 1 Klick direkt vom Bankfenster zum Band laufen kann.
- Intelligentes Warten am Bar Dispenser: Nach dem Auflegen von Erzen auf das Band wird der Bar Dispenser ("Wait for bars" / "Collect bars") markiert, bis die Barren geschmolzen und abgeholt sind, anstatt voreilig zur Bank-Chest zu leiten. Erst mit Barren im Inventar geht es zur Bank-Chest ("Bank bars").
- Rand-Markierung ("Border-only"): Items in Bank & Inventar werden mit einem klaren Rahmen ohne deckenden Fill umrandet, sodass jedes Icon deutlich erkennbar bleibt.
- Einstellungen und Beschriftungen vollständig auf Englisch (Menüs im Helpers-Card per Rechtsklick, Panel-Zeilen, Tooltips).
- Info-Panel: Coal in furnace, Bars in furnace, Coffer + Restzeit und berechnete Barren aus Bank-Ores.
- **Chat Helper** (`com.example.essentialsplus.helpers.ChatHelper*`):
- Automatisches Aufräumen (Auto-Clear) alter Chat-Nachrichten nach konfigurierbarer Dauer (Standard 60s / 1 Min., 30s, 2m, 5m). Selektiver Schutz für private Nachrichten & Clan-/Friends-Chats.
- Duplikat-Kompaktierung: Aufeinanderfolgende gleiche Meldungen werden zu `(x2, x3...)` komprimiert.
- Spam- & Noise-Filter: Unterdrückt überflüssigen Systemlärm ("Nothing interesting happens", etc.) und Werbebots.
- Namen-Highlighting: Eigener Spielername wird in öffentlichen/Clan-Chats hervorgehoben (Farbe & Fettdruck).
- Rechtsklick-Aktionen: "Copy message" (kopiert bereinigten Text) & "Clear chat" auf Tabs / Kontextmenü.
@@ -271,6 +271,17 @@ public interface EssentialsPlusConfig extends Config
return false;
}
@ConfigItem(
keyName = "blastFurnace",
name = "Blast Furnace helper",
description = "Highlights conveyor belt, bar dispenser, and bank chest, and shows coal in furnace, coffer time, and craftable bars from bank ores. Right-click the toggle for options.",
position = 38
)
default boolean blastFurnace()
{
return false;
}
@ConfigItem(
keyName = "enemyAttackStyleText",
name = "Enemy Attack Style: Text statt Icon",
@@ -583,5 +594,140 @@ public interface EssentialsPlusConfig extends Config
{
return 0;
}
@ConfigItem(
keyName = "chatHelper",
name = "Chat helper",
description = "Automatisches Aufräumen alter Nachrichten (nach 1 Minute), Duplikate (x2, x3), Spam-Filter, Namen-Highlight & Kopieren.",
position = 90
)
default boolean chatHelper()
{
return false;
}
@ConfigItem(
keyName = "chatAutoClear",
name = "Chat: Auto-Clear",
description = "Entfernt alte Nachrichten nach Ablauf der gewählten Zeit automatisch aus dem Chat.",
position = 91
)
default boolean chatAutoClear()
{
return true;
}
@ConfigItem(
keyName = "chatAutoClearTime",
name = "Chat: Auto-Clear Zeit",
description = "Zeit in Sekunden, nach der Nachrichten automatisch verschwinden.",
position = 92
)
@Range(min = 10, max = 600)
@Units(Units.SECONDS)
default int chatAutoClearTime()
{
return 60;
}
@ConfigItem(
keyName = "chatClearGame",
name = "Chat: Spiel-Nachrichten leeren",
description = "Spiel-, System- und Examine-Meldungen automatisch aufräumen.",
position = 93
)
default boolean chatClearGame()
{
return true;
}
@ConfigItem(
keyName = "chatClearPublic",
name = "Chat: Öffentlichen Chat leeren",
description = "Öffentliche Spielernachrichten automatisch aufräumen.",
position = 94
)
default boolean chatClearPublic()
{
return true;
}
@ConfigItem(
keyName = "chatClearPrivate",
name = "Chat: Private Nachrichten leeren",
description = "Private Nachrichten (PMs) ebenfalls aufräumen (Standard: Aus, um Konversationen zu behalten).",
position = 95
)
default boolean chatClearPrivate()
{
return false;
}
@ConfigItem(
keyName = "chatClearClan",
name = "Chat: Clan- & Freunde-Chat leeren",
description = "Clan- und Gruppen-Nachrichten ebenfalls aufräumen (Standard: Aus).",
position = 96
)
default boolean chatClearClan()
{
return false;
}
@ConfigItem(
keyName = "chatCollapseDuplicates",
name = "Chat: Duplikate zusammenfassen",
description = "Fasst aufeinanderfolgende gleiche Meldungen zu (x2, x3...) zusammen.",
position = 97
)
default boolean chatCollapseDuplicates()
{
return true;
}
@ConfigItem(
keyName = "chatFilterSpam",
name = "Chat: Spam & Systemlärm filtern",
description = "Filtert repetitive, unnötige Systemmeldungen ('Nothing interesting happens' etc.) und Spam.",
position = 98
)
default boolean chatFilterSpam()
{
return true;
}
@ConfigItem(
keyName = "chatHighlightName",
name = "Chat: Eigenen Namen hervorheben",
description = "Hebt deinen Spielernamen in Spielernachrichten farblich hervor.",
position = 99
)
default boolean chatHighlightName()
{
return true;
}
@ConfigItem(
keyName = "chatHighlightColor",
name = "Chat: Highlight-Farbe",
description = "Farbe für die Namens-Hervorhebung.",
position = 100
)
default Color chatHighlightColor()
{
return new Color(0xFFD700);
}
@ConfigItem(
keyName = "chatCopyToClipboard",
name = "Chat: Kopieren im Rechtsklick-Menü",
description = "Fügt beim Rechtsklick auf eine Chat-Nachricht die Option 'Copy message' hinzu.",
position = 101
)
default boolean chatCopyToClipboard()
{
return true;
}
}
@@ -256,6 +256,21 @@ public class EssentialsPlusPlugin extends Plugin
@Inject
private com.example.essentialsplus.helpers.ItemChargesOverlay itemChargesOverlay;
@Inject
private com.example.essentialsplus.helpers.BlastFurnace blastFurnace;
@Inject
private com.example.essentialsplus.helpers.BlastFurnaceOverlay blastFurnaceOverlay;
@Inject
private com.example.essentialsplus.helpers.BlastFurnacePanelOverlay blastFurnacePanelOverlay;
@Inject
private com.example.essentialsplus.helpers.BlastFurnaceItemsOverlay blastFurnaceItemsOverlay;
@Inject
private com.example.essentialsplus.helpers.ChatHelper chatHelper;
@Inject
private net.runelite.client.eventbus.EventBus eventBus;
@@ -384,10 +399,17 @@ public class EssentialsPlusPlugin extends Plugin
eventBus.register(attackStyles);
eventBus.register(treeCooldowns);
eventBus.register(itemCharges);
eventBus.register(blastFurnace);
eventBus.register(chatHelper);
overlayManager.add(attackTimerOverlay);
overlayManager.add(itemPriceOverlay);
overlayManager.add(treeCooldownOverlay);
overlayManager.add(itemChargesOverlay);
overlayManager.add(blastFurnaceOverlay);
overlayManager.add(blastFurnacePanelOverlay);
overlayManager.add(blastFurnaceItemsOverlay);
// only safe here: reading the config from the overlay constructor would run before the plugin fields exist
blastFurnacePanelOverlay.applyPosition();
currencyStore.load();
currencyTracker = new com.example.essentialsplus.currency.CurrencyTracker(client, currencyStore);
skillSaveTask = executor.scheduleWithFixedDelay(() ->
@@ -402,11 +424,7 @@ public class EssentialsPlusPlugin extends Plugin
com.example.essentialsplus.loot.gear.MonsterService monsters = new com.example.essentialsplus.loot.gear.MonsterService(
okHttpClient, gson, wikiExecutor, () -> fightService.onMonsterLoaded());
fightService = new com.example.essentialsplus.loot.gear.FightService(client, itemManager, combatTracker, attackStyles,
monsters, wikiExecutor, () ->
{
AccountData acc = activeKey != null ? store.get(activeKey) : null;
return acc != null ? acc.containers.get("bank") : null;
});
monsters, wikiExecutor, this::getActiveBank);
panel = new EssentialsPlusPanel(this);
BufferedImage icon = loadIcon();
@@ -459,14 +477,21 @@ public class EssentialsPlusPlugin extends Plugin
overlayManager.remove(itemPriceOverlay);
overlayManager.remove(treeCooldownOverlay);
overlayManager.remove(itemChargesOverlay);
overlayManager.remove(blastFurnaceOverlay);
overlayManager.remove(blastFurnacePanelOverlay);
overlayManager.remove(blastFurnaceItemsOverlay);
eventBus.unregister(attackTimers);
eventBus.unregister(attackStyles);
eventBus.unregister(treeCooldowns);
eventBus.unregister(itemCharges);
eventBus.unregister(blastFurnace);
eventBus.unregister(chatHelper);
attackTimers.reset();
attackStyles.reset();
treeCooldowns.reset();
itemCharges.reset();
blastFurnace.reset();
chatHelper.reset();
if (skillSaveTask != null)
{
skillSaveTask.cancel(false);
@@ -619,6 +644,10 @@ public class EssentialsPlusPlugin extends Plugin
return config.treeCooldowns();
case "itemCharges":
return config.itemCharges();
case "blastFurnace":
return config.blastFurnace();
case "chatHelper":
return config.chatHelper();
default:
return false;
}
@@ -649,6 +678,18 @@ public class EssentialsPlusPlugin extends Plugin
return itemCharges;
}
public com.example.essentialsplus.helpers.ChatHelper getChatHelper()
{
return chatHelper;
}
/** Last known bank contents of the logged in character (kept while the bank is closed), may be null. */
public Map<Integer, Long> getActiveBank()
{
AccountData acc = activeKey != null ? store.get(activeKey) : null;
return acc != null ? acc.containers.get("bank") : null;
}
public net.runelite.client.ui.components.colorpicker.ColorPickerManager getColorPickerManager()
{
return colorPickerManager;
@@ -1776,10 +1817,15 @@ public class EssentialsPlusPlugin extends Plugin
{
p.refreshSkillsLater();
}
if (p != null && (event.getKey().toLowerCase().contains("attacktimer") || event.getKey().toLowerCase().contains("attackstyle") || event.getKey().toLowerCase().contains("treecooldown") || event.getKey().toLowerCase().contains("itemcharge")))
if ("blastFurnacePosition".equals(event.getKey()))
{
blastFurnacePanelOverlay.applyPosition();
}
if (p != null && (event.getKey().toLowerCase().contains("attacktimer") || event.getKey().toLowerCase().contains("attackstyle") || event.getKey().toLowerCase().contains("treecooldown") || event.getKey().toLowerCase().contains("itemcharge") || event.getKey().toLowerCase().contains("blastfurnace") || event.getKey().toLowerCase().contains("chat")))
{
p.refreshHelpersLater();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,196 @@
package com.example.essentialsplus.helpers;
import com.example.essentialsplus.EssentialsPlusConfig;
import com.example.essentialsplus.EssentialsPlusPlugin;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import javax.inject.Inject;
import net.runelite.api.gameval.InterfaceID;
import net.runelite.api.gameval.ItemID;
import net.runelite.api.widgets.Widget;
import net.runelite.api.widgets.WidgetItem;
import net.runelite.api.widgets.WidgetUtil;
import net.runelite.client.ui.overlay.WidgetItemOverlay;
/**
* Marks the active Blast Furnace item in bank and inventory: coal bag to fill, bars to deposit,
* or the target ore in the bank to withdraw. Only a single item type is marked at any time,
* with border-only highlighting to keep items clearly recognizable.
*/
public class BlastFurnaceItemsOverlay extends WidgetItemOverlay
{
private final EssentialsPlusConfig config;
private final EssentialsPlusPlugin plugin;
private final BlastFurnace blastFurnace;
@Inject
public BlastFurnaceItemsOverlay(EssentialsPlusConfig config, EssentialsPlusPlugin plugin, BlastFurnace blastFurnace)
{
this.config = config;
this.plugin = plugin;
this.blastFurnace = blastFurnace;
showOnInventory();
showOnBank();
}
@Override
public void renderItemOverlay(Graphics2D g, int itemId, WidgetItem widgetItem)
{
if (!config.blastFurnace() || !blastFurnace.isAtBlastFurnace())
{
return;
}
Widget widget = widgetItem.getWidget();
if (widget == null)
{
return;
}
boolean bank = isBank(widget);
boolean bankOpen = blastFurnace.isBankOpen();
if (bankOpen)
{
// State 1: Deposit bars in inventory
if (blastFurnace.inventoryHasBars())
{
if (bank || !plugin.getHelperValue("blastFurnaceMarkItems", Boolean.class, true))
{
return;
}
if (!BlastFurnace.isBar(itemId))
{
return;
}
}
// State 2: Deposit empty vials in inventory
else if (blastFurnace.inventoryHasVials())
{
if (bank || !plugin.getHelperValue("blastFurnaceMarkItems", Boolean.class, true))
{
return;
}
if (itemId != ItemID.VIAL_EMPTY)
{
return;
}
}
// State 3: Deposit leftover stamina potion in inventory (if energy is already > 30)
else if (!blastFurnace.needsStamina() && blastFurnace.inventoryHasStaminaPotion())
{
if (bank || !plugin.getHelperValue("blastFurnaceMarkItems", Boolean.class, true))
{
return;
}
if (!BlastFurnace.isStaminaPotion(itemId))
{
return;
}
}
// State 4: Drink stamina potion if energy <= 30
else if (plugin.getHelperValue("blastFurnaceStamina", Boolean.class, true) && blastFurnace.needsStamina())
{
if (blastFurnace.inventoryHasStaminaPotion())
{
// Drink from inventory
if (bank || !plugin.getHelperValue("blastFurnaceMarkItems", Boolean.class, true))
{
return;
}
if (itemId != blastFurnace.getInventoryStaminaPotion())
{
return;
}
}
else
{
// Withdraw from bank
if (!bank || !plugin.getHelperValue("blastFurnaceMarkBankItems", Boolean.class, true))
{
return;
}
if (itemId != blastFurnace.getTargetBankStaminaPotion())
{
return;
}
}
}
// State 5: Inventory is full -> conveyor belt is marked in 3D world, nothing in bank/inventory
else if (blastFurnace.isInventoryFull())
{
return;
}
// State 6: Fill coal bag in inventory and withdraw target ore/coal from bank simultaneously
else
{
if (!bank)
{
// In inventory: highlight coal bag if it needs filling
if (!plugin.getHelperValue("blastFurnaceMarkItems", Boolean.class, true))
{
return;
}
if (!blastFurnace.coalBagNeedsFill())
{
return;
}
if (itemId != ItemID.COAL_BAG && itemId != ItemID.COAL_BAG_OPEN)
{
return;
}
}
else
{
// In bank: highlight target ore / coal to withdraw
if (!plugin.getHelperValue("blastFurnaceMarkBankItems", Boolean.class, true))
{
return;
}
int targetOre = blastFurnace.getTargetBankOre();
if (targetOre == -1 || itemId != targetOre)
{
return;
}
}
}
}
else
{
// Bank is closed:
// If coal in bag and no ores in inventory (e.g. at belt to empty bag)
if (blastFurnace.bagHasCoal() && !blastFurnace.inventoryHasOres() && blastFurnace.hasCoalBag())
{
if (bank || !plugin.getHelperValue("blastFurnaceMarkItems", Boolean.class, true))
{
return;
}
if (itemId != ItemID.COAL_BAG && itemId != ItemID.COAL_BAG_OPEN)
{
return;
}
}
else
{
return;
}
}
Rectangle bounds = widgetItem.getCanvasBounds();
if (bounds == null || bounds.width < 4 || bounds.height < 4)
{
return;
}
Color color = plugin.getHelperValue("blastFurnaceColor", Color.class, BlastFurnaceOverlay.DEFAULT_MARK_COLOR);
g.setColor(color);
g.drawRect(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1);
g.drawRect(bounds.x + 1, bounds.y + 1, bounds.width - 3, bounds.height - 3);
}
private static boolean isBank(Widget widget)
{
int group = WidgetUtil.componentToInterface(widget.getId());
return group == InterfaceID.BANKMAIN || group == 12; // 12 = InterfaceID.BANK
}
}
@@ -0,0 +1,157 @@
package com.example.essentialsplus.helpers;
import com.example.essentialsplus.EssentialsPlusConfig;
import com.example.essentialsplus.EssentialsPlusPlugin;
import com.example.essentialsplus.ui.Ui;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Shape;
import javax.inject.Inject;
import net.runelite.api.GameObject;
import net.runelite.api.Point;
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayLayer;
import net.runelite.client.ui.overlay.OverlayPosition;
/**
* Marks the Blast Furnace objects that are worth clicking right now: the conveyor belt while
* ores are waiting in the inventory, the bar dispenser when bars are ready and the bank chest
* when the bars in the inventory should be banked.
*/
public class BlastFurnaceOverlay extends Overlay
{
public static final Color DEFAULT_MARK_COLOR = new Color(0xFFB300);
public static final Color DEFAULT_READY_COLOR = new Color(0x66FF66);
private final EssentialsPlusConfig config;
private final EssentialsPlusPlugin plugin;
private final BlastFurnace blastFurnace;
@Inject
public BlastFurnaceOverlay(EssentialsPlusConfig config, EssentialsPlusPlugin plugin, BlastFurnace blastFurnace)
{
this.config = config;
this.plugin = plugin;
this.blastFurnace = blastFurnace;
setPosition(OverlayPosition.DYNAMIC);
setLayer(OverlayLayer.ABOVE_SCENE);
}
@Override
public Dimension render(Graphics2D g)
{
if (!config.blastFurnace() || !blastFurnace.isAtBlastFurnace())
{
return null;
}
boolean bankOpen = blastFurnace.isBankOpen();
Color markColor = plugin.getHelperValue("blastFurnaceColor", Color.class, DEFAULT_MARK_COLOR);
Color readyColor = plugin.getHelperValue("blastFurnaceReadyColor", Color.class, DEFAULT_READY_COLOR);
boolean labels = plugin.getHelperValue("blastFurnaceLabels", Boolean.class, true);
TimerTextSize size = plugin.getHelperValue("blastFurnaceTextSize", TimerTextSize.class, TimerTextSize.SMALL);
// 1. Bars in inventory -> bank chest (only if bank is closed)
if (blastFurnace.inventoryHasBars())
{
if (!bankOpen && plugin.getHelperValue("blastFurnaceMarkBank", Boolean.class, true))
{
mark(g, blastFurnace.getBankChest(), markColor, labels ? "Bank bars" : null, size);
}
return null;
}
// 2. Full inventory or ores in inventory -> conveyor belt (ALLOWED EVEN IF BANK IS OPEN!)
if (blastFurnace.isInventoryFull() || blastFurnace.inventoryHasOres())
{
if (plugin.getHelperValue("blastFurnaceMarkBelt", Boolean.class, true))
{
String label = blastFurnace.inventoryHasCoal() ? "Put coal on belt" : "Put ore on belt";
mark(g, blastFurnace.getConveyorBelt(), markColor, labels ? label : null, size);
}
return null;
}
// While bank is open and inventory is not full, player is interacting with bank items
if (bankOpen)
{
return null;
}
// 3. Bars ready to collect or waiting for ores to smelt -> bar dispenser!
if (blastFurnace.isWaitingForBars())
{
if (plugin.getHelperValue("blastFurnaceMarkDispenser", Boolean.class, true))
{
boolean ready = blastFurnace.barsReady();
Color color = ready ? readyColor : markColor;
String label = ready ? "Collect bars" : "Wait for bars";
mark(g, blastFurnace.getBarDispenser(), color, labels ? label : null, size);
}
return null;
}
// 4. Coal in coal bag to empty onto belt
if (blastFurnace.bagHasCoal() && blastFurnace.hasCoalBag())
{
if (plugin.getHelperValue("blastFurnaceMarkBelt", Boolean.class, true))
{
mark(g, blastFurnace.getConveyorBelt(), markColor, labels ? "Put coal on belt" : null, size);
}
return null;
}
// 5. Otherwise open bank
if (plugin.getHelperValue("blastFurnaceMarkBank", Boolean.class, true))
{
mark(g, blastFurnace.getBankChest(), markColor, labels ? "Open bank" : null, size);
}
return null;
}
private void mark(Graphics2D g, GameObject object, Color color, String label, TimerTextSize size)
{
if (object == null)
{
return;
}
Shape shape = object.getClickbox();
if (shape == null)
{
shape = object.getCanvasTilePoly();
}
if (shape != null)
{
g.setColor(color);
g.draw(shape);
g.setColor(Ui.alpha(color, 45));
g.fill(shape);
}
if (label == null)
{
return;
}
Font font = size.font();
g.setFont(font);
Point anchor = object.getCanvasTextLocation(g, label, 30);
if (anchor == null)
{
return;
}
FontMetrics fm = g.getFontMetrics();
int x = anchor.getX() - fm.stringWidth(label) / 2;
int y = anchor.getY();
g.setColor(Color.BLACK);
g.drawString(label, x + 1, y + 1);
g.setColor(color);
g.drawString(label, x, y);
}
}
@@ -0,0 +1,140 @@
package com.example.essentialsplus.helpers;
import com.example.essentialsplus.EssentialsPlusConfig;
import com.example.essentialsplus.EssentialsPlusPlugin;
import com.example.essentialsplus.ui.Ui;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import net.runelite.client.ui.overlay.OverlayPanel;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.components.LineComponent;
import net.runelite.client.ui.overlay.components.TitleComponent;
/**
* Panel with the Blast Furnace numbers: coal in the furnace, bars waiting in the dispenser,
* coffer and remaining time, and how many bars the ores in the bank can still be turned into.
*/
public class BlastFurnacePanelOverlay extends OverlayPanel
{
private final EssentialsPlusPlugin plugin;
private final EssentialsPlusConfig config;
private final BlastFurnace blastFurnace;
@Inject
BlastFurnacePanelOverlay(EssentialsPlusPlugin plugin, EssentialsPlusConfig config, BlastFurnace blastFurnace)
{
super(plugin);
this.plugin = plugin;
this.config = config;
this.blastFurnace = blastFurnace;
setPosition(OverlayPosition.TOP_LEFT);
panelComponent.setPreferredSize(new Dimension(190, 0));
}
/** Moves the panel to the corner picked in the helper menu (kept out of render so dragging works). */
public void applyPosition()
{
OverlayPosition position = plugin.getHelperValue("blastFurnacePosition", OverlayPosition.class, OverlayPosition.TOP_LEFT);
if (position != null)
{
setPosition(position);
}
}
@Override
public Dimension render(Graphics2D graphics)
{
if (!config.blastFurnace() || !blastFurnace.isAtBlastFurnace())
{
return null;
}
if (!plugin.getHelperValue("blastFurnacePanel", Boolean.class, true))
{
return null;
}
List<LineComponent> lines = new ArrayList<>();
if (plugin.getHelperValue("blastFurnaceShowCoal", Boolean.class, true))
{
int coal = blastFurnace.coalInFurnace();
BlastFurnace.BarType target = blastFurnace.getTargetBarType();
int needed = target != null ? blastFurnace.getRequiredFurnaceCoal(target) : 0;
Color coalColor = (needed > 0 && coal < needed) ? Ui.WARN : Ui.TEXT;
String text = Ui.gp(coal) + (needed > 0 ? " / " + needed : "");
lines.add(line("Coal in furnace", text, coalColor));
}
if (plugin.getHelperValue("blastFurnaceShowFurnaceBars", Boolean.class, true))
{
blastFurnace.barsInFurnace().forEach((type, amount) ->
lines.add(line(type.getLabel() + " (Furnace)", Ui.gp(amount), Ui.UP)));
}
long coffer = blastFurnace.cofferGp();
if (plugin.getHelperValue("blastFurnaceShowCoffer", Boolean.class, true))
{
lines.add(line("Coffer", Ui.gp(coffer) + " gp", Ui.GOLD));
}
if (plugin.getHelperValue("blastFurnaceShowCofferTime", Boolean.class, true))
{
long millis = BlastFurnace.cofferMillis(coffer);
Color color = millis < 5 * Ui.MINUTE ? Ui.DOWN : (millis < 30 * Ui.MINUTE ? Ui.WARN : Ui.TEXT);
lines.add(line("Coffer time", duration(millis), color));
}
if (plugin.getHelperValue("blastFurnaceShowBankBars", Boolean.class, true))
{
int limit = Math.max(0, plugin.getHelperValue("blastFurnaceBankLines", Integer.class, 3));
List<BlastFurnace.Estimate> estimates = blastFurnace.remainingBars();
for (int i = 0; i < Math.min(limit, estimates.size()); i++)
{
BlastFurnace.Estimate estimate = estimates.get(i);
String value = Ui.gp(estimate.bars) + (estimate.coalLimited ? " (Coal)" : "");
lines.add(line("Bank: " + estimate.type.getLabel(), value, estimate.coalLimited ? Ui.WARN : Ui.TEXT));
}
}
if (lines.isEmpty())
{
return null;
}
panelComponent.getChildren().add(TitleComponent.builder()
.text("Blast Furnace")
.color(Ui.ACCENT)
.build());
panelComponent.getChildren().addAll(lines);
return super.render(graphics);
}
private static LineComponent line(String left, String right, Color rightColor)
{
return LineComponent.builder()
.left(left)
.leftColor(Ui.MUTED)
.right(right)
.rightColor(rightColor)
.build();
}
private static String duration(long millis)
{
long seconds = Math.max(0, millis / 1000);
long hours = seconds / 3600;
long minutes = (seconds % 3600) / 60;
if (hours > 0)
{
return hours + "h " + minutes + "m";
}
if (minutes > 0)
{
return minutes + "m " + (seconds % 60) + "s";
}
return seconds + "s";
}
}
@@ -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);
});
}
}
@@ -1320,6 +1320,14 @@ public class ItemCharges
}
}
/** Tracked amount for a key of {@link #cachedCounts} (e.g. "coal_bag"), 0 while unknown. */
public int getTrackedCount(String key)
{
loadAccountDataIfNecessary();
Integer val = cachedCounts.get(key);
return val == null ? 0 : Math.max(0, val);
}
public void decrementCount(String key, int amount)
{
loadAccountDataIfNecessary();
@@ -1,6 +1,7 @@
package com.example.essentialsplus.ui;
import com.example.essentialsplus.EssentialsPlusPlugin;
import com.example.essentialsplus.helpers.BlastFurnaceOverlay;
import com.example.essentialsplus.helpers.TimerTextSize;
import java.awt.Color;
import java.awt.Dimension;
@@ -19,6 +20,7 @@ import javax.swing.JLabel;
import javax.swing.JPanel;
import net.runelite.client.ui.FontManager;
import net.runelite.client.ui.PluginPanel;
import net.runelite.client.ui.overlay.OverlayPosition;
/** Overview card with on/off switches for the small in-game helpers (attack timers, ...). */
public class HelpersCard extends JPanel
@@ -30,6 +32,8 @@ public class HelpersCard extends JPanel
private final JCheckBox enemyAttackTimer = box("Enemy attack timer", "Ticks until attacking NPCs hit next, above their heads. Right-click for options");
private final JCheckBox treeCooldowns = box("Tree cooldowns", "Respawn-Countdown auf gefällten Bäumen + Rest-Zeit auf langlebigen Bäumen. Rechtsklick für Optionen");
private final JCheckBox itemCharges = box("Item charges", "Charge-Zahl auf Item-Icons (Blowpipe, Trident, Tome, …) + Loot-Bag-Geld & Coal-Bag-Füllstand. Rechtsklick für Optionen");
private final JCheckBox blastFurnace = box("Blast Furnace", "Highlights conveyor belt, bar dispenser & bank chest, shows coal in furnace, coffer time, and bars craftable from bank ores. Right-click for options");
private final JCheckBox chatHelper = box("Chat helper", "Auto-Clear nach 1 Min, Duplikate (x2, x3), Spam-Filter, Namen-Highlight & Kopieren. Rechtsklick für Optionen");
public HelpersCard(EssentialsPlusPlugin plugin)
{
@@ -102,6 +106,32 @@ public class HelpersCard extends JPanel
}
});
add(itemCharges);
blastFurnace.addActionListener(e -> plugin.setHelperOption("blastFurnace", blastFurnace.isSelected()));
blastFurnace.addMouseListener(new java.awt.event.MouseAdapter()
{
@Override
public void mousePressed(java.awt.event.MouseEvent e)
{
if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))
{
blastFurnaceMenu().show(blastFurnace, e.getX(), e.getY());
}
}
});
add(blastFurnace);
chatHelper.addActionListener(e -> plugin.setHelperOption("chatHelper", chatHelper.isSelected()));
chatHelper.addMouseListener(new java.awt.event.MouseAdapter()
{
@Override
public void mousePressed(java.awt.event.MouseEvent e)
{
if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))
{
chatMenu().show(chatHelper, e.getX(), e.getY());
}
}
});
add(chatHelper);
refresh();
}
@@ -114,8 +144,11 @@ public class HelpersCard extends JPanel
itemPrices.setSelected(plugin.isHelperOption("itemPrices"));
treeCooldowns.setSelected(plugin.isHelperOption("treeCooldowns"));
itemCharges.setSelected(plugin.isHelperOption("itemCharges"));
blastFurnace.setSelected(plugin.isHelperOption("blastFurnace"));
chatHelper.setSelected(plugin.isHelperOption("chatHelper"));
}
private static final Color READY = new Color(0x66FF66);
private static final int[] HEIGHTS = {10, 30, 60, 100};
private static final String[] HEIGHT_LABELS = {"Low", "Normal", "High", "Very high"};
@@ -369,6 +402,187 @@ public class HelpersCard extends JPanel
return item;
}
private static final int[] BF_BANK_LINES = {1, 2, 3, 5};
private static final OverlayPosition[] BF_POSITIONS = {
OverlayPosition.TOP_LEFT, OverlayPosition.TOP_RIGHT, OverlayPosition.BOTTOM_LEFT, OverlayPosition.BOTTOM_RIGHT
};
private static final String[] BF_POSITION_LABELS = {"Top left", "Top right", "Bottom left", "Bottom right"};
/** Right-click on the Blast Furnace switch: what gets marked and which numbers the panel shows. */
private JPopupMenu blastFurnaceMenu()
{
JPopupMenu menu = new JPopupMenu();
for (String[] opt : new String[][]{
{"blastFurnaceMarkBelt", "Highlight conveyor belt"},
{"blastFurnaceMarkDispenser", "Highlight bar dispenser"},
{"blastFurnaceMarkBank", "Highlight bank chest"},
{"blastFurnaceMarkBankItems", "Highlight ores in bank"},
{"blastFurnaceMarkItems", "Highlight ores & bars in inventory"},
{"blastFurnaceStamina", "Drink stamina potion at <= 30 energy"},
{"blastFurnaceLabels", "Object labels"}
})
{
menu.add(toggle(opt[0], opt[1]));
}
menu.addSeparator();
menu.add(toggle("blastFurnacePanel", "Show info panel"));
JMenu lines = new JMenu("Info panel: Rows");
for (String[] opt : new String[][]{
{"blastFurnaceShowCoal", "Coal in furnace"},
{"blastFurnaceShowFurnaceBars", "Bars in furnace"},
{"blastFurnaceShowCoffer", "Coffer"},
{"blastFurnaceShowCofferTime", "Coffer time"},
{"blastFurnaceShowBankBars", "Bars from bank ores"}
})
{
lines.add(toggle(opt[0], opt[1]));
}
JMenu bankLines = new JMenu("Bank rows");
int currentLines = plugin.getHelperValue("blastFurnaceBankLines", Integer.class, 3);
for (int value : BF_BANK_LINES)
{
bankLines.add(radio(String.valueOf(value), value == currentLines, () -> plugin.setHelperOption("blastFurnaceBankLines", value)));
}
lines.add(bankLines);
menu.add(lines);
JMenu position = new JMenu("Info panel: Position");
OverlayPosition currentPosition = plugin.getHelperValue("blastFurnacePosition", OverlayPosition.class, OverlayPosition.TOP_LEFT);
for (int i = 0; i < BF_POSITIONS.length; i++)
{
OverlayPosition value = BF_POSITIONS[i];
position.add(radio(BF_POSITION_LABELS[i], value == currentPosition, () -> plugin.setHelperOption("blastFurnacePosition", value)));
}
menu.add(position);
menu.addSeparator();
JMenu size = new JMenu("Label text size");
TimerTextSize currentSize = plugin.getHelperValue("blastFurnaceTextSize", TimerTextSize.class, TimerTextSize.SMALL);
for (TimerTextSize s : TimerTextSize.values())
{
size.add(radio(s.toString(), s == currentSize, () -> plugin.setHelperOption("blastFurnaceTextSize", s)));
}
menu.add(size);
menu.add(colorItem("Color...", "blastFurnaceColor", "Blast Furnace highlight color", BlastFurnaceOverlay.DEFAULT_MARK_COLOR));
menu.add(colorItem("Ready color...", "blastFurnaceReadyColor", "Blast Furnace ready color", BlastFurnaceOverlay.DEFAULT_READY_COLOR));
menu.addSeparator();
JMenuItem reset = new JMenuItem("Reset appearance");
reset.addActionListener(e ->
{
for (String key : new String[]{
"blastFurnaceMarkBelt", "blastFurnaceMarkDispenser", "blastFurnaceMarkBank", "blastFurnaceMarkBankItems",
"blastFurnaceMarkItems", "blastFurnaceStamina", "blastFurnaceLabels", "blastFurnacePanel", "blastFurnaceShowCoal",
"blastFurnaceShowFurnaceBars", "blastFurnaceShowCoffer", "blastFurnaceShowCofferTime",
"blastFurnaceShowBankBars", "blastFurnaceBankLines", "blastFurnacePosition", "blastFurnaceTextSize",
"blastFurnaceColor", "blastFurnaceReadyColor"})
{
plugin.setHelperOption(key, null);
}
});
menu.add(reset);
return menu;
}
private static final int[] CHAT_CLEAR_TIMES = {0, 30, 60, 120, 300};
private static final String[] CHAT_CLEAR_TIME_LABELS = {"Aus (0s)", "30 Sekunden", "1 Minute (Standard)", "2 Minuten", "5 Minuten"};
private JPopupMenu chatMenu()
{
JPopupMenu menu = new JPopupMenu();
boolean autoClear = plugin.getHelperValue("chatAutoClear", Boolean.class, true);
JCheckBoxMenuItem autoClearItem = new JCheckBoxMenuItem("Automatisch aufräumen", autoClear);
autoClearItem.addActionListener(e -> plugin.setHelperOption("chatAutoClear", !autoClear));
menu.add(autoClearItem);
JMenu clearTimeMenu = new JMenu("Auto-Clear Zeit");
int currentTime = plugin.getHelperValue("chatAutoClearTime", Integer.class, 60);
for (int i = 0; i < CHAT_CLEAR_TIMES.length; i++)
{
int time = CHAT_CLEAR_TIMES[i];
clearTimeMenu.add(radio(CHAT_CLEAR_TIME_LABELS[i], time == currentTime, () -> plugin.setHelperOption("chatAutoClearTime", time)));
}
menu.add(clearTimeMenu);
JMenu typesMenu = new JMenu("Was wird aufgeräumt?");
for (String[] opt : new String[][]{
{"chatClearGame", "Spiel- & System-Meldungen", "true"},
{"chatClearPublic", "Öffentlicher Chat (Public)", "true"},
{"chatClearPrivate", "Private Nachrichten (PM)", "false"},
{"chatClearClan", "Clan- & Freunde-Chat", "false"}
})
{
boolean def = Boolean.parseBoolean(opt[2]);
boolean on = plugin.getHelperValue(opt[0], Boolean.class, def);
JCheckBoxMenuItem item = new JCheckBoxMenuItem(opt[1], on);
item.addActionListener(e -> plugin.setHelperOption(opt[0], !on));
typesMenu.add(item);
}
menu.add(typesMenu);
menu.addSeparator();
for (String[] opt : new String[][]{
{"chatCollapseDuplicates", "Duplikate zusammenfassen (x2, x3...)"},
{"chatFilterSpam", "Spam & Systemlärm filtern"},
{"chatHighlightName", "Eigenen Namen hervorheben"},
{"chatCopyToClipboard", "Rechtsklick 'Kopieren' aktivieren"}
})
{
boolean on = plugin.getHelperValue(opt[0], Boolean.class, true);
JCheckBoxMenuItem item = new JCheckBoxMenuItem(opt[1], on);
item.addActionListener(e -> plugin.setHelperOption(opt[0], !on));
menu.add(item);
}
menu.add(colorItem("Highlight-Farbe...", "chatHighlightColor", "Chat Highlight-Farbe", new Color(0xFFD700)));
menu.addSeparator();
JMenuItem clearNow = new JMenuItem("Chat jetzt leeren");
clearNow.addActionListener(e ->
{
if (plugin.getChatHelper() != null)
{
plugin.getChatHelper().clearAllChat();
}
});
menu.add(clearNow);
JMenuItem reset = new JMenuItem("Einstellungen zurücksetzen");
reset.addActionListener(e ->
{
for (String key : new String[]{
"chatAutoClear", "chatAutoClearTime", "chatClearGame", "chatClearPublic",
"chatClearPrivate", "chatClearClan", "chatCollapseDuplicates",
"chatFilterSpam", "chatHighlightName", "chatHighlightColor", "chatCopyToClipboard"
})
{
plugin.setHelperOption(key, null);
}
});
menu.add(reset);
return menu;
}
/** Checkbox for a switch that is on by default (turning it on stores the default explicitly). */
private JCheckBoxMenuItem toggle(String key, String label)
{
boolean on = plugin.getHelperValue(key, Boolean.class, true);
JCheckBoxMenuItem item = new JCheckBoxMenuItem(label, on);
item.addActionListener(e -> plugin.setHelperOption(key, !on));
return item;
}
private JMenuItem colorItem(String label, String key, String title, Color def)
{
JMenuItem item = new JMenuItem(label);
@@ -0,0 +1,239 @@
package com.example.essentialsplus.helpers;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.Map;
import net.runelite.api.gameval.ItemID;
import org.junit.Test;
public class BlastFurnaceTest
{
private static Map<Integer, Integer> ores(Object... pairs)
{
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i + 1 < pairs.length; i += 2)
{
map.put((Integer) pairs[i], (Integer) pairs[i + 1]);
}
return map;
}
private static BlastFurnace.Estimate estimate(BlastFurnace.BarType type, int coal, Object... pairs)
{
return BlastFurnace.estimate(type, ores(pairs), coal);
}
@Test
public void testCoalCostsAreTheBlastFurnaceOnes()
{
// the Blast Furnace needs half the coal of a normal furnace
assertEquals(0, BlastFurnace.BarType.IRON.getCoalPerBar());
assertEquals(1, BlastFurnace.BarType.STEEL.getCoalPerBar());
assertEquals(2, BlastFurnace.BarType.MITHRIL.getCoalPerBar());
assertEquals(3, BlastFurnace.BarType.ADAMANTITE.getCoalPerBar());
assertEquals(4, BlastFurnace.BarType.RUNITE.getCoalPerBar());
assertEquals(0, BlastFurnace.BarType.GOLD.getCoalPerBar());
}
@Test
public void testSteelBarsFromIronAndCoal()
{
BlastFurnace.Estimate e = estimate(BlastFurnace.BarType.STEEL, 5_000, ItemID.IRON_ORE, 1_000, ItemID.COAL, 5_000);
assertEquals(1_000, e.bars);
assertFalse(e.coalLimited);
}
@Test
public void testSteelBarsAreLimitedByCoal()
{
BlastFurnace.Estimate e = estimate(BlastFurnace.BarType.STEEL, 250, ItemID.IRON_ORE, 1_000);
assertEquals(250, e.bars);
assertTrue(e.coalLimited);
}
@Test
public void testIronBarsNeedNoCoal()
{
BlastFurnace.Estimate e = estimate(BlastFurnace.BarType.IRON, 0, ItemID.IRON_ORE, 742);
assertEquals(742, e.bars);
assertFalse(e.coalLimited);
}
@Test
public void testMithrilBarsUseTwoCoalPerBar()
{
assertEquals(75, estimate(BlastFurnace.BarType.MITHRIL, 150, ItemID.MITHRIL_ORE, 100).bars);
assertEquals(100, estimate(BlastFurnace.BarType.MITHRIL, 200, ItemID.MITHRIL_ORE, 100).bars);
assertTrue(estimate(BlastFurnace.BarType.MITHRIL, 150, ItemID.MITHRIL_ORE, 100).coalLimited);
}
@Test
public void testMissingOreMeansNoBars()
{
assertEquals(0, estimate(BlastFurnace.BarType.RUNITE, 10_000, ItemID.ADAMANTITE_ORE, 500).bars);
assertEquals(0, estimate(BlastFurnace.BarType.BRONZE, 10_000, ItemID.COPPER_ORE, 100).bars);
}
@Test
public void testOresNeedingMoreThanOneItem()
{
// bronze: 1 copper + 1 tin
assertEquals(4, estimate(BlastFurnace.BarType.BRONZE, 0, ItemID.COPPER_ORE, 10, ItemID.TIN_ORE, 4).bars);
// lead: 2 lead ore per bar
assertEquals(4, estimate(BlastFurnace.BarType.LEAD, 0, ItemID.LEAD_ORE, 9).bars);
// cupronickel: 2 copper + 1 nickel per bar
assertEquals(3, estimate(BlastFurnace.BarType.CUPRONICKEL, 0, ItemID.COPPER_ORE, 10, ItemID.NICKEL_ORE, 3).bars);
assertEquals(5, estimate(BlastFurnace.BarType.CUPRONICKEL, 0, ItemID.COPPER_ORE, 10, ItemID.NICKEL_ORE, 9).bars);
}
@Test
public void testNegativeAmountsAreClamped()
{
assertEquals(0, estimate(BlastFurnace.BarType.STEEL, -20, ItemID.IRON_ORE, 100).bars);
assertEquals(0, estimate(BlastFurnace.BarType.STEEL, 100, ItemID.IRON_ORE, -5).bars);
}
@Test
public void testCofferTimeIsFiftyMillisecondsPerGp()
{
assertEquals(3_600_000L, BlastFurnace.cofferMillis(72_000));
assertEquals(0L, BlastFurnace.cofferMillis(0));
assertEquals(0L, BlastFurnace.cofferMillis(-1_000));
}
@Test
public void testOreAndBarLookup()
{
assertTrue(BlastFurnace.isOre(ItemID.RUNITE_ORE));
assertTrue(BlastFurnace.isOre(ItemID.COAL));
assertFalse(BlastFurnace.isOre(ItemID.RUNITE_BAR));
assertTrue(BlastFurnace.isBar(ItemID.RUNITE_BAR));
assertFalse(BlastFurnace.isBar(ItemID.RUNITE_ORE));
assertEquals(BlastFurnace.BarType.RUNITE, BlastFurnace.barTypeOf(ItemID.RUNITE_BAR));
}
@Test
public void testRequiredFurnaceCoalFormula()
{
// Runite: 4 coal per bar. With coal bag + buffer: 4x ore slots = 4 * 27 = 108
assertEquals(108, BlastFurnace.getRequiredFurnaceCoal(BlastFurnace.BarType.RUNITE, 27, true));
// Runite without coal bag: 5 * 27 = 135
assertEquals(135, BlastFurnace.getRequiredFurnaceCoal(BlastFurnace.BarType.RUNITE, 27, false));
// Adamantite: 3 coal per bar. With coal bag + buffer: 3 * 27 = 81
assertEquals(81, BlastFurnace.getRequiredFurnaceCoal(BlastFurnace.BarType.ADAMANTITE, 27, true));
assertEquals(108, BlastFurnace.getRequiredFurnaceCoal(BlastFurnace.BarType.ADAMANTITE, 27, false));
// Mithril: 2 coal per bar. With coal bag + buffer: 2 * 27 = 54
assertEquals(54, BlastFurnace.getRequiredFurnaceCoal(BlastFurnace.BarType.MITHRIL, 27, true));
// Steel: 1 coal per bar. With coal bag + buffer: 1 * 27 = 27
assertEquals(27, BlastFurnace.getRequiredFurnaceCoal(BlastFurnace.BarType.STEEL, 27, true));
assertEquals(54, BlastFurnace.getRequiredFurnaceCoal(BlastFurnace.BarType.STEEL, 27, false));
// Gold: 0 coal
assertEquals(0, BlastFurnace.getRequiredFurnaceCoal(BlastFurnace.BarType.GOLD, 27, true));
}
@Test
public void testTargetBankOrePriorityRuniteFirstThenAdamantite()
{
Map<Integer, Integer> bank = new HashMap<>();
bank.put(ItemID.RUNITE_ORE, 100);
bank.put(ItemID.ADAMANTITE_ORE, 500);
bank.put(ItemID.MITHRIL_ORE, 1000);
bank.put(ItemID.COAL, 5000);
// With 99 Smithing, coal bag, and 120 coal in furnace (>= 108) -> Runite Ore
assertEquals(ItemID.RUNITE_ORE, BlastFurnace.pickTargetBankOre(99, 120, 27, true, id -> bank.getOrDefault(id, 0)));
// If furnace has only 80 coal (< 108 needed for Runite) -> Coal!
assertEquals(ItemID.COAL, BlastFurnace.pickTargetBankOre(99, 80, 27, true, id -> bank.getOrDefault(id, 0)));
// If no Runite present, but 90+ coal in furnace (>= 81 for Adamantite) -> Adamantite
bank.remove(ItemID.RUNITE_ORE);
assertEquals(ItemID.ADAMANTITE_ORE, BlastFurnace.pickTargetBankOre(99, 90, 27, true, id -> bank.getOrDefault(id, 0)));
// If no Runite present, and only 60 coal in furnace (< 81 for Adamantite) -> Coal!
assertEquals(ItemID.COAL, BlastFurnace.pickTargetBankOre(99, 60, 27, true, id -> bank.getOrDefault(id, 0)));
}
@Test
public void testTargetBankOreRespectsSmithingLevel()
{
Map<Integer, Integer> bank = new HashMap<>();
bank.put(ItemID.RUNITE_ORE, 100);
bank.put(ItemID.ADAMANTITE_ORE, 500);
bank.put(ItemID.MITHRIL_ORE, 1000);
bank.put(ItemID.COAL, 5000);
// At level 75 (cannot smelt Runite 85), should pick Adamantite when furnace has 90 coal (>= 81)
assertEquals(ItemID.ADAMANTITE_ORE, BlastFurnace.pickTargetBankOre(75, 90, 27, true, id -> bank.getOrDefault(id, 0)));
// At level 55 (cannot smelt Adamantite 70), should pick Mithril when furnace has 60 coal (>= 54)
assertEquals(ItemID.MITHRIL_ORE, BlastFurnace.pickTargetBankOre(55, 60, 27, true, id -> bank.getOrDefault(id, 0)));
}
@Test
public void testStaminaAndVialItemIds()
{
assertTrue(ItemID.VIAL_EMPTY > 0);
assertTrue(ItemID._1DOSESTAMINA > 0);
assertTrue(ItemID._2DOSESTAMINA > 0);
assertTrue(ItemID._3DOSESTAMINA > 0);
assertTrue(ItemID._4DOSESTAMINA > 0);
assertTrue(ItemID._1DOSE2STAMINA > 0);
assertTrue(ItemID._2DOSE2STAMINA > 0);
assertTrue(ItemID._3DOSE2STAMINA > 0);
assertTrue(ItemID._4DOSE2STAMINA > 0);
}
@Test
public void testStaminaPotionRankingPrefersOneDoseAndExtended()
{
// 1-dose extended is #1
assertEquals(ItemID._1DOSE2STAMINA, BlastFurnace.STAMINA_POTION_PRIORITY[0]);
// 1-dose normal is #2 (1-dose prioritized)
assertEquals(ItemID._1DOSESTAMINA, BlastFurnace.STAMINA_POTION_PRIORITY[1]);
// Extended 2-4 before normal 2-4
assertEquals(ItemID._2DOSE2STAMINA, BlastFurnace.STAMINA_POTION_PRIORITY[2]);
assertEquals(ItemID._3DOSE2STAMINA, BlastFurnace.STAMINA_POTION_PRIORITY[3]);
assertEquals(ItemID._4DOSE2STAMINA, BlastFurnace.STAMINA_POTION_PRIORITY[4]);
assertEquals(ItemID._2DOSESTAMINA, BlastFurnace.STAMINA_POTION_PRIORITY[5]);
assertEquals(ItemID._3DOSESTAMINA, BlastFurnace.STAMINA_POTION_PRIORITY[6]);
assertEquals(ItemID._4DOSESTAMINA, BlastFurnace.STAMINA_POTION_PRIORITY[7]);
}
@Test
public void testMinimumOreBatchOf27()
{
Map<Integer, Integer> bank = new HashMap<>();
bank.put(ItemID.RUNITE_ORE, 26); // Less than 27 -> not used!
bank.put(ItemID.ADAMANTITE_ORE, 50); // >= 27 -> used!
bank.put(ItemID.COAL, 5000);
// With 26 Runite ore, Runite is skipped, falling back to Adamantite
assertEquals(BlastFurnace.BarType.ADAMANTITE, BlastFurnace.pickTargetBarType(99, id -> bank.getOrDefault(id, 0)));
assertEquals(ItemID.ADAMANTITE_ORE, BlastFurnace.pickTargetBankOre(99, 90, 27, true, id -> bank.getOrDefault(id, 0)));
// If Runite has exactly 27, it IS used
bank.put(ItemID.RUNITE_ORE, 27);
assertEquals(BlastFurnace.BarType.RUNITE, BlastFurnace.pickTargetBarType(99, id -> bank.getOrDefault(id, 0)));
assertEquals(ItemID.RUNITE_ORE, BlastFurnace.pickTargetBankOre(99, 120, 27, true, id -> bank.getOrDefault(id, 0)));
// If all ores have < 27, finished! (null / -1)
bank.put(ItemID.RUNITE_ORE, 26);
bank.put(ItemID.ADAMANTITE_ORE, 15);
bank.put(ItemID.MITHRIL_ORE, 20);
org.junit.Assert.assertNull(BlastFurnace.pickTargetBarType(99, id -> bank.getOrDefault(id, 0)));
assertEquals(-1, BlastFurnace.pickTargetBankOre(99, 120, 27, true, id -> bank.getOrDefault(id, 0)));
}
}
@@ -0,0 +1,307 @@
package com.example.essentialsplus.helpers;
import com.example.essentialsplus.EssentialsPlusConfig;
import java.awt.Color;
import java.lang.reflect.Proxy;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.runelite.api.ChatLineBuffer;
import net.runelite.api.ChatMessageType;
import net.runelite.api.Client;
import net.runelite.api.IterableHashTable;
import net.runelite.api.MessageNode;
import net.runelite.api.Player;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.ScriptCallbackEvent;
import net.runelite.client.callback.ClientThread;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class ChatHelperTest
{
private Client client;
private EssentialsPlusConfig config;
private ClientThread clientThread;
private ChatHelper chatHelper;
private final Map<Integer, ChatLineBuffer> chatLineMap = new HashMap<>();
private final List<Object[]> scriptsRun = new ArrayList<>();
private final Map<String, Object> configValues = new HashMap<>();
private final Map<Long, MessageNode> messagesMap = new HashMap<>();
private int[] intStack;
private Object[] objStack;
private Player localPlayer;
@SuppressWarnings("unchecked")
private static <T> T mockInterface(Class<T> iface, java.lang.reflect.InvocationHandler handler)
{
return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class<?>[]{iface}, handler);
}
private MessageNode createMessageNode(int id, ChatMessageType type, String name, String value, int timestamp)
{
final String[] valRef = new String[]{value};
final int[] tsRef = new int[]{timestamp};
return mockInterface(MessageNode.class, (proxy, method, args) ->
{
switch (method.getName())
{
case "getId":
return id;
case "getType":
return type;
case "getName":
return name;
case "getValue":
return valRef[0];
case "setValue":
valRef[0] = (String) args[0];
return null;
case "getTimestamp":
return tsRef[0];
case "setTimestamp":
tsRef[0] = (Integer) args[0];
return null;
case "equals":
return proxy == args[0];
case "hashCode":
return System.identityHashCode(proxy);
default:
return null;
}
});
}
private ChatLineBuffer createChatLineBuffer(List<MessageNode> initialLines)
{
List<MessageNode> list = new ArrayList<>(initialLines);
return mockInterface(ChatLineBuffer.class, (proxy, method, args) ->
{
switch (method.getName())
{
case "getLines":
return list.toArray(new MessageNode[0]);
case "getLength":
return list.size();
case "removeMessageNode":
list.remove(args[0]);
return null;
default:
return null;
}
});
}
@Before
public void setup()
{
chatLineMap.clear();
scriptsRun.clear();
configValues.clear();
messagesMap.clear();
// Default config values
configValues.put("chatHelper", true);
configValues.put("chatAutoClear", true);
configValues.put("chatAutoClearTime", 60);
configValues.put("chatClearGame", true);
configValues.put("chatClearPublic", true);
configValues.put("chatClearPrivate", false);
configValues.put("chatClearClan", false);
configValues.put("chatCollapseDuplicates", true);
configValues.put("chatFilterSpam", true);
configValues.put("chatHighlightName", true);
configValues.put("chatHighlightColor", new Color(0xFFD700));
configValues.put("chatCopyToClipboard", true);
config = mockInterface(EssentialsPlusConfig.class, (proxy, method, args) ->
{
if (configValues.containsKey(method.getName()))
{
return configValues.get(method.getName());
}
return null;
});
localPlayer = mockInterface(Player.class, (proxy, method, args) ->
{
if ("getName".equals(method.getName()))
{
return "Zezima";
}
return null;
});
clientThread = new ClientThread()
{
@Override
public void invoke(Runnable r)
{
r.run();
}
@Override
public void invokeLater(Runnable r)
{
r.run();
}
};
client = mockInterface(Client.class, (proxy, method, args) ->
{
switch (method.getName())
{
case "getChatLineMap":
return chatLineMap;
case "runScript":
scriptsRun.add((Object[]) args);
return null;
case "getLocalPlayer":
return localPlayer;
case "getIntStack":
return intStack;
case "getIntStackSize":
return intStack != null ? intStack.length : 0;
case "getObjectStack":
return objStack;
case "getObjectStackSize":
return objStack != null ? objStack.length : 0;
case "getMessages":
return mockInterface(IterableHashTable.class, (p2, m2, a2) ->
{
if ("get".equals(m2.getName()))
{
return messagesMap.get((Long) a2[0]);
}
return null;
});
default:
return null;
}
});
chatHelper = new ChatHelper(client, config, clientThread);
}
@Test
public void testAutoClearOldMessages()
{
int now = (int) Instant.now().getEpochSecond();
MessageNode oldNode = createMessageNode(1, ChatMessageType.GAMEMESSAGE, "", "You cut a willow log.", now - 65);
MessageNode recentNode = createMessageNode(2, ChatMessageType.GAMEMESSAGE, "", "You cut an oak log.", now - 10);
List<MessageNode> bufferList = new ArrayList<>();
bufferList.add(oldNode);
bufferList.add(recentNode);
chatLineMap.put(0, createChatLineBuffer(bufferList));
chatHelper.onGameTick(new GameTick());
ChatLineBuffer buf = chatLineMap.get(0);
Assert.assertEquals(1, buf.getLines().length);
Assert.assertEquals("You cut an oak log.", buf.getLines()[0].getValue());
Assert.assertFalse(scriptsRun.isEmpty());
}
@Test
public void testPrivateMessageProtection()
{
int now = (int) Instant.now().getEpochSecond();
MessageNode pmNode = createMessageNode(1, ChatMessageType.PRIVATECHAT, "Friend", "Meet me at GE", now - 120);
List<MessageNode> bufferList = new ArrayList<>();
bufferList.add(pmNode);
chatLineMap.put(1, createChatLineBuffer(bufferList));
chatHelper.onGameTick(new GameTick());
// Private messages should NOT be cleared because chatClearPrivate is false
ChatLineBuffer buf = chatLineMap.get(1);
Assert.assertEquals(1, buf.getLines().length);
Assert.assertEquals("Meet me at GE", buf.getLines()[0].getValue());
}
@Test
public void testSpamFilter()
{
intStack = new int[]{1, ChatMessageType.GAMEMESSAGE.getType(), 10};
objStack = new Object[]{"Nothing interesting happens."};
ScriptCallbackEvent event = new ScriptCallbackEvent();
event.setEventName("chatFilterCheck");
chatHelper.onScriptCallbackEvent(event);
// intStack[0] (display flag) should be set to 0 (hidden)
Assert.assertEquals(0, intStack[0]);
}
@Test
public void testNameHighlight()
{
intStack = new int[]{1, ChatMessageType.PUBLICCHAT.getType(), 20};
objStack = new Object[]{"Hey Zezima where are you going?"};
ScriptCallbackEvent event = new ScriptCallbackEvent();
event.setEventName("chatFilterCheck");
chatHelper.onScriptCallbackEvent(event);
String result = (String) objStack[0];
Assert.assertTrue(result.contains("<col=ffd700><b>Zezima</b></col>"));
}
@Test
public void testClearAllChat()
{
int now = (int) Instant.now().getEpochSecond();
MessageNode node1 = createMessageNode(1, ChatMessageType.GAMEMESSAGE, "", "msg1", now);
MessageNode node2 = createMessageNode(2, ChatMessageType.PUBLICCHAT, "p1", "msg2", now);
List<MessageNode> list = new ArrayList<>();
list.add(node1);
list.add(node2);
chatLineMap.put(0, createChatLineBuffer(list));
chatHelper.clearAllChat();
Assert.assertEquals(0, chatLineMap.get(0).getLines().length);
Assert.assertFalse(scriptsRun.isEmpty());
}
@Test
public void testDuplicateCollapsing()
{
MessageNode node1 = createMessageNode(101, ChatMessageType.GAMEMESSAGE, "", "You catch a trout.", 1000);
MessageNode node2 = createMessageNode(102, ChatMessageType.GAMEMESSAGE, "", "You catch a trout.", 1001);
messagesMap.put(101L, node1);
messagesMap.put(102L, node2);
ChatMessage chatEvent1 = new ChatMessage(node1, ChatMessageType.GAMEMESSAGE, "", "You catch a trout.", "", 1000);
ChatMessage chatEvent2 = new ChatMessage(node2, ChatMessageType.GAMEMESSAGE, "", "You catch a trout.", "", 1001);
chatHelper.onChatMessage(chatEvent1);
chatHelper.onChatMessage(chatEvent2);
// Check earlier duplicate
intStack = new int[]{1, ChatMessageType.GAMEMESSAGE.getType(), 101};
objStack = new Object[]{"You catch a trout."};
ScriptCallbackEvent sc1 = new ScriptCallbackEvent();
sc1.setEventName("chatFilterCheck");
chatHelper.onScriptCallbackEvent(sc1);
Assert.assertEquals(0, intStack[0]);
// Check latest duplicate
intStack = new int[]{1, ChatMessageType.GAMEMESSAGE.getType(), 102};
objStack = new Object[]{"You catch a trout."};
ScriptCallbackEvent sc2 = new ScriptCallbackEvent();
sc2.setEventName("chatFilterCheck");
chatHelper.onScriptCallbackEvent(sc2);
Assert.assertEquals(1, intStack[0]);
Assert.assertTrue(((String) objStack[0]).contains("(x2)"));
}
}