Repository: runelite/runelite
Pull request #17802 (2024-04-28 21:51:40)

skillcalculator: Add irit tar
Add irit tar entry to the skill calculator.

As mentioned in #17801 


Patch:
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/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/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 225037203..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 = WorldPoint.fromCoord(locCoord);
-					Rock rock = Rock.ORE_VEIN;
-					RockRespawn rockRespawn = new RockRespawn(rock, worldPoint, Instant.now(), ticks * Constants.GAME_TICK_LENGTH, rock.getZOffset());
-					respawns.add(rockRespawn);
+					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/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/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;
+	}
+}