Repository: runelite/runelite
Pull request #17787 (2024-04-24 13:54:25)

mining: add crashed star anims
add crashed star mining animations from a9feb58bce8d4d076424c462859c5bded5ce84cf to mining plugin in order for session tracking to work when doing shooting stars activity


Patch:
diff --git a/runelite-api/src/main/interfaces/interfaces.toml b/runelite-api/src/main/interfaces/interfaces.toml
index 9857d0492..5b6ff84e9 100644
--- a/runelite-api/src/main/interfaces/interfaces.toml
+++ b/runelite-api/src/main/interfaces/interfaces.toml
@@ -671,6 +671,10 @@ points_infobox=3
 [raiding_party]
 id=500
 
+[report_abuse]
+id=875
+parent=0
+
 [resizable_viewport]
 id=161
 resizable_viewport_old_school_box=15
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/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/bosstimer/Boss.java b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java
index 665ba5e93..f47b75856 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java
@@ -40,19 +40,19 @@ enum Boss
 	KRIL_TSUTSAROTH(NpcID.KRIL_TSUTSAROTH, 90, ChronoUnit.SECONDS, ItemID.PET_KRIL_TSUTSAROTH),
 	KREEARRA(NpcID.KREEARRA, 90, ChronoUnit.SECONDS, ItemID.PET_KREEARRA),
 	COMMANDER_ZILYANA(NpcID.COMMANDER_ZILYANA, 90, ChronoUnit.SECONDS, ItemID.PET_ZILYANA),
-	CALLISTO(NpcID.CALLISTO_6609, 29, RSTimeUnit.GAME_TICKS, ItemID.CALLISTO_CUB),
-	ARTIO(NpcID.ARTIO, 29, RSTimeUnit.GAME_TICKS, ItemID.CALLISTO_CUB),
-	CHAOS_ELEMENTAL(NpcID.CHAOS_ELEMENTAL, 15, ChronoUnit.SECONDS, ItemID.PET_CHAOS_ELEMENTAL),
-	CHAOS_FANATIC(NpcID.CHAOS_FANATIC, 30, ChronoUnit.SECONDS, ItemID.ANCIENT_STAFF),
+	CALLISTO(NpcID.CALLISTO_6609, 11, RSTimeUnit.GAME_TICKS, ItemID.CALLISTO_CUB),
+	ARTIO(NpcID.ARTIO, 11, RSTimeUnit.GAME_TICKS, ItemID.CALLISTO_CUB),
+	CHAOS_ELEMENTAL(NpcID.CHAOS_ELEMENTAL, 14, RSTimeUnit.GAME_TICKS, ItemID.PET_CHAOS_ELEMENTAL),
+	CHAOS_FANATIC(NpcID.CHAOS_FANATIC, 14, RSTimeUnit.GAME_TICKS, ItemID.ANCIENT_STAFF),
 	CRAZY_ARCHAEOLOGIST(NpcID.CRAZY_ARCHAEOLOGIST, 30, ChronoUnit.SECONDS, ItemID.FEDORA),
 	KING_BLACK_DRAGON(NpcID.KING_BLACK_DRAGON, 9, ChronoUnit.SECONDS, ItemID.PRINCE_BLACK_DRAGON),
-	SCORPIA(NpcID.SCORPIA, 16, RSTimeUnit.GAME_TICKS, ItemID.SCORPIAS_OFFSPRING),
+	SCORPIA(NpcID.SCORPIA, 14, RSTimeUnit.GAME_TICKS, ItemID.SCORPIAS_OFFSPRING),
 	SCURRIUS(NpcID.SCURRIUS, 29, RSTimeUnit.GAME_TICKS, ItemID.SCURRY),
 	SCURRIUS_PRIVATE(NpcID.SCURRIUS_7222, 29, RSTimeUnit.GAME_TICKS, ItemID.SCURRY),
-	VENENATIS(NpcID.VENENATIS_6610, 28, RSTimeUnit.GAME_TICKS, ItemID.VENENATIS_SPIDERLING),
-	SPINDEL(NpcID.SPINDEL, 28, RSTimeUnit.GAME_TICKS, ItemID.VENENATIS_SPIDERLING),
-	VETION(NpcID.VETION_6612, 33, RSTimeUnit.GAME_TICKS, ItemID.VETION_JR),
-	CALVARION(NpcID.CALVARION_11994, 33, RSTimeUnit.GAME_TICKS, ItemID.VETION_JR),
+	VENENATIS(NpcID.VENENATIS_6610, 10, RSTimeUnit.GAME_TICKS, ItemID.VENENATIS_SPIDERLING),
+	SPINDEL(NpcID.SPINDEL, 9, RSTimeUnit.GAME_TICKS, ItemID.VENENATIS_SPIDERLING),
+	VETION(NpcID.VETION_6612, 15, RSTimeUnit.GAME_TICKS, ItemID.VETION_JR),
+	CALVARION(NpcID.CALVARION_11994, 14, RSTimeUnit.GAME_TICKS, ItemID.VETION_JR),
 	DAGANNOTH_PRIME(NpcID.DAGANNOTH_PRIME, 90, ChronoUnit.SECONDS, ItemID.PET_DAGANNOTH_PRIME),
 	DAGANNOTH_REX(NpcID.DAGANNOTH_REX, 90, ChronoUnit.SECONDS, ItemID.PET_DAGANNOTH_REX),
 	DAGANNOTH_SUPREME(NpcID.DAGANNOTH_SUPREME, 90, ChronoUnit.SECONDS, ItemID.PET_DAGANNOTH_SUPREME),
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/chatcommands/ChatCommandsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java
index 84225fca8..2eb26c54e 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
@@ -123,6 +123,7 @@ public class ChatCommandsPlugin extends Plugin
 	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=ff3045>([0-9,]+)</col> rumours for the Hunter Guild.");
 
 	private static final String TOTAL_LEVEL_COMMAND_STRING = "!total";
 	private static final String PRICE_COMMAND_STRING = "!price";
@@ -587,6 +588,13 @@ public class ChatCommandsPlugin extends Plugin
 			setKc("Hallowed Sepulchre", kc);
 		}
 
+		matcher = HUNTER_RUMOUR_KC_PATTERN.matcher(message);
+		if (matcher.find())
+		{
+			int kc = Integer.parseInt(matcher.group(1).replaceAll(",", ""));
+			setKc("Hunter Rumours", kc);
+		}
+
 		if (lastBossKill != null && lastBossTime != client.getTickCount())
 		{
 			lastBossKill = null;
@@ -2509,6 +2517,16 @@ public class ChatCommandsPlugin extends Plugin
 			case "moons of peril":
 				return "Lunar Chest";
 
+			// hunter rumour variants
+			case "hunterrumour":
+			case "hunter contract":
+			case "hunter contracts":
+			case "hunter tasks":
+			case "hunter task":
+			case "rumours":
+			case "rumour":
+				return "Hunter Rumours";
+
 			default:
 				return WordUtils.capitalize(boss);
 		}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuIntBuffer.java b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuIntBuffer.java
index 1e85111c1..4ea829e7a 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuIntBuffer.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuIntBuffer.java
@@ -42,6 +42,14 @@ class GpuIntBuffer
 		buffer.put(x).put(y).put(z).put(c);
 	}
 
+	void put(float x, float y, float z, int w)
+	{
+		buffer.put(Float.floatToIntBits(x))
+			.put(Float.floatToIntBits(y))
+			.put(Float.floatToIntBits(z))
+			.put(w);
+	}
+
 	void flip()
 	{
 		buffer.flip();
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 4e7f7afdb..2021b9fe0 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
@@ -183,8 +183,8 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 	private int vaoUiHandle;
 	private int vboUiHandle;
 
-	private int fboSceneHandle;
-	private int rboSceneHandle;
+	private int fboScene;
+	private int rboColorBuffer;
 
 	private final GLBuffer sceneVertexBuffer = new GLBuffer("scene vertex buffer");
 	private final GLBuffer sceneUvBuffer = new GLBuffer("scene tex buffer");
@@ -286,7 +286,7 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 		{
 			try
 			{
-				fboSceneHandle = rboSceneHandle = -1; // AA FBO
+				fboScene = rboColorBuffer = -1;
 				targetBufferOffset = 0;
 				unorderedModels = smallModels = largeModels = 0;
 
@@ -465,7 +465,7 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 				shutdownProgram();
 				shutdownVao();
 				shutdownBuffers();
-				shutdownAAFbo();
+				shutdownFbo();
 			}
 
 			// this must shutdown after the clgl buffers are freed
@@ -663,11 +663,15 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 
 		GL43C.glEnableVertexAttribArray(0);
 		GL43C.glBindBuffer(GL43C.GL_ARRAY_BUFFER, tmpOutBuffer.glBufferId);
-		GL43C.glVertexAttribIPointer(0, 4, GL43C.GL_INT, 0, 0);
+		GL43C.glVertexAttribPointer(0, 3, GL43C.GL_FLOAT, false, 16, 0);
 
 		GL43C.glEnableVertexAttribArray(1);
+		GL43C.glBindBuffer(GL43C.GL_ARRAY_BUFFER, tmpOutBuffer.glBufferId);
+		GL43C.glVertexAttribIPointer(1, 1, GL43C.GL_INT, 16, 12);
+
+		GL43C.glEnableVertexAttribArray(2);
 		GL43C.glBindBuffer(GL43C.GL_ARRAY_BUFFER, tmpOutUvBuffer.glBufferId);
-		GL43C.glVertexAttribPointer(1, 4, GL43C.GL_FLOAT, false, 0, 0);
+		GL43C.glVertexAttribPointer(2, 4, GL43C.GL_FLOAT, false, 0, 0);
 
 		// Create temp VAO
 		vaoTemp = GL43C.glGenVertexArrays();
@@ -675,11 +679,15 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 
 		GL43C.glEnableVertexAttribArray(0);
 		GL43C.glBindBuffer(GL43C.GL_ARRAY_BUFFER, tmpVertexBuffer.glBufferId);
-		GL43C.glVertexAttribIPointer(0, 4, GL43C.GL_INT, 0, 0);
+		GL43C.glVertexAttribPointer(0, 3, GL43C.GL_FLOAT, false, 16, 0);
 
 		GL43C.glEnableVertexAttribArray(1);
+		GL43C.glBindBuffer(GL43C.GL_ARRAY_BUFFER, tmpVertexBuffer.glBufferId);
+		GL43C.glVertexAttribIPointer(1, 1, GL43C.GL_INT, 16, 12);
+
+		GL43C.glEnableVertexAttribArray(2);
 		GL43C.glBindBuffer(GL43C.GL_ARRAY_BUFFER, tmpUvBuffer.glBufferId);
-		GL43C.glVertexAttribPointer(1, 4, GL43C.GL_FLOAT, false, 0, 0);
+		GL43C.glVertexAttribPointer(2, 4, GL43C.GL_FLOAT, false, 0, 0);
 
 		// Create UI VAO
 		vaoUiHandle = GL43C.glGenVertexArrays();
@@ -798,22 +806,11 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 	{
 		initGlBuffer(uniformBuffer);
 
-		IntBuffer uniformBuf = GpuIntBuffer.allocateDirect(8 + 2048 * 4);
-		uniformBuf.put(new int[8]); // uniform block
-		final int[] pad = new int[2];
-		for (int i = 0; i < 2048; i++)
-		{
-			uniformBuf.put(Perspective.SINE[i]);
-			uniformBuf.put(Perspective.COSINE[i]);
-			uniformBuf.put(pad); // ivec2 alignment in std140 is 16 bytes
-		}
-		uniformBuf.flip();
-
-		updateBuffer(uniformBuffer, GL43C.GL_UNIFORM_BUFFER, uniformBuf, GL43C.GL_DYNAMIC_DRAW, CL12.CL_MEM_READ_ONLY);
+		updateBuffer(uniformBuffer, GL43C.GL_UNIFORM_BUFFER, 8 * Integer.BYTES, GL43C.GL_DYNAMIC_DRAW, CL12.CL_MEM_READ_ONLY);
 		GL43C.glBindBuffer(GL43C.GL_UNIFORM_BUFFER, 0);
 	}
 
-	private void initAAFbo(int width, int height, int aaSamples)
+	private void initFbo(int width, int height, int aaSamples)
 	{
 		final GraphicsConfiguration graphicsConfiguration = clientUI.getGraphicsConfiguration();
 		final AffineTransform transform = graphicsConfiguration.getDefaultTransform();
@@ -821,15 +818,24 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 		width = getScaledValue(transform.getScaleX(), width);
 		height = getScaledValue(transform.getScaleY(), height);
 
+		if (aaSamples > 0)
+		{
+			GL43C.glEnable(GL43C.GL_MULTISAMPLE);
+		}
+		else
+		{
+			GL43C.glDisable(GL43C.GL_MULTISAMPLE);
+		}
+
 		// Create and bind the FBO
-		fboSceneHandle = GL43C.glGenFramebuffers();
-		GL43C.glBindFramebuffer(GL43C.GL_FRAMEBUFFER, fboSceneHandle);
+		fboScene = GL43C.glGenFramebuffers();
+		GL43C.glBindFramebuffer(GL43C.GL_FRAMEBUFFER, fboScene);
 
 		// Create color render buffer
-		rboSceneHandle = GL43C.glGenRenderbuffers();
-		GL43C.glBindRenderbuffer(GL43C.GL_RENDERBUFFER, rboSceneHandle);
+		rboColorBuffer = GL43C.glGenRenderbuffers();
+		GL43C.glBindRenderbuffer(GL43C.GL_RENDERBUFFER, rboColorBuffer);
 		GL43C.glRenderbufferStorageMultisample(GL43C.GL_RENDERBUFFER, aaSamples, GL43C.GL_RGBA, width, height);
-		GL43C.glFramebufferRenderbuffer(GL43C.GL_FRAMEBUFFER, GL43C.GL_COLOR_ATTACHMENT0, GL43C.GL_RENDERBUFFER, rboSceneHandle);
+		GL43C.glFramebufferRenderbuffer(GL43C.GL_FRAMEBUFFER, GL43C.GL_COLOR_ATTACHMENT0, GL43C.GL_RENDERBUFFER, rboColorBuffer);
 
 		int status = GL43C.glCheckFramebufferStatus(GL43C.GL_FRAMEBUFFER);
 		if (status != GL43C.GL_FRAMEBUFFER_COMPLETE)
@@ -842,18 +848,18 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 		GL43C.glBindRenderbuffer(GL43C.GL_RENDERBUFFER, 0);
 	}
 
-	private void shutdownAAFbo()
+	private void shutdownFbo()
 	{
-		if (fboSceneHandle != -1)
+		if (fboScene != -1)
 		{
-			GL43C.glDeleteFramebuffers(fboSceneHandle);
-			fboSceneHandle = -1;
+			GL43C.glDeleteFramebuffers(fboScene);
+			fboScene = -1;
 		}
 
-		if (rboSceneHandle != -1)
+		if (rboColorBuffer != -1)
 		{
-			GL43C.glDeleteRenderbuffers(rboSceneHandle);
-			rboSceneHandle = -1;
+			GL43C.glDeleteRenderbuffers(rboColorBuffer);
+			rboColorBuffer = -1;
 		}
 	}
 
@@ -877,7 +883,7 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 		// viewport buffer.
 		targetBufferOffset = 0;
 
-		// UBO. Only the first 32 bytes get modified here, the rest is the constant sin/cos table.
+		// UBO.
 		// We can reuse the vertex buffer since it isn't used yet.
 		vertexBuffer.clear();
 		vertexBuffer.ensureCapacity(32);
@@ -893,8 +899,7 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 			.put(Float.floatToIntBits((float) cameraZ));
 		uniformBuf.flip();
 
-		GL43C.glBindBuffer(GL43C.GL_UNIFORM_BUFFER, uniformBuffer.glBufferId);
-		GL43C.glBufferSubData(GL43C.GL_UNIFORM_BUFFER, 0, uniformBuf);
+		updateBuffer(uniformBuffer, GL43C.GL_UNIFORM_BUFFER, uniformBuf, GL43C.GL_DYNAMIC_DRAW, CL12.CL_MEM_READ_ONLY);
 		GL43C.glBindBuffer(GL43C.GL_UNIFORM_BUFFER, 0);
 
 		GL43C.glBindBufferBase(GL43C.GL_UNIFORM_BUFFER, 0, uniformBuffer.glBufferId);
@@ -1146,14 +1151,9 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 
 		prepareInterfaceTexture(canvasWidth, canvasHeight);
 
-		// Setup anti-aliasing
-		final AntiAliasingMode antiAliasingMode = config.antiAliasingMode();
-		final boolean aaEnabled = antiAliasingMode != AntiAliasingMode.DISABLED;
-
-		if (aaEnabled)
+		// Setup FBO and anti-aliasing
 		{
-			GL43C.glEnable(GL43C.GL_MULTISAMPLE);
-
+			final AntiAliasingMode antiAliasingMode = config.antiAliasingMode();
 			final Dimension stretchedDimensions = client.getStretchedDimensions();
 
 			final int stretchedCanvasWidth = client.isStretchedEnabled() ? stretchedDimensions.width : canvasWidth;
@@ -1164,7 +1164,7 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 				|| lastStretchedCanvasHeight != stretchedCanvasHeight
 				|| lastAntiAliasingMode != antiAliasingMode)
 			{
-				shutdownAAFbo();
+				shutdownFbo();
 
 				// Bind default FBO to check whether anti-aliasing is forced
 				GL43C.glBindFramebuffer(GL43C.GL_FRAMEBUFFER, awtContext.getFramebuffer(false));
@@ -1175,22 +1175,16 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 
 				log.debug("AA samples: {}, max samples: {}, forced samples: {}", samples, maxSamples, forcedAASamples);
 
-				initAAFbo(stretchedCanvasWidth, stretchedCanvasHeight, samples);
+				initFbo(stretchedCanvasWidth, stretchedCanvasHeight, samples);
 
 				lastStretchedCanvasWidth = stretchedCanvasWidth;
 				lastStretchedCanvasHeight = stretchedCanvasHeight;
+				lastAntiAliasingMode = antiAliasingMode;
 			}
 
-			GL43C.glBindFramebuffer(GL43C.GL_DRAW_FRAMEBUFFER, fboSceneHandle);
-		}
-		else
-		{
-			GL43C.glDisable(GL43C.GL_MULTISAMPLE);
-			shutdownAAFbo();
+			GL43C.glBindFramebuffer(GL43C.GL_DRAW_FRAMEBUFFER, fboScene);
 		}
 
-		lastAntiAliasingMode = antiAliasingMode;
-
 		// Clear scene
 		int sky = client.getSkyboxColor();
 		GL43C.glClearColor((sky >> 16 & 0xFF) / 255f, (sky >> 8 & 0xFF) / 255f, (sky & 0xFF) / 255f, 1f);
@@ -1324,7 +1318,7 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 			GL43C.glUseProgram(0);
 		}
 
-		if (aaEnabled)
+		// Blit FBO
 		{
 			int width = lastStretchedCanvasWidth;
 			int height = lastStretchedCanvasHeight;
@@ -1335,7 +1329,7 @@ public class GpuPlugin extends Plugin implements DrawCallbacks
 			width = getScaledValue(transform.getScaleX(), width);
 			height = getScaledValue(transform.getScaleY(), height);
 
-			GL43C.glBindFramebuffer(GL43C.GL_READ_FRAMEBUFFER, fboSceneHandle);
+			GL43C.glBindFramebuffer(GL43C.GL_READ_FRAMEBUFFER, fboScene);
 			GL43C.glBindFramebuffer(GL43C.GL_DRAW_FRAMEBUFFER, awtContext.getFramebuffer(false));
 			GL43C.glBlitFramebuffer(0, 0, width, height, 0, 0, width, height,
 				GL43C.GL_COLOR_BUFFER_BIT, GL43C.GL_NEAREST);
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 6aaf9acca..f47b12788 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
@@ -229,7 +229,7 @@ class SceneUploader
 	}
 
 	int upload(Scene scene, SceneTilePaint tile, int tileZ, int tileX, int tileY, GpuIntBuffer vertexBuffer, GpuFloatBuffer uvBuffer,
-		int lx, int lz, boolean padUvs)
+		int lx, int lz, boolean stream)
 	{
 		tileX += GpuPlugin.SCENE_OFFSET;
 		tileY += GpuPlugin.SCENE_OFFSET;
@@ -277,15 +277,15 @@ class SceneUploader
 		final int lz3 = lz + Perspective.LOCAL_TILE_SIZE;
 		final int hsl3 = nwColor;
 
-		vertexBuffer.put(lx2, ly2, lz2, hsl2);
-		vertexBuffer.put(lx3, ly3, lz3, hsl3);
-		vertexBuffer.put(lx1, ly1, lz1, hsl1);
+		vertexBuffer.put((float) lx2, ly2, lz2, hsl2);
+		vertexBuffer.put((float) lx3, ly3, lz3, hsl3);
+		vertexBuffer.put((float) lx1, ly1, lz1, hsl1);
 
-		vertexBuffer.put(lx0, ly0, lz0, hsl0);
-		vertexBuffer.put(lx1, ly1, lz1, hsl1);
-		vertexBuffer.put(lx3, ly3, lz3, hsl3);
+		vertexBuffer.put((float) lx0, ly0, lz0, hsl0);
+		vertexBuffer.put((float) lx1, ly1, lz1, hsl1);
+		vertexBuffer.put((float) lx3, ly3, lz3, hsl3);
 
-		if (padUvs || tile.getTexture() != -1)
+		if (stream || tile.getTexture() != -1)
 		{
 			int tex = tile.getTexture() + 1;
 			if (tile.isFlat())
@@ -310,7 +310,7 @@ class SceneUploader
 	}
 
 	int upload(SceneTileModel sceneTileModel, int lx, int lz,
-		GpuIntBuffer vertexBuffer, GpuFloatBuffer uvBuffer, boolean padUvs)
+		GpuIntBuffer vertexBuffer, GpuFloatBuffer uvBuffer, boolean stream)
 	{
 		final int[] faceX = sceneTileModel.getFaceX();
 		final int[] faceY = sceneTileModel.getFaceY();
@@ -362,11 +362,11 @@ class SceneUploader
 			int ly2 = vertexY[vertex2];
 			int lz2 = vertexZ[vertex2] - lz;
 
-			vertexBuffer.put(lx0, ly0, lz0, hsl0);
-			vertexBuffer.put(lx1, ly1, lz1, hsl1);
-			vertexBuffer.put(lx2, ly2, lz2, hsl2);
+			vertexBuffer.put((float) lx0, ly0, lz0, hsl0);
+			vertexBuffer.put((float) lx1, ly1, lz1, hsl1);
+			vertexBuffer.put((float) lx2, ly2, lz2, hsl2);
 
-			if (padUvs || triangleTextures != null)
+			if (stream || triangleTextures != null)
 			{
 				if (triangleTextures != null && triangleTextures[i] != -1)
 				{
@@ -508,9 +508,9 @@ class SceneUploader
 			int triangleB = indices2[face];
 			int triangleC = indices3[face];
 
-			vertexBuffer.put(vertexX[triangleA], vertexY[triangleA], vertexZ[triangleA], packAlphaPriority | color1);
-			vertexBuffer.put(vertexX[triangleB], vertexY[triangleB], vertexZ[triangleB], packAlphaPriority | color2);
-			vertexBuffer.put(vertexX[triangleC], vertexY[triangleC], vertexZ[triangleC], packAlphaPriority | color3);
+			vertexBuffer.put((float) vertexX[triangleA], vertexY[triangleA], vertexZ[triangleA], packAlphaPriority | color1);
+			vertexBuffer.put((float) vertexX[triangleB], vertexY[triangleB], vertexZ[triangleB], packAlphaPriority | color2);
+			vertexBuffer.put((float) vertexX[triangleC], vertexY[triangleC], vertexZ[triangleC], packAlphaPriority | color3);
 
 			if (faceTextures != null)
 			{
@@ -982,9 +982,9 @@ class SceneUploader
 			}
 		}
 
-		vertexBuffer.put(modelLocalX[triangleA], modelLocalY[triangleA], modelLocalZ[triangleA], packAlphaPriority | color1);
-		vertexBuffer.put(modelLocalX[triangleB], modelLocalY[triangleB], modelLocalZ[triangleB], packAlphaPriority | color2);
-		vertexBuffer.put(modelLocalX[triangleC], modelLocalY[triangleC], modelLocalZ[triangleC], packAlphaPriority | color3);
+		vertexBuffer.put((float) modelLocalX[triangleA], modelLocalY[triangleA], modelLocalZ[triangleA], packAlphaPriority | color1);
+		vertexBuffer.put((float) modelLocalX[triangleB], modelLocalY[triangleB], modelLocalZ[triangleB], packAlphaPriority | color2);
+		vertexBuffer.put((float) modelLocalX[triangleC], modelLocalY[triangleC], modelLocalZ[triangleC], packAlphaPriority | color3);
 
 		if (faceTextures != null && faceTextures[face] != -1)
 		{
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/keyremapping/KeyRemappingPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/keyremapping/KeyRemappingPlugin.java
index 64b8c0f3a..01d83b07d 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/keyremapping/KeyRemappingPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/keyremapping/KeyRemappingPlugin.java
@@ -111,6 +111,10 @@ public class KeyRemappingPlugin extends Plugin
 		return configManager.getConfig(KeyRemappingConfig.class);
 	}
 
+	/**
+	 * Check if something other than the chatbox is accepting key input.
+	 * @return
+	 */
 	boolean chatboxFocused()
 	{
 		Widget chatboxParent = client.getWidget(ComponentID.CHATBOX_PARENT);
@@ -119,10 +123,22 @@ public class KeyRemappingPlugin extends Plugin
 			return false;
 		}
 
-		// the search box on the world map can be focused, and chat input goes there, even
-		// though the chatbox still has its key listener.
+		// If the search box on the world map is open and focused, ~keypress_permit blocks the keypress
 		Widget worldMapSearch = client.getWidget(ComponentID.WORLD_MAP_SEARCH);
-		return worldMapSearch == null || client.getVarcIntValue(VarClientInt.WORLD_MAP_SEARCH_FOCUSED) != 1;
+		if (worldMapSearch != null && client.getVarcIntValue(VarClientInt.WORLD_MAP_SEARCH_FOCUSED) == 1)
+		{
+			return false;
+		}
+
+		// The report interface blocks input due to 162:54 being hidden, however player/npc dialog and
+		// options do this too, and so we can't disable remapping just due to 162:54 being hidden.
+		Widget report = client.getWidget(ComponentID.REPORT_ABUSE_PARENT);
+		if (report != null)
+		{
+			return false;
+		}
+
+		return true;
 	}
 
 	/**
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..124777483 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
@@ -65,7 +65,6 @@ import net.runelite.api.annotations.Component;
 import net.runelite.api.events.ClientTick;
 import net.runelite.api.events.MenuEntryAdded;
 import net.runelite.api.events.MenuOpened;
-import net.runelite.api.events.PostItemComposition;
 import net.runelite.api.events.PostMenuSort;
 import net.runelite.api.widgets.ComponentID;
 import net.runelite.api.widgets.InterfaceID;
@@ -78,7 +77,6 @@ import net.runelite.client.chat.ChatMessageManager;
 import net.runelite.client.chat.QueuedMessage;
 import net.runelite.client.config.ConfigManager;
 import net.runelite.client.eventbus.Subscribe;
-import net.runelite.client.events.ConfigChanged;
 import net.runelite.client.game.ItemManager;
 import net.runelite.client.game.ItemVariationMapping;
 import net.runelite.client.game.NpcUtil;
@@ -166,6 +164,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 	private final Multimap<String, Swap> swaps = LinkedHashMultimap.create();
 	private final ArrayListMultimap<String, Integer> optionIndexes = ArrayListMultimap.create();
 	private final Multimap<Integer, TeleportSwap> teleportSwaps = HashMultimap.create();
+	private boolean lastShift, curShift;
 
 	@Provides
 	MenuEntrySwapperConfig provideConfig(ConfigManager configManager)
@@ -422,24 +421,6 @@ public class MenuEntrySwapperPlugin extends Plugin
 		swaps.put(option, new Swap(alwaysTrue(), targetPredicate, swappedOption, enabled, false));
 	}
 
-	@Subscribe
-	public void onConfigChanged(ConfigChanged event)
-	{
-		if (event.getGroup().equals(MenuEntrySwapperConfig.GROUP) && event.getKey().equals("shiftClickCustomization"))
-		{
-			clientThread.invoke(this::resetItemCompositionCache);
-		}
-		else if (event.getGroup().equals(SHIFTCLICK_CONFIG_GROUP) && event.getKey().startsWith(ITEM_KEY_PREFIX))
-		{
-			clientThread.invoke(this::resetItemCompositionCache);
-		}
-	}
-
-	private void resetItemCompositionCache()
-	{
-		client.getItemCompositionCache().reset();
-	}
-
 	private Integer getItemSwapConfig(boolean shift, int itemId)
 	{
 		itemId = ItemVariationMapping.map(itemId);
@@ -1480,36 +1461,11 @@ public class MenuEntrySwapperPlugin extends Plugin
 		final String target = Text.removeTags(menuEntry.getTarget()).toLowerCase();
 
 		final Widget w = menuEntry.getParent() != null ? menuEntry.getParent().getWidget() : menuEntry.getWidget();
-		// Custom shift-click item swap
-		if (shiftModifier() && w != null && WidgetUtil.componentToInterface(w.getId()) == InterfaceID.INVENTORY)
-		{
-			if (config.shiftClickCustomization() && !option.equals("use"))
-			{
-				Integer customOption = getItemSwapConfig(true, w.getItemId());
-
-				// Special case use shift click due to items not actually containing a "Use" option, making
-				// the client unable to perform the swap itself.
-				if (customOption != null && customOption == -1)
-				{
-					swap(menuEntries, "use", target, index, true);
-				}
-				else if (customOption != null && menuEntry.getParent() != null && menuEntry.getOption().hashCode() == customOption)
-				{
-					swap(optionIndexes, menuEntries, index, menuEntries.length - 1);
-					menuEntry.setParent(null);
-				}
-			}
-
-			// don't perform swaps on items when shift is held; instead prefer the client menu swap, which
-			// we may have overwrote
-			return;
-		}
-
-		// Custom left-click item swap
+		// Custom item swap
 		if (w != null && WidgetUtil.componentToInterface(w.getId()) == InterfaceID.INVENTORY
-			&& config.leftClickCustomization())
+			&& (lastShift ? config.shiftClickCustomization() : config.leftClickCustomization()))
 		{
-			Integer swapIndex = getItemSwapConfig(false, w.getItemId());
+			Integer swapIndex = getItemSwapConfig(lastShift, w.getItemId());
 			if (swapIndex != null)
 			{
 				if (swapIndex == -1)
@@ -1649,6 +1605,9 @@ public class MenuEntrySwapperPlugin extends Plugin
 	@Subscribe
 	public void onClientTick(ClientTick clientTick)
 	{
+		lastShift = curShift;
+		curShift = shiftModifier();
+
 		if (client.isMenuOpen())
 		{
 			return;
@@ -1764,25 +1723,6 @@ public class MenuEntrySwapperPlugin extends Plugin
 		}
 	}
 
-	@Subscribe
-	public void onPostItemComposition(PostItemComposition event)
-	{
-		if (!config.shiftClickCustomization())
-		{
-			// since shift-click is done by the client we have to check if our shift click customization is on
-			// prior to altering the item shift click action index.
-			return;
-		}
-
-		ItemComposition itemComposition = event.getItemComposition();
-		Integer option = getItemSwapConfig(true, itemComposition.getId());
-
-		if (option != null && option >= 0 && option < itemComposition.getInventoryActions().length)
-		{
-			itemComposition.setShiftClickActionIndex(option);
-		}
-	}
-
 	private boolean swap(MenuEntry[] menuEntries, String option, String target, int index, boolean strict)
 	{
 		// find option to swap with
@@ -2155,6 +2095,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..cfa9a4d25 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
@@ -35,6 +35,7 @@ import javax.inject.Inject;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.AnimationID;
 import net.runelite.api.ChatMessageType;
 import net.runelite.api.Client;
@@ -42,26 +43,9 @@ import net.runelite.api.Constants;
 import net.runelite.api.GameObject;
 import net.runelite.api.GameState;
 import net.runelite.api.HintArrowType;
-import static net.runelite.api.ObjectID.BARRONITE_ROCKS;
-import static net.runelite.api.ObjectID.BARRONITE_ROCKS_41548;
-import static net.runelite.api.ObjectID.DEPLETED_VEIN;
-import static net.runelite.api.ObjectID.DEPLETED_VEIN_26665;
-import static net.runelite.api.ObjectID.DEPLETED_VEIN_26666;
-import static net.runelite.api.ObjectID.DEPLETED_VEIN_26667;
-import static net.runelite.api.ObjectID.DEPLETED_VEIN_26668;
-import static net.runelite.api.ObjectID.EMPTY_WALL;
-import static net.runelite.api.ObjectID.GOLD_VEIN;
-import static net.runelite.api.ObjectID.GOLD_VEIN_5990;
-import static net.runelite.api.ObjectID.GOLD_VEIN_5991;
-import static net.runelite.api.ObjectID.ORE_VEIN;
-import static net.runelite.api.ObjectID.ORE_VEIN_26662;
-import static net.runelite.api.ObjectID.ORE_VEIN_26663;
-import static net.runelite.api.ObjectID.ORE_VEIN_26664;
-import static net.runelite.api.ObjectID.ROCKS_41549;
-import static net.runelite.api.ObjectID.ROCKS_41550;
+import static net.runelite.api.ObjectID.*;
 import net.runelite.api.Player;
 import net.runelite.api.ScriptID;
-import net.runelite.api.WallObject;
 import net.runelite.api.coords.WorldPoint;
 import net.runelite.api.events.AnimationChanged;
 import net.runelite.api.events.ChatMessage;
@@ -70,7 +54,6 @@ import net.runelite.api.events.GameObjectSpawned;
 import net.runelite.api.events.GameStateChanged;
 import net.runelite.api.events.GameTick;
 import net.runelite.api.events.ScriptPreFired;
-import net.runelite.api.events.WallObjectSpawned;
 import net.runelite.client.config.ConfigManager;
 import net.runelite.client.eventbus.Subscribe;
 import net.runelite.client.plugins.Plugin;
@@ -78,6 +61,7 @@ import net.runelite.client.plugins.PluginDependency;
 import net.runelite.client.plugins.PluginDescriptor;
 import net.runelite.client.plugins.xptracker.XpTrackerPlugin;
 import net.runelite.client.ui.overlay.OverlayManager;
+import net.runelite.client.util.RSTimeUnit;
 
 @PluginDescriptor(
 	name = "Mining",
@@ -86,6 +70,7 @@ import net.runelite.client.ui.overlay.OverlayManager;
 	enabledByDefault = false
 )
 @PluginDependency(XpTrackerPlugin.class)
+@Slf4j
 public class MiningPlugin extends Plugin
 {
 	private static final Pattern MINING_PATTERN = Pattern.compile(
@@ -118,7 +103,6 @@ public class MiningPlugin extends Plugin
 
 	@Getter(AccessLevel.PACKAGE)
 	private final List<RockRespawn> respawns = new ArrayList<>();
-	private boolean recentlyLoggedIn;
 
 	@Getter
 	@Nullable
@@ -151,17 +135,9 @@ public class MiningPlugin extends Plugin
 	@Subscribe
 	public void onGameStateChanged(GameStateChanged event)
 	{
-		switch (event.getGameState())
+		if (event.getGameState() == GameState.HOPPING)
 		{
-			case HOPPING:
-				respawns.clear();
-				break;
-			case LOGGED_IN:
-				// After login rocks that are depleted will be changed,
-				// so wait for the next game tick before watching for
-				// rocks to deplete
-				recentlyLoggedIn = true;
-				break;
+			respawns.clear();
 		}
 	}
 
@@ -201,7 +177,6 @@ public class MiningPlugin extends Plugin
 	public void onGameTick(GameTick gameTick)
 	{
 		clearExpiredRespawns();
-		recentlyLoggedIn = false;
 
 		if (session == null || session.getLastMined() == null)
 		{
@@ -250,29 +225,13 @@ public class MiningPlugin extends Plugin
 	@Subscribe
 	public void onGameObjectDespawned(GameObjectDespawned event)
 	{
-		if (client.getGameState() != GameState.LOGGED_IN || recentlyLoggedIn)
-		{
-			return;
-		}
-
 		final GameObject object = event.getGameObject();
-		final int region = client.getLocalPlayer().getWorldLocation().getRegionID();
 
-		Rock rock = Rock.getRock(object.getId());
-		if (rock != null)
+		if (object.getId() == DAEYALT_ESSENCE_39095)
 		{
 			final WorldPoint point = object.getWorldLocation();
-
-			if (rock == Rock.DAEYALT_ESSENCE)
-			{
-				respawns.removeIf(rockRespawn -> rockRespawn.getWorldPoint().equals(point));
-				clearHintArrowAt(point);
-			}
-			else
-			{
-				RockRespawn rockRespawn = new RockRespawn(rock, point, Instant.now(), (int) rock.getRespawnTime(region).toMillis(), rock.getZOffset());
-				respawns.add(rockRespawn);
-			}
+			respawns.removeIf(rockRespawn -> rockRespawn.getWorldPoint().equals(point));
+			clearHintArrowAt(point);
 		}
 	}
 
@@ -287,81 +246,16 @@ public class MiningPlugin extends Plugin
 	@Subscribe
 	public void onGameObjectSpawned(GameObjectSpawned event)
 	{
-		if (client.getGameState() != GameState.LOGGED_IN || recentlyLoggedIn)
-		{
-			return;
-		}
-
 		GameObject object = event.getGameObject();
-		Rock rock = Rock.getRock(object.getId());
 
 		// Inverse timer to track daeyalt essence active duration
-		if (rock == Rock.DAEYALT_ESSENCE)
+		if (object.getId() == DAEYALT_ESSENCE_39095)
 		{
-			final int region = client.getLocalPlayer().getWorldLocation().getRegionID();
-			RockRespawn rockRespawn = new RockRespawn(rock, object.getWorldLocation(), Instant.now(), (int) rock.getRespawnTime(region).toMillis(), rock.getZOffset());
+			RockRespawn rockRespawn = new RockRespawn(Rock.DAEYALT_ESSENCE, object.getWorldLocation(), Instant.now(),
+				(int) Duration.of(MiningRocksOverlay.DAEYALT_MAX_RESPAWN_TIME, RSTimeUnit.GAME_TICKS).toMillis(), Rock.DAEYALT_ESSENCE.getZOffset());
 			respawns.add(rockRespawn);
 			client.setHintArrow(object.getWorldLocation());
 		}
-		// If the Lovakite ore respawns before the timer is up, remove it
-		else if (rock == Rock.LOVAKITE || rock == Rock.CALCIFIED_ROCK)
-		{
-			final WorldPoint point = object.getWorldLocation();
-			respawns.removeIf(rockRespawn -> rockRespawn.getWorldPoint().equals(point));
-		}
-	}
-
-	@Subscribe
-	public void onWallObjectSpawned(WallObjectSpawned event)
-	{
-		if (client.getGameState() != GameState.LOGGED_IN)
-		{
-			return;
-		}
-
-		final WallObject object = event.getWallObject();
-		final int region = client.getLocalPlayer().getWorldLocation().getRegionID();
-
-		switch (object.getId())
-		{
-			case EMPTY_WALL:
-			{
-				Rock rock = Rock.AMETHYST;
-				RockRespawn rockRespawn = new RockRespawn(rock, object.getWorldLocation(), Instant.now(), (int) rock.getRespawnTime(region).toMillis(), rock.getZOffset());
-				respawns.add(rockRespawn);
-				break;
-			}
-			case ROCKS_41549: // Depleted barronite vein
-			case ROCKS_41550: // Depleted barronite vein
-			{
-				Rock rock = Rock.BARRONITE;
-				RockRespawn rockRespawn = new RockRespawn(rock, object.getWorldLocation(), Instant.now(), (int) rock.getRespawnTime(region).toMillis(), rock.getZOffset());
-				respawns.add(rockRespawn);
-				break;
-			}
-			case DEPLETED_VEIN: // Depleted gold vein
-			{
-				Rock rock = Rock.MINERAL_VEIN;
-				RockRespawn rockRespawn = new RockRespawn(rock, object.getWorldLocation(), Instant.now(), (int) rock.getRespawnTime(region).toMillis(), rock.getZOffset());
-				respawns.add(rockRespawn);
-				break;
-			}
-			case ORE_VEIN: // Motherlode vein
-			case ORE_VEIN_26662: // Motherlode vein
-			case ORE_VEIN_26663: // Motherlode vein
-			case ORE_VEIN_26664: // Motherlode vein
-			case BARRONITE_ROCKS: // Barronite vein
-			case BARRONITE_ROCKS_41548: // Barronite vein
-			case GOLD_VEIN: // Arzinian gold vein
-			case GOLD_VEIN_5990: // Gold vein
-			case GOLD_VEIN_5991: // Gold vein
-			{
-				// If the vein respawns before the timer is up, remove it
-				final WorldPoint point = object.getWorldLocation();
-				respawns.removeIf(rockRespawn -> rockRespawn.getWorldPoint().equals(point));
-				break;
-			}
-		}
 	}
 
 	@Subscribe
@@ -398,13 +292,46 @@ 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);
-					Rock rock = Rock.ORE_VEIN;
-					RockRespawn rockRespawn = new RockRespawn(rock, worldPoint, Instant.now(), ticks * Constants.GAME_TICK_LENGTH, rock.getZOffset());
-					respawns.add(rockRespawn);
+					addRockRespawn(Rock.MLM_ORE_VEIN, WorldPoint.fromCoord(locCoord), ticks);
+					break;
+				}
+				case DEPLETED_VEIN: // Gold vein
+				case ROCKS_51486: // Calcified rock
+				case ROCKS_51488: // Calcified rock
+				{
+					addRockRespawn(Rock.ORE_VEIN, WorldPoint.fromCoord(locCoord), ticks);
+					break;
+				}
+				case ROCKS_11390:
+				case ROCKS_11391:
+				case ROCKS_11392:
+				case ROCKS_33253: // Basalt etc
+				case EMPTY_ASH_PILE:
+				{
+					addRockRespawn(Rock.ROCK, WorldPoint.fromCoord(locCoord), ticks);
+					break;
+				}
+				// Amethyst
+				case EMPTY_WALL:
+				{
+					addRockRespawn(Rock.AMETHYST, WorldPoint.fromCoord(locCoord), ticks);
+					break;
+				}
+				// Barronite
+				case ROCKS_41549:
+				case ROCKS_41550:
+				{
+					addRockRespawn(Rock.BARRONITE, WorldPoint.fromCoord(locCoord), ticks);
 					break;
 				}
 			}
 		}
 	}
+
+	private void addRockRespawn(Rock rock, WorldPoint point, int ticks)
+	{
+		RockRespawn rockRespawn = new RockRespawn(rock, point, Instant.now(), ticks * Constants.GAME_TICK_LENGTH, rock.getZOffset());
+		respawns.add(rockRespawn);
+		log.debug("Adding respawn for rock: {} coord: {} ticks: {}", rock, point, ticks);
+	}
 }
\ No newline at end of file
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningRocksOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningRocksOverlay.java
index 455832c7b..84c597da7 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningRocksOverlay.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningRocksOverlay.java
@@ -45,10 +45,6 @@ class MiningRocksOverlay extends Overlay
 	private static final int DAEYALT_MIN_RESPAWN_TIME = 91; // Game ticks
 	private static final float DAEYALT_RANDOM_PERCENT_THRESHOLD = (float) DAEYALT_MIN_RESPAWN_TIME / DAEYALT_MAX_RESPAWN_TIME;
 
-	static final int LOVAKITE_ORE_MAX_RESPAWN_TIME = 65; // Game ticks
-	private static final int LOVAKITE_ORE_MIN_RESPAWN_TIME = 50; // Game ticks
-	private static final float LOVAKITE_ORE_RANDOM_PERCENT_THRESHOLD = (float) LOVAKITE_ORE_MIN_RESPAWN_TIME / LOVAKITE_ORE_MAX_RESPAWN_TIME;
-
 	private static final Color DARK_GREEN = new Color(0, 100, 0);
 	private static final int MOTHERLODE_UPPER_FLOOR_HEIGHT = -500;
 
@@ -93,7 +89,7 @@ class MiningRocksOverlay extends Overlay
 
 			// Only draw timer for veins on the same level in motherlode mine
 			LocalPoint localLocation = client.getLocalPlayer().getLocalLocation();
-			if (rock == Rock.ORE_VEIN && isUpstairsMotherlode(localLocation) != isUpstairsMotherlode(loc))
+			if (rock == Rock.MLM_ORE_VEIN && isUpstairsMotherlode(localLocation) != isUpstairsMotherlode(loc))
 			{
 				continue;
 			}
@@ -102,8 +98,7 @@ class MiningRocksOverlay extends Overlay
 			Color pieBorderColor = Color.ORANGE;
 
 			// Recolour pie during the portion of the timer where they may respawn
-			if ((rock == Rock.DAEYALT_ESSENCE && percent > DAEYALT_RANDOM_PERCENT_THRESHOLD)
-				|| (rock == Rock.LOVAKITE && percent > LOVAKITE_ORE_RANDOM_PERCENT_THRESHOLD))
+			if (rock == Rock.DAEYALT_ESSENCE && percent > DAEYALT_RANDOM_PERCENT_THRESHOLD)
 			{
 				pieFillColor = Color.GREEN;
 				pieBorderColor = DARK_GREEN;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/mining/Pickaxe.java b/runelite-client/src/main/java/net/runelite/client/plugins/mining/Pickaxe.java
index 9ad460460..ea5bbc1d3 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/mining/Pickaxe.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/mining/Pickaxe.java
@@ -31,6 +31,21 @@ import static net.runelite.api.AnimationID.MINING_3A_PICKAXE;
 import static net.runelite.api.AnimationID.MINING_ADAMANT_PICKAXE;
 import static net.runelite.api.AnimationID.MINING_BLACK_PICKAXE;
 import static net.runelite.api.AnimationID.MINING_BRONZE_PICKAXE;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_3A;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_ADAMANT;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_BLACK;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_BRONZE;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_CRYSTAL;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_DRAGON;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_DRAGON_OR;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_DRAGON_OR_TRAILBLAZER;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_DRAGON_UPGRADED;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_GILDED;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_INFERNAL;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_IRON;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_MITHRIL;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_RUNE;
+import static net.runelite.api.AnimationID.MINING_CRASHEDSTAR_STEEL;
 import static net.runelite.api.AnimationID.MINING_CRYSTAL_PICKAXE;
 import static net.runelite.api.AnimationID.MINING_DRAGON_PICKAXE;
 import static net.runelite.api.AnimationID.MINING_DRAGON_PICKAXE_OR;
@@ -82,21 +97,21 @@ import net.runelite.api.Player;
 @Getter
 enum Pickaxe
 {
-	BRONZE(BRONZE_PICKAXE, MINING_BRONZE_PICKAXE, MINING_MOTHERLODE_BRONZE),
-	IRON(IRON_PICKAXE, MINING_IRON_PICKAXE, MINING_MOTHERLODE_IRON),
-	STEEL(STEEL_PICKAXE, MINING_STEEL_PICKAXE, MINING_MOTHERLODE_STEEL),
-	BLACK(BLACK_PICKAXE, MINING_BLACK_PICKAXE, MINING_MOTHERLODE_BLACK),
-	MITHRIL(MITHRIL_PICKAXE, MINING_MITHRIL_PICKAXE, MINING_MOTHERLODE_MITHRIL),
-	ADAMANT(ADAMANT_PICKAXE, MINING_ADAMANT_PICKAXE, MINING_MOTHERLODE_ADAMANT),
-	RUNE(RUNE_PICKAXE, MINING_RUNE_PICKAXE, MINING_MOTHERLODE_RUNE),
-	GILDED(GILDED_PICKAXE, MINING_GILDED_PICKAXE, MINING_MOTHERLODE_GILDED),
-	DRAGON(DRAGON_PICKAXE, MINING_DRAGON_PICKAXE, MINING_MOTHERLODE_DRAGON),
-	DRAGON_OR(DRAGON_PICKAXE_OR, MINING_DRAGON_PICKAXE_OR, MINING_MOTHERLODE_DRAGON_OR),
-	DRAGON_OR_TRAILBLAZER(DRAGON_PICKAXE_OR_25376, MINING_DRAGON_PICKAXE_OR_TRAILBLAZER, MINING_MOTHERLODE_DRAGON_OR_TRAILBLAZER),
-	DRAGON_UPGRADED(DRAGON_PICKAXE_12797, MINING_DRAGON_PICKAXE_UPGRADED, MINING_MOTHERLODE_DRAGON_UPGRADED),
-	INFERNAL(INFERNAL_PICKAXE, MINING_INFERNAL_PICKAXE, MINING_MOTHERLODE_INFERNAL),
-	THIRDAGE(_3RD_AGE_PICKAXE, MINING_3A_PICKAXE, MINING_MOTHERLODE_3A),
-	CRYSTAL(CRYSTAL_PICKAXE, MINING_CRYSTAL_PICKAXE, MINING_MOTHERLODE_CRYSTAL),
+	BRONZE(BRONZE_PICKAXE, MINING_BRONZE_PICKAXE, MINING_MOTHERLODE_BRONZE, MINING_CRASHEDSTAR_BRONZE),
+	IRON(IRON_PICKAXE, MINING_IRON_PICKAXE, MINING_MOTHERLODE_IRON, MINING_CRASHEDSTAR_IRON),
+	STEEL(STEEL_PICKAXE, MINING_STEEL_PICKAXE, MINING_MOTHERLODE_STEEL, MINING_CRASHEDSTAR_STEEL),
+	BLACK(BLACK_PICKAXE, MINING_BLACK_PICKAXE, MINING_MOTHERLODE_BLACK, MINING_CRASHEDSTAR_BLACK),
+	MITHRIL(MITHRIL_PICKAXE, MINING_MITHRIL_PICKAXE, MINING_MOTHERLODE_MITHRIL, MINING_CRASHEDSTAR_MITHRIL),
+	ADAMANT(ADAMANT_PICKAXE, MINING_ADAMANT_PICKAXE, MINING_MOTHERLODE_ADAMANT, MINING_CRASHEDSTAR_ADAMANT),
+	RUNE(RUNE_PICKAXE, MINING_RUNE_PICKAXE, MINING_MOTHERLODE_RUNE, MINING_CRASHEDSTAR_RUNE),
+	GILDED(GILDED_PICKAXE, MINING_GILDED_PICKAXE, MINING_MOTHERLODE_GILDED, MINING_CRASHEDSTAR_GILDED),
+	DRAGON(DRAGON_PICKAXE, MINING_DRAGON_PICKAXE, MINING_MOTHERLODE_DRAGON, MINING_CRASHEDSTAR_DRAGON),
+	DRAGON_OR(DRAGON_PICKAXE_OR, MINING_DRAGON_PICKAXE_OR, MINING_MOTHERLODE_DRAGON_OR, MINING_CRASHEDSTAR_DRAGON_OR),
+	DRAGON_OR_TRAILBLAZER(DRAGON_PICKAXE_OR_25376, MINING_DRAGON_PICKAXE_OR_TRAILBLAZER, MINING_MOTHERLODE_DRAGON_OR_TRAILBLAZER, MINING_CRASHEDSTAR_DRAGON_OR_TRAILBLAZER),
+	DRAGON_UPGRADED(DRAGON_PICKAXE_12797, MINING_DRAGON_PICKAXE_UPGRADED, MINING_MOTHERLODE_DRAGON_UPGRADED, MINING_CRASHEDSTAR_DRAGON_UPGRADED),
+	INFERNAL(INFERNAL_PICKAXE, MINING_INFERNAL_PICKAXE, MINING_MOTHERLODE_INFERNAL, MINING_CRASHEDSTAR_INFERNAL),
+	THIRDAGE(_3RD_AGE_PICKAXE, MINING_3A_PICKAXE, MINING_MOTHERLODE_3A, MINING_CRASHEDSTAR_3A),
+	CRYSTAL(CRYSTAL_PICKAXE, MINING_CRYSTAL_PICKAXE, MINING_MOTHERLODE_CRYSTAL, MINING_CRASHEDSTAR_CRYSTAL),
 	TRAILBLAZER(INFERNAL_PICKAXE_OR, MINING_TRAILBLAZER_PICKAXE, MINING_TRAILBLAZER_PICKAXE_2,
 		MINING_TRAILBLAZER_PICKAXE_3, MINING_MOTHERLODE_TRAILBLAZER);
 
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/mining/Rock.java b/runelite-client/src/main/java/net/runelite/client/plugins/mining/Rock.java
index bb5e4b4c1..03a2fb164 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/mining/Rock.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/mining/Rock.java
@@ -24,121 +24,24 @@
  */
 package net.runelite.client.plugins.mining;
 
-import com.google.common.collect.ImmutableMap;
-import java.time.Duration;
-import java.util.Map;
 import lombok.AccessLevel;
 import lombok.Getter;
-import static net.runelite.api.ObjectID.*;
-import static net.runelite.client.util.RSTimeUnit.*;
 
 enum Rock
 {
-	TIN(Duration.of(4, GAME_TICKS), 0, TIN_ROCKS_11360, TIN_ROCKS_11361),
-	COPPER(Duration.of(4, GAME_TICKS), 0, COPPER_ROCKS_10943, COPPER_ROCKS_11161),
-	IRON(Duration.of(9, GAME_TICKS), 0, IRON_ROCKS, IRON_ROCKS_11365, IRON_ROCKS_36203)
-		{
-			@Override
-			Duration getRespawnTime(int region)
-			{
-				return region == MINING_GUILD ? Duration.of(4, GAME_TICKS) : super.respawnTime;
-			}
-		},
-	COAL(Duration.of(49, GAME_TICKS), 0, COAL_ROCKS_11366, COAL_ROCKS_11367, COAL_ROCKS_36204)
-		{
-			@Override
-			Duration getRespawnTime(int region)
-			{
-				switch (region)
-				{
-					case MINING_GUILD:
-						return Duration.of(24, GAME_TICKS);
-					case MISCELLANIA:
-						return Duration.of(11, GAME_TICKS);
-					default:
-						return super.respawnTime;
-				}
-			}
-		},
-	SILVER(Duration.of(100, GAME_TICKS), 0, SILVER_ROCKS, SILVER_ROCKS_11369, SILVER_ROCKS_36205),
-	SANDSTONE(Duration.of(9, GAME_TICKS), 0, SANDSTONE_ROCKS),
-	GOLD(Duration.of(100, GAME_TICKS), 0, GOLD_ROCKS, GOLD_ROCKS_11371, GOLD_ROCKS_36206),
-	GRANITE(Duration.of(9, GAME_TICKS), 0, GRANITE_ROCKS),
-	MITHRIL(Duration.of(200, GAME_TICKS), 0, MITHRIL_ROCKS, MITHRIL_ROCKS_11373, MITHRIL_ROCKS_36207)
-		{
-			@Override
-			Duration getRespawnTime(int region)
-			{
-				return region == MINING_GUILD ? Duration.of(100, GAME_TICKS) : super.respawnTime;
-			}
-		},
-	LOVAKITE(Duration.of(MiningRocksOverlay.LOVAKITE_ORE_MAX_RESPAWN_TIME, GAME_TICKS), 0, LOVAKITE_ROCKS, LOVAKITE_ROCKS_28597),
-	ADAMANTITE(Duration.of(400, GAME_TICKS), 0, ADAMANTITE_ROCKS, ADAMANTITE_ROCKS_11375, ADAMANTITE_ROCKS_36208)
-		{
-			@Override
-			Duration getRespawnTime(int region)
-			{
-				return region == MINING_GUILD || region == WILDERNESS_RESOURCE_AREA ? Duration.of(200, GAME_TICKS) : super.respawnTime;
-			}
-		},
-	RUNITE(Duration.of(1200, GAME_TICKS), 0, RUNITE_ROCKS, RUNITE_ROCKS_11377, RUNITE_ROCKS_36209)
-		{
-			@Override
-			Duration getRespawnTime(int region)
-			{
-				return region == MINING_GUILD ? Duration.of(600, GAME_TICKS) : super.respawnTime;
-			}
-		},
-	ORE_VEIN(Duration.ZERO /* unknown */, 150),
-	AMETHYST(Duration.of(125, GAME_TICKS), 120),
-	ASH_VEIN(Duration.of(50, GAME_TICKS), 0, ASH_PILE),
-	GEM_ROCK(Duration.of(99, GAME_TICKS), 0, GEM_ROCKS, GEM_ROCKS_11381),
-	URT_SALT(Duration.of(9, GAME_TICKS), 0, URT_SALT_ROCKS),
-	EFH_SALT(Duration.of(9, GAME_TICKS), 0, EFH_SALT_ROCKS),
-	TE_SALT(Duration.of(9, GAME_TICKS), 0, TE_SALT_ROCKS),
-	BASALT(Duration.of(9, GAME_TICKS), 0, BASALT_ROCKS),
-	DAEYALT_ESSENCE(Duration.of(MiningRocksOverlay.DAEYALT_MAX_RESPAWN_TIME, GAME_TICKS), 0, DAEYALT_ESSENCE_39095),
-	BARRONITE(Duration.of(89, GAME_TICKS), 140),
-	MINERAL_VEIN(Duration.of(100, GAME_TICKS), 150),
-	CALCIFIED_ROCK(Duration.of(60, GAME_TICKS), 150, CALCIFIED_ROCKS, CALCIFIED_ROCKS_51487, CALCIFIED_ROCKS_51489, CALCIFIED_ROCKS_51491);
+	ROCK(0),
+	MLM_ORE_VEIN(150),
+	ORE_VEIN(150),
+	AMETHYST(120),
+	DAEYALT_ESSENCE(0),
+	BARRONITE(140),
+	;
 
-	private static final int WILDERNESS_RESOURCE_AREA = 12605;
-	private static final int MISCELLANIA = 10044;
-	private static final int MINING_GUILD = 12183;
-	private static final Map<Integer, Rock> ROCKS;
-
-	static
-	{
-		ImmutableMap.Builder<Integer, Rock> builder = new ImmutableMap.Builder<>();
-		for (Rock rock : values())
-		{
-			for (int id : rock.ids)
-			{
-				builder.put(id, rock);
-			}
-		}
-		ROCKS = builder.build();
-	}
-
-	private final Duration respawnTime;
 	@Getter(AccessLevel.PACKAGE)
 	private final int zOffset;
-	private final int[] ids;
 
-	Rock(Duration respawnTime, int zOffset, int... ids)
+	Rock(int zOffset)
 	{
-		this.respawnTime = respawnTime;
 		this.zOffset = zOffset;
-		this.ids = ids;
-	}
-
-	Duration getRespawnTime(int region)
-	{
-		return respawnTime;
-	}
-
-	static Rock getRock(int id)
-	{
-		return ROCKS.get(id);
 	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/HerbloreAction.java b/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/HerbloreAction.java
index 2e1ac43c4..244b75f1e 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/HerbloreAction.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/HerbloreAction.java
@@ -64,6 +64,7 @@ public enum HerbloreAction implements ItemSkillAction
 	SUPER_ENERGY_3(ItemID.SUPER_ENERGY3, 52, 117.5f),
 	HUNTER_POTION_3(ItemID.HUNTER_POTION3, 53, 120),
 	KWUARM(ItemID.KWUARM, 54, 11.3f),
+	IRIT_TAR(ItemID.IRIT_TAR, 55, 85),
 	SUPER_STRENGTH_3(ItemID.SUPER_STRENGTH3, 55, 125),
 	MAGIC_ESSENCE_POTION_3(ItemID.MAGIC_ESSENCE3, 57, 130),
 	SNAPDRAGON(ItemID.SNAPDRAGON, 59, 11.8f),
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/plugins/worldhopper/WorldTypeFilter.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTypeFilter.java
index a5ccdcb53..f1fb9e7f7 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTypeFilter.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldhopper/WorldTypeFilter.java
@@ -36,7 +36,7 @@ enum WorldTypeFilter
 			@Override
 			boolean matches(Set<WorldType> types)
 			{
-				EnumSet<WorldType> normal = EnumSet.of(WorldType.MEMBERS, WorldType.BOUNTY, WorldType.SKILL_TOTAL, WorldType.HIGH_RISK, WorldType.LAST_MAN_STANDING);
+				EnumSet<WorldType> normal = EnumSet.of(WorldType.MEMBERS, WorldType.BOUNTY, WorldType.SKILL_TOTAL, WorldType.LAST_MAN_STANDING);
 				EnumSet<WorldType> inverse = EnumSet.complementOf(normal);
 				return Sets.intersection(types, inverse).isEmpty();
 			}
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/net/runelite/client/plugins/gpu/cl_types.cl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/cl_types.cl
index be1d703a0..ce9bb7111 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/cl_types.cl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/cl_types.cl
@@ -32,7 +32,6 @@ struct uniform {
   float cameraX;
   float cameraY;
   float cameraZ;
-  int4 sinCosTable[2048];
 };
 
 struct shared_data {
@@ -53,3 +52,10 @@ struct modelinfo {
   int y;        // scene position y
   int z;        // scene position z
 };
+
+struct vert {
+  float x;
+  float y;
+  float z;
+  int ahsl;
+};
\ No newline at end of file
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/common.cl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/common.cl
index 623faacbb..8b949b20c 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/common.cl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/common.cl
@@ -49,16 +49,7 @@ float3 toScreen(float3 vertex, float cameraYaw, float cameraPitch, int centerX,
 /*
  * Rotate a vertex by a given orientation in JAU
  */
-int4 rotate_vertex(__constant struct uniform *uni, int4 vertex, int orientation) {
-  int4 sinCos = uni->sinCosTable[orientation];
-  int s = sinCos.x;
-  int c = sinCos.y;
-  int x = vertex.z * s + vertex.x * c >> 16;
-  int z = vertex.z * c - vertex.x * s >> 16;
-  return (int4)(x, vertex.y, z, vertex.w);
-}
-
-float4 rotatef_vertex(float4 vertex, int orientation) {
+float4 rotate_vertex(float4 vertex, int orientation) {
   float rad = orientation * UNIT;
   float s = sin(rad);
   float c = cos(rad);
@@ -70,15 +61,15 @@ float4 rotatef_vertex(float4 vertex, int orientation) {
 /*
  * Calculate the distance to a vertex given the camera angle
  */
-int vertex_distance(int4 vertex, float cameraYaw, float cameraPitch) {
-  int yawSin = (int)(65536.0f * sin(cameraYaw));
-  int yawCos = (int)(65536.0f * cos(cameraYaw));
+float vertex_distance(float4 vertex, float cameraYaw, float cameraPitch) {
+  float yawSin = sin(cameraYaw);
+  float yawCos = cos(cameraYaw);
 
-  int pitchSin = (int)(65536.0f * sin(cameraPitch));
-  int pitchCos = (int)(65536.0f * cos(cameraPitch));
+  float pitchSin = sin(cameraPitch);
+  float pitchCos = cos(cameraPitch);
 
-  int j = vertex.z * yawCos - vertex.x * yawSin >> 16;
-  int l = vertex.y * pitchSin + j * pitchCos >> 16;
+  float j = vertex.z * yawCos - vertex.x * yawSin;
+  float l = vertex.y * pitchSin + j * pitchCos;
 
   return l;
 }
@@ -86,28 +77,29 @@ int vertex_distance(int4 vertex, float cameraYaw, float cameraPitch) {
 /*
  * Calculate the distance to a face
  */
-int face_distance(int4 vA, int4 vB, int4 vC, float cameraYaw, float cameraPitch) {
-  int dvA = vertex_distance(vA, cameraYaw, cameraPitch);
-  int dvB = vertex_distance(vB, cameraYaw, cameraPitch);
-  int dvC = vertex_distance(vC, cameraYaw, cameraPitch);
-  int faceDistance = (dvA + dvB + dvC) / 3;
-  return faceDistance;
+float face_distance(float4 vA, float4 vB, float4 vC, float cameraYaw, float cameraPitch) {
+  float dvA = vertex_distance(vA, cameraYaw, cameraPitch);
+  float dvB = vertex_distance(vB, cameraYaw, cameraPitch);
+  float dvC = vertex_distance(vC, cameraYaw, cameraPitch);
+  float faceDistance = (dvA + dvB + dvC) / 3;
+  return (int)faceDistance;
 }
 
 /*
  * Test if a face is visible (not backward facing)
  */
-bool face_visible(__constant struct uniform *uni, int4 vA, int4 vB, int4 vC, int4 position) {
+bool face_visible(__constant struct uniform *uni, float3 vA, float3 vB, float3 vC, int4 position) {
   // Move model to scene location, and account for camera offset
-  float4 cameraPos = (float4)(uni->cameraX, uni->cameraY, uni->cameraZ, 0);
+  float3 cameraPos = (float3)(uni->cameraX, uni->cameraY, uni->cameraZ);
+  float3 modelPos = convert_float3(position.xyz);
 
-  float4 lA = convert_float4(vA + position) - cameraPos;
-  float4 lB = convert_float4(vB + position) - cameraPos;
-  float4 lC = convert_float4(vC + position) - cameraPos;
+  float3 lA = vA + modelPos - cameraPos;
+  float3 lB = vB + modelPos - cameraPos;
+  float3 lC = vC + modelPos - cameraPos;
 
-  float3 sA = toScreen(lA.xyz, uni->cameraYaw, uni->cameraPitch, uni->centerX, uni->centerY, uni->zoom);
-  float3 sB = toScreen(lB.xyz, uni->cameraYaw, uni->cameraPitch, uni->centerX, uni->centerY, uni->zoom);
-  float3 sC = toScreen(lC.xyz, uni->cameraYaw, uni->cameraPitch, uni->centerX, uni->centerY, uni->zoom);
+  float3 sA = toScreen(lA, uni->cameraYaw, uni->cameraPitch, uni->centerX, uni->centerY, uni->zoom);
+  float3 sB = toScreen(lB, uni->cameraYaw, uni->cameraPitch, uni->centerX, uni->centerY, uni->zoom);
+  float3 sC = toScreen(lC, uni->cameraYaw, uni->cameraPitch, uni->centerX, uni->centerY, uni->zoom);
 
   return (sA.x - sB.x) * (sC.y - sB.y) - (sC.x - sB.x) * (sA.y - sB.y) > 0;
 }
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/common.glsl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/common.glsl
index d9148fb34..14ceab09d 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/common.glsl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/common.glsl
@@ -28,35 +28,33 @@
 /*
  * Rotate a vertex by a given orientation in JAU
  */
-ivec4 rotate(ivec4 vertex, int orientation) {
-  ivec2 sinCos = sinCosTable[orientation];
-  int s = sinCos.x;
-  int c = sinCos.y;
-  int x = vertex.z * s + vertex.x * c >> 16;
-  int z = vertex.z * c - vertex.x * s >> 16;
-  return ivec4(x, vertex.y, z, vertex.w);
-}
-
-vec4 rotatef(vec4 vertex, int orientation) {
+vec4 rotate_vertex(vec4 vertex, int orientation) {
   float rad = orientation * UNIT;
   float s = sin(rad);
   float c = cos(rad);
-  mat4 m = mat4(c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1);
+  // clang-format off
+  mat4 m = mat4(
+    c, 0, s, 0,
+    0, 1, 0, 0,
+   -s, 0, c, 0,
+    0, 0, 0, 1
+  );
+  // clang-format on
   return vertex * m;
 }
 
 /*
  * Calculate the distance to a vertex given the camera angle
  */
-int distance(ivec4 vertex, float cameraYaw, float cameraPitch) {
-  int yawSin = int(65536.0f * sin(cameraYaw));
-  int yawCos = int(65536.0f * cos(cameraYaw));
+float distance(vec4 vertex, float cameraYaw, float cameraPitch) {
+  float yawSin = sin(cameraYaw);
+  float yawCos = cos(cameraYaw);
 
-  int pitchSin = int(65536.0f * sin(cameraPitch));
-  int pitchCos = int(65536.0f * cos(cameraPitch));
+  float pitchSin = sin(cameraPitch);
+  float pitchCos = cos(cameraPitch);
 
-  int j = vertex.z * yawCos - vertex.x * yawSin >> 16;
-  int l = vertex.y * pitchSin + j * pitchCos >> 16;
+  float j = vertex.z * yawCos - vertex.x * yawSin;
+  float l = vertex.y * pitchSin + j * pitchCos;
 
   return l;
 }
@@ -64,28 +62,28 @@ int distance(ivec4 vertex, float cameraYaw, float cameraPitch) {
 /*
  * Calculate the distance to a face
  */
-int face_distance(ivec4 vA, ivec4 vB, ivec4 vC, float cameraYaw, float cameraPitch) {
-  int dvA = distance(vA, cameraYaw, cameraPitch);
-  int dvB = distance(vB, cameraYaw, cameraPitch);
-  int dvC = distance(vC, cameraYaw, cameraPitch);
-  int faceDistance = (dvA + dvB + dvC) / 3;
-  return faceDistance;
+int face_distance(vec4 vA, vec4 vB, vec4 vC, float cameraYaw, float cameraPitch) {
+  float dvA = distance(vA, cameraYaw, cameraPitch);
+  float dvB = distance(vB, cameraYaw, cameraPitch);
+  float dvC = distance(vC, cameraYaw, cameraPitch);
+  float faceDistance = (dvA + dvB + dvC) / 3;
+  return int(faceDistance);
 }
 
 /*
  * Test if a face is visible (not backward facing)
  */
-bool face_visible(ivec4 vA, ivec4 vB, ivec4 vC, ivec4 position) {
+bool face_visible(vec3 vA, vec3 vB, vec3 vC, ivec4 position) {
   // Move model to scene location, and account for camera offset
-  vec4 cameraPos = vec4(cameraX, cameraY, cameraZ, 0);
+  vec3 cameraPos = vec3(cameraX, cameraY, cameraZ);
 
-  vec4 lA = vA + position - cameraPos;
-  vec4 lB = vB + position - cameraPos;
-  vec4 lC = vC + position - cameraPos;
+  vec3 lA = vA + position.xyz - cameraPos;
+  vec3 lB = vB + position.xyz - cameraPos;
+  vec3 lC = vC + position.xyz - cameraPos;
 
-  vec3 sA = toScreen(lA.xyz, cameraYaw, cameraPitch, centerX, centerY, zoom);
-  vec3 sB = toScreen(lB.xyz, cameraYaw, cameraPitch, centerX, centerY, zoom);
-  vec3 sC = toScreen(lC.xyz, cameraYaw, cameraPitch, centerX, centerY, zoom);
+  vec3 sA = toScreen(lA, cameraYaw, cameraPitch, centerX, centerY, zoom);
+  vec3 sB = toScreen(lB, cameraYaw, cameraPitch, centerX, centerY, zoom);
+  vec3 sC = toScreen(lC, cameraYaw, cameraPitch, centerX, centerY, zoom);
 
   return (sA.x - sB.x) * (sC.y - sB.y) - (sC.x - sB.x) * (sA.y - sB.y) > 0;
 }
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp.cl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp.cl
index f87258806..9049cd29a 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp.cl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp.cl
@@ -30,9 +30,10 @@
 #include "priority_render.cl"
 
 __kernel __attribute__((work_group_size_hint(256, 1, 1))) void computeLarge(__local struct shared_data *shared, __global const struct modelinfo *ol,
-                                                                            __global const int4 *vb, __global const int4 *tempvb, __global const float4 *texb,
-                                                                            __global const float4 *temptexb, __global int4 *vout, __global float4 *uvout,
-                                                                            __constant struct uniform *uni, read_only image3d_t tileHeightImage) {
+                                                                            __global const struct vert *vb, __global const struct vert *tempvb,
+                                                                            __global const float4 *texb, __global const float4 *temptexb,
+                                                                            __global struct vert *vout, __global float4 *uvout, __constant struct uniform *uni,
+                                                                            read_only image3d_t tileHeightImage) {
   size_t groupId = get_group_id(0);
   size_t localId = get_local_id(0) * FACE_COUNT;
   struct modelinfo minfo = ol[groupId];
@@ -51,12 +52,12 @@ __kernel __attribute__((work_group_size_hint(256, 1, 1))) void computeLarge(__lo
 
   int prio[FACE_COUNT];
   int dis[FACE_COUNT];
-  int4 v1[FACE_COUNT];
-  int4 v2[FACE_COUNT];
-  int4 v3[FACE_COUNT];
+  struct vert v1[FACE_COUNT];
+  struct vert v2[FACE_COUNT];
+  struct vert v3[FACE_COUNT];
 
   for (int i = 0; i < FACE_COUNT; i++) {
-    get_face(shared, uni, vb, tempvb, localId + i, minfo, uni->cameraYaw, uni->cameraPitch, &prio[i], &dis[i], &v1[i], &v2[i], &v3[i]);
+    get_face(shared, vb, tempvb, localId + i, minfo, uni->cameraYaw, uni->cameraPitch, &prio[i], &dis[i], &v1[i], &v2[i], &v3[i]);
   }
 
   barrier(CLK_LOCAL_MEM_FENCE);
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp.glsl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp.glsl
index 9354fa4f7..edf7d3ff2 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp.glsl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp.glsl
@@ -61,9 +61,9 @@ void main() {
 
   int prio[FACES_PER_THREAD];
   int dis[FACES_PER_THREAD];
-  ivec4 vA[FACES_PER_THREAD];
-  ivec4 vB[FACES_PER_THREAD];
-  ivec4 vC[FACES_PER_THREAD];
+  vert vA[FACES_PER_THREAD];
+  vert vB[FACES_PER_THREAD];
+  vert vC[FACES_PER_THREAD];
 
   for (int i = 0; i < FACES_PER_THREAD; i++) {
     get_face(localId + i, minfo, cameraYaw, cameraPitch, prio[i], dis[i], vA[i], vB[i], vC[i]);
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_common.glsl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_common.glsl
index f751476be..39dbed12f 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_common.glsl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_common.glsl
@@ -35,7 +35,6 @@ layout(std140) uniform uniforms {
   float cameraX;
   float cameraY;
   float cameraZ;
-  ivec2 sinCosTable[2048];
 };
 
 struct modelinfo {
@@ -49,20 +48,25 @@ struct modelinfo {
   int z;        // scene position z
 };
 
+struct vert {
+  vec3 pos;
+  int ahsl;
+};
+
 layout(std430, binding = 0) readonly buffer modelbuffer_in {
   modelinfo ol[];
 };
 
 layout(std430, binding = 1) readonly buffer vertexbuffer_in {
-  ivec4 vb[];
+  vert vb[];
 };
 
 layout(std430, binding = 2) readonly buffer tempvertexbuffer_in {
-  ivec4 tempvb[];
+  vert tempvb[];
 };
 
 layout(std430, binding = 3) writeonly buffer vertex_out {
-  ivec4 vout[];
+  vert vout[];
 };
 
 layout(std430, binding = 4) writeonly buffer uv_out {
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_unordered.cl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_unordered.cl
index 1ae52ed5e..738967216 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_unordered.cl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_unordered.cl
@@ -26,9 +26,10 @@
 
 #include "cl_types.cl"
 
-__kernel __attribute__((reqd_work_group_size(6, 1, 1))) void computeUnordered(__global const struct modelinfo *ol, __global const int4 *vb,
-                                                                              __global const int4 *tempvb, __global const float4 *texb,
-                                                                              __global const float4 *temptexb, __global int4 *vout, __global float4 *uvout) {
+__kernel __attribute__((reqd_work_group_size(6, 1, 1))) void computeUnordered(__global const struct modelinfo *ol, __global const struct vert *vb,
+                                                                              __global const struct vert *tempvb, __global const float4 *texb,
+                                                                              __global const float4 *temptexb, __global struct vert *vout,
+                                                                              __global float4 *uvout) {
   size_t groupId = get_group_id(0);
   size_t localId = get_local_id(0);
   struct modelinfo minfo = ol[groupId];
@@ -44,7 +45,7 @@ __kernel __attribute__((reqd_work_group_size(6, 1, 1))) void computeUnordered(__
   }
 
   uint ssboOffset = localId;
-  int4 thisA, thisB, thisC;
+  struct vert thisA, thisB, thisC;
 
   // Grab triangle vertices from the correct buffer
   if (flags < 0) {
@@ -58,13 +59,17 @@ __kernel __attribute__((reqd_work_group_size(6, 1, 1))) void computeUnordered(__
   }
 
   uint myOffset = localId;
-  int4 pos = (int4)(minfo.x, minfo.y, minfo.z, 0);
-  float4 texPos = convert_float4(pos.wxyz);
+  float3 pos = convert_float3((int3)(minfo.x, minfo.y, minfo.z));
+  float4 texPos = (float4)(0, minfo.x, minfo.y, minfo.z);
+
+  float3 vertA = (float3)(thisA.x, thisA.y, thisA.z) + pos;
+  float3 vertB = (float3)(thisB.x, thisB.y, thisB.z) + pos;
+  float3 vertC = (float3)(thisC.x, thisC.y, thisC.z) + pos;
 
   // position vertices in scene and write to out buffer
-  vout[outOffset + myOffset * 3] = pos + thisA;
-  vout[outOffset + myOffset * 3 + 1] = pos + thisB;
-  vout[outOffset + myOffset * 3 + 2] = pos + thisC;
+  vout[outOffset + myOffset * 3] = (struct vert){vertA.x, vertA.y, vertA.z, thisA.ahsl};
+  vout[outOffset + myOffset * 3 + 1] = (struct vert){vertB.x, vertB.y, vertB.z, thisB.ahsl};
+  vout[outOffset + myOffset * 3 + 2] = (struct vert){vertC.x, vertC.y, vertC.z, thisC.ahsl};
 
   if (toffset < 0) {
     uvout[outOffset + myOffset * 3] = (float4)(0.0f, 0.0f, 0.0f, 0.0f);
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_unordered.glsl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_unordered.glsl
index 2152091d2..a4eb42d23 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_unordered.glsl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/comp_unordered.glsl
@@ -45,7 +45,7 @@ void main() {
   }
 
   uint ssboOffset = localId;
-  ivec4 thisA, thisB, thisC;
+  vert thisA, thisB, thisC;
 
   // Grab triangle vertices from the correct buffer
   if (flags < 0) {
@@ -59,13 +59,17 @@ void main() {
   }
 
   uint myOffset = localId;
-  ivec4 pos = ivec4(minfo.x, minfo.y, minfo.z, 0);
-  ivec4 texPos = pos.wxyz;
+  vec3 pos = vec3(minfo.x, minfo.y, minfo.z);
+  ivec4 texPos = ivec4(0, pos);
+
+  vec3 vertA = thisA.pos + pos;
+  vec3 vertB = thisB.pos + pos;
+  vec3 vertC = thisC.pos + pos;
 
   // position vertices in scene and write to out buffer
-  vout[outOffset + myOffset * 3] = pos + thisA;
-  vout[outOffset + myOffset * 3 + 1] = pos + thisB;
-  vout[outOffset + myOffset * 3 + 2] = pos + thisC;
+  vout[outOffset + myOffset * 3] = vert(vertA, thisA.ahsl);
+  vout[outOffset + myOffset * 3 + 1] = vert(vertB, thisB.ahsl);
+  vout[outOffset + myOffset * 3 + 2] = vert(vertC, thisC.ahsl);
 
   if (toffset < 0) {
     uvout[outOffset + myOffset * 3] = vec4(0);
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/geom.glsl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/geom.glsl
index 2c5cc57bc..7fc899fab 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/geom.glsl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/geom.glsl
@@ -43,7 +43,6 @@ layout(std140) uniform uniforms {
   float cameraX;
   float cameraY;
   float cameraZ;
-  ivec2 sinCosTable[2048];
 };
 
 #include "uv.glsl"
@@ -52,7 +51,7 @@ uniform vec2 textureAnimations[128];
 uniform int tick;
 uniform mat4 projectionMatrix;
 
-in ivec3 gVertex[3];
+in vec3 gVertex[3];
 in vec4 gColor[3];
 in float gHsl[3];
 in int gTextureId[3];
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/priority_render.cl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/priority_render.cl
index 98cf779d2..f64781ec6 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/priority_render.cl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/priority_render.cl
@@ -89,9 +89,9 @@ int count_prio_offset(__local struct shared_data *shared, int priority) {
   return total;
 }
 
-void get_face(__local struct shared_data *shared, __constant struct uniform *uni, __global const int4 *vb, __global const int4 *tempvb, uint localId,
-              struct modelinfo minfo, float cameraYaw, float cameraPitch,
-              /* out */ int *prio, int *dis, int4 *o1, int4 *o2, int4 *o3) {
+void get_face(__local struct shared_data *shared, __global const struct vert *vb, __global const struct vert *tempvb, uint localId, struct modelinfo minfo,
+              float cameraYaw, float cameraPitch,
+              /* out */ int *prio, int *dis, struct vert *o1, struct vert *o2, struct vert *o3) {
   int size = minfo.size;
   int offset = minfo.offset;
   int flags = minfo.flags;
@@ -103,9 +103,9 @@ void get_face(__local struct shared_data *shared, __constant struct uniform *uni
     ssboOffset = 0;
   }
 
-  int4 thisA;
-  int4 thisB;
-  int4 thisC;
+  struct vert thisA;
+  struct vert thisB;
+  struct vert thisC;
 
   // Grab triangle vertices from the correct buffer
   if (flags < 0) {
@@ -122,34 +122,37 @@ void get_face(__local struct shared_data *shared, __constant struct uniform *uni
     int orientation = flags & 0x7ff;
 
     // rotate for model orientation
-    int4 thisrvA = rotate_vertex(uni, thisA, orientation);
-    int4 thisrvB = rotate_vertex(uni, thisB, orientation);
-    int4 thisrvC = rotate_vertex(uni, thisC, orientation);
+    float4 thisrvA = rotate_vertex((float4)(thisA.x, thisA.y, thisA.z, 0), orientation);
+    float4 thisrvB = rotate_vertex((float4)(thisB.x, thisB.y, thisB.z, 0), orientation);
+    float4 thisrvC = rotate_vertex((float4)(thisC.x, thisC.y, thisC.z, 0), orientation);
 
     // calculate distance to face
-    int thisPriority = (thisA.w >> 16) & 0xff;  // all vertices on the face have the same priority
+    int thisPriority = (thisA.ahsl >> 16) & 0xff;  // all vertices on the face have the same priority
     int thisDistance = face_distance(thisrvA, thisrvB, thisrvC, cameraYaw, cameraPitch);
 
-    *o1 = thisrvA;
-    *o2 = thisrvB;
-    *o3 = thisrvC;
+    *o1 = (struct vert){thisrvA.x, thisrvA.y, thisrvA.z, thisA.ahsl};
+    *o2 = (struct vert){thisrvB.x, thisrvB.y, thisrvB.z, thisB.ahsl};
+    *o3 = (struct vert){thisrvC.x, thisrvC.y, thisrvC.z, thisC.ahsl};
 
     *prio = thisPriority;
     *dis = thisDistance;
   } else {
-    *o1 = (int4)(0, 0, 0, 0);
-    *o2 = (int4)(0, 0, 0, 0);
-    *o3 = (int4)(0, 0, 0, 0);
+    *o1 = (struct vert){0, 0, 0, 0};
+    *o2 = (struct vert){0, 0, 0, 0};
+    *o3 = (struct vert){0, 0, 0, 0};
     *prio = 0;
     *dis = 0;
   }
 }
 
-void add_face_prio_distance(__local struct shared_data *shared, __constant struct uniform *uni, uint localId, struct modelinfo minfo, int4 thisrvA,
-                            int4 thisrvB, int4 thisrvC, int thisPriority, int thisDistance, int4 pos) {
+void add_face_prio_distance(__local struct shared_data *shared, __constant struct uniform *uni, uint localId, struct modelinfo minfo, struct vert thisrvA,
+                            struct vert thisrvB, struct vert thisrvC, int thisPriority, int thisDistance, int4 pos) {
   if (localId < minfo.size) {
     // if the face is not culled, it is calculated into priority distance averages
-    if (face_visible(uni, thisrvA, thisrvB, thisrvC, pos)) {
+    float3 posA = (float3)(thisrvA.x, thisrvA.y, thisrvA.z);
+    float3 posB = (float3)(thisrvB.x, thisrvB.y, thisrvB.z);
+    float3 posC = (float3)(thisrvC.x, thisrvC.y, thisrvC.z);
+    if (face_visible(uni, posA, posB, posC, pos)) {
       atomic_add(&shared->totalNum[thisPriority], 1);
       atomic_add(&shared->totalDistance[thisPriority], thisDistance);
 
@@ -214,21 +217,6 @@ int tile_height(read_only image3d_t tileHeightImage, int z, int x, int y) {
   return read_imagei(tileHeightImage, tileHeightSampler, coord).x << 3;
 }
 
-int4 hillskew_vertex(read_only image3d_t tileHeightImage, int4 v, int hillskew, int y, int plane) {
-  if (hillskew == 1) {
-    int px = v.x & 127;
-    int pz = v.z & 127;
-    int sx = v.x >> 7;
-    int sz = v.z >> 7;
-    int h1 = px * tile_height(tileHeightImage, plane, sx + 1, sz) + (128 - px) * tile_height(tileHeightImage, plane, sx, sz) >> 7;
-    int h2 = px * tile_height(tileHeightImage, plane, sx + 1, sz + 1) + (128 - px) * tile_height(tileHeightImage, plane, sx, sz + 1) >> 7;
-    int h3 = pz * h2 + (128 - pz) * h1 >> 7;
-    return (int4)(v.x, v.y + h3 - y, v.z, v.w);
-  } else {
-    return v;
-  }
-}
-
 float4 hillskew_vertexf(read_only image3d_t tileHeightImage, float4 v, int hillskew, int y, int plane) {
   if (hillskew == 1) {
     float fx = v.x / 128;
@@ -246,8 +234,8 @@ float4 hillskew_vertexf(read_only image3d_t tileHeightImage, float4 v, int hills
 }
 
 void sort_and_insert(__local struct shared_data *shared, __constant struct uniform *uni, __global const float4 *texb, __global const float4 *temptexb,
-                     __global int4 *vout, __global float4 *uvout, uint localId, struct modelinfo minfo, int thisPriority, int thisDistance, int4 thisrvA,
-                     int4 thisrvB, int4 thisrvC, read_only image3d_t tileHeightImage) {
+                     __global struct vert *vout, __global float4 *uvout, uint localId, struct modelinfo minfo, int thisPriority, int thisDistance,
+                     struct vert thisrvA, struct vert thisrvB, struct vert thisrvC, read_only image3d_t tileHeightImage) {
   int size = minfo.size;
 
   if (localId < size) {
@@ -271,22 +259,22 @@ void sort_and_insert(__local struct shared_data *shared, __constant struct unifo
     }
 
     // position into scene
-    int4 pos = (int4)(minfo.x, minfo.y, minfo.z, 0);
-    thisrvA += pos;
-    thisrvB += pos;
-    thisrvC += pos;
+    float4 pos = (float4)(minfo.x, minfo.y, minfo.z, 0);
+    float4 vertA = (float4)(thisrvA.x, thisrvA.y, thisrvA.z, 0) + pos;
+    float4 vertB = (float4)(thisrvB.x, thisrvB.y, thisrvB.z, 0) + pos;
+    float4 vertC = (float4)(thisrvC.x, thisrvC.y, thisrvC.z, 0) + pos;
 
     // apply hillskew
     int plane = (flags >> 24) & 3;
     int hillskew = (flags >> 26) & 1;
-    thisrvA = hillskew_vertex(tileHeightImage, thisrvA, hillskew, minfo.y, plane);
-    thisrvB = hillskew_vertex(tileHeightImage, thisrvB, hillskew, minfo.y, plane);
-    thisrvC = hillskew_vertex(tileHeightImage, thisrvC, hillskew, minfo.y, plane);
+    vertA = hillskew_vertexf(tileHeightImage, vertA, hillskew, minfo.y, plane);
+    vertB = hillskew_vertexf(tileHeightImage, vertB, hillskew, minfo.y, plane);
+    vertC = hillskew_vertexf(tileHeightImage, vertC, hillskew, minfo.y, plane);
 
     // write to out buffer
-    vout[outOffset + myOffset * 3] = thisrvA;
-    vout[outOffset + myOffset * 3 + 1] = thisrvB;
-    vout[outOffset + myOffset * 3 + 2] = thisrvC;
+    vout[outOffset + myOffset * 3] = (struct vert){vertA.x, vertA.y, vertA.z, thisrvA.ahsl};
+    vout[outOffset + myOffset * 3 + 1] = (struct vert){vertB.x, vertB.y, vertB.z, thisrvB.ahsl};
+    vout[outOffset + myOffset * 3 + 2] = (struct vert){vertC.x, vertC.y, vertC.z, thisrvC.ahsl};
 
     if (toffset < 0) {
       uvout[outOffset + myOffset * 3] = (float4)(0, 0, 0, 0);
@@ -311,13 +299,13 @@ void sort_and_insert(__local struct shared_data *shared, __constant struct unifo
       texB = texB.yzwx;
       texC = texC.yzwx;
       // rotate
-      texA = rotatef_vertex(texA, orientation);
-      texB = rotatef_vertex(texB, orientation);
-      texC = rotatef_vertex(texC, orientation);
+      texA = rotate_vertex(texA, orientation);
+      texB = rotate_vertex(texB, orientation);
+      texC = rotate_vertex(texC, orientation);
       // position
-      texA += convert_float4(pos);
-      texB += convert_float4(pos);
-      texC += convert_float4(pos);
+      texA += pos;
+      texB += pos;
+      texC += pos;
       // hillskew
       texA = hillskew_vertexf(tileHeightImage, texA, hillskew, minfo.y, plane);
       texB = hillskew_vertexf(tileHeightImage, texB, hillskew, minfo.y, plane);
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/priority_render.glsl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/priority_render.glsl
index 429b19b32..c674746e3 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/priority_render.glsl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/priority_render.glsl
@@ -91,7 +91,7 @@ int count_prio_offset(int priority) {
   return total;
 }
 
-void get_face(uint localId, modelinfo minfo, float cameraYaw, float cameraPitch, out int prio, out int dis, out ivec4 o1, out ivec4 o2, out ivec4 o3) {
+void get_face(uint localId, modelinfo minfo, float cameraYaw, float cameraPitch, out int prio, out int dis, out vert o1, out vert o2, out vert o3) {
   int size = minfo.size;
   int offset = minfo.offset;
   int flags = minfo.flags;
@@ -103,9 +103,9 @@ void get_face(uint localId, modelinfo minfo, float cameraYaw, float cameraPitch,
     ssboOffset = 0;
   }
 
-  ivec4 thisA;
-  ivec4 thisB;
-  ivec4 thisC;
+  vert thisA;
+  vert thisB;
+  vert thisC;
 
   // Grab triangle vertices from the correct buffer
   if (flags < 0) {
@@ -122,33 +122,44 @@ void get_face(uint localId, modelinfo minfo, float cameraYaw, float cameraPitch,
     int orientation = flags & 0x7ff;
 
     // rotate for model orientation
-    ivec4 thisrvA = rotate(thisA, orientation);
-    ivec4 thisrvB = rotate(thisB, orientation);
-    ivec4 thisrvC = rotate(thisC, orientation);
+    vec4 thisrvA = rotate_vertex(vec4(thisA.pos, 0), orientation);
+    vec4 thisrvB = rotate_vertex(vec4(thisB.pos, 0), orientation);
+    vec4 thisrvC = rotate_vertex(vec4(thisC.pos, 0), orientation);
 
     // calculate distance to face
-    int thisPriority = (thisA.w >> 16) & 0xff;  // all vertices on the face have the same priority
+    int thisPriority = (thisA.ahsl >> 16) & 0xff;  // all vertices on the face have the same priority
     int thisDistance = face_distance(thisrvA, thisrvB, thisrvC, cameraYaw, cameraPitch);
 
-    o1 = thisrvA;
-    o2 = thisrvB;
-    o3 = thisrvC;
+    o1.pos = thisrvA.xyz;
+    o1.ahsl = thisA.ahsl;
+
+    o2.pos = thisrvB.xyz;
+    o2.ahsl = thisB.ahsl;
+
+    o3.pos = thisrvC.xyz;
+    o3.ahsl = thisC.ahsl;
 
     prio = thisPriority;
     dis = thisDistance;
   } else {
-    o1 = ivec4(0);
-    o2 = ivec4(0);
-    o3 = ivec4(0);
+    o1.pos = vec3(0);
+    o1.ahsl = 0;
+
+    o2.pos = vec3(0);
+    o2.ahsl = 0;
+
+    o3.pos = vec3(0);
+    o3.ahsl = 0;
+
     prio = 0;
     dis = 0;
   }
 }
 
-void add_face_prio_distance(uint localId, modelinfo minfo, ivec4 thisrvA, ivec4 thisrvB, ivec4 thisrvC, int thisPriority, int thisDistance, ivec4 pos) {
+void add_face_prio_distance(uint localId, modelinfo minfo, vert thisrvA, vert thisrvB, vert thisrvC, int thisPriority, int thisDistance, ivec4 pos) {
   if (localId < minfo.size) {
     // if the face is not culled, it is calculated into priority distance averages
-    if (face_visible(thisrvA, thisrvB, thisrvC, pos)) {
+    if (face_visible(thisrvA.pos, thisrvB.pos, thisrvC.pos, pos)) {
       atomicAdd(totalNum[thisPriority], 1);
       atomicAdd(totalDistance[thisPriority], thisDistance);
 
@@ -226,22 +237,7 @@ vec4 hillskew_vertexf(vec4 v, int hillskew, int y, int plane) {
   }
 }
 
-ivec4 hillskew_vertex(ivec4 v, int hillskew, int y, int plane) {
-  if (hillskew == 1) {
-    int px = v.x & 127;
-    int pz = v.z & 127;
-    int sx = v.x >> 7;
-    int sz = v.z >> 7;
-    int h1 = px * tile_height(plane, sx + 1, sz) + (128 - px) * tile_height(plane, sx, sz) >> 7;
-    int h2 = px * tile_height(plane, sx + 1, sz + 1) + (128 - px) * tile_height(plane, sx, sz + 1) >> 7;
-    int h3 = pz * h2 + (128 - pz) * h1 >> 7;
-    return ivec4(v.x, v.y + h3 - y, v.z, v.w);
-  } else {
-    return v;
-  }
-}
-
-void sort_and_insert(uint localId, modelinfo minfo, int thisPriority, int thisDistance, ivec4 thisrvA, ivec4 thisrvB, ivec4 thisrvC) {
+void sort_and_insert(uint localId, modelinfo minfo, int thisPriority, int thisDistance, vert thisrvA, vert thisrvB, vert thisrvC) {
   int size = minfo.size;
 
   if (localId < size) {
@@ -265,22 +261,22 @@ void sort_and_insert(uint localId, modelinfo minfo, int thisPriority, int thisDi
     }
 
     // position into scene
-    ivec4 pos = ivec4(minfo.x, minfo.y, minfo.z, 0);
-    thisrvA += pos;
-    thisrvB += pos;
-    thisrvC += pos;
+    vec4 pos = vec4(minfo.x, minfo.y, minfo.z, 0);
+    vec4 vertA = vec4(thisrvA.pos, 0) + pos;
+    vec4 vertB = vec4(thisrvB.pos, 0) + pos;
+    vec4 vertC = vec4(thisrvC.pos, 0) + pos;
 
     // apply hillskew
     int plane = (flags >> 24) & 3;
     int hillskew = (flags >> 26) & 1;
-    thisrvA = hillskew_vertex(thisrvA, hillskew, minfo.y, plane);
-    thisrvB = hillskew_vertex(thisrvB, hillskew, minfo.y, plane);
-    thisrvC = hillskew_vertex(thisrvC, hillskew, minfo.y, plane);
+    vertA = hillskew_vertexf(vertA, hillskew, minfo.y, plane);
+    vertB = hillskew_vertexf(vertB, hillskew, minfo.y, plane);
+    vertC = hillskew_vertexf(vertC, hillskew, minfo.y, plane);
 
     // write to out buffer
-    vout[outOffset + myOffset * 3] = thisrvA;
-    vout[outOffset + myOffset * 3 + 1] = thisrvB;
-    vout[outOffset + myOffset * 3 + 2] = thisrvC;
+    vout[outOffset + myOffset * 3] = vert(vertA.xyz, thisrvA.ahsl);
+    vout[outOffset + myOffset * 3 + 1] = vert(vertB.xyz, thisrvB.ahsl);
+    vout[outOffset + myOffset * 3 + 2] = vert(vertC.xyz, thisrvC.ahsl);
 
     if (toffset < 0) {
       uvout[outOffset + myOffset * 3] = vec4(0);
@@ -305,9 +301,9 @@ void sort_and_insert(uint localId, modelinfo minfo, int thisPriority, int thisDi
       texB = texB.yzwx;
       texC = texC.yzwx;
       // rotate
-      texA = rotatef(texA, orientation);
-      texB = rotatef(texB, orientation);
-      texC = rotatef(texC, orientation);
+      texA = rotate_vertex(texA, orientation);
+      texB = rotate_vertex(texB, orientation);
+      texC = rotate_vertex(texC, orientation);
       // position
       texA += pos;
       texB += pos;
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/vert.glsl b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/vert.glsl
index 816bd52ee..7c37d22d7 100644
--- a/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/vert.glsl
+++ b/runelite-client/src/main/resources/net/runelite/client/plugins/gpu/vert.glsl
@@ -25,15 +25,16 @@
 
 #version 330
 
-#define TILE_SIZE 128
+#define TILE_SIZE 128.f
 
 #define FOG_SCENE_EDGE_MIN ((-expandedMapLoadingChunks * 8 + 1) * TILE_SIZE)
 #define FOG_SCENE_EDGE_MAX ((104 + expandedMapLoadingChunks * 8 - 1) * TILE_SIZE)
 #define FOG_CORNER_ROUNDING 1.5
 #define FOG_CORNER_ROUNDING_SQUARED (FOG_CORNER_ROUNDING * FOG_CORNER_ROUNDING)
 
-layout(location = 0) in ivec4 VertexPosition;
-layout(location = 1) in vec4 uv;
+layout(location = 0) in vec3 vert;
+layout(location = 1) in int ahsl;
+layout(location = 2) in vec4 uv;
 
 layout(std140) uniform uniforms {
   float cameraYaw;
@@ -44,7 +45,6 @@ layout(std140) uniform uniforms {
   float cameraX;
   float cameraY;
   float cameraZ;
-  ivec2 sinCosTable[2048];
 };
 
 uniform float brightness;
@@ -53,7 +53,7 @@ uniform int fogDepth;
 uniform int drawDistance;
 uniform int expandedMapLoadingChunks;
 
-out ivec3 gVertex;
+out vec3 gVertex;
 out vec4 gColor;
 out float gHsl;
 out int gTextureId;
@@ -67,14 +67,12 @@ float fogFactorLinear(const float dist, const float start, const float end) {
 }
 
 void main() {
-  ivec3 vertex = VertexPosition.xyz;
-  int ahsl = VertexPosition.w;
   int hsl = ahsl & 0xffff;
   float a = float(ahsl >> 24 & 0xff) / 255.f;
 
   vec3 rgb = hslToRgb(hsl);
 
-  gVertex = vertex;
+  gVertex = vert;
   gColor = vec4(rgb, 1.f - a);
   gHsl = float(hsl);
 
@@ -83,14 +81,14 @@ void main() {
 
   // the client draws one less tile to the north and east than it does to the south
   // and west, so subtract a tiles width from the north and east edges.
-  int fogWest = max(FOG_SCENE_EDGE_MIN, int(cameraX) - drawDistance);
-  int fogEast = min(FOG_SCENE_EDGE_MAX, int(cameraX) + drawDistance - TILE_SIZE);
-  int fogSouth = max(FOG_SCENE_EDGE_MIN, int(cameraZ) - drawDistance);
-  int fogNorth = min(FOG_SCENE_EDGE_MAX, int(cameraZ) + drawDistance - TILE_SIZE);
+  float fogWest = max(FOG_SCENE_EDGE_MIN, cameraX - drawDistance);
+  float fogEast = min(FOG_SCENE_EDGE_MAX, cameraX + drawDistance - TILE_SIZE);
+  float fogSouth = max(FOG_SCENE_EDGE_MIN, cameraZ - drawDistance);
+  float fogNorth = min(FOG_SCENE_EDGE_MAX, cameraZ + drawDistance - TILE_SIZE);
 
   // Calculate distance from the scene edge
-  int xDist = min(vertex.x - fogWest, fogEast - vertex.x);
-  int zDist = min(vertex.z - fogSouth, fogNorth - vertex.z);
+  float xDist = min(vert.x - fogWest, fogEast - vert.x);
+  float zDist = min(vert.z - fogSouth, fogNorth - vert.z);
   float nearestEdgeDistance = min(xDist, zDist);
   float secondNearestEdgeDistance = max(xDist, zDist);
   float fogDistance =


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/chatcommands/ChatCommandsPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/chatcommands/ChatCommandsPluginTest.java
index 7a289aec0..c4082ac28 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
@@ -1272,6 +1272,24 @@ public class ChatCommandsPluginTest
 		verify(configManager).setRSProfileConfiguration("killcount", "guardians of the rift", 167);
 	}
 
+	@Test
+	public void testHunterRumours()
+	{
+		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", "You have completed <col=ff3045>77</col> rumours for the Hunter Guild.", null, 0);
+		chatCommandsPlugin.onChatMessage(chatMessageEvent);
+
+		verify(configManager).setRSProfileConfiguration("killcount", "hunter rumours", 77);
+	}
+
+	@Test
+	public void testHunterRumoursOver1k()
+	{
+		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", "You have completed <col=ff3045>1,032</col> rumours for the Hunter Guild.", null, 0);
+		chatCommandsPlugin.onChatMessage(chatMessageEvent);
+
+		verify(configManager).setRSProfileConfiguration("killcount", "hunter rumours", 1032);
+	}
+
 	@Test
 	public void testReward()
 	{
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);