Repository: runelite/runelite
Pull request #18530 (2024-11-15 21:27:55)

gpu: regions: add KBD lair
Reported by felanbird.


**Before:**
![kbd lair before](https://github.com/user-attachments/assets/0a589431-c22f-468f-a7b5-6be8293ee4d1)


**After:**
![kbd lair after](https://github.com/user-attachments/assets/82a7490d-2709-40ad-84c2-7e84fea487e2)

Getting there (probably not needed for this one but including for reference:
*Bring an antidote++, super antifire, and anti-dragon shield or dragonfire shield and use Protect From Magic*
- Burning Amulet to Lava Maze
- Open the gate to the north and climb down the ladder
- Pull the lever
From there, run right all the way to the corner. That's where the before and after screenshots were taken.



Patch:
diff --git a/cache/pom.xml b/cache/pom.xml
index a310cc295..cf6fc6bdf 100644
--- a/cache/pom.xml
+++ b/cache/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.44-SNAPSHOT</version>
+		<version>1.10.45-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cache</artifactId>
diff --git a/pom.xml b/pom.xml
index ee8c05fdf..74f1280f1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
 
 	<groupId>net.runelite</groupId>
 	<artifactId>runelite-parent</artifactId>
-	<version>1.10.44-SNAPSHOT</version>
+	<version>1.10.45-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>RuneLite</name>
@@ -47,7 +47,7 @@
 		<checkstyle.skip>true</checkstyle.skip>
 		<glslang.path></glslang.path>
 
-		<rs.version>226</rs.version>
+		<rs.version>227</rs.version>
 	</properties>
 
 	<licenses>
diff --git a/runelite-api/pom.xml b/runelite-api/pom.xml
index 1b7f63e0a..cac4a2e4a 100644
--- a/runelite-api/pom.xml
+++ b/runelite-api/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.44-SNAPSHOT</version>
+		<version>1.10.45-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-api</artifactId>
diff --git a/runelite-api/src/main/interfaces/interfaces.toml b/runelite-api/src/main/interfaces/interfaces.toml
index 21875dca4..bf4187ea5 100644
--- a/runelite-api/src/main/interfaces/interfaces.toml
+++ b/runelite-api/src/main/interfaces/interfaces.toml
@@ -756,7 +756,6 @@ id=128
 
 [seed_vault]
 id=631
-inventory_item_container=1
 title_container=2
 item_container=15
 item_text=16
@@ -764,6 +763,7 @@ search_button=24
 
 [seed_vault_inventory]
 id=630
+item_container=1
 
 [settings]
 id=134
diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml
index 1894f2888..8cf12015c 100644
--- a/runelite-client/pom.xml
+++ b/runelite-client/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.44-SNAPSHOT</version>
+		<version>1.10.45-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>client</artifactId>
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsService.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsService.java
index 02de76962..48301c221 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsService.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsService.java
@@ -42,9 +42,9 @@ public interface BankTagsService
 	 */
 	int OPTION_ALLOW_MODIFICATIONS = 0x1;
 	/**
-	 * Option to hide the tag name from the "Remove-tag" menu option.
+	 * Option to hide the tag name.
 	 */
-	int OPTION_HIDE_REMOVE_TAG_NAME = 0x2;
+	int OPTION_HIDE_TAG_NAME = 0x2;
 	/**
 	 * Option to prevent {@link #openBankTag(String, int)} from performing a layout
 	 */
@@ -58,7 +58,7 @@ public interface BankTagsService
 	 * @param tag the tag name
 	 * @param options
 	 * @see #OPTION_ALLOW_MODIFICATIONS
-	 * @see #OPTION_HIDE_REMOVE_TAG_NAME
+	 * @see #OPTION_HIDE_TAG_NAME
 	 */
 	void openBankTag(String tag, int options);
 
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
index 78bcc6270..a948073a3 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
@@ -802,7 +802,7 @@ public class TabInterface
 					.setParam0(event.getActionParam0())
 					.setParam1(event.getActionParam1())
 					.setTarget(event.getTarget())
-					.setOption(!hidden && (activeOptions & BankTagsService.OPTION_HIDE_REMOVE_TAG_NAME) == 0 ? REMOVE_TAG + " (" + activeTag + ")" : REMOVE_TAG)
+					.setOption(!hidden && (activeOptions & BankTagsService.OPTION_HIDE_TAG_NAME) == 0 ? REMOVE_TAG + " (" + activeTag + ")" : REMOVE_TAG)
 					.setType(MenuAction.RUNELITE)
 					.setIdentifier(event.getIdentifier())
 					.setItemId(event.getItemId())
@@ -840,7 +840,8 @@ public class TabInterface
 		{
 			createMenuEntry(event, TAG_INVENTORY, event.getTarget());
 
-			if (activeTag != null && (activeOptions & BankTagsService.OPTION_ALLOW_MODIFICATIONS) != 0)
+			if (activeTag != null && !tagManager.isHidden(activeTag)
+				&& (activeOptions & (BankTagsService.OPTION_ALLOW_MODIFICATIONS | BankTagsService.OPTION_HIDE_TAG_NAME)) == BankTagsService.OPTION_ALLOW_MODIFICATIONS)
 			{
 				createMenuEntry(event, TAG_INVENTORY, ColorUtil.wrapWithColorTag(activeTag, HILIGHT_COLOR));
 			}
@@ -850,7 +851,8 @@ public class TabInterface
 		{
 			createMenuEntry(event, TAG_GEAR, event.getTarget());
 
-			if (activeTag != null && (activeOptions & BankTagsService.OPTION_ALLOW_MODIFICATIONS) != 0)
+			if (activeTag != null && !tagManager.isHidden(activeTag)
+				&& (activeOptions & (BankTagsService.OPTION_ALLOW_MODIFICATIONS | BankTagsService.OPTION_HIDE_TAG_NAME)) == BankTagsService.OPTION_ALLOW_MODIFICATIONS)
 			{
 				createMenuEntry(event, TAG_GEAR, ColorUtil.wrapWithColorTag(activeTag, HILIGHT_COLOR));
 			}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java
index 88596b367..ae46e3c79 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java
@@ -122,9 +122,10 @@ public class ChatCommandsPlugin extends Plugin
 	private static final Pattern HS_KC_FLOOR_PATTERN = Pattern.compile("You have completed Floor (\\d) of the Hallowed Sepulchre! Total completions: <col=ff0000>([0-9,]+)</col>\\.");
 	private static final Pattern HS_KC_GHC_PATTERN = Pattern.compile("You have opened the Grand Hallowed Coffin <col=ff0000>([0-9,]+)</col> times?!");
 	private static final Pattern COLLECTION_LOG_ITEM_PATTERN = Pattern.compile("New item added to your collection log: (.*)");
-	private static final Pattern GUARDIANS_OF_THE_RIFT_PATTERN = Pattern.compile("Amount of Rifts you have closed: <col=ff0000>([0-9,]+)</col>.", Pattern.CASE_INSENSITIVE);
-	private static final Pattern HUNTER_RUMOUR_KC_PATTERN = Pattern.compile("You have completed <col=[0-9a-f]{6}>([0-9,]+)</col> rumours? for the Hunter Guild.");
-	private static final Pattern BIRD_EGG_OFFERING_PATTERN = Pattern.compile("You have made <col=ff0000>(?<kc>[\\d,]+|one)</col> offerings?.");
+	private static final Pattern GUARDIANS_OF_THE_RIFT_PATTERN = Pattern.compile("Amount of Rifts you have closed: <col=ff0000>([0-9,]+)</col>\\.", Pattern.CASE_INSENSITIVE);
+	private static final Pattern HUNTER_RUMOUR_KC_PATTERN = Pattern.compile("You have completed <col=[0-9a-f]{6}>([0-9,]+)</col> rumours? for the Hunter Guild\\.");
+	private static final Pattern BIRD_EGG_OFFERING_PATTERN = Pattern.compile("You have made <col=ff0000>(?<kc>[\\d,]+|one)</col> offerings?\\.");
+	private static final Pattern CHEST_OPENING_PATTERN = Pattern.compile("You have (?<never>never )?opened (the )?(?<chest>crystal chest|Larran's big chest|Larran's small chest|Brimstone chest)( (?<kc>[\\d,]+ times|once))?\\.");
 
 	private static final String TOTAL_LEVEL_COMMAND_STRING = "!total";
 	private static final String PRICE_COMMAND_STRING = "!price";
@@ -643,6 +644,27 @@ public class ChatCommandsPlugin extends Plugin
 
 			setKc("Bird's egg offerings", kc);
 		}
+
+		matcher = CHEST_OPENING_PATTERN.matcher(message);
+		if (matcher.find())
+		{
+			int kc;
+			if (matcher.group("never") != null)
+			{
+				kc = 0;
+			}
+			else
+			{
+				String kcString = matcher.group("kc");
+				kc = kcString.equals("once")
+					? 1
+					: Integer.parseInt(kcString.split(" ")[0].replace(",", ""));
+			}
+
+			String chest = matcher.group("chest");
+
+			setKc(chest, kc);
+		}
 	}
 
 	@VisibleForTesting
@@ -2704,6 +2726,22 @@ public class ChatCommandsPlugin extends Plugin
 			case "huey":
 				return "Hueycoatl";
 
+			case "crystal chest":
+				return "crystal chest";
+
+			case "larran small chest":
+			case "larran's small chest":
+				return "Larran's small chest";
+
+			case "larran chest":
+			case "larran's chest":
+			case "larran big chest":
+			case "larran's big chest":
+				return "Larran's big chest";
+
+			case "brimstone chest":
+				return "Brimstone chest";
+
 			default:
 				return WordUtils.capitalize(boss);
 		}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CrypticClue.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CrypticClue.java
index 94521a1ab..d94ebcf8a 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CrypticClue.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CrypticClue.java
@@ -1722,7 +1722,7 @@ public class CrypticClue extends ClueScroll implements NpcClueScroll, ObjectClue
 		CrypticClue.builder()
 			.text("Dig outside the bank on Aldarin.")
 			.location(new WorldPoint(1390, 2926, 0))
-			.solution("Dig outside the bank on Aldarin.")
+			.solution("Dig outside the bank on Aldarin. Fairy ring CKQ.")
 			.build()
 	);
 
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuPlugin.java
index 1b9cfed96..fed853ed6 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuPlugin.java
@@ -1372,7 +1372,21 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 				return;
 			}
 
-			throw ex;
+			log.error("error swapping buffers", ex);
+
+			// try to stop the plugin
+			SwingUtilities.invokeLater(() ->
+			{
+				try
+				{
+					pluginManager.stopPlugin(this);
+				}
+				catch (PluginInstantiationException ex2)
+				{
+					log.error("error stopping plugin", ex2);
+				}
+			});
+			return;
 		}
 
 		drawManager.processDrawComplete(this::screenshot);
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/SceneUploader.java b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/SceneUploader.java
index 42d3b5fe8..53d64c76f 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/SceneUploader.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/SceneUploader.java
@@ -573,10 +573,12 @@ class SceneUploader
 	{
 		int MAX_VERTEX_COUNT = 6500;
 		int MAX_DIAMETER = 6000;
+		int ZSORT_GROUP_SIZE = 1024; // was 512
+		int MAX_FACES_PER_PRIORITY = 4000; // was 2000
 
 		distances = new int[MAX_VERTEX_COUNT];
 		distanceFaceCount = new char[MAX_DIAMETER];
-		distanceToFaces = new char[MAX_DIAMETER][512];
+		distanceToFaces = new char[MAX_DIAMETER][ZSORT_GROUP_SIZE];
 
 		modelCanvasX = new float[MAX_VERTEX_COUNT];
 		modelCanvasY = new float[MAX_VERTEX_COUNT];
@@ -586,10 +588,10 @@ class SceneUploader
 		modelLocalZ = new float[MAX_VERTEX_COUNT];
 
 		numOfPriority = new int[12];
-		eq10 = new int[2000];
-		eq11 = new int[2000];
+		eq10 = new int[MAX_FACES_PER_PRIORITY];
+		eq11 = new int[MAX_FACES_PER_PRIORITY];
 		lt10 = new int[12];
-		orderedFaces = new int[12][2000];
+		orderedFaces = new int[12][MAX_FACES_PER_PRIORITY];
 	}
 
 	void releaseSortingBuffers()
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesOverlay.java
deleted file mode 100644
index 41c85827b..000000000
--- a/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesOverlay.java
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- * Copyright (c) 2018, Charlie Waters
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- *    list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- *    this list of conditions and the following disclaimer in the documentation
- *    and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package net.runelite.client.plugins.itemprices;
-
-import java.awt.Color;
-import java.awt.Dimension;
-import java.awt.Graphics2D;
-import javax.inject.Inject;
-import net.runelite.api.Client;
-import net.runelite.api.InventoryID;
-import net.runelite.api.Item;
-import net.runelite.api.ItemComposition;
-import net.runelite.api.ItemContainer;
-import net.runelite.api.ItemID;
-import net.runelite.api.MenuAction;
-import net.runelite.api.MenuEntry;
-import net.runelite.api.widgets.ComponentID;
-import net.runelite.api.widgets.InterfaceID;
-import net.runelite.api.widgets.WidgetUtil;
-import net.runelite.client.game.ItemManager;
-import net.runelite.client.ui.overlay.Overlay;
-import net.runelite.client.ui.overlay.OverlayPosition;
-import net.runelite.client.ui.overlay.tooltip.Tooltip;
-import net.runelite.client.ui.overlay.tooltip.TooltipManager;
-import net.runelite.client.util.ColorUtil;
-import net.runelite.client.util.QuantityFormatter;
-
-class ItemPricesOverlay extends Overlay
-{
-	private static final int INVENTORY_ITEM_WIDGETID = ComponentID.INVENTORY_CONTAINER;
-	private static final int BANK_INVENTORY_ITEM_WIDGETID = ComponentID.BANK_INVENTORY_ITEM_CONTAINER;
-	private static final int BANK_ITEM_WIDGETID = ComponentID.BANK_ITEM_CONTAINER;
-	private static final int EXPLORERS_RING_ITEM_WIDGETID = ComponentID.EXPLORERS_RING_INVENTORY;
-	private static final int SEED_VAULT_ITEM_WIDGETID = ComponentID.SEED_VAULT_ITEM_CONTAINER;
-	private static final int SEED_VAULT_INVENTORY_ITEM_WIDGETID = ComponentID.SEED_VAULT_INVENTORY_ITEM_CONTAINER;
-	private static final int POH_TREASURE_CHEST_INVENTORY_ITEM_WIDGETID = ComponentID.POH_TREASURE_CHEST_INV_CONTAINER;
-
-	private final Client client;
-	private final ItemPricesConfig config;
-	private final TooltipManager tooltipManager;
-	private final StringBuilder itemStringBuilder = new StringBuilder();
-
-	@Inject
-	ItemManager itemManager;
-
-	@Inject
-	ItemPricesOverlay(Client client, ItemPricesConfig config, TooltipManager tooltipManager)
-	{
-		setPosition(OverlayPosition.DYNAMIC);
-		this.client = client;
-		this.config = config;
-		this.tooltipManager = tooltipManager;
-	}
-
-	@Override
-	public Dimension render(Graphics2D graphics)
-	{
-		if (client.isMenuOpen())
-		{
-			return null;
-		}
-
-		final MenuEntry[] menuEntries = client.getMenuEntries();
-		final int last = menuEntries.length - 1;
-
-		if (last < 0)
-		{
-			return null;
-		}
-
-		final MenuEntry menuEntry = menuEntries[last];
-		final MenuAction action = menuEntry.getType();
-		final int widgetId = menuEntry.getParam1();
-		final int groupId = WidgetUtil.componentToInterface(widgetId);
-		final boolean isAlching = menuEntry.getOption().equals("Cast") && menuEntry.getTarget().contains("High Level Alchemy");
-
-		// Tooltip action type handling
-		switch (action)
-		{
-			case WIDGET_TARGET_ON_WIDGET:
-				// Check target widget is the inventory
-				if (menuEntry.getWidget().getId() != ComponentID.INVENTORY_CONTAINER)
-				{
-					break;
-				}
-				// FALLTHROUGH
-			case WIDGET_USE_ON_ITEM:
-				// Require showWhileAlching and Cast High Level Alchemy
-				if (!config.showWhileAlching() || !isAlching)
-				{
-					break;
-				}
-				// FALLTHROUGH
-			case CC_OP:
-			case ITEM_USE:
-			case ITEM_FIRST_OPTION:
-			case ITEM_SECOND_OPTION:
-			case ITEM_THIRD_OPTION:
-			case ITEM_FOURTH_OPTION:
-			case ITEM_FIFTH_OPTION:
-				addTooltip(menuEntry, isAlching, groupId);
-				break;
-			case WIDGET_TARGET:
-				// Check that this is the inventory
-				if (menuEntry.getWidget().getId() == ComponentID.INVENTORY_CONTAINER)
-				{
-					addTooltip(menuEntry, isAlching, groupId);
-				}
-		}
-
-		return null;
-	}
-
-	private void addTooltip(MenuEntry menuEntry, boolean isAlching, int groupId)
-	{
-		// Item tooltip values
-		switch (groupId)
-		{
-			case InterfaceID.EXPLORERS_RING:
-				if (!config.showWhileAlching())
-				{
-					return;
-				}
-			case InterfaceID.INVENTORY:
-			case InterfaceID.POH_TREASURE_CHEST_INV:
-				if (config.hideInventory() && (!config.showWhileAlching() || !isAlching))
-				{
-					return;
-				}
-				// FALLTHROUGH
-			case InterfaceID.BANK:
-			case InterfaceID.BANK_INVENTORY:
-			case InterfaceID.SEED_VAULT:
-			case InterfaceID.SEED_VAULT_INVENTORY:
-				// Make tooltip
-				final String text = makeValueTooltip(menuEntry);
-				if (text != null)
-				{
-					tooltipManager.add(new Tooltip(ColorUtil.prependColorTag(text, new Color(238, 238, 238))));
-				}
-		}
-	}
-
-	private String makeValueTooltip(MenuEntry menuEntry)
-	{
-		// Disabling both disables all value tooltips
-		if (!config.showGEPrice() && !config.showHAValue())
-		{
-			return null;
-		}
-
-		final int widgetId = menuEntry.getParam1();
-		ItemContainer container = null;
-
-		// Inventory item
-		if (widgetId == INVENTORY_ITEM_WIDGETID ||
-			widgetId == BANK_INVENTORY_ITEM_WIDGETID ||
-			widgetId == EXPLORERS_RING_ITEM_WIDGETID ||
-			widgetId == SEED_VAULT_INVENTORY_ITEM_WIDGETID ||
-			widgetId == POH_TREASURE_CHEST_INVENTORY_ITEM_WIDGETID)
-		{
-			container = client.getItemContainer(InventoryID.INVENTORY);
-		}
-		// Bank item
-		else if (widgetId == BANK_ITEM_WIDGETID)
-		{
-			container = client.getItemContainer(InventoryID.BANK);
-		}
-		// Seed vault item
-		else if (widgetId == SEED_VAULT_ITEM_WIDGETID)
-		{
-			container = client.getItemContainer(InventoryID.SEED_VAULT);
-		}
-
-		if (container == null)
-		{
-			return null;
-		}
-
-		// Find the item in the container to get stack size
-		final int index = menuEntry.getParam0();
-		final Item item = container.getItem(index);
-		if (item != null)
-		{
-			return getItemStackValueText(item);
-		}
-
-		return null;
-	}
-
-	private String getItemStackValueText(Item item)
-	{
-		int id = itemManager.canonicalize(item.getId());
-		int qty = item.getQuantity();
-
-		// Special case for coins and platinum tokens
-		if (id == ItemID.COINS_995)
-		{
-			return QuantityFormatter.formatNumber(qty) + " gp";
-		}
-		else if (id == ItemID.PLATINUM_TOKEN)
-		{
-			return QuantityFormatter.formatNumber(qty * 1000L) + " gp";
-		}
-
-		ItemComposition itemDef = itemManager.getItemComposition(id);
-
-		// Only check prices for things with store prices
-		if (itemDef.getPrice() <= 0)
-		{
-			return null;
-		}
-
-		int gePrice = 0;
-		int haPrice = 0;
-		int haProfit = 0;
-		final int itemHaPrice = itemDef.getHaPrice();
-
-		if (config.showGEPrice())
-		{
-			gePrice = itemManager.getItemPrice(id);
-		}
-		if (config.showHAValue())
-		{
-			haPrice = itemHaPrice;
-		}
-		if (gePrice > 0 && itemHaPrice > 0 && config.showAlchProfit())
-		{
-			haProfit = calculateHAProfit(itemHaPrice, gePrice);
-		}
-
-		if (gePrice > 0 || haPrice > 0)
-		{
-			return stackValueText(qty, gePrice, haPrice, haProfit);
-		}
-
-		return null;
-	}
-
-	private String stackValueText(int qty, int gePrice, int haValue, int haProfit)
-	{
-		if (gePrice > 0)
-		{
-			itemStringBuilder.append("GE: ")
-				.append(QuantityFormatter.quantityToStackSize((long) gePrice * qty))
-				.append(" gp");
-			if (config.showEA() && qty > 1)
-			{
-				itemStringBuilder.append(" (")
-					.append(QuantityFormatter.quantityToStackSize(gePrice))
-					.append(" ea)");
-			}
-		}
-		if (haValue > 0)
-		{
-			if (gePrice > 0)
-			{
-				itemStringBuilder.append("</br>");
-			}
-
-			itemStringBuilder.append("HA: ")
-				.append(QuantityFormatter.quantityToStackSize((long) haValue * qty))
-				.append(" gp");
-			if (config.showEA() && qty > 1)
-			{
-				itemStringBuilder.append(" (")
-					.append(QuantityFormatter.quantityToStackSize(haValue))
-					.append(" ea)");
-			}
-		}
-
-		if (haProfit != 0)
-		{
-			Color haColor = haProfitColor(haProfit);
-
-			itemStringBuilder.append("</br>");
-			itemStringBuilder.append("HA Profit: ")
-				.append(ColorUtil.wrapWithColorTag(String.valueOf((long) haProfit * qty), haColor))
-				.append(" gp");
-			if (config.showEA() && qty > 1)
-			{
-				itemStringBuilder.append(" (")
-					.append(ColorUtil.wrapWithColorTag(String.valueOf(haProfit), haColor))
-					.append(" ea)");
-			}
-		}
-
-		// Build string and reset builder
-		final String text = itemStringBuilder.toString();
-		itemStringBuilder.setLength(0);
-		return text;
-	}
-
-	private int calculateHAProfit(int haPrice, int gePrice)
-	{
-		int natureRunePrice = itemManager.getItemPrice(ItemID.NATURE_RUNE);
-		return haPrice - gePrice - natureRunePrice;
-	}
-
-	private static Color haProfitColor(int haProfit)
-	{
-		return haProfit >= 0 ? Color.GREEN : Color.RED;
-	}
-}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesPlugin.java
index 2434faade..8b38ca216 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/itemprices/ItemPricesPlugin.java
@@ -25,11 +25,27 @@
 package net.runelite.client.plugins.itemprices;
 
 import com.google.inject.Provides;
+import java.awt.Color;
 import javax.inject.Inject;
+import net.runelite.api.Client;
+import net.runelite.api.ItemComposition;
+import net.runelite.api.ItemID;
+import net.runelite.api.MenuAction;
+import net.runelite.api.MenuEntry;
+import net.runelite.api.events.BeforeRender;
+import net.runelite.api.widgets.ComponentID;
+import net.runelite.api.widgets.InterfaceID;
+import net.runelite.api.widgets.Widget;
+import net.runelite.api.widgets.WidgetUtil;
 import net.runelite.client.config.ConfigManager;
+import net.runelite.client.eventbus.Subscribe;
+import net.runelite.client.game.ItemManager;
 import net.runelite.client.plugins.Plugin;
 import net.runelite.client.plugins.PluginDescriptor;
-import net.runelite.client.ui.overlay.OverlayManager;
+import net.runelite.client.ui.overlay.tooltip.Tooltip;
+import net.runelite.client.ui.overlay.tooltip.TooltipManager;
+import net.runelite.client.util.ColorUtil;
+import net.runelite.client.util.QuantityFormatter;
 
 @PluginDescriptor(
 	name = "Item Prices",
@@ -40,10 +56,28 @@ import net.runelite.client.ui.overlay.OverlayManager;
 public class ItemPricesPlugin extends Plugin
 {
 	@Inject
-	private OverlayManager overlayManager;
+	private Client client;
 
 	@Inject
-	private ItemPricesOverlay overlay;
+	private ItemPricesConfig config;
+
+	@Inject
+	private TooltipManager tooltipManager;
+
+	@Inject
+	private ItemManager itemManager;
+
+	private final StringBuilder itemStringBuilder = new StringBuilder();
+
+	@Override
+	protected void startUp()
+	{
+	}
+
+	@Override
+	protected void shutDown()
+	{
+	}
 
 	@Provides
 	ItemPricesConfig getConfig(ConfigManager configManager)
@@ -51,15 +85,237 @@ public class ItemPricesPlugin extends Plugin
 		return configManager.getConfig(ItemPricesConfig.class);
 	}
 
-	@Override
-	protected void startUp() throws Exception
+	@Subscribe
+	public void onBeforeRender(BeforeRender event)
 	{
-		overlayManager.add(overlay);
+		if (client.isMenuOpen())
+		{
+			return;
+		}
+
+		final MenuEntry[] menuEntries = client.getMenuEntries();
+		final int last = menuEntries.length - 1;
+
+		if (last < 0)
+		{
+			return;
+		}
+
+		final MenuEntry menuEntry = menuEntries[last];
+		final MenuAction action = menuEntry.getType();
+		final int widgetId = menuEntry.getParam1();
+		final int groupId = WidgetUtil.componentToInterface(widgetId);
+		final boolean isAlching = menuEntry.getOption().equals("Cast") && menuEntry.getTarget().contains("High Level Alchemy");
+
+		// Tooltip action type handling
+		switch (action)
+		{
+			case WIDGET_TARGET_ON_WIDGET:
+				// Check target widget is the inventory
+				if (menuEntry.getWidget().getId() != ComponentID.INVENTORY_CONTAINER)
+				{
+					break;
+				}
+				// FALLTHROUGH
+			case WIDGET_USE_ON_ITEM:
+				// Require showWhileAlching and Cast High Level Alchemy
+				if (!config.showWhileAlching() || !isAlching)
+				{
+					break;
+				}
+				// FALLTHROUGH
+			case CC_OP:
+			case ITEM_USE:
+			case ITEM_FIRST_OPTION:
+			case ITEM_SECOND_OPTION:
+			case ITEM_THIRD_OPTION:
+			case ITEM_FOURTH_OPTION:
+			case ITEM_FIFTH_OPTION:
+				addTooltip(menuEntry, isAlching, groupId);
+				break;
+			case WIDGET_TARGET:
+				// Check that this is the inventory
+				if (menuEntry.getWidget().getId() == ComponentID.INVENTORY_CONTAINER)
+				{
+					addTooltip(menuEntry, isAlching, groupId);
+				}
+		}
 	}
 
-	@Override
-	protected void shutDown() throws Exception
+	private void addTooltip(MenuEntry menuEntry, boolean isAlching, int groupId)
+	{
+		// Item tooltip values
+		switch (groupId)
+		{
+			case InterfaceID.EXPLORERS_RING:
+				if (!config.showWhileAlching())
+				{
+					return;
+				}
+			case InterfaceID.INVENTORY:
+			case InterfaceID.POH_TREASURE_CHEST_INV:
+				if (config.hideInventory() && (!config.showWhileAlching() || !isAlching))
+				{
+					return;
+				}
+				// FALLTHROUGH
+			case InterfaceID.BANK:
+			case InterfaceID.BANK_INVENTORY:
+			case InterfaceID.SEED_VAULT:
+			case InterfaceID.SEED_VAULT_INVENTORY:
+				// Make tooltip
+				final String text = makeValueTooltip(menuEntry);
+				if (text != null)
+				{
+					tooltipManager.add(new Tooltip(ColorUtil.prependColorTag(text, new Color(238, 238, 238))));
+				}
+		}
+	}
+
+	private String makeValueTooltip(MenuEntry menuEntry)
+	{
+		// Disabling both disables all value tooltips
+		if (!config.showGEPrice() && !config.showHAValue())
+		{
+			return null;
+		}
+
+		final int componentId = menuEntry.getParam1();
+
+		// Inventory item
+		if (componentId == ComponentID.INVENTORY_CONTAINER ||
+			componentId == ComponentID.BANK_INVENTORY_ITEM_CONTAINER ||
+			componentId == ComponentID.EXPLORERS_RING_INVENTORY ||
+			componentId == ComponentID.SEED_VAULT_INVENTORY_ITEM_CONTAINER ||
+			componentId == ComponentID.POH_TREASURE_CHEST_INV_CONTAINER ||
+			// Bank item
+			componentId == ComponentID.BANK_ITEM_CONTAINER ||
+			// Seed vault item
+			componentId == ComponentID.SEED_VAULT_ITEM_CONTAINER
+		)
+		{
+			Widget w = menuEntry.getWidget();
+			if (w == null)
+			{
+				return null;
+			}
+
+			return getItemStackValueText(w.getItemId(), w.getItemQuantity());
+		}
+
+		return null;
+	}
+
+	private String getItemStackValueText(int id, int qty)
+	{
+		id = itemManager.canonicalize(id);
+
+		// Special case for coins and platinum tokens
+		if (id == ItemID.COINS_995)
+		{
+			return QuantityFormatter.formatNumber(qty) + " gp";
+		}
+		else if (id == ItemID.PLATINUM_TOKEN)
+		{
+			return QuantityFormatter.formatNumber(qty * 1000L) + " gp";
+		}
+
+		ItemComposition itemDef = itemManager.getItemComposition(id);
+
+		// Only check prices for things with store prices
+		if (itemDef.getPrice() <= 0)
+		{
+			return null;
+		}
+
+		int gePrice = 0;
+		int haPrice = 0;
+		int haProfit = 0;
+		final int itemHaPrice = itemDef.getHaPrice();
+
+		if (config.showGEPrice())
+		{
+			gePrice = itemManager.getItemPrice(id);
+		}
+		if (config.showHAValue())
+		{
+			haPrice = itemHaPrice;
+		}
+		if (gePrice > 0 && itemHaPrice > 0 && config.showAlchProfit())
+		{
+			haProfit = calculateHAProfit(itemHaPrice, gePrice);
+		}
+
+		if (gePrice > 0 || haPrice > 0)
+		{
+			return stackValueText(qty, gePrice, haPrice, haProfit);
+		}
+
+		return null;
+	}
+
+	private String stackValueText(int qty, int gePrice, int haValue, int haProfit)
+	{
+		if (gePrice > 0)
+		{
+			itemStringBuilder.append("GE: ")
+				.append(QuantityFormatter.quantityToStackSize((long) gePrice * qty))
+				.append(" gp");
+			if (config.showEA() && qty > 1)
+			{
+				itemStringBuilder.append(" (")
+					.append(QuantityFormatter.quantityToStackSize(gePrice))
+					.append(" ea)");
+			}
+		}
+		if (haValue > 0)
+		{
+			if (gePrice > 0)
+			{
+				itemStringBuilder.append("</br>");
+			}
+
+			itemStringBuilder.append("HA: ")
+				.append(QuantityFormatter.quantityToStackSize((long) haValue * qty))
+				.append(" gp");
+			if (config.showEA() && qty > 1)
+			{
+				itemStringBuilder.append(" (")
+					.append(QuantityFormatter.quantityToStackSize(haValue))
+					.append(" ea)");
+			}
+		}
+
+		if (haProfit != 0)
+		{
+			Color haColor = haProfitColor(haProfit);
+
+			itemStringBuilder.append("</br>");
+			itemStringBuilder.append("HA Profit: ")
+				.append(ColorUtil.wrapWithColorTag(String.valueOf((long) haProfit * qty), haColor))
+				.append(" gp");
+			if (config.showEA() && qty > 1)
+			{
+				itemStringBuilder.append(" (")
+					.append(ColorUtil.wrapWithColorTag(String.valueOf(haProfit), haColor))
+					.append(" ea)");
+			}
+		}
+
+		// Build string and reset builder
+		final String text = itemStringBuilder.toString();
+		itemStringBuilder.setLength(0);
+		return text;
+	}
+
+	private int calculateHAProfit(int haPrice, int gePrice)
+	{
+		int natureRunePrice = itemManager.getItemPrice(ItemID.NATURE_RUNE);
+		return haPrice - gePrice - natureRunePrice;
+	}
+
+	private static Color haProfitColor(int haProfit)
 	{
-		overlayManager.remove(overlay);
+		return haProfit >= 0 ? Color.GREEN : Color.RED;
 	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java
index a86d85b8a..eb4d0f292 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java
@@ -1302,13 +1302,18 @@ public class LootTrackerPlugin extends Plugin
 					case ItemID.HUNTERS_LOOT_SACK_EXPERT:
 					case ItemID.HUNTERS_LOOT_SACK_MASTER:
 						final int itemId = event.getItemId();
+						final Map<String, Integer> levels = new ImmutableMap.Builder<String, Integer>().
+							put("WOODCUTTING", client.getBoostedSkillLevel(Skill.WOODCUTTING)).
+							put("HERBLORE", client.getBoostedSkillLevel(Skill.HERBLORE)).
+							put("HUNTER", client.getBoostedSkillLevel(Skill.HUNTER)).
+							build();
 						onInvChange((((invItems, groundItems, removedItems) ->
 						{
 							int cnt = removedItems.count(itemId);
 							if (cnt > 0)
 							{
 								String name = itemManager.getItemComposition(itemId).getMembersName();
-								addLoot(name, -1, LootRecordType.EVENT, null, invItems, cnt);
+								addLoot(name, -1, LootRecordType.EVENT, levels, invItems, cnt);
 							}
 						})));
 						break;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotConfig.java
index 2c19c5c27..8869f64e1 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotConfig.java
@@ -303,11 +303,23 @@ public interface ScreenshotConfig extends Config
 		return true;
 	}
 
+	@ConfigItem(
+		keyName = "screenshotLeagueTasks",
+		name = "Screenshot league tasks",
+		description = "Take a screenshot when completing a league task.",
+		position = 21,
+		section = whatSection
+	)
+	default boolean screenshotLeagueTasks()
+	{
+		return true;
+	}
+
 	@ConfigItem(
 		keyName = "wildernessLootChest",
 		name = "Screenshot wilderness loot chest",
 		description = "Take a screenshot when opening wilderness loot chest.",
-		position = 21,
+		position = 22,
 		section = whatSection
 	)
 	default boolean screenshotWildernessLootChest()
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotPlugin.java
index b434cfdf5..8f1eddb88 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotPlugin.java
@@ -127,6 +127,8 @@ public class ScreenshotPlugin extends Plugin
 	private static final String SD_DEATHS = "Deaths";
 	private static final String SD_COMBAT_ACHIEVEMENTS = "Combat Achievements";
 	private static final String SD_WILDERNESS_LOOT_CHEST = "Wilderness Loot Chest";
+	private static final String SD_LEVELS = "Levels";
+	private static final String SD_LEAGUE_TASKS = "League Tasks";
 
 	private String clueType;
 	private Integer clueNumber;
@@ -255,7 +257,7 @@ public class ScreenshotPlugin extends Plugin
 		if (client.getWidget(ComponentID.LEVEL_UP_LEVEL) != null)
 		{
 			fileName = parseLevelUpWidget(ComponentID.LEVEL_UP_LEVEL);
-			screenshotSubDir = "Levels";
+			screenshotSubDir = SD_LEVELS;
 		}
 		else if (client.getWidget(ComponentID.DIALOG_SPRITE_TEXT) != null)
 		{
@@ -273,7 +275,7 @@ public class ScreenshotPlugin extends Plugin
 				if (config.screenshotLevels())
 				{
 					fileName = parseLevelUpWidget(ComponentID.DIALOG_SPRITE_TEXT);
-					screenshotSubDir = "Levels";
+					screenshotSubDir = SD_LEVELS;
 				}
 			}
 		}
@@ -529,8 +531,7 @@ public class ScreenshotPlugin extends Plugin
 				String skillName = StringUtils.capitalize(m.group("skill") != null ? m.group("skill") : m.group("skill99"));
 				String skillLevel = m.group("level") != null ? m.group("level") : "99";
 				String fileName = skillName + "(" + skillLevel + ")";
-				String screenshotSubDir = "Levels";
-				takeScreenshot(fileName, screenshotSubDir);
+				takeScreenshot(fileName, SD_LEVELS);
 			}
 		}
 	}
@@ -737,8 +738,12 @@ public class ScreenshotPlugin extends Plugin
 				{
 					return;
 				}
+
 				String topText = client.getVarcStrValue(VarClientStr.NOTIFICATION_TOP_TEXT);
 				String bottomText = client.getVarcStrValue(VarClientStr.NOTIFICATION_BOTTOM_TEXT);
+
+				log.debug("Notification: top: {} bottom: {}", topText, bottomText);
+
 				if (topText.equalsIgnoreCase("Collection log") && config.screenshotCollectionLogEntries())
 				{
 					String entry = Text.removeTags(bottomText).substring("New item:".length());
@@ -752,6 +757,13 @@ public class ScreenshotPlugin extends Plugin
 					String fileName = "Combat task (" + task.replaceAll("[:?]", "") + ")";
 					takeScreenshot(fileName, SD_COMBAT_ACHIEVEMENTS);
 				}
+				if (topText.equalsIgnoreCase("League Task Complete!") && config.screenshotLeagueTasks())
+				{
+					String[] s = bottomText.split("<.*?>");
+					String task = s[1];
+					String fileName = "League task (" + task.replaceAll("[:?]", "") + ")";
+					takeScreenshot(fileName, SD_LEAGUE_TASKS);
+				}
 				notificationStarted = false;
 				break;
 		}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsPlugin.java
index dccaf8422..52a00e3b0 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsPlugin.java
@@ -1167,17 +1167,20 @@ public class TimersAndBuffsPlugin extends Plugin
 
 		if (actor.getGraphic() == BIND.getGraphicId() && config.showFreezes())
 		{
-			createGameTimer(BIND);
+			freezeTimer = createGameTimer(BIND);
+			freezeTime = client.getTickCount();
 		}
 
 		if (actor.getGraphic() == SNARE.getGraphicId() && config.showFreezes())
 		{
-			createGameTimer(SNARE);
+			freezeTimer = createGameTimer(SNARE);
+			freezeTime = client.getTickCount();
 		}
 
 		if (actor.getGraphic() == ENTANGLE.getGraphicId() && config.showFreezes())
 		{
-			createGameTimer(ENTANGLE);
+			freezeTimer = createGameTimer(ENTANGLE);
+			freezeTime = client.getTickCount();
 		}
 
 		// downgrade freeze based on graphic, if at the same tick as the freeze message
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/regions/regions.txt b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/regions/regions.txt
index 0d1b87f78..10aa9b280 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/regions/regions.txt
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/regions/regions.txt
@@ -368,4 +368,8 @@ r 43 146
 
 // tunnel of chaos (what lies below)
 n
-r 49 81
\ No newline at end of file
+r 49 81
+
+// king black dragon lair
+n
+r 35 73
\ No newline at end of file
diff --git a/runelite-jshell/pom.xml b/runelite-jshell/pom.xml
index 8626938b3..8e1172885 100644
--- a/runelite-jshell/pom.xml
+++ b/runelite-jshell/pom.xml
@@ -30,7 +30,7 @@
     <parent>
         <groupId>net.runelite</groupId>
         <artifactId>runelite-parent</artifactId>
-        <version>1.10.44-SNAPSHOT</version>
+        <version>1.10.45-SNAPSHOT</version>
     </parent>
 
     <artifactId>jshell</artifactId>
diff --git a/runelite-maven-plugin/pom.xml b/runelite-maven-plugin/pom.xml
index dc770e714..8f2e96932 100644
--- a/runelite-maven-plugin/pom.xml
+++ b/runelite-maven-plugin/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.44-SNAPSHOT</version>
+		<version>1.10.45-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-maven-plugin</artifactId>


Test patch:
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/chatcommands/ChatCommandsPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/chatcommands/ChatCommandsPluginTest.java
index 50955d95d..7524fc5d6 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/chatcommands/ChatCommandsPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/chatcommands/ChatCommandsPluginTest.java
@@ -211,10 +211,7 @@ public class ChatCommandsPluginTest
 	@Test
 	public void testCorporealBeastKill()
 	{
-		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", "Your Corporeal Beast kill count is: <col=ff0000>4</col>.", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessageEvent);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "corporeal beast", 4);
+		testKillCountChatMessage("corporeal beast", "Your Corporeal Beast kill count is: <col=ff0000>4</col>.", 4);
 	}
 
 	@Test
@@ -287,55 +284,37 @@ public class ChatCommandsPluginTest
 	@Test
 	public void testWintertodt()
 	{
-		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", "Your subdued Wintertodt count is: <col=ff0000>4</col>.", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessageEvent);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "wintertodt", 4);
+		testKillCountChatMessage("wintertodt", "Your subdued Wintertodt count is: <col=ff0000>4</col>.", 4);
 	}
 
 	@Test
 	public void testKreearra()
 	{
-		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", "Your Kree'arra kill count is: <col=ff0000>4</col>.", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessageEvent);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "kree'arra", 4);
+		testKillCountChatMessage("kree'arra", "Your Kree'arra kill count is: <col=ff0000>4</col>.", 4);
 	}
 
 	@Test
 	public void testBarrows()
 	{
-		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", "Your Barrows chest count is: <col=ff0000>277</col>.", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessageEvent);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "barrows chests", 277);
+		testKillCountChatMessage("barrows chests", "Your Barrows chest count is: <col=ff0000>277</col>.", 277);
 	}
 
 	@Test
 	public void testHerbiboar()
 	{
-		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", "Your herbiboar harvest count is: <col=ff0000>4091</col>.", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessageEvent);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "herbiboar", 4091);
+		testKillCountChatMessage("herbiboar", "Your herbiboar harvest count is: <col=ff0000>4091</col>.", 4091);
 	}
 
 	@Test
 	public void testGauntlet()
 	{
-		ChatMessage gauntletMessage = new ChatMessage(null, GAMEMESSAGE, "", "Your Gauntlet completion count is: <col=ff0000>123</col>.", null, 0);
-		chatCommandsPlugin.onChatMessage(gauntletMessage);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "gauntlet", 123);
+		testKillCountChatMessage("gauntlet", "Your Gauntlet completion count is: <col=ff0000>123</col>.", 123);
 	}
 
 	@Test
 	public void testCorruptedGauntlet()
 	{
-		ChatMessage corruptedGauntletMessage = new ChatMessage(null, GAMEMESSAGE, "", "Your Corrupted Gauntlet completion count is: <col=ff0000>4729</col>.", null, 0);
-		chatCommandsPlugin.onChatMessage(corruptedGauntletMessage);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "corrupted gauntlet", 4729);
+		testKillCountChatMessage("corrupted gauntlet", "Your Corrupted Gauntlet completion count is: <col=ff0000>4729</col>.", 4729);
 	}
 
 	@Test
@@ -785,19 +764,13 @@ public class ChatCommandsPluginTest
 	@Test
 	public void testHsFloorKc()
 	{
-		ChatMessage chatMessage = new ChatMessage(null, GAMEMESSAGE, "", "You have completed Floor 5 of the Hallowed Sepulchre! Total completions: <col=ff0000>1,114</col>.", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessage);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "hallowed sepulchre floor 5", 1114);
+		testKillCountChatMessage("hallowed sepulchre floor 5", "You have completed Floor 5 of the Hallowed Sepulchre! Total completions: <col=ff0000>1,114</col>.", 1114);
 	}
 
 	@Test
 	public void testHsGhcKc()
 	{
-		ChatMessage chatMessage = new ChatMessage(null, GAMEMESSAGE, "", "You have opened the Grand Hallowed Coffin <col=ff0000>1,542</col> times!", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessage);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "hallowed sepulchre", 1542);
+		testKillCountChatMessage("hallowed sepulchre", "You have opened the Grand Hallowed Coffin <col=ff0000>1,542</col> times!", 1542);
 	}
 
 	@Test
@@ -1266,49 +1239,57 @@ public class ChatCommandsPluginTest
 	@Test
 	public void testGuardiansOfTheRift()
 	{
-		ChatMessage chatMessage = new ChatMessage(null, GAMEMESSAGE, "", "Amount of rifts you have closed: <col=ff0000>1,627</col>.", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessage);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "guardians of the rift", 1627);
+		testKillCountChatMessage("guardians of the rift", "Amount of rifts you have closed: <col=ff0000>1,627</col>.", 1627);
 	}
 
 	@Test
 	public void testBirdsEgg()
 	{
-		ChatMessage chatMessage = new ChatMessage(null, GAMEMESSAGE, "", "You have made <col=ff0000>one</col> offering.", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessage);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "bird's egg offerings", 1);
+		testKillCountChatMessage("bird's egg offerings", "You have made <col=ff0000>one</col> offering.", 1);
+		testKillCountChatMessage("bird's egg offerings", "You have made <col=ff0000>420</col> offerings.", 420);
+		testKillCountChatMessage("bird's egg offerings", "You have made <col=ff0000>10,000</col> offerings.", 10_000);
+	}
 
-		chatMessage = new ChatMessage(null, GAMEMESSAGE, "", "You have made <col=ff0000>420</col> offerings.", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessage);
+	@Test
+	public void testBrimstoneChest()
+	{
+		testKillCountChatMessage("brimstone chest", "You have opened the Brimstone chest 0 times.", 0);
+		testKillCountChatMessage("brimstone chest", "You have opened the Brimstone chest once.", 1);
+		testKillCountChatMessage("brimstone chest", "You have opened the Brimstone chest 2 times.", 2);
+		testKillCountChatMessage("brimstone chest", "You have opened the Brimstone chest 1,234 times.", 1234);
+	}
 
-		verify(configManager).setRSProfileConfiguration("killcount", "bird's egg offerings", 420);
+	@Test
+	public void testLarransChests()
+	{
+		testKillCountChatMessage("larran's small chest", "You have opened Larran's small chest 0 times.", 0);
+		testKillCountChatMessage("larran's small chest", "You have opened Larran's small chest once.", 1);
+		testKillCountChatMessage("larran's small chest", "You have opened Larran's small chest 2 times.", 2);
 
-		chatMessage = new ChatMessage(null, GAMEMESSAGE, "", "You have made <col=ff0000>10,000</col> offerings.", null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessage);
+		testKillCountChatMessage("larran's big chest", "You have opened Larran's big chest 0 times.", 0);
+		testKillCountChatMessage("larran's big chest", "You have opened Larran's big chest once.", 1);
+		testKillCountChatMessage("larran's big chest", "You have opened Larran's big chest 26,644 times.", 26644);
+	}
 
-		verify(configManager).setRSProfileConfiguration("killcount", "bird's egg offerings", 10_000);
+	@Test
+	public void testCrystalChest()
+	{
+		testKillCountChatMessage("crystal chest", "You have never opened the crystal chest.", 0);
+		testKillCountChatMessage("crystal chest", "You have opened the crystal chest once.", 1);
+		testKillCountChatMessage("crystal chest", "You have opened the crystal chest 172 times.", 172);
+		testKillCountChatMessage("crystal chest", "You have opened the crystal chest 2,015 times.", 2015);
 	}
 
 	@Test
 	public void testHunterRumours()
 	{
-		testHunterRumourChatMessage("You have completed <col=ff3045>77</col> rumours for the Hunter Guild.", 77);
+		testKillCountChatMessage("hunter rumours", "You have completed <col=ff3045>77</col> rumours for the Hunter Guild.", 77);
 		// single kc has no s.
-		testHunterRumourChatMessage("You have completed <col=ff3045>1</col> rumour for the Hunter Guild.", 1);
+		testKillCountChatMessage("hunter rumours", "You have completed <col=ff3045>1</col> rumour for the Hunter Guild.", 1);
 		// opaque chatbox has different color
-		testHunterRumourChatMessage("You have completed <col=e00a19>2</col> rumours for the Hunter Guild.", 2);
+		testKillCountChatMessage("hunter rumours", "You have completed <col=e00a19>2</col> rumours for the Hunter Guild.", 2);
 		// with comma in number
-		testHunterRumourChatMessage("You have completed <col=ff3045>1,032</col> rumours for the Hunter Guild.", 1032);
-	}
-
-	private void testHunterRumourChatMessage(String message, int kc)
-	{
-		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", message, null, 0);
-		chatCommandsPlugin.onChatMessage(chatMessageEvent);
-
-		verify(configManager).setRSProfileConfiguration("killcount", "hunter rumours", kc);
+		testKillCountChatMessage("hunter rumours", "You have completed <col=ff3045>1,032</col> rumours for the Hunter Guild.", 1032);
 	}
 
 	@Test
@@ -1398,4 +1379,12 @@ public class ChatCommandsPluginTest
 		verify(configManager).setRSProfileConfiguration("personalbest", "tombs of amascut entry mode", 20 * 60 + 31.);
 		verify(configManager).setRSProfileConfiguration("personalbest", "tombs of amascut entry mode 2 players", 20 * 60 + 31.);
 	}
+
+	private void testKillCountChatMessage(String key, String message, int kc)
+	{
+		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", message, null, 0);
+		chatCommandsPlugin.onChatMessage(chatMessageEvent);
+
+		verify(configManager).setRSProfileConfiguration("killcount", key, kc);
+	}
 }