Repository: runelite/runelite
Pull request #17765 (2024-04-20 15:08:40)

menu-entry-swapper: support teleport submenus on hunter cape
Support teleport submenus on hunter cape in the menu entry swapper plugin.

![image](https://github.com/runelite/runelite/assets/11622150/aadedcaf-7289-49eb-8473-5c4685b212fb)


Adam-: We could probably get away with the hunter cape, since it is a subset of the max cape, but I do not think we can for jewellery

![image](https://github.com/runelite/runelite/assets/309739/10cc308a-ac52-4200-93c1-3933dcccef84)

Felanbird: this needs to also update the setting description

Patch:
diff --git a/cache/pom.xml b/cache/pom.xml
index c5d52cd1b..f8ba078fa 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.28-SNAPSHOT</version>
+		<version>1.10.29-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cache</artifactId>
diff --git a/pom.xml b/pom.xml
index 0740d88f0..4c3ecc48c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
 
 	<groupId>net.runelite</groupId>
 	<artifactId>runelite-parent</artifactId>
-	<version>1.10.28-SNAPSHOT</version>
+	<version>1.10.29-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>RuneLite</name>
diff --git a/runelite-api/pom.xml b/runelite-api/pom.xml
index 3c1f02a29..2d47de52f 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.28-SNAPSHOT</version>
+		<version>1.10.29-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-api</artifactId>
diff --git a/runelite-api/src/main/java/net/runelite/api/ItemID.java b/runelite-api/src/main/java/net/runelite/api/ItemID.java
index bb5462df6..daecbfb1b 100644
--- a/runelite-api/src/main/java/net/runelite/api/ItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ItemID.java
@@ -13970,19 +13970,21 @@ public final class ItemID
 	public static final int SUNFIRE_FANATIC_ARMOUR_SET = 29424;
 	public static final int HERBALISTS_NOTES = 29427;
 	public static final int CHEFS_NOTES = 29428;
-	public static final int YOLKLORE_OF_AYASTER = 29431;
 	public static final int BOOK_OF_EGG = 29433;
 	public static final int BOOK_OF_EGG_29435 = 29435;
 	public static final int EGG_PRIEST_ROBE = 29437;
 	public static final int EGG_PRIEST_ROBE_TOP = 29439;
 	public static final int EGG_PRIEST_NECKLACE = 29441;
 	public static final int EGG_PRIEST_MITRE = 29443;
-	public static final int EASTER_EGG_MINED = 29445;
-	public static final int EGGNAPPIN_SACK = 29446;
 	public static final int IMBUED_MAGE_ARENA_CAPE = 29448;
 	public static final int ZOMBIE_PIRATE_KEY = 29449;
 	public static final int TELEPORT_ANCHORING_SCROLL = 29455;
 	public static final int ADAMANT_SEEDS = 29458;
 	public static final int WILDERNESS_AGILITY_TICKET = 29460;
+	public static final int SMALL_MEAT_POUCH_OPEN = 29462;
+	public static final int LARGE_MEAT_POUCH_OPEN = 29464;
+	public static final int SMALL_FUR_POUCH_OPEN = 29466;
+	public static final int MEDIUM_FUR_POUCH_OPEN = 29468;
+	public static final int LARGE_FUR_POUCH_OPEN = 29470;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/NpcID.java b/runelite-api/src/main/java/net/runelite/api/NpcID.java
index e53832fcc..f23363a07 100644
--- a/runelite-api/src/main/java/net/runelite/api/NpcID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NpcID.java
@@ -11286,49 +11286,6 @@ public final class NpcID
 	public static final int GUILD_HUNTER_FOX_13426 = 13426;
 	public static final int RED_SKINK = 13427;
 	public static final int RED_SKINK_13428 = 13428;
-	public static final int ANIMATED_EGG = 13429;
-	public static final int ANIMATED_EGG_13430 = 13430;
-	public static final int ANIMATED_EGG_13431 = 13431;
-	public static final int ANIMATED_EGG_13432 = 13432;
-	public static final int EASTER_BUNNY = 13433;
-	public static final int EASTER_BUNNY_13434 = 13434;
-	public static final int EASTER_BUNNY_13435 = 13435;
-	public static final int EG = 13436;
-	public static final int EGG_13437 = 13437;
-	public static final int EGGY = 13438;
-	public static final int EASTER_EGG_BIRD = 13439;
-	public static final int EASTER_EGG_BIRD_13440 = 13440;
-	public static final int EGGUS_MANUS = 13441;
-	public static final int EGGUS_MANUS_13442 = 13442;
-	public static final int EGG_PRIEST = 13443;
-	public static final int EGG_PRIEST_13444 = 13444;
-	public static final int EGG_PRIEST_13445 = 13445;
-	public static final int EGG_PRIEST_13446 = 13446;
-	public static final int EGG_PRIEST_13447 = 13447;
-	public static final int EGG_PRIEST_13448 = 13448;
-	public static final int EGG_PRIEST_13449 = 13449;
-	public static final int EGG_PRIEST_13450 = 13450;
-	public static final int EGG_PRIEST_13451 = 13451;
-	public static final int EGG_PRIEST_13452 = 13452;
-	public static final int SHIFTY_LOOKING_PRIEST = 13453;
-	public static final int PROTESTER_13454 = 13454;
-	public static final int UPSET_EASTER_EGG_HUNTER = 13455;
-	public static final int RASCALLY_CHILD = 13456;
-	public static final int DISAPPOINTED_MOTHER = 13457;
-	public static final int DISAPPOINTED_DAUGHTER = 13458;
-	public static final int DISAPPOINTED_FATHER = 13459;
-	public static final int EGG_MINE_CART = 13460;
-	public static final int EGG_MINE_CART_13461 = 13461;
-	public static final int MISBEHAVING_MINE_CART = 13462;
-	public static final int MISBEHAVING_MINE_CART_13463 = 13463;
-	public static final int MISBEHAVING_MINE_CART_13464 = 13464;
-	public static final int MISBEHAVING_MINE_CART_13465 = 13465;
-	public static final int EGG_PRIEST_13466 = 13466;
-	public static final int EGG_PRIEST_13467 = 13467;
-	public static final int EGG_PRIEST_13468 = 13468;
-	public static final int RABBIT_13469 = 13469;
-	public static final int RABBIT_13470 = 13470;
-	public static final int CHICKEN_13471 = 13471;
 	public static final int ENRAGED_BLOOD_MOON = 13485;
 	public static final int ENRAGED_BLUE_MOON = 13486;
 	public static final int ENRAGED_ECLIPSE_MOON = 13487;
diff --git a/runelite-api/src/main/java/net/runelite/api/NullItemID.java b/runelite-api/src/main/java/net/runelite/api/NullItemID.java
index b97374b48..4b666c304 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullItemID.java
@@ -15250,6 +15250,7 @@ public final class NullItemID
 	public static final int NULL_29426 = 29426;
 	public static final int NULL_29429 = 29429;
 	public static final int NULL_29430 = 29430;
+	public static final int NULL_29431 = 29431;
 	public static final int NULL_29432 = 29432;
 	public static final int NULL_29434 = 29434;
 	public static final int NULL_29436 = 29436;
@@ -15257,6 +15258,8 @@ public final class NullItemID
 	public static final int NULL_29440 = 29440;
 	public static final int NULL_29442 = 29442;
 	public static final int NULL_29444 = 29444;
+	public static final int NULL_29445 = 29445;
+	public static final int NULL_29446 = 29446;
 	public static final int NULL_29447 = 29447;
 	public static final int NULL_29450 = 29450;
 	public static final int NULL_29451 = 29451;
@@ -15267,5 +15270,10 @@ public final class NullItemID
 	public static final int NULL_29457 = 29457;
 	public static final int NULL_29459 = 29459;
 	public static final int NULL_29461 = 29461;
+	public static final int NULL_29463 = 29463;
+	public static final int NULL_29465 = 29465;
+	public static final int NULL_29467 = 29467;
+	public static final int NULL_29469 = 29469;
+	public static final int NULL_29471 = 29471;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/NullNpcID.java b/runelite-api/src/main/java/net/runelite/api/NullNpcID.java
index 2f623a39a..6b64f94e0 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullNpcID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullNpcID.java
@@ -2124,18 +2124,5 @@ public final class NullNpcID
 	public static final int NULL_13418 = 13418;
 	public static final int NULL_13419 = 13419;
 	public static final int NULL_13422 = 13422;
-	public static final int NULL_13472 = 13472;
-	public static final int NULL_13473 = 13473;
-	public static final int NULL_13474 = 13474;
-	public static final int NULL_13475 = 13475;
-	public static final int NULL_13476 = 13476;
-	public static final int NULL_13477 = 13477;
-	public static final int NULL_13478 = 13478;
-	public static final int NULL_13479 = 13479;
-	public static final int NULL_13480 = 13480;
-	public static final int NULL_13481 = 13481;
-	public static final int NULL_13482 = 13482;
-	public static final int NULL_13483 = 13483;
-	public static final int NULL_13484 = 13484;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/NullObjectID.java b/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
index ab4a330ee..b2c3c967b 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
@@ -27497,7 +27497,6 @@ public final class NullObjectID
 	public static final int NULL_53043 = 53043;
 	public static final int NULL_53044 = 53044;
 	public static final int NULL_53047 = 53047;
-	public static final int NULL_53048 = 53048;
 	public static final int NULL_53051 = 53051;
 	public static final int NULL_53052 = 53052;
 	public static final int NULL_53053 = 53053;
@@ -27585,14 +27584,17 @@ public final class NullObjectID
 	public static final int NULL_53211 = 53211;
 	public static final int NULL_53212 = 53212;
 	public static final int NULL_53213 = 53213;
-	public static final int NULL_53214 = 53214;
-	public static final int NULL_53215 = 53215;
-	public static final int NULL_53216 = 53216;
-	public static final int NULL_53217 = 53217;
-	public static final int NULL_53218 = 53218;
 	public static final int NULL_53219 = 53219;
 	public static final int NULL_53220 = 53220;
 	public static final int NULL_53221 = 53221;
 	public static final int NULL_53223 = 53223;
+	public static final int NULL_53226 = 53226;
+	public static final int NULL_53227 = 53227;
+	public static final int NULL_53228 = 53228;
+	public static final int NULL_53229 = 53229;
+	public static final int NULL_53230 = 53230;
+	public static final int NULL_53231 = 53231;
+	public static final int NULL_53232 = 53232;
+	public static final int NULL_53233 = 53233;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/ObjectID.java b/runelite-api/src/main/java/net/runelite/api/ObjectID.java
index 551b195c6..108182aaa 100644
--- a/runelite-api/src/main/java/net/runelite/api/ObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ObjectID.java
@@ -25534,7 +25534,8 @@ public final class ObjectID
 	public static final int COLLAPSED_TRAP_53024 = 53024;
 	public static final int CRYPT_STAIRCASE = 53045;
 	public static final int CRYPT_STAIRCASE_53046 = 53046;
-	public static final int UNLIT_BRAZIER = 53049;
+	public static final int UNLIT_BRAZIER = 53048;
+	public static final int UNLIT_BRAZIER_53049 = 53049;
 	public static final int BRAZIER_53050 = 53050;
 	public static final int TRAIL_OF_CHOCOLATE = 53054;
 	public static final int TRAIL_OF_CHOCOLATE_53055 = 53055;
@@ -25612,7 +25613,13 @@ public final class ObjectID
 	public static final int SKELETON_53190 = 53190;
 	public static final int SKELETON_53191 = 53191;
 	public static final int SKELETON_53192 = 53192;
+	public static final int EASTER_MINE_CART_53214 = 53214;
+	public static final int EASTER_MINE_CART_53215 = 53215;
+	public static final int EASTER_MINE_CART_53216 = 53216;
+	public static final int EASTER_MINE_CART_53217 = 53217;
+	public static final int EASTER_MINE_CART_53218 = 53218;
 	public static final int ZOMBIE_PIRATES_LOCKER = 53222;
 	public static final int AGILITY_DISPENSER = 53224;
+	public static final int SPIKES_53225 = 53225;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/TileItem.java b/runelite-api/src/main/java/net/runelite/api/TileItem.java
index 0a92f982a..3fe30f00c 100644
--- a/runelite-api/src/main/java/net/runelite/api/TileItem.java
+++ b/runelite-api/src/main/java/net/runelite/api/TileItem.java
@@ -24,11 +24,18 @@
  */
 package net.runelite.api;
 
+import org.intellij.lang.annotations.MagicConstant;
+
 /**
  * Represents an item inside an {@link ItemLayer}.
  */
 public interface TileItem extends Renderable
 {
+	int OWNERSHIP_NONE = 0;
+	int OWNERSHIP_SELF = 1;
+	int OWNERSHIP_OTHER = 2;
+	int OWNERSHIP_GROUP = 3;
+
 	/**
 	 * @return the ID of the item
 	 * @see ItemID
@@ -38,15 +45,29 @@ public interface TileItem extends Renderable
 	int getQuantity();
 
 	/**
-	 * Get the time, in server ticks, when the item despawns, relative to the spawn time.
+	 * Get the time, in server ticks, when the item becomes visible to other players
+	 * @see Client#getTickCount()
+	 * @return
+	 */
+	int getVisibleTime();
+
+	/**
+	 * Get the time, in server ticks, when the item despawns
+	 * @see Client#getTickCount()
 	 * @return
 	 */
 	int getDespawnTime();
 
 	/**
-	 * Get the time, in server ticks, when the item becomes visible to other players, relative
-	 * to the spawn time.
+	 * Get the item ownership
 	 * @return
 	 */
-	int getVisibleTime();
+	@MagicConstant(intValues = {OWNERSHIP_NONE, OWNERSHIP_SELF, OWNERSHIP_OTHER, OWNERSHIP_GROUP})
+	int getOwnership();
+
+	/**
+	 * Test whether the item is private
+	 * @return
+	 */
+	boolean isPrivate();
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java
index 65be8088b..cc14fa46d 100644
--- a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java
+++ b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java
@@ -34,7 +34,9 @@ package net.runelite.api;
  */
 public final class VarPlayer
 {
+	public static final int CANNON_STATE = 2;
 	public static final int CANNON_AMMO = 3;
+	public static final int CANNON_COORD = 4;
 	public static final int ATTACK_STYLE = 43;
 	public static final int QUEST_POINTS = 101;
 	/**
diff --git a/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java b/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java
index 707604f2c..077e004b1 100644
--- a/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java
+++ b/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java
@@ -543,4 +543,12 @@ public class WorldPoint
 	{
 		return new WorldArea(this, 1, 1);
 	}
+
+	/**
+	 * Create a WorldPoint from a packed Jagex coordinate
+	 */
+	public static WorldPoint fromCoord(int c)
+	{
+		return new WorldPoint((c >>> 14) & 0x3FFF, c & 0x3FFF, (c >>> 28) & 0x3);
+	}
 }
diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml
index aa4931d48..c29a8a6b7 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.28-SNAPSHOT</version>
+		<version>1.10.29-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>client</artifactId>
diff --git a/runelite-client/src/main/java/net/runelite/client/config/Keybind.java b/runelite-client/src/main/java/net/runelite/client/config/Keybind.java
index eba500b85..4ba1ada45 100644
--- a/runelite-client/src/main/java/net/runelite/client/config/Keybind.java
+++ b/runelite-client/src/main/java/net/runelite/client/config/Keybind.java
@@ -125,9 +125,17 @@ public class Keybind
 			keyCode = KeyEvent.VK_UNDEFINED;
 		}
 
-		if (e.getID() == KeyEvent.KEY_RELEASED && keyCode != KeyEvent.VK_UNDEFINED)
+		if (e.getID() == KeyEvent.KEY_RELEASED)
 		{
-			return this.keyCode == keyCode;
+			if (keyCode != KeyEvent.VK_UNDEFINED)
+			{
+				return this.keyCode == keyCode;
+			}
+			else
+			{
+				assert mf != null;
+				return this.keyCode == keyCode && (this.modifiers & modifiers) == this.modifiers && ((mf & this.modifiers) == mf);
+			}
 		}
 
 		if (ignoreModifiers && keyCode != KeyEvent.VK_UNDEFINED)
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java
index bb602c735..88362c9d5 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java
@@ -31,29 +31,19 @@ import java.util.List;
 import javax.inject.Inject;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
-import net.runelite.api.AnimationID;
 import net.runelite.api.ChatMessageType;
 import net.runelite.api.Client;
-import net.runelite.api.GameObject;
 import net.runelite.api.GameState;
 import net.runelite.api.InventoryID;
 import net.runelite.api.Item;
 import net.runelite.api.ItemID;
-import net.runelite.api.MenuAction;
-import net.runelite.api.ObjectID;
-import net.runelite.api.Player;
 import net.runelite.api.VarPlayer;
-import net.runelite.api.coords.LocalPoint;
 import net.runelite.api.coords.WorldArea;
 import net.runelite.api.coords.WorldPoint;
 import net.runelite.api.events.ChatMessage;
-import net.runelite.api.events.GameObjectSpawned;
 import net.runelite.api.events.GameStateChanged;
 import net.runelite.api.events.ItemContainerChanged;
-import net.runelite.api.events.MenuOptionClicked;
 import net.runelite.api.events.VarbitChanged;
-import net.runelite.api.widgets.ComponentID;
-import net.runelite.api.widgets.Widget;
 import net.runelite.client.Notifier;
 import net.runelite.client.callback.ClientThread;
 import net.runelite.client.config.ConfigManager;
@@ -77,8 +67,6 @@ public class CannonPlugin extends Plugin
 
 	private CannonCounter counter;
 	private boolean cannonBallNotificationSent;
-	private WorldPoint clickedCannonLocation;
-	private boolean firstCannonLoad;
 
 	@Getter
 	private int cballsLeft;
@@ -92,9 +80,6 @@ public class CannonPlugin extends Plugin
 	@Getter
 	private int cannonWorld = -1;
 
-	@Getter
-	private GameObject cannon;
-
 	@Getter
 	private List<WorldPoint> spotPoints = new ArrayList<>();
 
@@ -247,65 +232,40 @@ public class CannonPlugin extends Plugin
 	}
 
 	@Subscribe
-	public void onGameObjectSpawned(GameObjectSpawned event)
+	public void onVarbitChanged(VarbitChanged varbitChanged)
 	{
-		GameObject gameObject = event.getGameObject();
-
-		Player localPlayer = client.getLocalPlayer();
-		if ((gameObject.getId() == ObjectID.CANNON_BASE || gameObject.getId() == ObjectID.CANNON_BASE_43029) && !cannonPlaced)
+		if (varbitChanged.getVarpId() == VarPlayer.CANNON_AMMO)
 		{
-			if (localPlayer.getWorldLocation().distanceTo(gameObject.getWorldLocation()) <= 2
-				&& localPlayer.getAnimation() == AnimationID.BURYING_BONES)
+			int old = cballsLeft;
+			cballsLeft = varbitChanged.getValue();
+
+			if (cballsLeft > old)
 			{
-				cannonPosition = buildCannonWorldArea(gameObject.getWorldLocation());
-				cannonWorld = client.getWorld();
-				cannon = gameObject;
+				cannonBallNotificationSent = false;
 			}
-		}
-	}
-
-	@Subscribe
-	public void onMenuOptionClicked(MenuOptionClicked event)
-	{
-		if (cannonPosition != null || (event.getId() != ObjectID.DWARF_MULTICANNON && event.getId() != ObjectID.DWARF_MULTICANNON_43027))
-		{
-			return;
-		}
 
-		// Check if cannonballs are being used on the cannon
-		if (event.getMenuAction() == MenuAction.WIDGET_TARGET_ON_GAME_OBJECT
-			&& client.getSelectedWidget() != null
-			&& client.getSelectedWidget().getId() == ComponentID.INVENTORY_CONTAINER)
-		{
-			final Widget selected = client.getSelectedWidget();
-			final int itemId = selected.getItemId();
-			if (itemId != ItemID.CANNONBALL && itemId != ItemID.GRANITE_CANNONBALL)
+			if (!cannonBallNotificationSent && cballsLeft > 0 && config.lowWarningThreshold() >= cballsLeft)
 			{
-				return;
+				notifier.notify(config.showCannonNotifications(), String.format("Your cannon has %d cannon balls remaining!", cballsLeft));
+				cannonBallNotificationSent = true;
 			}
 		}
-		// Check for the Fire option being selected on the cannon.
-		else if (event.getMenuAction() != MenuAction.GAME_OBJECT_FIRST_OPTION)
+		else if (varbitChanged.getVarpId() == VarPlayer.CANNON_COORD)
 		{
-			return;
+			WorldPoint c = WorldPoint.fromCoord(varbitChanged.getValue());
+			cannonPosition = buildCannonWorldArea(c);
 		}
-
-		// Store the click location as a WorldPoint to avoid issues with scene loads
-		clickedCannonLocation = WorldPoint.fromScene(client, event.getParam0(), event.getParam1(), client.getPlane());
-		log.debug("Updated cannon location: {}", clickedCannonLocation);
-	}
-
-	@Subscribe
-	public void onVarbitChanged(VarbitChanged varbitChanged)
-	{
-		if (varbitChanged.getVarpId() == VarPlayer.CANNON_AMMO)
+		else if (varbitChanged.getVarpId() == VarPlayer.CANNON_STATE)
 		{
-			cballsLeft = varbitChanged.getValue();
+			cannonPlaced = varbitChanged.getValue() == 4;
 
-			if (!cannonBallNotificationSent && cballsLeft > 0 && config.lowWarningThreshold() >= cballsLeft)
+			if (cannonPlaced)
 			{
-				notifier.notify(config.showCannonNotifications(), String.format("Your cannon has %d cannon balls remaining!", cballsLeft));
-				cannonBallNotificationSent = true;
+				addCounter();
+			}
+			else
+			{
+				removeCounter();
 			}
 		}
 	}
@@ -320,58 +280,12 @@ public class CannonPlugin extends Plugin
 
 		if (event.getMessage().equals("You add the furnace."))
 		{
-			cannonPlaced = true;
-			addCounter();
-			firstCannonLoad = true;
-		}
-		else if (event.getMessage().contains("You pick up the cannon")
-			|| event.getMessage().contains("Your cannon has decayed. Speak to Nulodion to get a new one!")
-			|| event.getMessage().contains("Your cannon has been destroyed!"))
-		{
-			cannonPlaced = false;
-			removeCounter();
-			cannonPosition = null;
-		}
-		else if (event.getMessage().startsWith("You load the cannon with"))
-		{
-			// Set the cannon's position and object if the player's animation was interrupted during setup
-			if (cannonPosition == null && clickedCannonLocation != null)
-			{
-				// There is a window of 1 tick where the player can add the furnace, click on another cannon, and then
-				// the initial cannon load message arrives. This can cause the client to confuse the other cannon with
-				// the player's, so ignore that first message when deciding the cannon's location.
-				if (firstCannonLoad)
-				{
-					firstCannonLoad = false;
-				}
-				else
-				{
-					LocalPoint lp = LocalPoint.fromWorld(client, clickedCannonLocation);
-					if (lp != null)
-					{
-						GameObject[] objects = client.getScene().getTiles()[client.getPlane()][lp.getSceneX()][lp.getSceneY()].getGameObjects();
-						if (objects.length > 0 && client.getLocalPlayer().getWorldLocation().distanceTo(objects[0].getWorldLocation()) <= 2)
-						{
-							cannonPlaced = true;
-							cannonWorld = client.getWorld();
-							cannon = objects[0];
-							cannonPosition = buildCannonWorldArea(cannon.getWorldLocation());
-						}
-					}
-				}
-				clickedCannonLocation = null;
-			}
-
-			cannonBallNotificationSent = false;
+			cannonWorld = client.getWorld();
 		}
 		else if (event.getMessage().contains("Your cannon is out of ammo!"))
 		{
 			notifier.notify(config.showCannonNotifications(), "Your cannon is out of ammo!");
 		}
-		else if (event.getMessage().equals("This isn't your cannon!") || event.getMessage().equals("This is not your cannon."))
-		{
-			clickedCannonLocation = null;
-		}
 	}
 
 	Color getStateColor()
@@ -414,6 +328,6 @@ public class CannonPlugin extends Plugin
 
 	private static WorldArea buildCannonWorldArea(WorldPoint worldPoint)
 	{
-		return new WorldArea(worldPoint.getX() - 1, worldPoint.getY() - 1, 3, 3, worldPoint.getPlane());
+		return new WorldArea(worldPoint.getX(), worldPoint.getY(), 3, 3, worldPoint.getPlane());
 	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CoordinateClue.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CoordinateClue.java
index d50e114cc..c62e666c9 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CoordinateClue.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CoordinateClue.java
@@ -209,7 +209,7 @@ public class CoordinateClue extends ClueScroll implements LocationClueScroll
 		.put(new WorldPoint(3369, 3894, 0), new CoordinateClueInfo("Wilderness. Fountain of Rune.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
 		.put(new WorldPoint(2065, 3923, 0), new CoordinateClueInfo("Outside the western wall on Lunar Isle.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
 		.put(new WorldPoint(3188, 3933, 0), new CoordinateClueInfo("Wilderness. Resource Area.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
-		.put(new WorldPoint(2997, 3953, 0), new CoordinateClueInfo("Wilderness. Inside Agility Training Area.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
+		.put(new WorldPoint(3043, 3940, 0), new CoordinateClueInfo("Wilderness. South of Pirates' Hideout.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
 		.put(new WorldPoint(3380, 3963, 0), new CoordinateClueInfo("Wilderness. North of Volcano.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
 		.put(new WorldPoint(3051, 3736, 0), new CoordinateClueInfo("East of the Wilderness Obelisk in 28 Wilderness.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
 		.put(new WorldPoint(2316, 3814, 0), new CoordinateClueInfo("West of Neitiznot, near the bridge.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java
index 9cbfaf667..785711b46 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java
@@ -80,8 +80,6 @@ class GrandExchangeSearchPanel extends JPanel
 	/*  The error panel, this displays an error message */
 	private final PluginErrorPanel errorPanel = new PluginErrorPanel();
 
-	private final List<GrandExchangeItems> itemsList = new ArrayList<>();
-
 	@Inject
 	private GrandExchangeSearchPanel(ClientThread clientThread, ItemManager itemManager,
 		ScheduledExecutorService executor, RuneLiteConfig runeLiteConfig, GrandExchangePlugin grandExchangePlugin)
@@ -195,7 +193,7 @@ class GrandExchangeSearchPanel extends JPanel
 
 	private void processResult(List<ItemPrice> result, String lookup, boolean exactMatch)
 	{
-		itemsList.clear();
+		final List<GrandExchangeItems> itemsList = new ArrayList<>();
 
 		cardLayout.show(centerPanel, RESULTS_PANEL);
 
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java
index 44bfd0336..677fa6706 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java
@@ -27,11 +27,15 @@ package net.runelite.client.plugins.grounditems;
 import java.awt.Color;
 import java.time.Duration;
 import java.time.Instant;
-import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import lombok.Builder;
 import lombok.Data;
+import static net.runelite.api.TileItem.OWNERSHIP_GROUP;
+import static net.runelite.api.TileItem.OWNERSHIP_NONE;
+import static net.runelite.api.TileItem.OWNERSHIP_OTHER;
+import static net.runelite.api.TileItem.OWNERSHIP_SELF;
 import net.runelite.api.coords.WorldPoint;
+import org.intellij.lang.annotations.MagicConstant;
 
 @Data
 @Builder
@@ -47,13 +51,13 @@ class GroundItem
 	private int gePrice;
 	private int offset;
 	private boolean tradeable;
-	@Nonnull
-	private LootType lootType;
+	@MagicConstant(intValues = {OWNERSHIP_NONE, OWNERSHIP_SELF, OWNERSHIP_OTHER, OWNERSHIP_GROUP})
+	private int ownership;
+	private boolean isPrivate;
 	@Nullable
 	private Instant spawnTime;
 	private boolean stackable;
 	private Duration despawnTime;
-	@Nullable
 	private Duration visibleTime;
 
 	// cached values derived from config
@@ -73,7 +77,7 @@ class GroundItem
 
 	boolean isMine()
 	{
-		return lootType != LootType.UNKNOWN;
+		return ownership != OWNERSHIP_OTHER;
 	}
 
 	void reset()
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java
index 6a9c0216b..169350546 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java
@@ -363,11 +363,11 @@ public interface GroundItemsConfig extends Config
 
 	@ConfigItem(
 		keyName = "onlyShowLoot",
-		name = "Only show loot",
-		description = "Only shows drops from NPCs and players",
+		name = "Only show own items",
+		description = "Only shows items that are yours or you can pick up",
 		position = 25
 	)
-	default boolean onlyShowLoot()
+	default boolean onlyShowOwnItems()
 	{
 		return false;
 	}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java
index 5e1d1f871..38366542f 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java
@@ -168,7 +168,7 @@ public class GroundItemsOverlay extends Overlay
 		plugin.setHiddenBoxBounds(null);
 		plugin.setHighlightBoxBounds(null);
 
-		final boolean onlyShowLoot = config.onlyShowLoot();
+		final boolean onlyShowLoot = config.onlyShowOwnItems();
 		final DespawnTimerMode groundItemTimers = config.groundItemTimers();
 		final boolean outline = config.textOutline();
 
@@ -406,9 +406,8 @@ public class GroundItemsOverlay extends Overlay
 		final Instant now = Instant.now();
 		final Instant despawnTime = spawnTime.plus(groundItem.getDespawnTime());
 
-		if (groundItem.getVisibleTime() == null)
+		if (groundItem.isPrivate())
 		{
-			// if there is no visible time, it is always private
 			if (despawnTime.isAfter(now))
 			{
 				return PRIVATE_TIMER_COLOR;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java
index a6a1b1c31..253058dbf 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java
@@ -28,7 +28,6 @@ package net.runelite.client.plugins.grounditems;
 import com.google.common.base.MoreObjects;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.LoadingCache;
-import com.google.common.collect.EvictingQueue;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Table;
@@ -47,7 +46,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Queue;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -76,8 +74,6 @@ import net.runelite.api.events.ItemDespawned;
 import net.runelite.api.events.ItemQuantityChanged;
 import net.runelite.api.events.ItemSpawned;
 import net.runelite.api.events.MenuEntryAdded;
-import net.runelite.api.events.MenuOptionClicked;
-import net.runelite.api.widgets.ComponentID;
 import net.runelite.client.Notifier;
 import net.runelite.client.callback.ClientThread;
 import net.runelite.client.config.ConfigManager;
@@ -194,8 +190,6 @@ public class GroundItemsPlugin extends Plugin
 	private List<PriceHighlight> priceChecks = ImmutableList.of();
 	private LoadingCache<NamedQuantity, Boolean> highlightedItems;
 	private LoadingCache<NamedQuantity, Boolean> hiddenItems;
-	private final Queue<Integer> droppedItemQueue = EvictingQueue.create(16); // recently dropped items
-	private int lastUsedItem;
 	private final Map<WorldPoint, Lootbeam> lootbeams = new HashMap<>();
 
 	@Provides
@@ -211,7 +205,6 @@ public class GroundItemsPlugin extends Plugin
 		mouseManager.registerMouseListener(mouseAdapter);
 		keyManager.registerKeyListener(hotkeyListener);
 		executor.execute(this::reset);
-		lastUsedItem = -1;
 	}
 
 	@Override
@@ -268,7 +261,7 @@ public class GroundItemsPlugin extends Plugin
 			collectedGroundItems.put(tile.getWorldLocation(), item.getId(), groundItem);
 		}
 
-		if (!config.onlyShowLoot())
+		if (!config.onlyShowOwnItems())
 		{
 			notifyHighlightedItem(groundItem);
 		}
@@ -343,14 +336,14 @@ public class GroundItemsPlugin extends Plugin
 	public void onNpcLootReceived(NpcLootReceived npcLootReceived)
 	{
 		Collection<ItemStack> items = npcLootReceived.getItems();
-		lootReceived(items, LootType.PVM);
+		lootReceived(items);
 	}
 
 	@Subscribe
 	public void onPlayerLootReceived(PlayerLootReceived playerLootReceived)
 	{
 		Collection<ItemStack> items = playerLootReceived.getItems();
-		lootReceived(items, LootType.PVP);
+		lootReceived(items);
 	}
 
 	@Subscribe
@@ -402,7 +395,7 @@ public class GroundItemsPlugin extends Plugin
 		}).toArray(MenuEntry[]::new));
 	}
 
-	private void lootReceived(Collection<ItemStack> items, LootType lootType)
+	private void lootReceived(Collection<ItemStack> items)
 	{
 		for (ItemStack itemStack : items)
 		{
@@ -410,9 +403,7 @@ public class GroundItemsPlugin extends Plugin
 			GroundItem groundItem = collectedGroundItems.get(location, itemStack.getId());
 			if (groundItem != null)
 			{
-				groundItem.setLootType(lootType);
-
-				if (config.onlyShowLoot())
+				if (config.onlyShowOwnItems())
 				{
 					notifyHighlightedItem(groundItem);
 				}
@@ -434,8 +425,8 @@ public class GroundItemsPlugin extends Plugin
 		final ItemComposition itemComposition = itemManager.getItemComposition(itemId);
 		final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemId;
 		final int alchPrice = itemComposition.getHaPrice();
-		final boolean dropped = tile.getWorldLocation().equals(client.getLocalPlayer().getWorldLocation()) && droppedItemQueue.remove(itemId);
-		final boolean table = itemId == lastUsedItem && tile.getItemLayer().getHeight() > 0;
+		final int despawnTime = item.getDespawnTime() - client.getTickCount();
+		final int visibleTime = item.getVisibleTime() - client.getTickCount();
 
 		final GroundItem groundItem = GroundItem.builder()
 			.id(itemId)
@@ -446,11 +437,12 @@ public class GroundItemsPlugin extends Plugin
 			.haPrice(alchPrice)
 			.height(tile.getItemLayer().getHeight())
 			.tradeable(itemComposition.isTradeable())
-			.lootType(dropped ? LootType.DROPPED : (table ? LootType.TABLE : LootType.UNKNOWN))
+			.ownership(item.getOwnership())
+			.isPrivate(item.isPrivate())
 			.spawnTime(Instant.now())
 			.stackable(itemComposition.isStackable())
-			.despawnTime(Duration.of(item.getDespawnTime(), RSTimeUnit.GAME_TICKS))
-			.visibleTime(item.getVisibleTime() > 0 ? Duration.of(item.getVisibleTime(), RSTimeUnit.GAME_TICKS) : null)
+			.despawnTime(Duration.of(despawnTime, RSTimeUnit.GAME_TICKS))
+			.visibleTime(Duration.of(visibleTime, RSTimeUnit.GAME_TICKS))
 			.build();
 
 		// Update item price in case it is coins
@@ -770,24 +762,6 @@ public class GroundItemsPlugin extends Plugin
 		}
 	}
 
-	@Subscribe
-	public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked)
-	{
-		if (menuOptionClicked.isItemOp() && menuOptionClicked.getMenuOption().equals("Drop"))
-		{
-			int itemId = menuOptionClicked.getItemId();
-			// Keep a queue of recently dropped items to better detect
-			// item spawns that are drops
-			droppedItemQueue.add(itemId);
-		}
-		else if (menuOptionClicked.getMenuAction() == MenuAction.WIDGET_TARGET_ON_GAME_OBJECT
-			&& client.getSelectedWidget() != null
-			&& client.getSelectedWidget().getId() == ComponentID.INVENTORY_CONTAINER)
-		{
-			lastUsedItem = client.getSelectedWidget().getItemId();
-		}
-	}
-
 	private void handleLootbeam(WorldPoint worldPoint)
 	{
 		/*
@@ -805,7 +779,7 @@ public class GroundItemsPlugin extends Plugin
 		Collection<GroundItem> groundItems = collectedGroundItems.row(worldPoint).values();
 		for (GroundItem groundItem : groundItems)
 		{
-			if ((config.onlyShowLoot() && !groundItem.isMine()))
+			if ((config.onlyShowOwnItems() && !groundItem.isMine()))
 			{
 				continue;
 			}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java
deleted file mode 100644
index 604815732..000000000
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (c) 2019, Adam <Adam@sigterm.info>
- * 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.grounditems;
-
-enum LootType
-{
-	UNKNOWN,
-	TABLE,
-	DROPPED,
-	PVP,
-	PVM;
-}
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 5ff0061cc..3bf802401 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
@@ -181,6 +181,7 @@ public class LootTrackerPlugin extends Plugin
 		put(7827, "Dark Chest").
 		put(13117, "Rogues' Chest").
 		put(13156, "Chest (Ancient Vault)").
+		put(12348, "Muddy Chest").
 		build();
 
 	// Chests opened with keys from slayer tasks
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperConfig.java
index a41428b61..82745513e 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperConfig.java
@@ -427,7 +427,7 @@ public interface MenuEntrySwapperConfig extends Config
 	@ConfigItem(
 		keyName = "teleportSubmenus",
 		name = "Teleport submenus",
-		description = "Use submenus for max, construction, and diary cape teleports",
+		description = "Use submenus for max, construction, hunter, and diary cape teleports",
 		section = itemSection
 	)
 	default boolean teleportSubmenus()
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPlugin.java
index 554b4e189..6edea3530 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPlugin.java
@@ -2155,6 +2155,15 @@ public class MenuEntrySwapperPlugin extends Plugin
 			.addSub("Elder Gnome child", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 14))
 			.addSub("Twiggy O'Korn", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 15));
 		// endregion
+
+		// region Hunter cape
+		teleportSwap("Teleport", ItemID.HUNTER_CAPE, ItemID.HUNTER_CAPET)
+			.worn()
+			.held()
+			.addSub("Carnivorous Chinchompas", () -> pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 1))
+			.addSub("Black Chinchompas", () -> pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 2))
+			.addSub("Hunter Guild", () -> pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 3));
+		// endregion
 	}
 
 	private TeleportSwap teleportSwap(String option, int... items)
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningPlugin.java
index f9ad5c4c9..225037203 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningPlugin.java
@@ -398,7 +398,7 @@ public class MiningPlugin extends Plugin
 				case DEPLETED_VEIN_26667: // Depleted motherlode vein
 				case DEPLETED_VEIN_26668: // Depleted motherlode vein
 				{
-					WorldPoint worldPoint = new WorldPoint((locCoord >>> 14) & 0x3FFF, locCoord & 0x3FFF, (locCoord >>> 28) & 0x3);
+					WorldPoint worldPoint = WorldPoint.fromCoord(locCoord);
 					Rock rock = Rock.ORE_VEIN;
 					RockRespawn rockRespawn = new RockRespawn(rock, worldPoint, Instant.now(), ticks * Constants.GAME_TICK_LENGTH, rock.getZOffset());
 					respawns.add(rockRespawn);
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java
index 1ec3069d4..c1371b50e 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java
@@ -542,7 +542,7 @@ public class WoodcuttingPlugin extends Plugin
 				case NullObjectID.NULL_34637:
 				case NullObjectID.NULL_34639:
 				{
-					WorldPoint worldPoint = new WorldPoint((locCoord >>> 14) & 0x3FFF, locCoord & 0x3FFF, (locCoord >>> 28) & 0x3);
+					WorldPoint worldPoint = WorldPoint.fromCoord(locCoord);
 					GameObject gameObject = findObject(worldPoint);
 					if (gameObject == null)
 					{
diff --git a/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java b/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java
index 8451ca134..d7ba5d675 100644
--- a/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java
+++ b/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java
@@ -81,7 +81,7 @@ public abstract class HotkeyListener implements KeyListener
 	@Override
 	public void keyReleased(KeyEvent e)
 	{
-		if (keybind.get().matches(e))
+		if (keybind.get().matches(e) && isPressed)
 		{
 			isPressed = false;
 			isConsumingTyped = false;
diff --git a/runelite-client/src/main/resources/item_variations.json b/runelite-client/src/main/resources/item_variations.json
index d0019a5d5..2eeeffae4 100644
--- a/runelite-client/src/main/resources/item_variations.json
+++ b/runelite-client/src/main/resources/item_variations.json
@@ -2533,8 +2533,7 @@
     11028,
     11029,
     11030,
-    26926,
-    29445
+    26926
   ],
   "banana": [
     1963,
@@ -11922,6 +11921,26 @@
     29248,
     29250
   ],
+  "small meat pouch": [
+    29295,
+    29462
+  ],
+  "large meat pouch": [
+    29297,
+    29464
+  ],
+  "small fur pouch": [
+    29299,
+    29466
+  ],
+  "medium fur pouch": [
+    29301,
+    29468
+  ],
+  "large fur pouch": [
+    29303,
+    29470
+  ],
   "blessed bone statuette": [
     29338,
     29340,
diff --git a/runelite-client/src/main/scripts/ChatBuilder.hash b/runelite-client/src/main/scripts/ChatBuilder.hash
index 9b63917e4..3c0446f53 100644
--- a/runelite-client/src/main/scripts/ChatBuilder.hash
+++ b/runelite-client/src/main/scripts/ChatBuilder.hash
@@ -1,1 +1,1 @@
-4A78CE396EF7493A1FE42C4A29A706F3BA62EE65F99AB118765DD93504ABC682
\ No newline at end of file
+99EBEC97A1D42C6CE5D82295910B6E2C61629AA1D437582D25CA20F3231AEB75
\ No newline at end of file
diff --git a/runelite-client/src/main/scripts/ChatBuilder.rs2asm b/runelite-client/src/main/scripts/ChatBuilder.rs2asm
index 4e458adf9..345b071a1 100644
--- a/runelite-client/src/main/scripts/ChatBuilder.rs2asm
+++ b/runelite-client/src/main/scripts/ChatBuilder.rs2asm
@@ -415,12 +415,12 @@ LABEL393:
    iload                  10
    iconst                 -1
    if_icmpne              LABEL397
-   jump                   LABEL1946
+   jump                   LABEL1997
 LABEL397:
    iload                  9
    iconst                 -1
    if_icmpne              LABEL401
-   jump                   LABEL1946
+   jump                   LABEL1997
 LABEL401:
    iload                  10
    chat_gethistoryex_byuid
@@ -439,7 +439,7 @@ LABEL401:
    invoke                 193
    iconst                 1
    if_icmpeq              CHAT_FILTER
-   jump                   LABEL1942
+   jump                   LABEL1993
 CHAT_FILTER:
    sload                  20                ; Load the message
    iconst                 1                 ; Gets changed to 0 if message is blocked
@@ -452,7 +452,7 @@ CHAT_FILTER:
    iconst                 1                 ; 2nd half of conditional
    sstore                 20                ; Override the message with our filtered message
    if_icmpeq              LABEL419          ; Check if we are building this message
-   jump                   LABEL1942          ; continue to next message, skipping this
+   jump                   LABEL1993          ; continue to next message, skipping this
 LABEL419:
    iload                  11
    sload                  18
@@ -468,7 +468,7 @@ LABEL419:
    invoke                 90
    iconst                 1
    if_icmpeq              LABEL434
-   jump                   LABEL1942
+   jump                   LABEL1993
 LABEL434:
    iconst                 0 ; splitpmbox
    iload                  10 ; message uid
@@ -1671,15 +1671,16 @@ LABEL1509:
       106: LABEL1514
       41: LABEL1514
       44: LABEL1514
-      101: LABEL1631
-      103: LABEL1683
-      104: LABEL1683
-      110: LABEL1683
-      14: LABEL1726
-      109: LABEL1787
-      111: LABEL1830
-      112: LABEL1873
-   jump                   LABEL1916
+      0: LABEL1631
+      101: LABEL1682
+      103: LABEL1734
+      104: LABEL1734
+      110: LABEL1734
+      14: LABEL1777
+      109: LABEL1838
+      111: LABEL1881
+      112: LABEL1924
+   jump                   LABEL1967
 LABEL1514:
    sconst                 "<col=ffffff>"
    sload                  18
@@ -1815,8 +1816,64 @@ LABEL1626:
    iload                  9
    if_setop              
 LABEL1630:
-   jump                   LABEL1928
+   jump                   LABEL1979
 LABEL1631:
+   sload                  18
+   string_length         
+   iconst                 0
+   if_icmpgt              LABEL1636
+   jump                   LABEL1669
+LABEL1636:
+   sload                  0
+   sload                  18
+   removetags            
+   compare               
+   iconst                 0
+   if_icmpne              LABEL1643
+   jump                   LABEL1669
+LABEL1643:
+   iconst                 8
+   sconst                 "Report"
+   iload                  9
+   if_setop              
+   sconst                 "<col=ffffff>"
+   sload                  18
+   sconst                 "</col>"
+   join_string            3
+   iload                  9
+   if_setopbase          
+   iconst                 86
+   iconst                 -2147483644
+   sconst                 "event_opbase"
+   iload                  11
+   sconst                 "isi"
+   iload                  9
+   if_setonop            
+   iconst                 -1
+   sconst                 ""
+   iload                  9
+   if_setonmouseover     
+   iconst                 -1
+   sconst                 ""
+   iload                  9
+   if_setonmouseleave    
+   jump                   LABEL1681
+LABEL1669:
+   iconst                 -1
+   sconst                 ""
+   iload                  9
+   if_setonop            
+   iconst                 -1
+   sconst                 ""
+   iload                  9
+   if_setonmouseover     
+   iconst                 -1
+   sconst                 ""
+   iload                  9
+   if_setonmouseleave    
+LABEL1681:
+   jump                   LABEL1979
+LABEL1682:
    sconst                 "<col=ffffff>"
    sload                  18
    sconst                 "</col>"
@@ -1840,31 +1897,31 @@ LABEL1631:
    if_setonmouseleave    
    invoke                 5548
    iconst                 1
-   if_icmpeq              LABEL1656
-   jump                   LABEL1661
-LABEL1656:
+   if_icmpeq              LABEL1707
+   jump                   LABEL1712
+LABEL1707:
    iconst                 1
    sconst                 "Accept invitation"
    iload                  9
    if_setop              
-   jump                   LABEL1665
-LABEL1661:
+   jump                   LABEL1716
+LABEL1712:
    iconst                 1
    sconst                 "Accept trade"
    iload                  9
    if_setop              
-LABEL1665:
+LABEL1716:
    iload                  15
    iconst                 1
-   if_icmpeq              LABEL1669
-   jump                   LABEL1674
-LABEL1669:
+   if_icmpeq              LABEL1720
+   jump                   LABEL1725
+LABEL1720:
    iconst                 6
    sconst                 "Message"
    iload                  9
    if_setop              
-   jump                   LABEL1682
-LABEL1674:
+   jump                   LABEL1733
+LABEL1725:
    iconst                 6
    sconst                 "Add friend"
    iload                  9
@@ -1873,9 +1930,9 @@ LABEL1674:
    sconst                 "Add ignore"
    iload                  9
    if_setop              
-LABEL1682:
-   jump                   LABEL1928
-LABEL1683:
+LABEL1733:
+   jump                   LABEL1979
+LABEL1734:
    sconst                 "<col=ffffff>"
    sload                  18
    sconst                 "</col>"
@@ -1903,15 +1960,15 @@ LABEL1683:
    if_setop              
    iload                  15
    iconst                 1
-   if_icmpeq              LABEL1712
-   jump                   LABEL1717
-LABEL1712:
+   if_icmpeq              LABEL1763
+   jump                   LABEL1768
+LABEL1763:
    iconst                 6
    sconst                 "Message"
    iload                  9
    if_setop              
-   jump                   LABEL1725
-LABEL1717:
+   jump                   LABEL1776
+LABEL1768:
    iconst                 6
    sconst                 "Add friend"
    iload                  9
@@ -1920,20 +1977,20 @@ LABEL1717:
    sconst                 "Add ignore"
    iload                  9
    if_setop              
-LABEL1725:
-   jump                   LABEL1928
-LABEL1726:
+LABEL1776:
+   jump                   LABEL1979
+LABEL1777:
    sload                  21
    string_length         
    iconst                 0
-   if_icmpgt              LABEL1731
-   jump                   LABEL1760
-LABEL1731:
+   if_icmpgt              LABEL1782
+   jump                   LABEL1811
+LABEL1782:
    iload                  13
    iconst                 -1
-   if_icmpne              LABEL1735
-   jump                   LABEL1760
-LABEL1735:
+   if_icmpne              LABEL1786
+   jump                   LABEL1811
+LABEL1786:
    iconst                 6
    sconst                 "Open"
    iload                  9
@@ -1958,8 +2015,8 @@ LABEL1735:
    sconst                 "Iii"
    iload                  9
    if_setonmouseleave    
-   jump                   LABEL1768
-LABEL1760:
+   jump                   LABEL1819
+LABEL1811:
    iconst                 -1
    sconst                 ""
    iload                  9
@@ -1968,7 +2025,7 @@ LABEL1760:
    sconst                 ""
    iload                  9
    if_setonmouseleave    
-LABEL1768:
+LABEL1819:
    iconst                 9
    sconst                 "Clear history"
    iload                  9
@@ -1987,8 +2044,8 @@ LABEL1768:
    sconst                 "iisi"
    iload                  9
    if_setonop            
-   jump                   LABEL1928
-LABEL1787:
+   jump                   LABEL1979
+LABEL1838:
    sconst                 "<col=0xffffff>"
    sload                  18
    sconst                 "</col>"
@@ -2016,15 +2073,15 @@ LABEL1787:
    if_setonmouseleave    
    iload                  15
    iconst                 1
-   if_icmpeq              LABEL1816
-   jump                   LABEL1821
-LABEL1816:
+   if_icmpeq              LABEL1867
+   jump                   LABEL1872
+LABEL1867:
    iconst                 6
    sconst                 "Message"
    iload                  9
    if_setop              
-   jump                   LABEL1829
-LABEL1821:
+   jump                   LABEL1880
+LABEL1872:
    iconst                 6
    sconst                 "Add friend"
    iload                  9
@@ -2033,9 +2090,9 @@ LABEL1821:
    sconst                 "Add ignore"
    iload                  9
    if_setop              
-LABEL1829:
-   jump                   LABEL1928
-LABEL1830:
+LABEL1880:
+   jump                   LABEL1979
+LABEL1881:
    sconst                 "<col=0xffffff>"
    sload                  18
    sconst                 "</col>"
@@ -2063,15 +2120,15 @@ LABEL1830:
    if_setonmouseleave    
    iload                  15
    iconst                 1
-   if_icmpeq              LABEL1859
-   jump                   LABEL1864
-LABEL1859:
+   if_icmpeq              LABEL1910
+   jump                   LABEL1915
+LABEL1910:
    iconst                 6
    sconst                 "Message"
    iload                  9
    if_setop              
-   jump                   LABEL1872
-LABEL1864:
+   jump                   LABEL1923
+LABEL1915:
    iconst                 6
    sconst                 "Add friend"
    iload                  9
@@ -2080,9 +2137,9 @@ LABEL1864:
    sconst                 "Add ignore"
    iload                  9
    if_setop              
-LABEL1872:
-   jump                   LABEL1928
-LABEL1873:
+LABEL1923:
+   jump                   LABEL1979
+LABEL1924:
    sconst                 "<col=0xffffff>"
    sload                  18
    sconst                 "</col>"
@@ -2110,15 +2167,15 @@ LABEL1873:
    if_setonmouseleave    
    iload                  15
    iconst                 1
-   if_icmpeq              LABEL1902
-   jump                   LABEL1907
-LABEL1902:
+   if_icmpeq              LABEL1953
+   jump                   LABEL1958
+LABEL1953:
    iconst                 6
    sconst                 "Message"
    iload                  9
    if_setop              
-   jump                   LABEL1915
-LABEL1907:
+   jump                   LABEL1966
+LABEL1958:
    iconst                 6
    sconst                 "Add friend"
    iload                  9
@@ -2127,9 +2184,9 @@ LABEL1907:
    sconst                 "Add ignore"
    iload                  9
    if_setop              
-LABEL1915:
-   jump                   LABEL1928
-LABEL1916:
+LABEL1966:
+   jump                   LABEL1979
+LABEL1967:
    iconst                 -1
    sconst                 ""
    iload                  9
@@ -2142,7 +2199,7 @@ LABEL1916:
    sconst                 ""
    iload                  9
    if_setonmouseleave    
-LABEL1928:
+LABEL1979:
    iload                  6
    iload                  7
    sub                   
@@ -2157,20 +2214,20 @@ LABEL1928:
    iload                  8
    enum                  
    istore                 9
-LABEL1942:
+LABEL1993:
    iload                  10
    chat_getprevuid       
    istore                 10
    jump                   LABEL393
-LABEL1946:
+LABEL1997:
    iload                  8
    istore                 22
-LABEL1948:
+LABEL1999:
    iload                  9
    iconst                 -1
-   if_icmpne              LABEL1952
-   jump                   LABEL2035
-LABEL1952:
+   if_icmpne              LABEL2003
+   jump                   LABEL2086
+LABEL2003:
    iload                  9
    if_clearops           
    iconst                 -1
@@ -2197,14 +2254,14 @@ LABEL1952:
    multiply              
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL1980
-   jump                   LABEL1984
-LABEL1980:
+   if_icmpeq              LABEL2031
+   jump                   LABEL2035
+LABEL2031:
    sconst                 ""
    cc_settext            
    iconst                 1
    cc_sethide            
-LABEL1984:
+LABEL2035:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2213,14 +2270,14 @@ LABEL1984:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL1994
-   jump                   LABEL1998
-LABEL1994:
+   if_icmpeq              LABEL2045
+   jump                   LABEL2049
+LABEL2045:
    sconst                 ""
    cc_settext            
    iconst                 1
    cc_sethide            
-LABEL1998:
+LABEL2049:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2229,14 +2286,14 @@ LABEL1998:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2008
-   jump                   LABEL2012
-LABEL2008:
+   if_icmpeq              LABEL2059
+   jump                   LABEL2063
+LABEL2059:
    sconst                 ""
    cc_settext            
    iconst                 1
    cc_sethide            
-LABEL2012:
+LABEL2063:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2245,12 +2302,12 @@ LABEL2012:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2022
-   jump                   LABEL2024
-LABEL2022:
+   if_icmpeq              LABEL2073
+   jump                   LABEL2075
+LABEL2073:
    iconst                 1
    cc_sethide            
-LABEL2024:
+LABEL2075:
    iload                  8
    iconst                 1
    add                   
@@ -2261,8 +2318,8 @@ LABEL2024:
    iload                  8
    enum                  
    istore                 9
-   jump                   LABEL1948
-LABEL2035:
+   jump                   LABEL1999
+LABEL2086:
    iload                  6
    iconst                 2
    sub                   
@@ -2276,20 +2333,20 @@ LABEL2035:
    istore                 23
    iload                  6
    iload                  23
-   if_icmpgt              LABEL2050
-   jump                   LABEL2052
-LABEL2050:
+   if_icmpgt              LABEL2101
+   jump                   LABEL2103
+LABEL2101:
    iload                  6
    istore                 23
-LABEL2052:
+LABEL2103:
    iload                  22
    istore                 8
-LABEL2054:
+LABEL2105:
    iload                  8
    iconst                 0
-   if_icmpgt              LABEL2058
-   jump                   LABEL2141
-LABEL2058:
+   if_icmpgt              LABEL2109
+   jump                   LABEL2192
+LABEL2109:
    iload                  8
    iconst                 1
    sub                   
@@ -2320,15 +2377,15 @@ LABEL2058:
    multiply              
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2090
-   jump                   LABEL2095
-LABEL2090:
+   if_icmpeq              LABEL2141
+   jump                   LABEL2146
+LABEL2141:
    cc_getx               
    iload                  6
    iconst                 0
    iconst                 0
    cc_setposition        
-LABEL2095:
+LABEL2146:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2337,15 +2394,15 @@ LABEL2095:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2105
-   jump                   LABEL2110
-LABEL2105:
+   if_icmpeq              LABEL2156
+   jump                   LABEL2161
+LABEL2156:
    cc_getx               
    iload                  6
    iconst                 0
    iconst                 0
    cc_setposition        
-LABEL2110:
+LABEL2161:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2354,15 +2411,15 @@ LABEL2110:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2120
-   jump                   LABEL2125
-LABEL2120:
+   if_icmpeq              LABEL2171
+   jump                   LABEL2176
+LABEL2171:
    cc_getx               
    iload                  6
    iconst                 0
    iconst                 0
    cc_setposition        
-LABEL2125:
+LABEL2176:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2371,17 +2428,17 @@ LABEL2125:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2135
-   jump                   LABEL2140
-LABEL2135:
+   if_icmpeq              LABEL2186
+   jump                   LABEL2191
+LABEL2186:
    cc_getx               
    iload                  6
    iconst                 0
    iconst                 0
    cc_setposition        
-LABEL2140:
-   jump                   LABEL2054
-LABEL2141:
+LABEL2191:
+   jump                   LABEL2105
+LABEL2192:
    iconst                 0
    iload                  23
    iconst                 10616888
diff --git a/runelite-jshell/pom.xml b/runelite-jshell/pom.xml
index 58bf6ddbd..704852cdb 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.28-SNAPSHOT</version>
+        <version>1.10.29-SNAPSHOT</version>
     </parent>
 
     <artifactId>jshell</artifactId>
diff --git a/runelite-maven-plugin/pom.xml b/runelite-maven-plugin/pom.xml
index 5b8c10eab..2fb805abb 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.28-SNAPSHOT</version>
+		<version>1.10.29-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-maven-plugin</artifactId>


Test patch:
diff --git a/runelite-client/src/test/java/net/runelite/client/config/KeybindTest.java b/runelite-client/src/test/java/net/runelite/client/config/KeybindTest.java
new file mode 100644
index 000000000..14c1a34f2
--- /dev/null
+++ b/runelite-client/src/test/java/net/runelite/client/config/KeybindTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2024, Adam <Adam@sigterm.info>
+ * 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.config;
+
+import static java.awt.event.InputEvent.CTRL_DOWN_MASK;
+import static java.awt.event.InputEvent.SHIFT_DOWN_MASK;
+import java.awt.event.KeyEvent;
+import static java.awt.event.KeyEvent.KEY_PRESSED;
+import static java.awt.event.KeyEvent.KEY_RELEASED;
+import static java.awt.event.KeyEvent.VK_CONTROL;
+import static java.awt.event.KeyEvent.VK_SHIFT;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class KeybindTest
+{
+	/**
+	 * Regression test for a bug where a modifier key keybind will not match key release if other modifiers are held down.
+	 */
+	@Test
+	public void testModifierOnlyKeybindReleaseWithOtherModifierKeyHeldDown()
+	{
+		Keybind keybind = new Keybind(VK_CONTROL, CTRL_DOWN_MASK);
+
+		// when a modifier key is pressed, the keyevent will have the modifier in the modifiers field and in the keyCode field.
+		// when it is released, it will only be in the keyCode field.
+		Assert.assertTrue(keybind.matches(createKeyEvent(KEY_PRESSED, CTRL_DOWN_MASK, VK_CONTROL)));
+		Assert.assertFalse(keybind.matches(createKeyEvent(KEY_PRESSED, SHIFT_DOWN_MASK | CTRL_DOWN_MASK, VK_SHIFT)));
+		Assert.assertTrue(keybind.matches(createKeyEvent(KEY_RELEASED, SHIFT_DOWN_MASK, VK_CONTROL)));
+		Assert.assertFalse(keybind.matches(createKeyEvent(KEY_RELEASED, 0, VK_SHIFT)));
+	}
+
+	private KeyEvent createKeyEvent(int id, int modifiersEx, int extendedKeyCode)
+	{
+		KeyEvent mock = Mockito.mock(KeyEvent.class);
+		Mockito.when(mock.getExtendedKeyCode()).thenReturn(extendedKeyCode);
+		Mockito.when(mock.getModifiersEx()).thenReturn(modifiersEx);
+		Mockito.when(mock.getID()).thenReturn(id);
+		return mock;
+	}
+}
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java
index a13e29e04..47efe5541 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java
@@ -104,11 +104,11 @@ public class CannonPluginTest
 	@Test
 	public void testAmmoCountOnPlace()
 	{
-		ChatMessage chatMessage = new ChatMessage();
-		chatMessage.setType(ChatMessageType.SPAM);
-		chatMessage.setMessage("You add the furnace.");
+		VarbitChanged varbitChanged = new VarbitChanged();
+		varbitChanged.setVarpId(VarPlayer.CANNON_STATE);
+		varbitChanged.setValue(4);
 
-		plugin.onChatMessage(chatMessage);
+		plugin.onVarbitChanged(varbitChanged);
 		assertTrue(plugin.isCannonPlaced());
 
 		plugin.onVarbitChanged(cannonAmmoChanged);
@@ -126,11 +126,11 @@ public class CannonPluginTest
 	{
 		when(config.showInfobox()).thenReturn(true);
 
-		ChatMessage chatMessage = new ChatMessage();
-		chatMessage.setType(ChatMessageType.SPAM);
-		chatMessage.setMessage("You add the furnace.");
+		VarbitChanged varbitChanged = new VarbitChanged();
+		varbitChanged.setVarpId(VarPlayer.CANNON_STATE);
+		varbitChanged.setValue(4);
 
-		plugin.onChatMessage(chatMessage);
+		plugin.onVarbitChanged(varbitChanged);
 		assertTrue(plugin.isCannonPlaced());
 
 		assertEquals(0, plugin.getCballsLeft());
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginTest.java
index 6ed28b867..1c3d2e238 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginTest.java
@@ -33,7 +33,6 @@ import net.runelite.api.Client;
 import net.runelite.api.ItemComposition;
 import net.runelite.api.ItemID;
 import net.runelite.api.ItemLayer;
-import net.runelite.api.Player;
 import net.runelite.api.Tile;
 import net.runelite.api.TileItem;
 import net.runelite.api.coords.WorldPoint;
@@ -114,7 +113,6 @@ public class GroundItemsPluginTest
 			return null;
 		}).when(executor).execute(any(Runnable.class));
 
-		when(client.getLocalPlayer()).thenReturn(mock(Player.class));
 		when(config.getHiddenItems()).thenReturn("");
 		when(config.showLootbeamForHighlighted()).thenReturn(false);
 		when(config.showLootbeamTier()).thenReturn(HighlightTier.OFF);