Repository: runelite/runelite
Pull request #17738 (2024-04-17 09:49:17)

events: mark ConfigChanged's newValue as nullable
As the comment already explained, this value is null when a single config is unset/reset



Patch:
diff --git a/cache/pom.xml b/cache/pom.xml
index 11baf04ef..f8ba078fa 100644
--- a/cache/pom.xml
+++ b/cache/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.27-SNAPSHOT</version>
+		<version>1.10.29-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cache</artifactId>
diff --git a/pom.xml b/pom.xml
index 87a216b2e..4c3ecc48c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
 
 	<groupId>net.runelite</groupId>
 	<artifactId>runelite-parent</artifactId>
-	<version>1.10.27-SNAPSHOT</version>
+	<version>1.10.29-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>RuneLite</name>
diff --git a/runelite-api/pom.xml b/runelite-api/pom.xml
index 68aa73683..2d47de52f 100644
--- a/runelite-api/pom.xml
+++ b/runelite-api/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.27-SNAPSHOT</version>
+		<version>1.10.29-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-api</artifactId>
diff --git a/runelite-api/src/main/interfaces/interfaces.toml b/runelite-api/src/main/interfaces/interfaces.toml
index 74c6d4c61..5b6ff84e9 100644
--- a/runelite-api/src/main/interfaces/interfaces.toml
+++ b/runelite-api/src/main/interfaces/interfaces.toml
@@ -7,9 +7,11 @@ id=741
 title=2
 text=3
 
+# this is actually just a generic menu
 [adventure_log]
 id=187
 container=0
+options=3
 
 [agility_arena]
 id=5
@@ -295,6 +297,7 @@ emote_scrollbar=4
 [equipment]
 id=387
 inventory_item_container=0
+cape=16
 
 [equipment_bonuses]
 id=84
@@ -668,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
@@ -723,6 +730,7 @@ emote_icon=48
 music_icon=49
 inventory_tab=55
 prayer_tab=57
+magic_tab=58
 cmb_icon=59
 skills_icon=60
 quests_icon=61
@@ -800,6 +808,7 @@ inventory_item_container=0
 
 [spellbook]
 id=218
+parent=0
 kourend_home_teleport=4
 catherby_home_teleport=5
 lumbridge_home_teleport=7
diff --git a/runelite-api/src/main/java/net/runelite/api/Client.java b/runelite-api/src/main/java/net/runelite/api/Client.java
index d94aa3382..6aef7976c 100644
--- a/runelite-api/src/main/java/net/runelite/api/Client.java
+++ b/runelite-api/src/main/java/net/runelite/api/Client.java
@@ -1415,6 +1415,21 @@ public interface Client extends OAuthApi, GameEngine
 	 */
 	String[] getStringStack();
 
+	/**
+	 * Get the size of one of the cs2 vm's arrays.
+	 * @param arrayId the array id
+	 * @return
+	 */
+	int getArraySizes(int arrayId);
+
+	/**
+	 * Get one of the cs2 vm's arrays. Use {@link #getArraySizes(int)} to get
+	 * the array length.
+	 * @param arrayId the array id
+	 * @return
+	 */
+	int[] getArray(int arrayId);
+
 	/**
 	 * Gets the cs2 vm's active widget
 	 *
@@ -2147,4 +2162,6 @@ public interface Client extends OAuthApi, GameEngine
 	void setMinimapTileDrawer(TileFunction drawTile);
 
 	Rasterizer getRasterizer();
+
+	void menuAction(int p0, int p1, MenuAction action, int id, int itemId, String option, String target);
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/EnumID.java b/runelite-api/src/main/java/net/runelite/api/EnumID.java
index 478aeffcc..c8618f6d1 100644
--- a/runelite-api/src/main/java/net/runelite/api/EnumID.java
+++ b/runelite-api/src/main/java/net/runelite/api/EnumID.java
@@ -90,4 +90,7 @@ public final class EnumID
 	 * val: enum
 	 */
 	public static final int WEAPON_STYLES = 3908;
+
+	public static final int SPELLBOOKS = 1981;
+	public static final int SPELLBOOKS_SUB = 5280;
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/ItemID.java b/runelite-api/src/main/java/net/runelite/api/ItemID.java
index bb5462df6..daecbfb1b 100644
--- a/runelite-api/src/main/java/net/runelite/api/ItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ItemID.java
@@ -13970,19 +13970,21 @@ public final class ItemID
 	public static final int SUNFIRE_FANATIC_ARMOUR_SET = 29424;
 	public static final int HERBALISTS_NOTES = 29427;
 	public static final int CHEFS_NOTES = 29428;
-	public static final int YOLKLORE_OF_AYASTER = 29431;
 	public static final int BOOK_OF_EGG = 29433;
 	public static final int BOOK_OF_EGG_29435 = 29435;
 	public static final int EGG_PRIEST_ROBE = 29437;
 	public static final int EGG_PRIEST_ROBE_TOP = 29439;
 	public static final int EGG_PRIEST_NECKLACE = 29441;
 	public static final int EGG_PRIEST_MITRE = 29443;
-	public static final int EASTER_EGG_MINED = 29445;
-	public static final int EGGNAPPIN_SACK = 29446;
 	public static final int IMBUED_MAGE_ARENA_CAPE = 29448;
 	public static final int ZOMBIE_PIRATE_KEY = 29449;
 	public static final int TELEPORT_ANCHORING_SCROLL = 29455;
 	public static final int ADAMANT_SEEDS = 29458;
 	public static final int WILDERNESS_AGILITY_TICKET = 29460;
+	public static final int SMALL_MEAT_POUCH_OPEN = 29462;
+	public static final int LARGE_MEAT_POUCH_OPEN = 29464;
+	public static final int SMALL_FUR_POUCH_OPEN = 29466;
+	public static final int MEDIUM_FUR_POUCH_OPEN = 29468;
+	public static final int LARGE_FUR_POUCH_OPEN = 29470;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/MenuAction.java b/runelite-api/src/main/java/net/runelite/api/MenuAction.java
index 1a590c0d9..41bb1af3f 100644
--- a/runelite-api/src/main/java/net/runelite/api/MenuAction.java
+++ b/runelite-api/src/main/java/net/runelite/api/MenuAction.java
@@ -244,6 +244,12 @@ public enum MenuAction
 	 */
 	WIDGET_TARGET_ON_WIDGET(58),
 
+	/**
+	 * Submenu parent that is also a widget.
+	 * see {@link #RUNELITE_SUBMENU}
+	 */
+	RUNELITE_SUBMENU_WIDGET(998),
+
 	/**
 	 * Menu action for high priority runelite options
 	 */
diff --git a/runelite-api/src/main/java/net/runelite/api/NpcID.java b/runelite-api/src/main/java/net/runelite/api/NpcID.java
index e53832fcc..f23363a07 100644
--- a/runelite-api/src/main/java/net/runelite/api/NpcID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NpcID.java
@@ -11286,49 +11286,6 @@ public final class NpcID
 	public static final int GUILD_HUNTER_FOX_13426 = 13426;
 	public static final int RED_SKINK = 13427;
 	public static final int RED_SKINK_13428 = 13428;
-	public static final int ANIMATED_EGG = 13429;
-	public static final int ANIMATED_EGG_13430 = 13430;
-	public static final int ANIMATED_EGG_13431 = 13431;
-	public static final int ANIMATED_EGG_13432 = 13432;
-	public static final int EASTER_BUNNY = 13433;
-	public static final int EASTER_BUNNY_13434 = 13434;
-	public static final int EASTER_BUNNY_13435 = 13435;
-	public static final int EG = 13436;
-	public static final int EGG_13437 = 13437;
-	public static final int EGGY = 13438;
-	public static final int EASTER_EGG_BIRD = 13439;
-	public static final int EASTER_EGG_BIRD_13440 = 13440;
-	public static final int EGGUS_MANUS = 13441;
-	public static final int EGGUS_MANUS_13442 = 13442;
-	public static final int EGG_PRIEST = 13443;
-	public static final int EGG_PRIEST_13444 = 13444;
-	public static final int EGG_PRIEST_13445 = 13445;
-	public static final int EGG_PRIEST_13446 = 13446;
-	public static final int EGG_PRIEST_13447 = 13447;
-	public static final int EGG_PRIEST_13448 = 13448;
-	public static final int EGG_PRIEST_13449 = 13449;
-	public static final int EGG_PRIEST_13450 = 13450;
-	public static final int EGG_PRIEST_13451 = 13451;
-	public static final int EGG_PRIEST_13452 = 13452;
-	public static final int SHIFTY_LOOKING_PRIEST = 13453;
-	public static final int PROTESTER_13454 = 13454;
-	public static final int UPSET_EASTER_EGG_HUNTER = 13455;
-	public static final int RASCALLY_CHILD = 13456;
-	public static final int DISAPPOINTED_MOTHER = 13457;
-	public static final int DISAPPOINTED_DAUGHTER = 13458;
-	public static final int DISAPPOINTED_FATHER = 13459;
-	public static final int EGG_MINE_CART = 13460;
-	public static final int EGG_MINE_CART_13461 = 13461;
-	public static final int MISBEHAVING_MINE_CART = 13462;
-	public static final int MISBEHAVING_MINE_CART_13463 = 13463;
-	public static final int MISBEHAVING_MINE_CART_13464 = 13464;
-	public static final int MISBEHAVING_MINE_CART_13465 = 13465;
-	public static final int EGG_PRIEST_13466 = 13466;
-	public static final int EGG_PRIEST_13467 = 13467;
-	public static final int EGG_PRIEST_13468 = 13468;
-	public static final int RABBIT_13469 = 13469;
-	public static final int RABBIT_13470 = 13470;
-	public static final int CHICKEN_13471 = 13471;
 	public static final int ENRAGED_BLOOD_MOON = 13485;
 	public static final int ENRAGED_BLUE_MOON = 13486;
 	public static final int ENRAGED_ECLIPSE_MOON = 13487;
diff --git a/runelite-api/src/main/java/net/runelite/api/NullItemID.java b/runelite-api/src/main/java/net/runelite/api/NullItemID.java
index b97374b48..4b666c304 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullItemID.java
@@ -15250,6 +15250,7 @@ public final class NullItemID
 	public static final int NULL_29426 = 29426;
 	public static final int NULL_29429 = 29429;
 	public static final int NULL_29430 = 29430;
+	public static final int NULL_29431 = 29431;
 	public static final int NULL_29432 = 29432;
 	public static final int NULL_29434 = 29434;
 	public static final int NULL_29436 = 29436;
@@ -15257,6 +15258,8 @@ public final class NullItemID
 	public static final int NULL_29440 = 29440;
 	public static final int NULL_29442 = 29442;
 	public static final int NULL_29444 = 29444;
+	public static final int NULL_29445 = 29445;
+	public static final int NULL_29446 = 29446;
 	public static final int NULL_29447 = 29447;
 	public static final int NULL_29450 = 29450;
 	public static final int NULL_29451 = 29451;
@@ -15267,5 +15270,10 @@ public final class NullItemID
 	public static final int NULL_29457 = 29457;
 	public static final int NULL_29459 = 29459;
 	public static final int NULL_29461 = 29461;
+	public static final int NULL_29463 = 29463;
+	public static final int NULL_29465 = 29465;
+	public static final int NULL_29467 = 29467;
+	public static final int NULL_29469 = 29469;
+	public static final int NULL_29471 = 29471;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/NullNpcID.java b/runelite-api/src/main/java/net/runelite/api/NullNpcID.java
index 2f623a39a..6b64f94e0 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullNpcID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullNpcID.java
@@ -2124,18 +2124,5 @@ public final class NullNpcID
 	public static final int NULL_13418 = 13418;
 	public static final int NULL_13419 = 13419;
 	public static final int NULL_13422 = 13422;
-	public static final int NULL_13472 = 13472;
-	public static final int NULL_13473 = 13473;
-	public static final int NULL_13474 = 13474;
-	public static final int NULL_13475 = 13475;
-	public static final int NULL_13476 = 13476;
-	public static final int NULL_13477 = 13477;
-	public static final int NULL_13478 = 13478;
-	public static final int NULL_13479 = 13479;
-	public static final int NULL_13480 = 13480;
-	public static final int NULL_13481 = 13481;
-	public static final int NULL_13482 = 13482;
-	public static final int NULL_13483 = 13483;
-	public static final int NULL_13484 = 13484;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/NullObjectID.java b/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
index ab4a330ee..b2c3c967b 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
@@ -27497,7 +27497,6 @@ public final class NullObjectID
 	public static final int NULL_53043 = 53043;
 	public static final int NULL_53044 = 53044;
 	public static final int NULL_53047 = 53047;
-	public static final int NULL_53048 = 53048;
 	public static final int NULL_53051 = 53051;
 	public static final int NULL_53052 = 53052;
 	public static final int NULL_53053 = 53053;
@@ -27585,14 +27584,17 @@ public final class NullObjectID
 	public static final int NULL_53211 = 53211;
 	public static final int NULL_53212 = 53212;
 	public static final int NULL_53213 = 53213;
-	public static final int NULL_53214 = 53214;
-	public static final int NULL_53215 = 53215;
-	public static final int NULL_53216 = 53216;
-	public static final int NULL_53217 = 53217;
-	public static final int NULL_53218 = 53218;
 	public static final int NULL_53219 = 53219;
 	public static final int NULL_53220 = 53220;
 	public static final int NULL_53221 = 53221;
 	public static final int NULL_53223 = 53223;
+	public static final int NULL_53226 = 53226;
+	public static final int NULL_53227 = 53227;
+	public static final int NULL_53228 = 53228;
+	public static final int NULL_53229 = 53229;
+	public static final int NULL_53230 = 53230;
+	public static final int NULL_53231 = 53231;
+	public static final int NULL_53232 = 53232;
+	public static final int NULL_53233 = 53233;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/ObjectID.java b/runelite-api/src/main/java/net/runelite/api/ObjectID.java
index 551b195c6..108182aaa 100644
--- a/runelite-api/src/main/java/net/runelite/api/ObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ObjectID.java
@@ -25534,7 +25534,8 @@ public final class ObjectID
 	public static final int COLLAPSED_TRAP_53024 = 53024;
 	public static final int CRYPT_STAIRCASE = 53045;
 	public static final int CRYPT_STAIRCASE_53046 = 53046;
-	public static final int UNLIT_BRAZIER = 53049;
+	public static final int UNLIT_BRAZIER = 53048;
+	public static final int UNLIT_BRAZIER_53049 = 53049;
 	public static final int BRAZIER_53050 = 53050;
 	public static final int TRAIL_OF_CHOCOLATE = 53054;
 	public static final int TRAIL_OF_CHOCOLATE_53055 = 53055;
@@ -25612,7 +25613,13 @@ public final class ObjectID
 	public static final int SKELETON_53190 = 53190;
 	public static final int SKELETON_53191 = 53191;
 	public static final int SKELETON_53192 = 53192;
+	public static final int EASTER_MINE_CART_53214 = 53214;
+	public static final int EASTER_MINE_CART_53215 = 53215;
+	public static final int EASTER_MINE_CART_53216 = 53216;
+	public static final int EASTER_MINE_CART_53217 = 53217;
+	public static final int EASTER_MINE_CART_53218 = 53218;
 	public static final int ZOMBIE_PIRATES_LOCKER = 53222;
 	public static final int AGILITY_DISPENSER = 53224;
+	public static final int SPIKES_53225 = 53225;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/ParamID.java b/runelite-api/src/main/java/net/runelite/api/ParamID.java
index 00babf805..cebb1e63e 100644
--- a/runelite-api/src/main/java/net/runelite/api/ParamID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ParamID.java
@@ -67,4 +67,8 @@ public final class ParamID
 	public static final int SLAYER_TASK_NAME = 1801;
 
 	public static final int ATTACK_STYLE_NAME = 1407;
+
+	public static final int SPELL_BUTTON = 596;
+	public static final int SPELL_NAME = 601;
+	public static final int SPELL_LEVELREQ = 604;
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/ScriptID.java b/runelite-api/src/main/java/net/runelite/api/ScriptID.java
index ec9691a63..062ae1a9c 100644
--- a/runelite-api/src/main/java/net/runelite/api/ScriptID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ScriptID.java
@@ -452,4 +452,7 @@ public final class ScriptID
 
 	@ScriptArguments(integer = 6)
 	public static final int EQUIPMENT_SET_STAT_BONUS_SETUP = 3517;
+
+	@ScriptArguments(integer = 12, string = 2)
+	public static final int MAGIC_SPELLBOOK_INITIALISESPELLS = 2616;
 }
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 b4d3a5d98..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;
 	/**
@@ -240,4 +242,12 @@ public final class VarPlayer
 	 * Assigned slayer task location. The mapping of value to name can be found in {@link EnumID#SLAYER_TASK_LOCATION}
 	 */
 	public static final int SLAYER_TASK_LOCATION = 2096;
+
+	/**
+	 * Determines whether the woodcutting group bonus should be displayed on the buff bar or not.
+	 * 96 = displayed (including the woodcutting guild).
+	 * 0 = not displayed (after login until cutting a tree except normal trees or trees grown through farming).
+	 * -1 = not displayed (including normal trees or trees grown through farming).
+	 */
+	public static final int BUFF_BAR_WC_GROUP_BONUS = 4007;
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/Varbits.java b/runelite-api/src/main/java/net/runelite/api/Varbits.java
index f538fd62e..2384a2c96 100644
--- a/runelite-api/src/main/java/net/runelite/api/Varbits.java
+++ b/runelite-api/src/main/java/net/runelite/api/Varbits.java
@@ -886,4 +886,7 @@ public final class Varbits
 	 * 1 = active
 	 */
 	public static final int SPELLBOOK_SWAP = 3617;
+
+	public static final int SPELLBOOK = 4070;
+	public static final int SPELLBOOK_SUBMENU = 9730;
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java b/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java
index 707604f2c..077e004b1 100644
--- a/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java
+++ b/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java
@@ -543,4 +543,12 @@ public class WorldPoint
 	{
 		return new WorldArea(this, 1, 1);
 	}
+
+	/**
+	 * Create a WorldPoint from a packed Jagex coordinate
+	 */
+	public static WorldPoint fromCoord(int c)
+	{
+		return new WorldPoint((c >>> 14) & 0x3FFF, c & 0x3FFF, (c >>> 28) & 0x3);
+	}
 }
diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml
index 6f353d404..c29a8a6b7 100644
--- a/runelite-client/pom.xml
+++ b/runelite-client/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.27-SNAPSHOT</version>
+		<version>1.10.29-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>client</artifactId>
diff --git a/runelite-client/src/main/java/net/runelite/client/config/Keybind.java b/runelite-client/src/main/java/net/runelite/client/config/Keybind.java
index eba500b85..4ba1ada45 100644
--- a/runelite-client/src/main/java/net/runelite/client/config/Keybind.java
+++ b/runelite-client/src/main/java/net/runelite/client/config/Keybind.java
@@ -125,9 +125,17 @@ public class Keybind
 			keyCode = KeyEvent.VK_UNDEFINED;
 		}
 
-		if (e.getID() == KeyEvent.KEY_RELEASED && keyCode != KeyEvent.VK_UNDEFINED)
+		if (e.getID() == KeyEvent.KEY_RELEASED)
 		{
-			return this.keyCode == keyCode;
+			if (keyCode != KeyEvent.VK_UNDEFINED)
+			{
+				return this.keyCode == keyCode;
+			}
+			else
+			{
+				assert mf != null;
+				return this.keyCode == keyCode && (this.modifiers & modifiers) == this.modifiers && ((mf & this.modifiers) == mf);
+			}
 		}
 
 		if (ignoreModifiers && keyCode != KeyEvent.VK_UNDEFINED)
diff --git a/runelite-client/src/main/java/net/runelite/client/events/ConfigChanged.java b/runelite-client/src/main/java/net/runelite/client/events/ConfigChanged.java
index db79034c2..9f7762d88 100644
--- a/runelite-client/src/main/java/net/runelite/client/events/ConfigChanged.java
+++ b/runelite-client/src/main/java/net/runelite/client/events/ConfigChanged.java
@@ -60,5 +60,6 @@ public class ConfigChanged
 	/**
 	 * The new value of the entry, null if the entry has been unset.
 	 */
+	@Nullable
 	private String newValue;
 }
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/cluescrolls/clues/CoordinateClue.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CoordinateClue.java
index ac49013e2..c62e666c9 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CoordinateClue.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CoordinateClue.java
@@ -179,7 +179,7 @@ public class CoordinateClue extends ClueScroll implements LocationClueScroll
 		.put(new WorldPoint(3189, 3963, 0), new CoordinateClueInfo("Wilderness. North of Resource Area, near magic axe hut.", ZAMORAK_WIZARD))
 		.put(new WorldPoint(2341, 3697, 0), new CoordinateClueInfo("North-east of the Piscatoris Fishing Colony bank.", SARADOMIN_WIZARD))
 		.put(new WorldPoint(3143, 3774, 0), new CoordinateClueInfo("In level 32 Wilderness, by the black chinchompa hunting area.", ZAMORAK_WIZARD))
-		.put(new WorldPoint(2992, 3941, 0), new CoordinateClueInfo("Wilderness Agility Course, past the log balance.", ZAMORAK_WIZARD))
+		.put(new WorldPoint(2970, 3913, 0), new CoordinateClueInfo("Frozen Waste Plateau, south-west of Wilderness Agility Course.", ZAMORAK_WIZARD))
 		.put(new WorldPoint(1410, 3611, 0), new CoordinateClueInfo("Lake Molch dock west of Shayzien Encampment.", SARADOMIN_WIZARD))
 		.put(new WorldPoint(1409, 3483, 0), new CoordinateClueInfo("South of Shayziens' Wall.", SARADOMIN_WIZARD))
 		// Elite
@@ -209,7 +209,7 @@ public class CoordinateClue extends ClueScroll implements LocationClueScroll
 		.put(new WorldPoint(3369, 3894, 0), new CoordinateClueInfo("Wilderness. Fountain of Rune.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
 		.put(new WorldPoint(2065, 3923, 0), new CoordinateClueInfo("Outside the western wall on Lunar Isle.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
 		.put(new WorldPoint(3188, 3933, 0), new CoordinateClueInfo("Wilderness. Resource Area.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
-		.put(new WorldPoint(2997, 3953, 0), new CoordinateClueInfo("Wilderness. Inside Agility Training Area.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
+		.put(new WorldPoint(3043, 3940, 0), new CoordinateClueInfo("Wilderness. South of Pirates' Hideout.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
 		.put(new WorldPoint(3380, 3963, 0), new CoordinateClueInfo("Wilderness. North of Volcano.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
 		.put(new WorldPoint(3051, 3736, 0), new CoordinateClueInfo("East of the Wilderness Obelisk in 28 Wilderness.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
 		.put(new WorldPoint(2316, 3814, 0), new CoordinateClueInfo("West of Neitiznot, near the bridge.", ARMADYLEAN_OR_BANDOSIAN_GUARD))
@@ -248,7 +248,7 @@ public class CoordinateClue extends ClueScroll implements LocationClueScroll
 		.put(new WorldPoint(3380, 3929, 0), new CoordinateClueInfo("Wilderness. Near Volcano.", ANCIENT_WIZARDS))
 		.put(new WorldPoint(3188, 3939, 0), new CoordinateClueInfo("Wilderness. Resource Area.", BRASSICAN_MAGE))
 		.put(new WorldPoint(3304, 3941, 0), new CoordinateClueInfo("Wilderness. East of Rogues' Castle.", ANCIENT_WIZARDS))
-		.put(new WorldPoint(2994, 3961, 0), new CoordinateClueInfo("Wilderness. Inside Agility Training Area.", BRASSICAN_MAGE))
+		.put(new WorldPoint(3028, 3928, 0), new CoordinateClueInfo("Wilderness. South-east of Agility Training Area.", BRASSICAN_MAGE))
 		.put(new WorldPoint(1769, 3418, 0), new CoordinateClueInfo("Crabclaw Isle", ANCIENT_WIZARDS))
 		.build();
 
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java
index 3071d3d7c..572595cf5 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java
@@ -490,6 +490,13 @@ class ConfigPanel extends PluginPanel
 		JFormattedTextField spinnerTextField = ((JSpinner.DefaultEditor) editor).getTextField();
 		spinnerTextField.setColumns(SPINNER_FIELD_WIDTH);
 		spinner.addChangeListener(ce -> changeConfiguration(spinner, cd, cid));
+
+		Units units = cid.getUnits();
+		if (units != null)
+		{
+			spinnerTextField.setFormatterFactory(new UnitFormatterFactory(units.value()));
+		}
+
 		return spinner;
 	}
 
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/NotificationPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/NotificationPanel.java
index 23a559cf2..d1098df24 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/config/NotificationPanel.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/NotificationPanel.java
@@ -343,7 +343,7 @@ class NotificationPanel extends PluginPanel
 					saveNotification(n.withFlash((FlashNotification) comboboxFlash.getSelectedItem()));
 				}
 			});
-			item("Flash", "Flashes the game frame as a notification", combobox(FlashNotification.class, notif.getFlash()));
+			item("Flash", "Flashes the game frame as a notification", comboboxFlash);
 
 			var colorpickerFlashColor = createColorPicker("Flash color", notif.getFlashColor(), c ->
 			{
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java
index 5a753baba..05408ee19 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java
@@ -57,11 +57,6 @@ class PluginConfigurationDescriptor
 	@Nullable
 	private final List<String> conflicts;
 
-	boolean hasConfigurables()
-	{
-		return configDescriptor != null && !configDescriptor.getItems().stream().allMatch(item -> item.getItem().hidden());
-	}
-
 	PluginConfigurationDescriptor(String name, String description, String[] tags, Config config, ConfigDescriptor configDescriptor)
 	{
 		this(name, description, tags, null, config, configDescriptor, null);
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java
index 880da7877..91436e2b5 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java
@@ -128,7 +128,7 @@ class PluginListItem extends JPanel implements SearchablePlugin
 		add(buttonPanel, BorderLayout.LINE_END);
 
 		JMenuItem configMenuItem = null;
-		if (pluginConfig.hasConfigurables())
+		if (pluginConfig.getConfigDescriptor() != null)
 		{
 			JButton configButton = new JButton(ConfigPanel.CONFIG_ICON);
 			SwingUtil.removeButtonDecorations(configButton);
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 ea51c350b..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,8 +51,9 @@ 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;
@@ -72,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 eb6da70f0..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();
 
@@ -405,13 +405,24 @@ public class GroundItemsOverlay extends Overlay
 
 		final Instant now = Instant.now();
 		final Instant despawnTime = spawnTime.plus(groundItem.getDespawnTime());
-		final Instant visibleTime = spawnTime.plus(groundItem.getVisibleTime());
 
-		if (visibleTime != null && visibleTime.isAfter(now))
+		if (groundItem.isPrivate())
+		{
+			if (despawnTime.isAfter(now))
+			{
+				return PRIVATE_TIMER_COLOR;
+			}
+
+			return null;
+		}
+
+		// otherwise it is private until visibleTime, then it is public
+		final Instant visibleTime = spawnTime.plus(groundItem.getVisibleTime());
+		if (visibleTime.isAfter(now))
 		{
 			return PRIVATE_TIMER_COLOR;
 		}
-		if (despawnTime != null && despawnTime.isAfter(now))
+		if (despawnTime.isAfter(now))
 		{
 			return PUBLIC_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 02326f703..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(Duration.of(item.getVisibleTime(), RSTimeUnit.GAME_TICKS))
+			.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/idlenotifier/IdleNotifierPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java
index 5e711f4ea..79361c1d9 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java
@@ -52,6 +52,7 @@ import net.runelite.api.events.GraphicChanged;
 import net.runelite.api.events.HitsplatApplied;
 import net.runelite.api.events.InteractingChanged;
 import net.runelite.api.events.NpcChanged;
+import net.runelite.api.events.VarbitChanged;
 import net.runelite.client.Notifier;
 import net.runelite.client.config.ConfigManager;
 import net.runelite.client.eventbus.Subscribe;
@@ -103,6 +104,7 @@ public class IdleNotifierPlugin extends Plugin
 	private Instant sixHourWarningTime;
 	private boolean ready;
 	private boolean lastInteractWasCombat;
+	private static final int BUFF_BAR_NOT_DISPLAYED = -1;
 
 	@Provides
 	IdleNotifierConfig provideConfig(ConfigManager configManager)
@@ -575,6 +577,17 @@ public class IdleNotifierPlugin extends Plugin
 		}
 	}
 
+	@Subscribe
+	public void onVarbitChanged(VarbitChanged event)
+	{
+		if (event.getVarpId() == VarPlayer.BUFF_BAR_WC_GROUP_BONUS && event.getValue() == BUFF_BAR_NOT_DISPLAYED)
+		{
+			resetTimers();
+			lastAnimation = WOODCUTTING_RUNE;
+			lastAnimating = Instant.now();
+		}
+	}
+
 	private void checkNpcInteraction(final NPC target)
 	{
 		final NPCComposition npcComposition = target.getComposition();
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 7069265d7..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
@@ -424,6 +424,17 @@ public interface MenuEntrySwapperConfig extends Config
 		return false;
 	}
 
+	@ConfigItem(
+		keyName = "teleportSubmenus",
+		name = "Teleport submenus",
+		description = "Use submenus for max, construction, hunter, and diary cape teleports",
+		section = itemSection
+	)
+	default boolean teleportSubmenus()
+	{
+		return false;
+	}
+
 	@ConfigItem(
 		keyName = "swapTeleToPoh",
 		name = "Tele to POH",
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 7ea832c36..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
@@ -27,10 +27,12 @@
 package net.runelite.client.plugins.menuentryswapper;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import static com.google.common.base.Predicates.alwaysTrue;
 import static com.google.common.base.Predicates.equalTo;
 import com.google.common.base.Strings;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.LinkedHashMultimap;
@@ -45,11 +47,13 @@ import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 import javax.inject.Inject;
 import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.ChatMessageType;
 import net.runelite.api.Client;
 import net.runelite.api.ItemComposition;
+import net.runelite.api.ItemID;
 import net.runelite.api.KeyCode;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
@@ -57,10 +61,12 @@ import net.runelite.api.NPC;
 import net.runelite.api.NPCComposition;
 import net.runelite.api.ObjectComposition;
 import net.runelite.api.ParamID;
+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;
 import net.runelite.api.widgets.Widget;
 import net.runelite.api.widgets.WidgetConfig;
@@ -71,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;
@@ -158,6 +163,8 @@ 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)
@@ -169,6 +176,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 	public void startUp()
 	{
 		setupSwaps();
+		setupTeleportSwaps();
 		removeOldSwaps();
 	}
 
@@ -176,6 +184,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 	public void shutDown()
 	{
 		swaps.clear();
+		teleportSwaps.clear();
 	}
 
 	@VisibleForTesting
@@ -412,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);
@@ -525,13 +516,6 @@ public class MenuEntrySwapperPlugin extends Plugin
 						continue;
 					}
 
-					if ("Build".equals(actions[actionIdx])
-						|| "Remove".equals(actions[actionIdx]))
-					{
-						// https://secure.runescape.com/m=news/third-party-client-guidelines?oldschool=1
-						continue;
-					}
-
 					final MenuAction menuAction = OBJECT_MENU_TYPES.get(actionIdx);
 					if (menuAction != currentAction)
 					{
@@ -895,6 +879,34 @@ public class MenuEntrySwapperPlugin extends Plugin
 						}
 					}
 
+					if (config.teleportSubmenus())
+					{
+						var subSwaps = teleportSwaps.get(itemComposition.getId())
+							.stream()
+							.filter(ts -> ts.worn)
+							.collect(Collectors.toList());
+						for (TeleportSwap top : subSwaps)
+						{
+							for (TeleportSub sub : top.subs)
+							{
+								if (leftClickOp == null || leftClickOp != sub.option.hashCode())
+								{
+									leftClickMenus.add(client.createMenuEntry(idx)
+										.setOption(sub.option)
+										.setType(MenuAction.RUNELITE)
+										.onClick(wornItemConsumer(itemComposition, sub.option, sub.option.hashCode(), false)));
+								}
+								if (shiftClickOp == null || shiftClickOp != sub.option.hashCode())
+								{
+									shiftClickMenus.add(client.createMenuEntry(idx)
+										.setOption(sub.option)
+										.setType(MenuAction.RUNELITE)
+										.onClick(wornItemConsumer(itemComposition, sub.option, sub.option.hashCode(), true)));
+								}
+							}
+						}
+					}
+
 					if (leftClickOp != null)
 					{
 						leftClickMenus.add(client.createMenuEntry(idx)
@@ -1053,6 +1065,34 @@ public class MenuEntrySwapperPlugin extends Plugin
 					}
 				}
 
+				if (config.teleportSubmenus())
+				{
+					var subSwaps = teleportSwaps.get(itemComposition.getId())
+						.stream()
+						.filter(ts -> ts.held)
+						.collect(Collectors.toList());
+					for (TeleportSwap top : subSwaps)
+					{
+						for (TeleportSub sub : top.subs)
+						{
+							if (leftClickOp == null || leftClickOp != sub.option.hashCode())
+							{
+								leftClickMenus.add(client.createMenuEntry(idx)
+									.setOption(sub.option)
+									.setType(MenuAction.RUNELITE)
+									.onClick(heldItemConsumer(itemComposition, sub.option, sub.option.hashCode(), false)));
+							}
+							if (shiftClickOp == null || shiftClickOp != sub.option.hashCode())
+							{
+								shiftClickMenus.add(client.createMenuEntry(idx)
+									.setOption(sub.option)
+									.setType(MenuAction.RUNELITE)
+									.onClick(heldItemConsumer(itemComposition, sub.option, sub.option.hashCode(), true)));
+							}
+						}
+					}
+				}
+
 				if (leftClickOp != null && config.leftClickCustomization())
 				{
 					leftClickMenus.add(client.createMenuEntry(idx)
@@ -1420,51 +1460,29 @@ public class MenuEntrySwapperPlugin extends Plugin
 		final String option = Text.removeTags(menuEntry.getOption()).toLowerCase();
 		final String target = Text.removeTags(menuEntry.getTarget()).toLowerCase();
 
-		final boolean itemOp = menuEntry.isItemOp();
-		// Custom shift-click item swap
-		if (shiftModifier() && itemOp)
+		final Widget w = menuEntry.getParent() != null ? menuEntry.getParent().getWidget() : menuEntry.getWidget();
+		// Custom item swap
+		if (w != null && WidgetUtil.componentToInterface(w.getId()) == InterfaceID.INVENTORY
+			&& (lastShift ? config.shiftClickCustomization() : config.leftClickCustomization()))
 		{
-			// Special case use shift click due to items not actually containing a "Use" option, making
-			// the client unable to perform the swap itself.
-			if (config.shiftClickCustomization() && !option.equals("use"))
-			{
-				Integer customOption = getItemSwapConfig(true, menuEntry.getItemId());
-
-				if (customOption != null && customOption == -1)
-				{
-					swap(menuEntries, "use", target, index, true);
-				}
-			}
-
-			// 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
-		if (itemOp && config.leftClickCustomization())
-		{
-			Integer swapIndex = getItemSwapConfig(false, menuEntry.getItemId());
+			Integer swapIndex = getItemSwapConfig(lastShift, w.getItemId());
 			if (swapIndex != null)
 			{
-				final int swapAction = swapIndex >= 0
-					? 1 + swapIndex
-					: -1;
-
-				if (swapAction == -1)
+				if (swapIndex == -1)
 				{
 					swap(menuEntries, "use", target, index, true);
 				}
-				else if (swapAction == menuEntry.getItemOp())
+				else if (swapIndex + 1 == menuEntry.getItemOp()
+					|| (menuEntry.getParent() != null && menuEntry.getOption().hashCode() == swapIndex))
 				{
 					swap(optionIndexes, menuEntries, index, menuEntries.length - 1);
+					menuEntry.setParent(null);
 				}
 				return;
 			}
 		}
 
 		// Worn items swap
-		final Widget w = menuEntry.getWidget();
 		if (w != null && WidgetUtil.componentToInterface(w.getId()) == InterfaceID.EQUIPMENT)
 		{
 			Widget child = w.getChild(1);
@@ -1473,9 +1491,11 @@ public class MenuEntrySwapperPlugin extends Plugin
 				final Integer wornItemSwapConfig = getWornItemSwapConfig(shiftModifier(), child.getItemId());
 				if (wornItemSwapConfig != null)
 				{
-					if (wornItemSwapConfig == menuEntry.getIdentifier())
+					if (wornItemSwapConfig == menuEntry.getIdentifier()
+						|| (menuEntry.getParent() != null && menuEntry.getOption().hashCode() == wornItemSwapConfig))
 					{
 						swap(optionIndexes, menuEntries, index, menuEntries.length - 1);
+						menuEntry.setParent(null); // Move from a submenu to a top level menu
 					}
 					return;
 				}
@@ -1535,7 +1555,8 @@ public class MenuEntrySwapperPlugin extends Plugin
 		if ((menuAction == MenuAction.CC_OP || menuAction == MenuAction.CC_OP_LOW_PRIORITY || menuAction == MenuAction.WIDGET_TARGET)
 			&& w != null && (w.getIndex() == -1 || w.getItemId() != -1)
 			&& w.getActions() != null
-			&& !itemOp && WidgetUtil.componentToInterface(w.getId()) != InterfaceID.EQUIPMENT)
+			&& WidgetUtil.componentToInterface(w.getId()) != InterfaceID.INVENTORY
+			&& WidgetUtil.componentToInterface(w.getId()) != InterfaceID.EQUIPMENT)
 		{
 			// fast check to avoid hitting config on components with single ops
 			if ((index > 0 && menuEntries[index - 1].getWidget() == w) ||
@@ -1584,6 +1605,9 @@ public class MenuEntrySwapperPlugin extends Plugin
 	@Subscribe
 	public void onClientTick(ClientTick clientTick)
 	{
+		lastShift = curShift;
+		curShift = shiftModifier();
+
 		if (client.isMenuOpen())
 		{
 			return;
@@ -1699,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 < itemComposition.getInventoryActions().length)
-		{
-			itemComposition.setShiftClickActionIndex(option);
-		}
-	}
-
 	private boolean swap(MenuEntry[] menuEntries, String option, String target, int index, boolean strict)
 	{
 		// find option to swap with
@@ -1945,4 +1950,259 @@ public class MenuEntrySwapperPlugin extends Plugin
 		configManager.unsetConfiguration(MenuEntrySwapperConfig.GROUP,
 			(shift ? UI_SHIFT_KEY_PREFIX : UI_KEY_PREFIX) + componentId + (itemId != -1 ? "_" + itemId : ""));
 	}
+
+	private void setupTeleportSwaps()
+	{
+		// region Max cape opworn
+		teleportSwap("Fishing Teleports", ItemID.MAX_CAPE_13342)
+			.worn()
+			.addSub("Fishing Guild", () -> pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 1))
+			.addSub("Otto's Grotto", () -> pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 2));
+		teleportSwap("POH Portals", ItemID.MAX_CAPE_13342)
+			.worn()
+			.addSub("Home", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 0))
+			.addSub("Rimmington", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 1))
+			.addSub("Taverley", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 2))
+			.addSub("Pollnivneach", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 3))
+			.addSub("Hosidius", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 4))
+			.addSub("Rellekka", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 5))
+			.addSub("Brimhaven", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6))
+			.addSub("Yanille", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 7))
+			.addSub("Prifddinas", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 8));
+		teleportSwap("Other Teleports", ItemID.MAX_CAPE_13342)
+			.worn()
+			.addSub("Feldip hills", () ->
+			{
+				pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 1); // Chinchompa Teleports
+				pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 1); // Carnivorous chinchompas (Feldip Hills)
+			})
+			.addSub("Black chinchompas", () ->
+			{
+				pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 1); // Chinchompa Teleports
+				pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 2); // Black chinchompas (Wilderness)
+			})
+			.addSub("Hunter Guild", () ->
+			{
+				pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 1); // Chinchompa Teleports
+				pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 3); // Hunter Guild
+			})
+			.addSub("Farming Guild", () -> pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 2));
+		// endregion
+
+		// region Max cape opheld
+		teleportSwap("Teleports", ItemID.MAX_CAPE)
+			.held()
+			.addSub("Warriors' Guild", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 0))
+			.addSub("Fishing Guild", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 1))
+			.addSub("Crafting Guild", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 2))
+			.addSub("Farming Guild", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 3))
+			.addSub("Otto's Grotto", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 4))
+			.addSub("Feldip hills", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 5); // Chinchompas
+				pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 1); // Carnivorous chinchompas (Feldip Hills)
+			})
+			.addSub("Black chinchompas", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 5); // Chinchompas
+				pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 2); // Black chinchompas (Wilderness)
+			})
+			.addSub("Hunter Guild", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 5); // Chinchompas
+				pauseresume(ComponentID.DIALOG_OPTION_OPTIONS, 3); // Hunter Guild
+			})
+			.addSub("Home", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6); // POH Portals
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 0);
+			})
+			.addSub("Rimmington", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6); // POH Portals
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 1);
+			})
+			.addSub("Taverley", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6); // POH Portals
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 2);
+			})
+			.addSub("Pollnivneach", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6); // POH Portals
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 3);
+			})
+			.addSub("Hosidius", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6); // POH Portals
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 4);
+			})
+			.addSub("Rellekka", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6); // POH Portals
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 5);
+			})
+			.addSub("Brimhaven", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6); // POH Portals
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6);
+			})
+			.addSub("Yanille", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6); // POH Portals
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 7);
+			})
+			.addSub("Prifddinas", () ->
+			{
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6); // POH Portals
+				pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 8);
+			});
+		// endregion
+
+		// region Con cape
+		teleportSwap("Teleport", ItemID.CONSTRUCT_CAPE, ItemID.CONSTRUCT_CAPET)
+			.worn()
+			.held()
+			.addSub("Home", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 0))
+			.addSub("Rimmington", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 1))
+			.addSub("Taverley", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 2))
+			.addSub("Pollnivneach", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 3))
+			.addSub("Hosidius", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 4))
+			.addSub("Rellekka", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 5))
+			.addSub("Brimhaven", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6))
+			.addSub("Yanille", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 7))
+			.addSub("Prifddinas", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 8));
+		// endregion
+
+		// region Achievement diary cape
+		teleportSwap("Teleport", ItemID.ACHIEVEMENT_DIARY_CAPE, ItemID.ACHIEVEMENT_DIARY_CAPE_T)
+			.worn()
+			.held()
+			.addSub("Two-pints", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 0))
+			.addSub("Jarr", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 1))
+			.addSub("Sir Rebral",  () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 2))
+			.addSub("Thorodin",  () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 3))
+			.addSub("Flax keeper",  () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 4))
+			.addSub("Pirate Jackie the Fruit",  () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 5))
+			.addSub("Kaleb Paramaya",  () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 6))
+			.addSub("Jungle forester", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 7))
+			.addSub("TzHaar-Mej", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 8))
+			.addSub("Elise", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 9))
+			.addSub("Hatius Cosaintus", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 10))
+			.addSub("Le-sabrè", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 11))
+			.addSub("Toby", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 12))
+			.addSub("Lesser Fanatic", () -> pauseresume(ComponentID.ADVENTURE_LOG_OPTIONS, 13))
+			.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)
+	{
+		Preconditions.checkArgument(items.length > 0, "no items");
+		var ts = new TeleportSwap();
+		ts.option = option;
+		for (int item : items)
+		{
+			teleportSwaps.put(item, ts);
+		}
+		return ts;
+	}
+
+	@Subscribe
+	public void onMenuEntryAdded(MenuEntryAdded menuEntryAdded)
+	{
+		var me = menuEntryAdded.getMenuEntry();
+		if (me.getWidget() != null && me.getWidget().getId() == ComponentID.EQUIPMENT_CAPE)
+		{
+			var item = me.getWidget().getChild(1);
+			var swap = teleportSwaps.get(item.getItemId())
+				.stream()
+				.filter(ts -> ts.worn)
+				.filter(ts -> ts.option.equals(me.getOption()))
+				.findAny()
+				.orElse(null);
+			if (swap != null && config.teleportSubmenus())
+			{
+				me.setType(MenuAction.RUNELITE_SUBMENU_WIDGET);
+				me.onClick(e -> client.menuAction(e.getParam0(), e.getParam1(), MenuAction.CC_OP,
+					e.getIdentifier(), e.getItemId(), e.getOption(), e.getTarget()));
+
+				int off = 0;
+				final int p0 = me.getParam0();
+				final int p1 = me.getParam1();
+				final int id = me.getIdentifier();
+				final int itemId = me.getItemId();
+				final String option = me.getOption();
+				final String target = me.getTarget();
+				for (TeleportSub sub : swap.subs)
+				{
+					client.createMenuEntry(-2 - off++)
+						.setParam0(p0)
+						.setParam1(p1)
+						.setOption(sub.option)
+						.setTarget(target)
+						.setType(MenuAction.RUNELITE)
+						.setParent(me)
+						.onClick(e -> clientThread.invokeLater(() ->
+						{
+							client.menuAction(p0, p1, MenuAction.CC_OP, id, itemId, option, target);
+							sub.execute.run();
+						}));
+				}
+			}
+		}
+		else if (me.getWidget() != null && me.getWidget().getId() == ComponentID.INVENTORY_CONTAINER)
+		{
+			var swap = teleportSwaps.get(me.getItemId())
+				.stream()
+				.filter(ts -> ts.held)
+				.filter(ts -> ts.option.equals(me.getOption()))
+				.findAny()
+				.orElse(null);
+			if (swap != null && config.teleportSubmenus())
+			{
+				me.setType(MenuAction.RUNELITE_SUBMENU_WIDGET);
+				me.onClick(e -> client.menuAction(e.getParam0(), e.getParam1(), MenuAction.CC_OP,
+					e.getIdentifier(), e.getItemId(), e.getOption(), e.getTarget()));
+
+				int off = 0;
+				final int p0 = me.getParam0();
+				final int p1 = me.getParam1();
+				final int id = me.getIdentifier();
+				final int itemId = me.getItemId();
+				final String option = me.getOption();
+				final String target = me.getTarget();
+				for (TeleportSub sub : swap.subs)
+				{
+					client.createMenuEntry(-2 - off++)
+						.setParam0(p0)
+						.setParam1(p1)
+						.setOption(sub.option)
+						.setTarget(target)
+						.setType(MenuAction.RUNELITE)
+						.setParent(me)
+						.onClick(e -> clientThread.invokeLater(() ->
+						{
+							client.menuAction(p0, p1, MenuAction.CC_OP, id, itemId, option, target);
+							sub.execute.run();
+						}));
+				}
+			}
+		}
+	}
+
+	private void pauseresume(@Component int comp, int op)
+	{
+		client.menuAction(op, comp, MenuAction.WIDGET_CONTINUE, -1, -1, "", "");
+	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/mta/telekinetic/Maze.java b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/TeleportSwap.java
similarity index 56%
rename from runelite-client/src/main/java/net/runelite/client/plugins/mta/telekinetic/Maze.java
rename to runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/TeleportSwap.java
index 7e884dffc..b88ad961e 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/mta/telekinetic/Maze.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/menuentryswapper/TeleportSwap.java
@@ -1,73 +1,63 @@
-/*
- * Copyright (c) 2018, Jasper Ketelaar <Jasper0781@gmail.com>
- * 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.mta.telekinetic;
-
-import net.runelite.api.coords.LocalPoint;
-
-public enum Maze
-{
-	MAZE_1(100, new LocalPoint(6848, 3904)),
-	MAZE_2(124, new LocalPoint(4928, 6848)),
-	MAZE_3(129, new LocalPoint(7104, 5312)),
-	MAZE_4(53, new LocalPoint(6208, 4928)),
-	MAZE_5(108, new LocalPoint(5056, 5184)),
-	MAZE_6(121, new LocalPoint(3648, 5440)),
-	MAZE_7(71, new LocalPoint(6080, 5696)),
-	MAZE_8(98, new LocalPoint(5952, 7360)),
-	MAZE_9(87, new LocalPoint(5184, 6208)),
-	MAZE_10(91, new LocalPoint(5440, 9024));
-
-	private final int walls;
-	private final LocalPoint start;
-
-	Maze(int walls, LocalPoint start)
-	{
-		this.walls = walls;
-		this.start = start;
-	}
-
-	public static Maze fromWalls(int walls)
-	{
-		for (Maze maze : values())
-		{
-			if (maze.getWalls() == walls)
-			{
-				return maze;
-			}
-		}
-
-		return null;
-	}
-
-	public int getWalls()
-	{
-		return walls;
-	}
-
-	public LocalPoint getStart()
-	{
-		return start;
-	}
-}
\ No newline at end of file
+/*
+ * 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.plugins.menuentryswapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class TeleportSwap
+{
+	boolean worn;
+	boolean held;
+	String option;
+	List<TeleportSub> subs = new ArrayList<>();
+
+	TeleportSwap addSub(String option, Runnable r)
+	{
+		var sub = new TeleportSub();
+		sub.option = option;
+		sub.execute = r;
+		subs.add(sub);
+		return this;
+	}
+
+	TeleportSwap worn()
+	{
+		worn = true;
+		return this;
+	}
+
+	TeleportSwap held()
+	{
+		held = true;
+		return this;
+	}
+}
+
+class TeleportSub
+{
+	String option;
+	Runnable execute;
+}
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/mta/telekinetic/TelekineticRoom.java b/runelite-client/src/main/java/net/runelite/client/plugins/mta/telekinetic/TelekineticRoom.java
index e985214fe..fb13df200 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/mta/telekinetic/TelekineticRoom.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/mta/telekinetic/TelekineticRoom.java
@@ -81,7 +81,7 @@ public class TelekineticRoom extends MTARoom
 	private WorldPoint finishLocation;
 	private Rectangle bounds;
 	private NPC guardian;
-	private Maze maze;
+	private int numMazeWalls;
 
 	@Inject
 	private TelekineticRoom(MTAConfig config, Client client)
@@ -132,19 +132,17 @@ public class TelekineticRoom extends MTARoom
 	@Subscribe
 	public void onGameTick(GameTick event)
 	{
-		if (!config.telekinetic()
-				|| !inside()
-				|| client.getGameState() != GameState.LOGGED_IN)
+		if (!inside() || !config.telekinetic())
 		{
-			maze = null;
+			numMazeWalls = 0;
 			moves.clear();
 			return;
 		}
 
-		if (maze == null || telekineticWalls.size() != maze.getWalls())
+		if (telekineticWalls.size() != numMazeWalls)
 		{
 			bounds = getBounds(telekineticWalls.toArray(new WallObject[0]));
-			maze = Maze.fromWalls(telekineticWalls.size());
+			numMazeWalls = telekineticWalls.size();
 			client.clearHintArrow();
 		}
 		else if (guardian != null)
@@ -220,7 +218,7 @@ public class TelekineticRoom extends MTARoom
 	@Override
 	public void under(Graphics2D graphics2D)
 	{
-		if (inside() && maze != null && guardian != null)
+		if (inside() && numMazeWalls > 0 && guardian != null)
 		{
 			if (destination != null)
 			{
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/grounditems/LootType.java b/runelite-client/src/main/java/net/runelite/client/plugins/spellbook/SpellbookConfig.java
similarity index 81%
rename from runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java
rename to runelite-client/src/main/java/net/runelite/client/plugins/spellbook/SpellbookConfig.java
index 604815732..e11b9d70f 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/spellbook/SpellbookConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, Adam <Adam@sigterm.info>
+ * Copyright (c) 2024, Adam <Adam@sigterm.info>
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -22,13 +22,13 @@
  * (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;
+package net.runelite.client.plugins.spellbook;
 
-enum LootType
+import net.runelite.client.config.Config;
+import net.runelite.client.config.ConfigGroup;
+
+@ConfigGroup(SpellbookConfig.GROUP)
+public interface SpellbookConfig extends Config
 {
-	UNKNOWN,
-	TABLE,
-	DROPPED,
-	PVP,
-	PVM;
+	String GROUP = "spellbook";
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/spellbook/SpellbookPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/spellbook/SpellbookPlugin.java
new file mode 100644
index 000000000..813643977
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/spellbook/SpellbookPlugin.java
@@ -0,0 +1,491 @@
+/*
+ * 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.plugins.spellbook;
+
+import com.google.inject.Provides;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.stream.IntStream;
+import javax.inject.Inject;
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.api.ChatMessageType;
+import net.runelite.api.Client;
+import net.runelite.api.EnumComposition;
+import net.runelite.api.EnumID;
+import net.runelite.api.ItemComposition;
+import net.runelite.api.ParamID;
+import net.runelite.api.ScriptID;
+import net.runelite.api.Varbits;
+import net.runelite.api.events.DraggingWidgetChanged;
+import net.runelite.api.events.ScriptCallbackEvent;
+import net.runelite.api.events.ScriptPreFired;
+import net.runelite.api.widgets.ComponentID;
+import net.runelite.api.widgets.InterfaceID;
+import net.runelite.api.widgets.JavaScriptCallback;
+import net.runelite.api.widgets.Widget;
+import static net.runelite.api.widgets.WidgetConfig.DRAG;
+import static net.runelite.api.widgets.WidgetConfig.DRAG_ON;
+import net.runelite.api.widgets.WidgetUtil;
+import net.runelite.client.callback.ClientThread;
+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.ProfileChanged;
+import net.runelite.client.menus.MenuManager;
+import net.runelite.client.menus.WidgetMenuOption;
+import net.runelite.client.plugins.Plugin;
+import net.runelite.client.plugins.PluginDescriptor;
+
+@PluginDescriptor(
+	name = "Spellbook",
+	description = "Reorder and hide spells"
+)
+@Slf4j
+public class SpellbookPlugin extends Plugin
+{
+	private static final String LOCK = "Disable spell reordering";
+	private static final String UNLOCK = "Enable spell reordering";
+	// [proc,magic_spellbook_modifyops] shifts around ops, but seems to only use
+	// 1 2 3 4 5 10. So use 6 for Hide/Unhide.
+	private static final int HIDE_UNHIDE_OP = 6;
+
+	private static final WidgetMenuOption FIXED_MAGIC_TAB_LOCK = new WidgetMenuOption(LOCK,
+		"", ComponentID.FIXED_VIEWPORT_MAGIC_TAB);
+
+	private static final WidgetMenuOption FIXED_MAGIC_TAB_UNLOCK = new WidgetMenuOption(UNLOCK,
+		"", ComponentID.FIXED_VIEWPORT_MAGIC_TAB);
+
+	private static final WidgetMenuOption RESIZABLE_MAGIC_TAB_LOCK = new WidgetMenuOption(LOCK,
+		"", ComponentID.RESIZABLE_VIEWPORT_MAGIC_TAB);
+
+	private static final WidgetMenuOption RESIZABLE_MAGIC_TAB_UNLOCK = new WidgetMenuOption(UNLOCK,
+		"", ComponentID.RESIZABLE_VIEWPORT_MAGIC_TAB);
+
+	private static final WidgetMenuOption RESIZABLE_BOTTOM_LINE_MAGIC_TAB_LOCK = new WidgetMenuOption(LOCK,
+		"", ComponentID.RESIZABLE_VIEWPORT_BOTTOM_LINE_MAGIC_TAB);
+
+	private static final WidgetMenuOption RESIZABLE_BOTTOM_LINE_MAGIC_TAB_UNLOCK = new WidgetMenuOption(UNLOCK,
+		"", ComponentID.RESIZABLE_VIEWPORT_BOTTOM_LINE_MAGIC_TAB);
+
+	@Inject
+	private Client client;
+
+	@Inject
+	private ClientThread clientThread;
+
+	@Inject
+	private MenuManager menuManager;
+
+	@Inject
+	private ChatMessageManager chatMessageManager;
+
+	@Inject
+	private ConfigManager configManager;
+
+	private boolean reordering;
+
+	@Provides
+	SpellbookConfig getConfig(ConfigManager configManager)
+	{
+		return configManager.getConfig(SpellbookConfig.class);
+	}
+
+	@Override
+	protected void startUp()
+	{
+		refreshReorderMenus();
+		clientThread.invokeLater(this::reinitializeSpellbook);
+	}
+
+	@Override
+	protected void shutDown()
+	{
+		clearReoderMenus();
+		clientThread.invokeLater(this::reinitializeSpellbook);
+	}
+
+	@Subscribe
+	public void onProfileChanged(ProfileChanged event)
+	{
+		clientThread.invokeLater(this::redrawSpellbook);
+	}
+
+	@Override
+	public void resetConfiguration()
+	{
+		for (var key : configManager.getConfigurationKeys(SpellbookConfig.GROUP + ".spell_"))
+		{
+			String[] str = key.split("\\.", 2);
+			if (str.length == 2)
+			{
+				configManager.unsetConfiguration(str[0], str[1]);
+			}
+		}
+
+		clientThread.invokeLater(this::redrawSpellbook);
+
+		log.debug("Reset spellbook");
+	}
+
+	private void clearReoderMenus()
+	{
+		menuManager.removeManagedCustomMenu(FIXED_MAGIC_TAB_LOCK);
+		menuManager.removeManagedCustomMenu(RESIZABLE_MAGIC_TAB_LOCK);
+		menuManager.removeManagedCustomMenu(RESIZABLE_BOTTOM_LINE_MAGIC_TAB_LOCK);
+		menuManager.removeManagedCustomMenu(FIXED_MAGIC_TAB_UNLOCK);
+		menuManager.removeManagedCustomMenu(RESIZABLE_MAGIC_TAB_UNLOCK);
+		menuManager.removeManagedCustomMenu(RESIZABLE_BOTTOM_LINE_MAGIC_TAB_UNLOCK);
+	}
+
+	private void refreshReorderMenus()
+	{
+		clearReoderMenus();
+		if (reordering)
+		{
+			menuManager.addManagedCustomMenu(FIXED_MAGIC_TAB_LOCK, e -> reordering(false));
+			menuManager.addManagedCustomMenu(RESIZABLE_MAGIC_TAB_LOCK, e -> reordering(false));
+			menuManager.addManagedCustomMenu(RESIZABLE_BOTTOM_LINE_MAGIC_TAB_LOCK, e -> reordering(false));
+		}
+		else
+		{
+			menuManager.addManagedCustomMenu(FIXED_MAGIC_TAB_UNLOCK, e -> reordering(true));
+			menuManager.addManagedCustomMenu(RESIZABLE_MAGIC_TAB_UNLOCK, e -> reordering(true));
+			menuManager.addManagedCustomMenu(RESIZABLE_BOTTOM_LINE_MAGIC_TAB_UNLOCK, e -> reordering(true));
+		}
+	}
+
+	private void reordering(boolean state)
+	{
+		reordering = state;
+
+		var message = reordering ?
+			"Spell book reordering is now enabled." :
+			"Spell book reordering is now disabled.";
+
+		chatMessageManager.queue(QueuedMessage.builder()
+			.type(ChatMessageType.CONSOLE)
+			.runeLiteFormattedMessage(message)
+			.build());
+
+		refreshReorderMenus();
+
+		redrawSpellbook();
+	}
+
+	@Subscribe
+	public void onScriptPreFired(ScriptPreFired event)
+	{
+		if (event.getScriptId() == ScriptID.MAGIC_SPELLBOOK_INITIALISESPELLS)
+		{
+			int[] stack = client.getIntStack();
+			int sz = client.getIntStackSize();
+			int spellBookEnum = stack[sz - 12]; // eg 1982, 5285, 1983, 1984, 1985
+			clientThread.invokeLater(() -> initializeSpells(spellBookEnum));
+		}
+	}
+
+	@Subscribe
+	public void onDraggingWidgetChanged(DraggingWidgetChanged event)
+	{
+		if (event.isDraggingWidget() && client.getMouseCurrentButton() == 0)
+		{
+			Widget draggedWidget = client.getDraggedWidget();
+			Widget draggedOnWidget = client.getDraggedOnWidget();
+			if (draggedWidget == null || draggedOnWidget == null)
+			{
+				return;
+			}
+
+			int draggedGroupId = WidgetUtil.componentToInterface(draggedWidget.getId());
+			int draggedOnGroupId = WidgetUtil.componentToInterface(draggedOnWidget.getId());
+			if (draggedGroupId != InterfaceID.SPELLBOOK || draggedOnGroupId != InterfaceID.SPELLBOOK)
+			{
+				return;
+			}
+
+			// from ~magic_spellbook_redraw
+			int subSpellbookId = client.getEnum(EnumID.SPELLBOOKS_SUB).getIntValue(client.getVarbitValue(Varbits.SPELLBOOK));
+			int spellbookId = client.getEnum(subSpellbookId).getIntValue(client.getVarbitValue(Varbits.SPELLBOOK_SUBMENU));
+
+			EnumComposition spellbook = client.getEnum(spellbookId);
+			int[] order = calculateSpellbookOrder(spellbookId, spellbook); // in enum indices
+
+			int fromIdx = findSpellIdxForComponent(spellbook, order, draggedWidget);
+			int toIdx = findSpellIdxForComponent(spellbook, order, draggedOnWidget);
+
+			ItemComposition fromSpell = client.getItemDefinition(spellbook.getIntValue(order[fromIdx]));
+			ItemComposition toSpell = client.getItemDefinition(spellbook.getIntValue(order[toIdx]));
+
+			log.debug("Insert {} ({}) at {} ({}) spellbook {}",
+				fromSpell.getStringValue(ParamID.SPELL_NAME), fromIdx,
+				toSpell.getStringValue(ParamID.SPELL_NAME), toIdx,
+				spellbookId);
+
+			log.debug("Set {} to {}", client.getItemDefinition(spellbook.getIntValue(order[fromIdx])).getStringValue(ParamID.SPELL_NAME), toIdx);
+			setPosition(spellbookId, spellbook.getIntValue(order[fromIdx]), toIdx);
+			if (fromIdx < toIdx)
+			{
+				for (int i = fromIdx + 1; i <= toIdx; ++i)
+				{
+					log.debug("Set {} to {}", client.getItemDefinition(spellbook.getIntValue(order[i])).getStringValue(ParamID.SPELL_NAME), i - 1);
+					setPosition(spellbookId, spellbook.getIntValue(order[i]), i - 1);
+				}
+			}
+			else
+			{
+				for (int i = toIdx; i < fromIdx; ++i)
+				{
+					log.debug("Set {} to {}", client.getItemDefinition(spellbook.getIntValue(order[i])).getStringValue(ParamID.SPELL_NAME), i + 1);
+					setPosition(spellbookId, spellbook.getIntValue(order[i]), i + 1);
+				}
+			}
+
+			redrawSpellbook();
+		}
+	}
+
+	private int findSpellIdxForComponent(EnumComposition spellbook, int[] spells, Widget c)
+	{
+		for (int i = 0; i < spells.length; ++i)
+		{
+			ItemComposition spellObj = client.getItemDefinition(spellbook.getIntValue(spells[i]));
+			Widget w = client.getWidget(spellObj.getIntValue(ParamID.SPELL_BUTTON));
+			if (w == c)
+			{
+				return i;
+			}
+		}
+		return -1;
+	}
+
+	@Subscribe
+	public void onScriptCallbackEvent(ScriptCallbackEvent event)
+	{
+		if (!"spellbookSort".equals(event.getEventName()))
+		{
+			return;
+		}
+
+		// this is called after ~magic_spellbook_redraw has built and sorted the array of visible spells
+		// based on the vanilla filtering
+
+		int[] stack = client.getIntStack();
+		int size = client.getIntStackSize();
+
+		int spellbookEnumId = stack[size - 3];
+		int spellArrayId = stack[size - 2];
+		int numSpells = stack[size - 1];
+
+		EnumComposition spellbookEnum = client.getEnum(spellbookEnumId);
+		int[] spells = client.getArray(spellArrayId); // enum indices
+		int[] newSpells = new int[numSpells];
+		int numNewSpells = 0;
+		for (int i = 0; i < numSpells; ++i)
+		{
+			ItemComposition spellObj = client.getItemDefinition(spellbookEnum.getIntValue(spells[i]));
+			Widget w = client.getWidget(spellObj.getIntValue(ParamID.SPELL_BUTTON));
+			boolean hidden = isHidden(spellbookEnumId, spellObj.getId());
+
+			int widgetConfig = w.getClickMask();
+			if (reordering)
+			{
+				if (hidden)
+				{
+					w.setOpacity(100);
+					w.setAction(HIDE_UNHIDE_OP, "Unhide");
+				}
+				else
+				{
+					w.setOpacity(0);
+					w.setAction(HIDE_UNHIDE_OP, "Hide");
+				}
+
+				newSpells[numNewSpells++] = spells[i];
+				widgetConfig |= DRAG | DRAG_ON;
+			}
+			else
+			{
+				if (hidden)
+				{
+					w.setHidden(true);
+				}
+				else
+				{
+					newSpells[numNewSpells++] = spells[i];
+					w.setOpacity(0);
+					w.setAction(HIDE_UNHIDE_OP, null);
+				}
+
+				widgetConfig &= ~(DRAG | DRAG_ON);
+			}
+			w.setClickMask(widgetConfig);
+		}
+
+		// Sort newSpells based on their configured order
+		int[] order = calculateSpellbookOrder(spellbookEnumId, spellbookEnum);
+		int[] indices = new int[order.length];
+		for (int i = 0; i < order.length; ++i)
+		{
+			indices[order[i]] = i;
+		}
+		newSpells = Arrays.stream(newSpells, 0, numNewSpells)
+			.boxed()
+			.sorted(Comparator.comparingInt(i -> indices[i]))
+			.mapToInt(i -> i)
+			.toArray();
+
+		System.arraycopy(newSpells, 0, spells, 0, numNewSpells);
+		stack[size - 1] = numSpells = numNewSpells;
+	}
+
+	private void initializeSpells(int spellbookEnum)
+	{
+		EnumComposition spellbook = client.getEnum(spellbookEnum);
+		for (int i = 0; i < spellbook.size(); ++i)
+		{
+			int spellObjId = spellbook.getIntValue(i);
+			ItemComposition spellObj = client.getItemDefinition(spellObjId);
+			int spellComponent = spellObj.getIntValue(ParamID.SPELL_BUTTON);
+			Widget w = client.getWidget(spellComponent);
+
+			// spells with no target mask have an existing op listener, capture it to
+			// call it later
+			Object[] opListener = w.getOnOpListener();
+			w.setOnOpListener((JavaScriptCallback) e ->
+			{
+				if (e.getOp() == HIDE_UNHIDE_OP + 1)
+				{
+					Widget s = e.getSource();
+
+					// Spells can be shared between spellbooks, so we can't assume spellbookEnum is the current spellbook.
+					// from ~magic_spellbook_redraw
+					int subSpellbookId = client.getEnum(EnumID.SPELLBOOKS_SUB).getIntValue(client.getVarbitValue(Varbits.SPELLBOOK));
+					int spellbookId = client.getEnum(subSpellbookId).getIntValue(client.getVarbitValue(Varbits.SPELLBOOK_SUBMENU));
+
+					boolean hidden = isHidden(spellbookId, spellObjId);
+					hidden = !hidden;
+
+					log.debug("Changing {} to hidden: {}", s.getName(), hidden);
+					setHidden(spellbookId, spellObjId, hidden);
+
+					s.setOpacity(hidden ? 100 : 0);
+					s.setAction(HIDE_UNHIDE_OP, hidden ? "Unhide" : "Hide");
+					return;
+				}
+
+				if (opListener != null)
+				{
+					client.runScript(opListener);
+				}
+			});
+		}
+	}
+
+	private void reinitializeSpellbook()
+	{
+		Widget w = client.getWidget(ComponentID.SPELLBOOK_PARENT);
+		if (w != null && w.getOnLoadListener() != null)
+		{
+			client.createScriptEvent(w.getOnLoadListener())
+				.setSource(w)
+				.run();
+		}
+	}
+
+	private void redrawSpellbook()
+	{
+		Widget w = client.getWidget(ComponentID.SPELLBOOK_PARENT);
+		if (w != null && w.getOnInvTransmitListener() != null)
+		{
+			client.createScriptEvent(w.getOnInvTransmitListener())
+				.setSource(w)
+				.run();
+		}
+	}
+
+	private int[] calculateSpellbookOrder(int spellbookId, EnumComposition spellbook)
+	{
+		int[] spells = defaultSpellbookOrder(spellbook);
+		int[] indices = new int[spells.length]; // spell to desired index
+		for (int i = 0; i < spells.length; ++i)
+		{
+			int pos = getPosition(spellbookId, spellbook.getIntValue(spells[i]));
+			indices[spells[i]] = pos != -1 ? pos : i;
+		}
+
+		// sort by desired index
+		return Arrays.stream(spells)
+			.boxed()
+			.sorted(Comparator.comparingInt(i -> indices[i]))
+			.mapToInt(i -> i)
+			.toArray();
+	}
+
+	private int[] defaultSpellbookOrder(EnumComposition spellbook)
+	{
+		return IntStream.range(0, spellbook.size())
+			.boxed()
+			.sorted((idx1, idx2) ->
+			{
+				var i1 = client.getItemDefinition(spellbook.getIntValue(idx1));
+				var i2 = client.getItemDefinition(spellbook.getIntValue(idx2));
+				int l1 = i1.getIntValue(ParamID.SPELL_LEVELREQ);
+				int l2 = i2.getIntValue(ParamID.SPELL_LEVELREQ);
+				return Integer.compare(l1, l2);
+			})
+			.mapToInt(i -> i)
+			.toArray();
+	}
+
+	private boolean isHidden(int spellbook, int spell)
+	{
+		Boolean b = configManager.getConfiguration(SpellbookConfig.GROUP, "spell_hidden_book_" + spellbook + "_" + spell, boolean.class);
+		return b == Boolean.TRUE;
+	}
+
+	private void setHidden(int spellbook, int spell, boolean hidden)
+	{
+		if (hidden)
+		{
+			configManager.setConfiguration(SpellbookConfig.GROUP, "spell_hidden_book_" + spellbook + "_" + spell, true);
+		}
+		else
+		{
+			configManager.unsetConfiguration(SpellbookConfig.GROUP, "spell_hidden_book_" + spellbook + "_" + spell);
+		}
+	}
+
+	private int getPosition(int spellbook, int spell)
+	{
+		Integer pos = configManager.getConfiguration(SpellbookConfig.GROUP, "spell_pos_book_" + spellbook + "_" + spell, int.class);
+		return pos == null ? -1 : pos;
+	}
+
+	private void setPosition(int spellbook, int spell, int position)
+	{
+		configManager.setConfiguration(SpellbookConfig.GROUP, "spell_pos_book_" + spellbook + "_" + spell, position);
+	}
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingOverlay.java
index 75c97a4f0..34f0f8186 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingOverlay.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingOverlay.java
@@ -31,6 +31,7 @@ import javax.inject.Inject;
 import net.runelite.api.Client;
 import static net.runelite.api.MenuAction.RUNELITE_OVERLAY;
 import static net.runelite.api.MenuAction.RUNELITE_OVERLAY_CONFIG;
+import net.runelite.api.VarPlayer;
 import static net.runelite.client.ui.overlay.OverlayManager.OPTION_CONFIGURE;
 import net.runelite.client.ui.overlay.OverlayPanel;
 import net.runelite.client.ui.overlay.OverlayPosition;
@@ -40,6 +41,7 @@ import net.runelite.client.ui.overlay.components.TitleComponent;
 class WoodcuttingOverlay extends OverlayPanel
 {
 	private static final String WOODCUTTING_RESET = "Reset";
+	private static final int BUFF_BAR_DISPLAYED = 96;
 
 	private final Client client;
 	private final WoodcuttingPlugin plugin;
@@ -66,7 +68,8 @@ class WoodcuttingOverlay extends OverlayPanel
 			return null;
 		}
 
-		if (WoodcuttingPlugin.WOODCUTTING_ANIMS.contains(client.getLocalPlayer().getAnimation()))
+		if (WoodcuttingPlugin.WOODCUTTING_ANIMS.contains(client.getLocalPlayer().getAnimation())
+			|| client.getVarpValue(VarPlayer.BUFF_BAR_WC_GROUP_BONUS) == BUFF_BAR_DISPLAYED)
 		{
 			panelComponent.getChildren().add(TitleComponent.builder()
 				.text("Woodcutting")
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/ui/overlay/components/TextComponent.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/TextComponent.java
index 0b6054a46..28b5fc680 100644
--- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/TextComponent.java
+++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/components/TextComponent.java
@@ -30,18 +30,16 @@ import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Graphics2D;
 import java.awt.Point;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import javax.annotation.Nullable;
 import lombok.Setter;
 import net.runelite.client.ui.overlay.RenderableEntity;
-import net.runelite.client.util.ColorUtil;
-import net.runelite.client.util.Text;
 
 @Setter
 public class TextComponent implements RenderableEntity
 {
-	private static final String COL_TAG_REGEX = "(<col=([0-9a-fA-F]){2,6}>)";
-	private static final Pattern COL_TAG_PATTERN_W_LOOKAHEAD = Pattern.compile("(?=" + COL_TAG_REGEX + ")");
+	private static final Pattern COL_TAG_PATTERN = Pattern.compile("<col=([0-9a-fA-F]{2,6})>");
 
 	private String text;
 	private Point position = new Point();
@@ -65,61 +63,28 @@ public class TextComponent implements RenderableEntity
 
 		final FontMetrics fontMetrics = graphics.getFontMetrics();
 
-		if (COL_TAG_PATTERN_W_LOOKAHEAD.matcher(text).find())
+		Matcher matcher = COL_TAG_PATTERN.matcher(text);
+		Color textColor = color;
+		int idx = 0;
+		int width = 0;
+		while (matcher.find())
 		{
-			final String[] parts = COL_TAG_PATTERN_W_LOOKAHEAD.split(text);
-			int x = position.x;
-
-			for (String textSplitOnCol : parts)
-			{
-				final String textWithoutCol = Text.removeTags(textSplitOnCol);
-				final String colColor = textSplitOnCol.substring(textSplitOnCol.indexOf("=") + 1, textSplitOnCol.indexOf(">"));
-
-				graphics.setColor(Color.BLACK);
-
-				if (outline)
-				{
-					graphics.drawString(textWithoutCol, x, position.y + 1);
-					graphics.drawString(textWithoutCol, x, position.y - 1);
-					graphics.drawString(textWithoutCol, x + 1, position.y);
-					graphics.drawString(textWithoutCol, x - 1, position.y);
-				}
-				else
-				{
-					// shadow
-					graphics.drawString(textWithoutCol, x + 1, position.y + 1);
-				}
-
-				// actual text
-				graphics.setColor(Color.decode("#" + colColor));
-				graphics.drawString(textWithoutCol, x, position.y);
-
-				x += fontMetrics.stringWidth(textWithoutCol);
-			}
+			String color = matcher.group(1);
+			String s = text.substring(idx, matcher.start());
+			idx = matcher.end();
+
+			renderText(graphics, textColor, position.x + width, position.y, s);
+			width += fontMetrics.stringWidth(s);
+
+			textColor = Color.decode("#" + color);
 		}
-		else
+
 		{
-			graphics.setColor(Color.BLACK);
-
-			if (outline)
-			{
-				graphics.drawString(text, position.x, position.y + 1);
-				graphics.drawString(text, position.x, position.y - 1);
-				graphics.drawString(text, position.x + 1, position.y);
-				graphics.drawString(text, position.x - 1, position.y);
-			}
-			else
-			{
-				// shadow
-				graphics.drawString(text, position.x + 1, position.y + 1);
-			}
-
-			// actual text
-			graphics.setColor(ColorUtil.colorWithAlpha(color, 0xFF));
-			graphics.drawString(text, position.x, position.y);
+			String s = text.substring(idx);
+			renderText(graphics, textColor, position.x + width, position.y, s);
+			width += fontMetrics.stringWidth(s);
 		}
 
-		int width = fontMetrics.stringWidth(text);
 		int height = fontMetrics.getHeight();
 
 		if (originalFont != null)
@@ -129,4 +94,31 @@ public class TextComponent implements RenderableEntity
 
 		return new Dimension(width, height);
 	}
+
+	private void renderText(Graphics2D graphics, Color color, int x, int y, String text)
+	{
+		if (text.isEmpty())
+		{
+			return;
+		}
+
+		graphics.setColor(Color.BLACK);
+
+		if (outline)
+		{
+			graphics.drawString(text, x, y + 1);
+			graphics.drawString(text, x, y - 1);
+			graphics.drawString(text, x + 1, y);
+			graphics.drawString(text, x - 1, y);
+		}
+		else
+		{
+			// shadow
+			graphics.drawString(text, x + 1, y + 1);
+		}
+
+		// actual text
+		graphics.setColor(color);
+		graphics.drawString(text, x, y);
+	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java b/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java
index 8451ca134..d7ba5d675 100644
--- a/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java
+++ b/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java
@@ -81,7 +81,7 @@ public abstract class HotkeyListener implements KeyListener
 	@Override
 	public void keyReleased(KeyEvent e)
 	{
-		if (keybind.get().matches(e))
+		if (keybind.get().matches(e) && isPressed)
 		{
 			isPressed = false;
 			isConsumingTyped = false;
diff --git a/runelite-client/src/main/resources/item_variations.json b/runelite-client/src/main/resources/item_variations.json
index d0019a5d5..2eeeffae4 100644
--- a/runelite-client/src/main/resources/item_variations.json
+++ b/runelite-client/src/main/resources/item_variations.json
@@ -2533,8 +2533,7 @@
     11028,
     11029,
     11030,
-    26926,
-    29445
+    26926
   ],
   "banana": [
     1963,
@@ -11922,6 +11921,26 @@
     29248,
     29250
   ],
+  "small meat pouch": [
+    29295,
+    29462
+  ],
+  "large meat pouch": [
+    29297,
+    29464
+  ],
+  "small fur pouch": [
+    29299,
+    29466
+  ],
+  "medium fur pouch": [
+    29301,
+    29468
+  ],
+  "large fur pouch": [
+    29303,
+    29470
+  ],
   "blessed bone statuette": [
     29338,
     29340,
diff --git a/runelite-client/src/main/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 =
diff --git a/runelite-client/src/main/scripts/ChatBuilder.hash b/runelite-client/src/main/scripts/ChatBuilder.hash
index 9b63917e4..3c0446f53 100644
--- a/runelite-client/src/main/scripts/ChatBuilder.hash
+++ b/runelite-client/src/main/scripts/ChatBuilder.hash
@@ -1,1 +1,1 @@
-4A78CE396EF7493A1FE42C4A29A706F3BA62EE65F99AB118765DD93504ABC682
\ No newline at end of file
+99EBEC97A1D42C6CE5D82295910B6E2C61629AA1D437582D25CA20F3231AEB75
\ No newline at end of file
diff --git a/runelite-client/src/main/scripts/ChatBuilder.rs2asm b/runelite-client/src/main/scripts/ChatBuilder.rs2asm
index 4e458adf9..345b071a1 100644
--- a/runelite-client/src/main/scripts/ChatBuilder.rs2asm
+++ b/runelite-client/src/main/scripts/ChatBuilder.rs2asm
@@ -415,12 +415,12 @@ LABEL393:
    iload                  10
    iconst                 -1
    if_icmpne              LABEL397
-   jump                   LABEL1946
+   jump                   LABEL1997
 LABEL397:
    iload                  9
    iconst                 -1
    if_icmpne              LABEL401
-   jump                   LABEL1946
+   jump                   LABEL1997
 LABEL401:
    iload                  10
    chat_gethistoryex_byuid
@@ -439,7 +439,7 @@ LABEL401:
    invoke                 193
    iconst                 1
    if_icmpeq              CHAT_FILTER
-   jump                   LABEL1942
+   jump                   LABEL1993
 CHAT_FILTER:
    sload                  20                ; Load the message
    iconst                 1                 ; Gets changed to 0 if message is blocked
@@ -452,7 +452,7 @@ CHAT_FILTER:
    iconst                 1                 ; 2nd half of conditional
    sstore                 20                ; Override the message with our filtered message
    if_icmpeq              LABEL419          ; Check if we are building this message
-   jump                   LABEL1942          ; continue to next message, skipping this
+   jump                   LABEL1993          ; continue to next message, skipping this
 LABEL419:
    iload                  11
    sload                  18
@@ -468,7 +468,7 @@ LABEL419:
    invoke                 90
    iconst                 1
    if_icmpeq              LABEL434
-   jump                   LABEL1942
+   jump                   LABEL1993
 LABEL434:
    iconst                 0 ; splitpmbox
    iload                  10 ; message uid
@@ -1671,15 +1671,16 @@ LABEL1509:
       106: LABEL1514
       41: LABEL1514
       44: LABEL1514
-      101: LABEL1631
-      103: LABEL1683
-      104: LABEL1683
-      110: LABEL1683
-      14: LABEL1726
-      109: LABEL1787
-      111: LABEL1830
-      112: LABEL1873
-   jump                   LABEL1916
+      0: LABEL1631
+      101: LABEL1682
+      103: LABEL1734
+      104: LABEL1734
+      110: LABEL1734
+      14: LABEL1777
+      109: LABEL1838
+      111: LABEL1881
+      112: LABEL1924
+   jump                   LABEL1967
 LABEL1514:
    sconst                 "<col=ffffff>"
    sload                  18
@@ -1815,8 +1816,64 @@ LABEL1626:
    iload                  9
    if_setop              
 LABEL1630:
-   jump                   LABEL1928
+   jump                   LABEL1979
 LABEL1631:
+   sload                  18
+   string_length         
+   iconst                 0
+   if_icmpgt              LABEL1636
+   jump                   LABEL1669
+LABEL1636:
+   sload                  0
+   sload                  18
+   removetags            
+   compare               
+   iconst                 0
+   if_icmpne              LABEL1643
+   jump                   LABEL1669
+LABEL1643:
+   iconst                 8
+   sconst                 "Report"
+   iload                  9
+   if_setop              
+   sconst                 "<col=ffffff>"
+   sload                  18
+   sconst                 "</col>"
+   join_string            3
+   iload                  9
+   if_setopbase          
+   iconst                 86
+   iconst                 -2147483644
+   sconst                 "event_opbase"
+   iload                  11
+   sconst                 "isi"
+   iload                  9
+   if_setonop            
+   iconst                 -1
+   sconst                 ""
+   iload                  9
+   if_setonmouseover     
+   iconst                 -1
+   sconst                 ""
+   iload                  9
+   if_setonmouseleave    
+   jump                   LABEL1681
+LABEL1669:
+   iconst                 -1
+   sconst                 ""
+   iload                  9
+   if_setonop            
+   iconst                 -1
+   sconst                 ""
+   iload                  9
+   if_setonmouseover     
+   iconst                 -1
+   sconst                 ""
+   iload                  9
+   if_setonmouseleave    
+LABEL1681:
+   jump                   LABEL1979
+LABEL1682:
    sconst                 "<col=ffffff>"
    sload                  18
    sconst                 "</col>"
@@ -1840,31 +1897,31 @@ LABEL1631:
    if_setonmouseleave    
    invoke                 5548
    iconst                 1
-   if_icmpeq              LABEL1656
-   jump                   LABEL1661
-LABEL1656:
+   if_icmpeq              LABEL1707
+   jump                   LABEL1712
+LABEL1707:
    iconst                 1
    sconst                 "Accept invitation"
    iload                  9
    if_setop              
-   jump                   LABEL1665
-LABEL1661:
+   jump                   LABEL1716
+LABEL1712:
    iconst                 1
    sconst                 "Accept trade"
    iload                  9
    if_setop              
-LABEL1665:
+LABEL1716:
    iload                  15
    iconst                 1
-   if_icmpeq              LABEL1669
-   jump                   LABEL1674
-LABEL1669:
+   if_icmpeq              LABEL1720
+   jump                   LABEL1725
+LABEL1720:
    iconst                 6
    sconst                 "Message"
    iload                  9
    if_setop              
-   jump                   LABEL1682
-LABEL1674:
+   jump                   LABEL1733
+LABEL1725:
    iconst                 6
    sconst                 "Add friend"
    iload                  9
@@ -1873,9 +1930,9 @@ LABEL1674:
    sconst                 "Add ignore"
    iload                  9
    if_setop              
-LABEL1682:
-   jump                   LABEL1928
-LABEL1683:
+LABEL1733:
+   jump                   LABEL1979
+LABEL1734:
    sconst                 "<col=ffffff>"
    sload                  18
    sconst                 "</col>"
@@ -1903,15 +1960,15 @@ LABEL1683:
    if_setop              
    iload                  15
    iconst                 1
-   if_icmpeq              LABEL1712
-   jump                   LABEL1717
-LABEL1712:
+   if_icmpeq              LABEL1763
+   jump                   LABEL1768
+LABEL1763:
    iconst                 6
    sconst                 "Message"
    iload                  9
    if_setop              
-   jump                   LABEL1725
-LABEL1717:
+   jump                   LABEL1776
+LABEL1768:
    iconst                 6
    sconst                 "Add friend"
    iload                  9
@@ -1920,20 +1977,20 @@ LABEL1717:
    sconst                 "Add ignore"
    iload                  9
    if_setop              
-LABEL1725:
-   jump                   LABEL1928
-LABEL1726:
+LABEL1776:
+   jump                   LABEL1979
+LABEL1777:
    sload                  21
    string_length         
    iconst                 0
-   if_icmpgt              LABEL1731
-   jump                   LABEL1760
-LABEL1731:
+   if_icmpgt              LABEL1782
+   jump                   LABEL1811
+LABEL1782:
    iload                  13
    iconst                 -1
-   if_icmpne              LABEL1735
-   jump                   LABEL1760
-LABEL1735:
+   if_icmpne              LABEL1786
+   jump                   LABEL1811
+LABEL1786:
    iconst                 6
    sconst                 "Open"
    iload                  9
@@ -1958,8 +2015,8 @@ LABEL1735:
    sconst                 "Iii"
    iload                  9
    if_setonmouseleave    
-   jump                   LABEL1768
-LABEL1760:
+   jump                   LABEL1819
+LABEL1811:
    iconst                 -1
    sconst                 ""
    iload                  9
@@ -1968,7 +2025,7 @@ LABEL1760:
    sconst                 ""
    iload                  9
    if_setonmouseleave    
-LABEL1768:
+LABEL1819:
    iconst                 9
    sconst                 "Clear history"
    iload                  9
@@ -1987,8 +2044,8 @@ LABEL1768:
    sconst                 "iisi"
    iload                  9
    if_setonop            
-   jump                   LABEL1928
-LABEL1787:
+   jump                   LABEL1979
+LABEL1838:
    sconst                 "<col=0xffffff>"
    sload                  18
    sconst                 "</col>"
@@ -2016,15 +2073,15 @@ LABEL1787:
    if_setonmouseleave    
    iload                  15
    iconst                 1
-   if_icmpeq              LABEL1816
-   jump                   LABEL1821
-LABEL1816:
+   if_icmpeq              LABEL1867
+   jump                   LABEL1872
+LABEL1867:
    iconst                 6
    sconst                 "Message"
    iload                  9
    if_setop              
-   jump                   LABEL1829
-LABEL1821:
+   jump                   LABEL1880
+LABEL1872:
    iconst                 6
    sconst                 "Add friend"
    iload                  9
@@ -2033,9 +2090,9 @@ LABEL1821:
    sconst                 "Add ignore"
    iload                  9
    if_setop              
-LABEL1829:
-   jump                   LABEL1928
-LABEL1830:
+LABEL1880:
+   jump                   LABEL1979
+LABEL1881:
    sconst                 "<col=0xffffff>"
    sload                  18
    sconst                 "</col>"
@@ -2063,15 +2120,15 @@ LABEL1830:
    if_setonmouseleave    
    iload                  15
    iconst                 1
-   if_icmpeq              LABEL1859
-   jump                   LABEL1864
-LABEL1859:
+   if_icmpeq              LABEL1910
+   jump                   LABEL1915
+LABEL1910:
    iconst                 6
    sconst                 "Message"
    iload                  9
    if_setop              
-   jump                   LABEL1872
-LABEL1864:
+   jump                   LABEL1923
+LABEL1915:
    iconst                 6
    sconst                 "Add friend"
    iload                  9
@@ -2080,9 +2137,9 @@ LABEL1864:
    sconst                 "Add ignore"
    iload                  9
    if_setop              
-LABEL1872:
-   jump                   LABEL1928
-LABEL1873:
+LABEL1923:
+   jump                   LABEL1979
+LABEL1924:
    sconst                 "<col=0xffffff>"
    sload                  18
    sconst                 "</col>"
@@ -2110,15 +2167,15 @@ LABEL1873:
    if_setonmouseleave    
    iload                  15
    iconst                 1
-   if_icmpeq              LABEL1902
-   jump                   LABEL1907
-LABEL1902:
+   if_icmpeq              LABEL1953
+   jump                   LABEL1958
+LABEL1953:
    iconst                 6
    sconst                 "Message"
    iload                  9
    if_setop              
-   jump                   LABEL1915
-LABEL1907:
+   jump                   LABEL1966
+LABEL1958:
    iconst                 6
    sconst                 "Add friend"
    iload                  9
@@ -2127,9 +2184,9 @@ LABEL1907:
    sconst                 "Add ignore"
    iload                  9
    if_setop              
-LABEL1915:
-   jump                   LABEL1928
-LABEL1916:
+LABEL1966:
+   jump                   LABEL1979
+LABEL1967:
    iconst                 -1
    sconst                 ""
    iload                  9
@@ -2142,7 +2199,7 @@ LABEL1916:
    sconst                 ""
    iload                  9
    if_setonmouseleave    
-LABEL1928:
+LABEL1979:
    iload                  6
    iload                  7
    sub                   
@@ -2157,20 +2214,20 @@ LABEL1928:
    iload                  8
    enum                  
    istore                 9
-LABEL1942:
+LABEL1993:
    iload                  10
    chat_getprevuid       
    istore                 10
    jump                   LABEL393
-LABEL1946:
+LABEL1997:
    iload                  8
    istore                 22
-LABEL1948:
+LABEL1999:
    iload                  9
    iconst                 -1
-   if_icmpne              LABEL1952
-   jump                   LABEL2035
-LABEL1952:
+   if_icmpne              LABEL2003
+   jump                   LABEL2086
+LABEL2003:
    iload                  9
    if_clearops           
    iconst                 -1
@@ -2197,14 +2254,14 @@ LABEL1952:
    multiply              
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL1980
-   jump                   LABEL1984
-LABEL1980:
+   if_icmpeq              LABEL2031
+   jump                   LABEL2035
+LABEL2031:
    sconst                 ""
    cc_settext            
    iconst                 1
    cc_sethide            
-LABEL1984:
+LABEL2035:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2213,14 +2270,14 @@ LABEL1984:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL1994
-   jump                   LABEL1998
-LABEL1994:
+   if_icmpeq              LABEL2045
+   jump                   LABEL2049
+LABEL2045:
    sconst                 ""
    cc_settext            
    iconst                 1
    cc_sethide            
-LABEL1998:
+LABEL2049:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2229,14 +2286,14 @@ LABEL1998:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2008
-   jump                   LABEL2012
-LABEL2008:
+   if_icmpeq              LABEL2059
+   jump                   LABEL2063
+LABEL2059:
    sconst                 ""
    cc_settext            
    iconst                 1
    cc_sethide            
-LABEL2012:
+LABEL2063:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2245,12 +2302,12 @@ LABEL2012:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2022
-   jump                   LABEL2024
-LABEL2022:
+   if_icmpeq              LABEL2073
+   jump                   LABEL2075
+LABEL2073:
    iconst                 1
    cc_sethide            
-LABEL2024:
+LABEL2075:
    iload                  8
    iconst                 1
    add                   
@@ -2261,8 +2318,8 @@ LABEL2024:
    iload                  8
    enum                  
    istore                 9
-   jump                   LABEL1948
-LABEL2035:
+   jump                   LABEL1999
+LABEL2086:
    iload                  6
    iconst                 2
    sub                   
@@ -2276,20 +2333,20 @@ LABEL2035:
    istore                 23
    iload                  6
    iload                  23
-   if_icmpgt              LABEL2050
-   jump                   LABEL2052
-LABEL2050:
+   if_icmpgt              LABEL2101
+   jump                   LABEL2103
+LABEL2101:
    iload                  6
    istore                 23
-LABEL2052:
+LABEL2103:
    iload                  22
    istore                 8
-LABEL2054:
+LABEL2105:
    iload                  8
    iconst                 0
-   if_icmpgt              LABEL2058
-   jump                   LABEL2141
-LABEL2058:
+   if_icmpgt              LABEL2109
+   jump                   LABEL2192
+LABEL2109:
    iload                  8
    iconst                 1
    sub                   
@@ -2320,15 +2377,15 @@ LABEL2058:
    multiply              
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2090
-   jump                   LABEL2095
-LABEL2090:
+   if_icmpeq              LABEL2141
+   jump                   LABEL2146
+LABEL2141:
    cc_getx               
    iload                  6
    iconst                 0
    iconst                 0
    cc_setposition        
-LABEL2095:
+LABEL2146:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2337,15 +2394,15 @@ LABEL2095:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2105
-   jump                   LABEL2110
-LABEL2105:
+   if_icmpeq              LABEL2156
+   jump                   LABEL2161
+LABEL2156:
    cc_getx               
    iload                  6
    iconst                 0
    iconst                 0
    cc_setposition        
-LABEL2110:
+LABEL2161:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2354,15 +2411,15 @@ LABEL2110:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2120
-   jump                   LABEL2125
-LABEL2120:
+   if_icmpeq              LABEL2171
+   jump                   LABEL2176
+LABEL2171:
    cc_getx               
    iload                  6
    iconst                 0
    iconst                 0
    cc_setposition        
-LABEL2125:
+LABEL2176:
    iconst                 10616888
    iload                  8
    iconst                 4
@@ -2371,17 +2428,17 @@ LABEL2125:
    add                   
    cc_find               
    iconst                 1
-   if_icmpeq              LABEL2135
-   jump                   LABEL2140
-LABEL2135:
+   if_icmpeq              LABEL2186
+   jump                   LABEL2191
+LABEL2186:
    cc_getx               
    iload                  6
    iconst                 0
    iconst                 0
    cc_setposition        
-LABEL2140:
-   jump                   LABEL2054
-LABEL2141:
+LABEL2191:
+   jump                   LABEL2105
+LABEL2192:
    iconst                 0
    iload                  23
    iconst                 10616888
diff --git a/runelite-client/src/main/scripts/MagicSpellbookRedraw.hash b/runelite-client/src/main/scripts/MagicSpellbookRedraw.hash
new file mode 100644
index 000000000..1d586f516
--- /dev/null
+++ b/runelite-client/src/main/scripts/MagicSpellbookRedraw.hash
@@ -0,0 +1,1 @@
+ACCA9A2E3AE4280269643C9319582C301F5DFE90E03D11EE124824DB4D6E4C17
\ No newline at end of file
diff --git a/runelite-client/src/main/scripts/MagicSpellbookRedraw.rs2asm b/runelite-client/src/main/scripts/MagicSpellbookRedraw.rs2asm
new file mode 100644
index 000000000..61e9635ad
--- /dev/null
+++ b/runelite-client/src/main/scripts/MagicSpellbookRedraw.rs2asm
@@ -0,0 +1,954 @@
+.id                 2611
+.int_stack_count    11
+.string_stack_count 2
+.int_var_count      39
+.string_var_count   2
+   iconst                 190
+   istore                 11
+   iconst                 261
+   istore                 12
+   iconst                 0
+   istore                 13
+   iload                  10
+   iconst                 1
+   if_icmpeq              LABEL10
+   jump                   LABEL76
+LABEL10:
+   iconst                 6
+   iconst                 240
+   iconst                 1
+   iconst                 0
+   iload                  3
+   if_setsize            
+   iconst                 190
+   iconst                 6
+   sub                   
+   iconst                 240
+   istore                 12
+   istore                 11
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iconst                 0
+   iload                  3
+   if_setposition        
+   iconst                 0
+   iload                  4
+   if_sethide            
+   sload                  0
+   iconst                 190
+   iconst                 494
+   parawidth             
+   sload                  1
+   iconst                 190
+   iconst                 494
+   parawidth             
+   invoke                 1045
+   iconst                 14
+   add                   
+   istore                 13
+   iload                  13
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iload                  5
+   if_setsize            
+   iload                  13
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iload                  6
+   if_setsize            
+   iconst                 190
+   iload                  13
+   iconst                 2
+   multiply              
+   sub                   
+   iconst                 3
+   div                   
+   istore                 13
+   iload                  13
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iload                  5
+   if_setposition        
+   iload                  13
+   iconst                 0
+   iconst                 2
+   iconst                 1
+   iload                  6
+   if_setposition        
+   jump                   LABEL147
+LABEL76:
+   get_varbit             6718
+   iconst                 1
+   if_icmpeq              LABEL80
+   jump                   LABEL109
+LABEL80:
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iconst                 1
+   iload                  3
+   if_setsize            
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iconst                 1
+   iload                  3
+   if_setposition        
+   iconst                 1
+   iload                  4
+   if_sethide            
+   iconst                 -1
+   iload                  10
+   iload                  5
+   iload                  6
+   iload                  0
+   iload                  1
+   iload                  7
+   iload                  8
+   iload                  9
+   iload                  2
+   sload                  0
+   sload                  1
+   invoke                 2603
+   jump                   LABEL147
+LABEL109:
+   iconst                 6
+   iconst                 240
+   iconst                 1
+   iconst                 0
+   iload                  3
+   if_setsize            
+   iconst                 190
+   iconst                 6
+   sub                   
+   iconst                 240
+   istore                 12
+   istore                 11
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iconst                 0
+   iload                  3
+   if_setposition        
+   iconst                 0
+   iload                  4
+   if_sethide            
+   sload                  1
+   iconst                 190
+   iconst                 494
+   parawidth             
+   iconst                 14
+   add                   
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iload                  6
+   if_setsize            
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iconst                 1
+   iload                  6
+   if_setposition        
+LABEL147:
+   iload                  0
+   cc_deleteall          
+   iload                  1
+   cc_deleteall          
+   iload                  2
+   cc_deleteall          
+   iconst                 105
+   iconst                 103
+   iconst                 1981
+   get_varbit             4070
+   enum                  
+   istore                 14
+   iconst                 105
+   iconst                 103
+   iconst                 5280
+   get_varbit             4070
+   enum                  
+   istore                 15
+   get_varbit             9730
+   iconst                 0
+   if_icmpge              LABEL169
+   jump                   LABEL175
+LABEL169:
+   iconst                 105
+   iconst                 103
+   iload                  15
+   get_varbit             9730
+   enum                  
+   istore                 14
+LABEL175:
+   iconst                 0
+   istore                 16
+   iconst                 1981
+   enum_getoutputcount   
+   istore                 17
+   iconst                 1
+   istore                 18
+   iconst                 0
+   istore                 19
+   iconst                 -1
+   istore                 20
+LABEL186:
+   iload                  16
+   iload                  17
+   if_icmplt              LABEL190
+   jump                   LABEL239
+LABEL190:
+   iconst                 105
+   iconst                 103
+   iconst                 1981
+   iload                  16
+   enum                  
+   istore                 20
+   iload                  20
+   iload                  14
+   if_icmpne              LABEL200
+   jump                   LABEL202
+LABEL200:
+   iload                  20
+   invoke                 2618
+LABEL202:
+   iconst                 105
+   iconst                 103
+   iconst                 5280
+   iload                  16
+   enum                  
+   istore                 15
+   iconst                 1
+   istore                 18
+   iload                  15
+   enum_getoutputcount   
+   istore                 19
+LABEL213:
+   iload                  18
+   iload                  19
+   if_icmple              LABEL217
+   jump                   LABEL234
+LABEL217:
+   iconst                 105
+   iconst                 103
+   iload                  15
+   iload                  18
+   enum                  
+   istore                 20
+   iload                  20
+   iload                  14
+   if_icmpne              LABEL227
+   jump                   LABEL229
+LABEL227:
+   iload                  20
+   invoke                 2618
+LABEL229:
+   iload                  18
+   iconst                 1
+   add                   
+   istore                 18
+   jump                   LABEL213
+LABEL234:
+   iload                  16
+   iconst                 1
+   add                   
+   istore                 16
+   jump                   LABEL186
+LABEL239:
+   iload                  14
+   iconst                 -1
+   if_icmpeq              LABEL243
+   jump                   LABEL244
+LABEL243:
+   return                
+LABEL244:
+   iload                  14
+   enum_getoutputcount   
+   istore                 21
+   iload                  21
+   define_array           105
+   iconst                 0
+   istore                 22
+   iconst                 -1
+   istore                 23
+   iconst                 0
+   istore                 24
+   iload                  10
+   iconst                 0
+   if_icmpeq              LABEL259
+   jump                   LABEL298
+LABEL259:
+   get_varbit             6718
+   iconst                 1
+   if_icmpeq              LABEL263
+   jump                   LABEL298
+LABEL263:
+   iload                  24
+   iload                  21
+   if_icmplt              LABEL267
+   jump                   LABEL297
+LABEL267:
+   iconst                 105
+   iconst                 111
+   iload                  14
+   iload                  24
+   enum                  
+   istore                 23
+   invoke                 3160
+   iconst                 1
+   if_icmpeq              LABEL277
+   jump                   LABEL280
+LABEL277:
+   iload                  23
+   invoke                 3159
+   istore                 23
+LABEL280:
+   iconst                 0
+   iload                  23
+   iconst                 596
+   oc_param              
+   if_sethide            
+   iload                  22
+   iload                  24
+   set_array_int         
+   iload                  22
+   iconst                 1
+   add                   
+   istore                 22
+   iload                  24
+   iconst                 1
+   add                   
+   istore                 24
+   jump                   LABEL263
+LABEL297:
+   jump                   LABEL343
+LABEL298:
+   iload                  24
+   iload                  21
+   if_icmplt              LABEL302
+   jump                   LABEL343
+LABEL302:
+   iconst                 105
+   iconst                 111
+   iload                  14
+   iload                  24
+   enum                  
+   istore                 23
+   invoke                 3160
+   iconst                 1
+   if_icmpeq              LABEL312
+   jump                   LABEL315
+LABEL312:
+   iload                  23
+   invoke                 3159
+   istore                 23
+LABEL315:
+   iload                  23
+   invoke                 2619
+   iconst                 1
+   if_icmpeq              LABEL320
+   jump                   LABEL333
+LABEL320:
+   iconst                 0
+   iload                  23
+   iconst                 596
+   oc_param              
+   if_sethide            
+   iload                  22
+   iload                  24
+   set_array_int         
+   iload                  22
+   iconst                 1
+   add                   
+   istore                 22
+   jump                   LABEL338
+LABEL333:
+   iconst                 1
+   iload                  23
+   iconst                 596
+   oc_param              
+   if_sethide            
+LABEL338:
+   iload                  24
+   iconst                 1
+   add                   
+   istore                 24
+   jump                   LABEL298
+LABEL343:
+   iconst                 0
+   istore                 25
+   iload                  22
+   iconst                 1 ; run our filter+sort even when there is only one vanilla spell
+   if_icmpge              LABEL349
+   jump                   LABEL357
+LABEL349:
+   iconst                 0
+   iconst                 0
+   iload                  22
+   iconst                 1
+   sub                   
+   iload                  14
+   invoke                 2621
+
+   ; after sorting the spell array
+   iload                  14 ; spellbook enum
+   iconst                 0  ; spell array id
+   iload                  22 ; number of spells
+   sconst                 "spellbookSort"
+   runelite_callback
+   istore                 22 ; number of spells
+   pop_int                   ; spell array id
+   pop_int                   ; spellbook enum
+
+   jump                   LABEL392
+LABEL357:
+   iload                  22
+   iconst                 0
+   if_icmple              LABEL361
+   jump                   LABEL392
+LABEL361:
+   iload                  0
+   iconst                 4
+   iload                  25
+   cc_create             
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iconst                 1
+   cc_setsize            
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iconst                 1
+   cc_setposition        
+   iconst                 16750623
+   cc_setcolour          
+   iconst                 495
+   cc_settextfont        
+   iconst                 1
+   cc_settextshadow      
+   iconst                 1
+   iconst                 1
+   iconst                 0
+   cc_settextalign       
+   sconst                 "No spells match your selected filters."
+   cc_settext            
+   iload                  25
+   iconst                 1
+   add                   
+   istore                 25
+   return                
+LABEL392:
+   iconst                 24
+   istore                 26
+   iconst                 0
+   istore                 27
+   iconst                 0
+   istore                 28
+   iconst                 0
+   istore                 29
+   iconst                 0
+   istore                 30
+   iload                  12
+   istore                 31
+   iload                  10
+   iconst                 0
+   if_icmpeq              LABEL408
+   jump                   LABEL458
+LABEL408:
+   get_varbit             6718
+   iconst                 1
+   if_icmpeq              LABEL412
+   jump                   LABEL458
+LABEL412:
+   get_varbit             4070
+   invoke                 6419
+   istore                 29
+   istore                 30
+   istore                 28
+   istore                 27
+   get_varbit             4070
+   switch                
+      1: LABEL427
+      2: LABEL434
+      3: LABEL441
+   iconst                 1
+   iconst                 15
+   iconst                 1
+   iconst                 0
+   iload                  0
+   if_setposition        
+   jump                   LABEL447
+LABEL427:
+   iconst                 2
+   iconst                 8
+   iconst                 1
+   iconst                 0
+   iload                  0
+   if_setposition        
+   jump                   LABEL447
+LABEL434:
+   iconst                 0
+   iconst                 8
+   iconst                 1
+   iconst                 0
+   iload                  0
+   if_setposition        
+   jump                   LABEL447
+LABEL441:
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iconst                 0
+   iload                  0
+   if_setposition        
+LABEL447:
+   iload                  28
+   iload                  26
+   multiply              
+   iload                  28
+   iconst                 1
+   sub                   
+   iload                  29
+   multiply              
+   add                   
+   istore                 31
+   jump                   LABEL641
+LABEL458:
+   get_varbit             6548
+   iconst                 1
+   if_icmpeq              LABEL472
+   iload                  10
+   iconst                 0
+   if_icmpeq              LABEL465
+   jump                   LABEL552
+LABEL465:
+   get_varbit             8121
+   iconst                 1
+   if_icmpeq              LABEL472
+   get_varbit             6549
+   iconst                 1
+   if_icmpeq              LABEL472
+   jump                   LABEL552
+LABEL472:
+   iload                  22
+   iconst                 28
+   if_icmple              LABEL476
+   jump                   LABEL479
+LABEL476:
+   iconst                 4
+   istore                 27
+   jump                   LABEL489
+LABEL479:
+   iconst                 4
+   iconst                 7
+   iload                  22
+   iconst                 8
+   add                   
+   iconst                 9
+   div                   
+   invoke                 1046
+   invoke                 1045
+   istore                 27
+LABEL489:
+   iconst                 0
+   iload                  26
+   iload                  11
+   iload                  26
+   iload                  27
+   multiply              
+   sub                   
+   iload                  27
+   iconst                 1
+   sub                   
+   div                   
+   invoke                 1046
+   invoke                 1045
+   istore                 30
+   iconst                 1
+   iload                  22
+   iload                  27
+   iconst                 1
+   sub                   
+   add                   
+   iload                  27
+   div                   
+   invoke                 1045
+   istore                 28
+   iload                  28
+   iconst                 2
+   if_icmpge              LABEL517
+   jump                   LABEL531
+LABEL517:
+   iconst                 0
+   iload                  30
+   iload                  12
+   iload                  26
+   iload                  28
+   multiply              
+   sub                   
+   iload                  28
+   iconst                 1
+   sub                   
+   div                   
+   invoke                 1046
+   invoke                 1045
+   istore                 29
+LABEL531:
+   iload                  28
+   iload                  26
+   multiply              
+   iload                  28
+   iconst                 1
+   sub                   
+   iload                  29
+   multiply              
+   add                   
+   iload                  12
+   iconst                 30
+   sub                   
+   invoke                 1045
+   istore                 31
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iconst                 1
+   iload                  0
+   if_setposition        
+   jump                   LABEL641
+LABEL552:
+   iload                  22
+   iconst                 15
+   if_icmple              LABEL556
+   jump                   LABEL561
+LABEL556:
+   iconst                 40
+   iconst                 3
+   istore                 27
+   istore                 26
+   jump                   LABEL580
+LABEL561:
+   iload                  22
+   iconst                 20
+   if_icmple              LABEL565
+   jump                   LABEL570
+LABEL565:
+   iconst                 40
+   iconst                 4
+   istore                 27
+   istore                 26
+   jump                   LABEL580
+LABEL570:
+   iconst                 4
+   iconst                 7
+   iload                  22
+   iconst                 8
+   add                   
+   iconst                 9
+   div                   
+   invoke                 1046
+   invoke                 1045
+   istore                 27
+LABEL580:
+   iconst                 0
+   iconst                 5
+   iconst                 7
+   iload                  26
+   scale                 
+   iload                  11
+   iload                  26
+   iload                  27
+   multiply              
+   sub                   
+   iload                  27
+   iconst                 1
+   sub                   
+   div                   
+   invoke                 1046
+   invoke                 1045
+   istore                 30
+   iconst                 1
+   iload                  22
+   iload                  27
+   iconst                 1
+   sub                   
+   add                   
+   iload                  27
+   div                   
+   invoke                 1045
+   istore                 28
+   iload                  28
+   iconst                 2
+   if_icmpge              LABEL611
+   jump                   LABEL625
+LABEL611:
+   iconst                 0
+   iload                  30
+   iload                  12
+   iload                  26
+   iload                  28
+   multiply              
+   sub                   
+   iload                  28
+   iconst                 1
+   sub                   
+   div                   
+   invoke                 1046
+   invoke                 1045
+   istore                 29
+LABEL625:
+   iload                  28
+   iload                  26
+   multiply              
+   iload                  28
+   iconst                 1
+   sub                   
+   iload                  29
+   multiply              
+   add                   
+   istore                 31
+   iconst                 0
+   iconst                 0
+   iconst                 1
+   iconst                 1
+   iload                  0
+   if_setposition        
+LABEL641:
+   iload                  27
+   iload                  26
+   multiply              
+   iload                  27
+   iconst                 1
+   sub                   
+   iload                  30
+   multiply              
+   add                   
+   iload                  31
+   iconst                 0
+   iconst                 0
+   iload                  0
+   if_setsize            
+   iconst                 0
+   istore                 32
+   iconst                 -1
+   istore                 33
+   iload                  26
+   iload                  30
+   add                   
+   istore                 34
+   iload                  26
+   iload                  29
+   add                   
+   istore                 35
+   iconst                 -1
+   istore                 36
+   iconst                 0
+   istore                 37
+   iconst                 0
+   istore                 38
+   iconst                 0
+   istore                 24
+LABEL675:
+   iload                  24
+   iload                  22
+   if_icmplt              LABEL679
+   jump                   LABEL856
+LABEL679:
+   iconst                 105
+   iconst                 111
+   iload                  14
+   iload                  24
+   get_array_int         
+   enum                  
+   istore                 23
+   invoke                 3160
+   iconst                 1
+   if_icmpeq              LABEL690
+   jump                   LABEL693
+LABEL690:
+   iload                  23
+   invoke                 3159
+   istore                 23
+LABEL693:
+   iload                  23
+   iconst                 596
+   oc_param              
+   istore                 33
+   iload                  26
+   iload                  26
+   iconst                 0
+   iconst                 0
+   iload                  33
+   if_setsize            
+   iload                  24
+   iload                  27
+   mod                   
+   iload                  34
+   multiply              
+   iload                  24
+   iload                  27
+   div                   
+   iload                  35
+   multiply              
+   istore                 38
+   istore                 37
+   iload                  37
+   iload                  38
+   iconst                 0
+   iconst                 0
+   iload                  33
+   if_setposition        
+   iload                  23
+   iload                  33
+   iload                  26
+   invoke                 2614
+   istore                 36
+   istore                 32
+   iload                  32
+   iconst                 1
+   if_icmpeq              LABEL731
+   jump                   LABEL747
+LABEL731:
+   iload                  26
+   iconst                 40
+   if_icmpge              LABEL735
+   jump                   LABEL741
+LABEL735:
+   iload                  23
+   iconst                 599
+   oc_param              
+   iload                  33
+   if_setgraphic         
+   jump                   LABEL746
+LABEL741:
+   iload                  23
+   iconst                 597
+   oc_param              
+   iload                  33
+   if_setgraphic         
+LABEL746:
+   jump                   LABEL786
+LABEL747:
+   iload                  26
+   iconst                 40
+   if_icmpge              LABEL751
+   jump                   LABEL757
+LABEL751:
+   iload                  23
+   iconst                 600
+   oc_param              
+   iload                  33
+   if_setgraphic         
+   jump                   LABEL762
+LABEL757:
+   iload                  23
+   iconst                 598
+   oc_param              
+   iload                  33
+   if_setgraphic         
+LABEL762:
+   iload                  36
+   iconst                 -1
+   if_icmpne              LABEL766
+   jump                   LABEL786
+LABEL766:
+   iload                  0
+   iconst                 5
+   iload                  25
+   cc_create             
+   iload                  26
+   iload                  26
+   iconst                 0
+   iconst                 0
+   cc_setsize            
+   iload                  37
+   iload                  38
+   iconst                 0
+   iconst                 0
+   cc_setposition        
+   iload                  36
+   cc_setgraphic         
+   iload                  25
+   iconst                 1
+   add                   
+   istore                 25
+LABEL786:
+   iload                  33
+   invoke                 2615
+   iload                  10
+   iconst                 1
+   if_icmpeq              LABEL792
+   jump                   LABEL819
+LABEL792:
+   iload                  1
+   iconst                 5
+   iload                  24
+   cc_create             
+   iload                  26
+   iload                  26
+   iconst                 0
+   iconst                 0
+   cc_setsize            
+   iload                  37
+   iload                  38
+   iconst                 0
+   iconst                 0
+   cc_setposition        
+   iload                  23
+   iconst                 1
+   cc_setobject          
+   iconst                 255
+   cc_settrans           
+   iconst                 2612
+   iload                  23
+   iload                  1
+   iload                  2
+   iload                  12
+   sconst                 "oIIi"
+   cc_setonclick         
+   jump                   LABEL851
+LABEL819:
+   iconst                 2622
+   iconst                 1
+   iload                  23
+   iconst                 -2147483645
+   iconst                 -1
+   iload                  2
+   iload                  12
+   iconst                 94
+   iconst                 3
+   inv_getobj            
+   iconst                 94
+   iconst                 5
+   inv_getobj            
+   sconst                 "1oIiIioo"
+   iload                  33
+   if_setonmouserepeat   
+   iconst                 2622
+   iconst                 0
+   iload                  23
+   iconst                 -2147483645
+   iconst                 -1
+   iload                  2
+   iload                  12
+   iconst                 94
+   iconst                 3
+   inv_getobj            
+   iconst                 94
+   iconst                 5
+   inv_getobj            
+   sconst                 "1oIiIioo"
+   iload                  33
+   if_setonmouseleave    
+LABEL851:
+   iload                  24
+   iconst                 1
+   add                   
+   istore                 24
+   jump                   LABEL675
+LABEL856:
+   return                
diff --git a/runelite-jshell/pom.xml b/runelite-jshell/pom.xml
index 8c536caa2..704852cdb 100644
--- a/runelite-jshell/pom.xml
+++ b/runelite-jshell/pom.xml
@@ -30,7 +30,7 @@
     <parent>
         <groupId>net.runelite</groupId>
         <artifactId>runelite-parent</artifactId>
-        <version>1.10.27-SNAPSHOT</version>
+        <version>1.10.29-SNAPSHOT</version>
     </parent>
 
     <artifactId>jshell</artifactId>
diff --git a/runelite-maven-plugin/pom.xml b/runelite-maven-plugin/pom.xml
index e443faed1..2fb805abb 100644
--- a/runelite-maven-plugin/pom.xml
+++ b/runelite-maven-plugin/pom.xml
@@ -29,7 +29,7 @@
 	<parent>
 		<groupId>net.runelite</groupId>
 		<artifactId>runelite-parent</artifactId>
-		<version>1.10.27-SNAPSHOT</version>
+		<version>1.10.29-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-maven-plugin</artifactId>


Test patch:
diff --git a/runelite-client/src/test/java/net/runelite/client/config/KeybindTest.java b/runelite-client/src/test/java/net/runelite/client/config/KeybindTest.java
new file mode 100644
index 000000000..14c1a34f2
--- /dev/null
+++ b/runelite-client/src/test/java/net/runelite/client/config/KeybindTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2024, Adam <Adam@sigterm.info>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.client.config;
+
+import static java.awt.event.InputEvent.CTRL_DOWN_MASK;
+import static java.awt.event.InputEvent.SHIFT_DOWN_MASK;
+import java.awt.event.KeyEvent;
+import static java.awt.event.KeyEvent.KEY_PRESSED;
+import static java.awt.event.KeyEvent.KEY_RELEASED;
+import static java.awt.event.KeyEvent.VK_CONTROL;
+import static java.awt.event.KeyEvent.VK_SHIFT;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class KeybindTest
+{
+	/**
+	 * Regression test for a bug where a modifier key keybind will not match key release if other modifiers are held down.
+	 */
+	@Test
+	public void testModifierOnlyKeybindReleaseWithOtherModifierKeyHeldDown()
+	{
+		Keybind keybind = new Keybind(VK_CONTROL, CTRL_DOWN_MASK);
+
+		// when a modifier key is pressed, the keyevent will have the modifier in the modifiers field and in the keyCode field.
+		// when it is released, it will only be in the keyCode field.
+		Assert.assertTrue(keybind.matches(createKeyEvent(KEY_PRESSED, CTRL_DOWN_MASK, VK_CONTROL)));
+		Assert.assertFalse(keybind.matches(createKeyEvent(KEY_PRESSED, SHIFT_DOWN_MASK | CTRL_DOWN_MASK, VK_SHIFT)));
+		Assert.assertTrue(keybind.matches(createKeyEvent(KEY_RELEASED, SHIFT_DOWN_MASK, VK_CONTROL)));
+		Assert.assertFalse(keybind.matches(createKeyEvent(KEY_RELEASED, 0, VK_SHIFT)));
+	}
+
+	private KeyEvent createKeyEvent(int id, int modifiersEx, int extendedKeyCode)
+	{
+		KeyEvent mock = Mockito.mock(KeyEvent.class);
+		Mockito.when(mock.getExtendedKeyCode()).thenReturn(extendedKeyCode);
+		Mockito.when(mock.getModifiersEx()).thenReturn(modifiersEx);
+		Mockito.when(mock.getID()).thenReturn(id);
+		return mock;
+	}
+}
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java
index a13e29e04..47efe5541 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java
@@ -104,11 +104,11 @@ public class CannonPluginTest
 	@Test
 	public void testAmmoCountOnPlace()
 	{
-		ChatMessage chatMessage = new ChatMessage();
-		chatMessage.setType(ChatMessageType.SPAM);
-		chatMessage.setMessage("You add the furnace.");
+		VarbitChanged varbitChanged = new VarbitChanged();
+		varbitChanged.setVarpId(VarPlayer.CANNON_STATE);
+		varbitChanged.setValue(4);
 
-		plugin.onChatMessage(chatMessage);
+		plugin.onVarbitChanged(varbitChanged);
 		assertTrue(plugin.isCannonPlaced());
 
 		plugin.onVarbitChanged(cannonAmmoChanged);
@@ -126,11 +126,11 @@ public class CannonPluginTest
 	{
 		when(config.showInfobox()).thenReturn(true);
 
-		ChatMessage chatMessage = new ChatMessage();
-		chatMessage.setType(ChatMessageType.SPAM);
-		chatMessage.setMessage("You add the furnace.");
+		VarbitChanged varbitChanged = new VarbitChanged();
+		varbitChanged.setVarpId(VarPlayer.CANNON_STATE);
+		varbitChanged.setValue(4);
 
-		plugin.onChatMessage(chatMessage);
+		plugin.onVarbitChanged(varbitChanged);
 		assertTrue(plugin.isCannonPlaced());
 
 		assertEquals(0, plugin.getCballsLeft());
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/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);
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPluginTest.java
index 97f75e8e3..6d2cc692c 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/menuentryswapper/MenuEntrySwapperPluginTest.java
@@ -41,6 +41,7 @@ import net.runelite.client.chat.ChatMessageManager;
 import net.runelite.client.config.ConfigManager;
 import net.runelite.client.game.ItemManager;
 import net.runelite.client.menus.TestMenuEntry;
+import org.junit.After;
 import static org.junit.Assert.assertArrayEquals;
 import org.junit.Before;
 import org.junit.Test;
@@ -111,7 +112,13 @@ public class MenuEntrySwapperPluginTest
 			return null;
 		}).when(client).setMenuEntries(any(MenuEntry[].class));
 
-		menuEntrySwapperPlugin.setupSwaps();
+		menuEntrySwapperPlugin.startUp();
+	}
+
+	@After
+	public void after()
+	{
+		menuEntrySwapperPlugin.shutDown();
 	}
 
 	private MenuEntry menu(String option, String target, MenuAction menuAction)
diff --git a/runelite-client/src/test/java/net/runelite/client/ui/overlay/components/TextComponentTest.java b/runelite-client/src/test/java/net/runelite/client/ui/overlay/components/TextComponentTest.java
index aedfb8d92..5f944f361 100644
--- a/runelite-client/src/test/java/net/runelite/client/ui/overlay/components/TextComponentTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/ui/overlay/components/TextComponentTest.java
@@ -32,8 +32,9 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import org.mockito.InOrder;
 import org.mockito.Mock;
-import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -60,7 +61,7 @@ public class TextComponentTest
 		textComponent.setColor(Color.RED);
 		textComponent.render(graphics);
 		verify(graphics, times(2)).drawString(eq("test"), anyInt(), anyInt());
-		verify(graphics, atLeastOnce()).setColor(Color.RED);
+		verify(graphics).setColor(Color.RED);
 	}
 
 	@Test
@@ -70,7 +71,7 @@ public class TextComponentTest
 		textComponent.setText("<col=0000ff>test");
 		textComponent.render(graphics);
 		verify(graphics, times(2)).drawString(eq("test"), anyInt(), anyInt());
-		verify(graphics, atLeastOnce()).setColor(Color.BLUE);
+		verify(graphics).setColor(Color.BLUE);
 	}
 
 	@Test
@@ -79,9 +80,28 @@ public class TextComponentTest
 		TextComponent textComponent = new TextComponent();
 		textComponent.setText("<col=0000ff>test<col=00ff00> test");
 		textComponent.render(graphics);
-		verify(graphics, atLeastOnce()).drawString(eq("test"), anyInt(), anyInt());
-		verify(graphics, atLeastOnce()).drawString(eq(" test"), anyInt(), anyInt());
-		verify(graphics, atLeastOnce()).setColor(Color.BLUE);
-		verify(graphics, atLeastOnce()).setColor(Color.GREEN);
+		verify(graphics, times(2)).drawString(eq("test"), anyInt(), anyInt());
+		verify(graphics, times(2)).drawString(eq(" test"), anyInt(), anyInt());
+		verify(graphics).setColor(Color.BLUE);
+		verify(graphics).setColor(Color.GREEN);
+	}
+
+	@Test
+	public void testRender4()
+	{
+		TextComponent textComponent = new TextComponent();
+		textComponent.setText("test<col=0000ff>test2");
+		textComponent.render(graphics);
+
+		InOrder g = inOrder(graphics);
+		g.verify(graphics).setColor(Color.BLACK);
+		g.verify(graphics).drawString(eq("test"), anyInt(), anyInt());
+		g.verify(graphics).setColor(Color.WHITE);
+		g.verify(graphics).drawString(eq("test"), anyInt(), anyInt());
+
+		g.verify(graphics).setColor(Color.BLACK);
+		g.verify(graphics).drawString(eq("test2"), anyInt(), anyInt());
+		g.verify(graphics).setColor(Color.BLUE);
+		g.verify(graphics).drawString(eq("test2"), anyInt(), anyInt());
 	}
 }