Repository: runelite/runelite
Pull request #18144 (2024-08-09 00:56:08)

animsmoothing: add Demonbane spells, windmills to blocklist
Addresses what animations from #16371 I was personally able to verify were still problematic with animation smoothing enabled:
- Harmony Island and Gwenith windmills
- Lithkren generator 
- All 3 Arceuus spellbook Demonbane spells.


Patch:
diff --git a/cache/pom.xml b/cache/pom.xml
index 9726c12e8..76aa604a4 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.36-SNAPSHOT</version>
+		<version>1.10.37-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cache</artifactId>
diff --git a/cache/src/main/java/net/runelite/cache/definitions/ItemDefinition.java b/cache/src/main/java/net/runelite/cache/definitions/ItemDefinition.java
index e1465b465..6f384b66b 100644
--- a/cache/src/main/java/net/runelite/cache/definitions/ItemDefinition.java
+++ b/cache/src/main/java/net/runelite/cache/definitions/ItemDefinition.java
@@ -71,6 +71,7 @@ public class ItemDefinition
 	public int[] countObj;
 
 	public String[] options = new String[]{null, null, "Take", null, null};
+	public String[][] subops;
 
 	public String[] interfaceOptions = new String[]{null, null, null, null, "Drop"};
 
diff --git a/cache/src/main/java/net/runelite/cache/definitions/loaders/ItemLoader.java b/cache/src/main/java/net/runelite/cache/definitions/loaders/ItemLoader.java
index bc227dbb7..65ac73bdb 100644
--- a/cache/src/main/java/net/runelite/cache/definitions/loaders/ItemLoader.java
+++ b/cache/src/main/java/net/runelite/cache/definitions/loaders/ItemLoader.java
@@ -187,6 +187,35 @@ public class ItemLoader
 		{
 			def.shiftClickDropIndex = stream.readByte();
 		}
+		else if (opcode == 43)
+		{
+			int opId = stream.readUnsignedByte();
+			if (def.subops == null)
+			{
+				def.subops = new String[5][];
+			}
+
+			boolean valid = opId >= 0 && opId < 5;
+			if (valid && def.subops[opId] == null)
+			{
+				def.subops[opId] = new String[20];
+			}
+
+			while (true)
+			{
+				int subopId = stream.readUnsignedByte() - 1;
+				if (subopId == -1)
+				{
+					break;
+				}
+
+				String op = stream.readString();
+				if (valid && subopId >= 0 && subopId < 20)
+				{
+					def.subops[opId][subopId] = op;
+				}
+			}
+		}
 		else if (opcode == 65)
 		{
 			def.isTradeable = true;
diff --git a/pom.xml b/pom.xml
index f641c4fc3..a9def056e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
 
 	<groupId>net.runelite</groupId>
 	<artifactId>runelite-parent</artifactId>
-	<version>1.10.36-SNAPSHOT</version>
+	<version>1.10.37-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>RuneLite</name>
@@ -47,7 +47,7 @@
 		<checkstyle.skip>true</checkstyle.skip>
 		<glslang.path></glslang.path>
 
-		<rs.version>223</rs.version>
+		<rs.version>224</rs.version>
 	</properties>
 
 	<licenses>
diff --git a/runelite-api/pom.xml b/runelite-api/pom.xml
index 4bf68789c..9386c6751 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.36-SNAPSHOT</version>
+		<version>1.10.37-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-api</artifactId>
diff --git a/runelite-api/src/main/java/net/runelite/api/AnimationID.java b/runelite-api/src/main/java/net/runelite/api/AnimationID.java
index 3f31e303c..a2c6c6516 100644
--- a/runelite-api/src/main/java/net/runelite/api/AnimationID.java
+++ b/runelite-api/src/main/java/net/runelite/api/AnimationID.java
@@ -324,6 +324,7 @@ public final class AnimationID
 
 	// Arceuus spellbook
 	public static final int MAGIC_ARCEUUS_RESURRECT_CROPS = 7118;
+	public static final int MAGIC_ARCEUUS_DEMONBANE = 8977;    // Shared by all 3 Demonbane spells
 
 	// Battlestaff Crafting
 	public static final int CRAFTING_BATTLESTAVES = 7531;
@@ -363,5 +364,8 @@ public final class AnimationID
 	public static final int VIGGORAS_CHAINMACE_IDLE = 244;
 
 	public static final int MLM_WATER_WHEEL_SPINNING = 1051;
+	public static final int HARMONY_ISLAND_WINDMILL_SPINNING = 5857;
+	public static final int GWENITH_WINDMILL_SPINNING = 6495;
+	public static final int LITHKREN_GENERATOR_SPINNING = 7898;
 	public static final int GIANTS_FOUNDRY_WATER_WHEEL_SPINNING = 9450;
 }
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 28e67a21f..8c70ec7ab 100644
--- a/runelite-api/src/main/java/net/runelite/api/Client.java
+++ b/runelite-api/src/main/java/net/runelite/api/Client.java
@@ -597,11 +597,20 @@ public interface Client extends OAuthApi, GameEngine
 	 */
 	World[] getWorldList();
 
+	/**
+	 * Get the client menu.
+	 */
+	@Nonnull
+	Menu getMenu();
+
 	/**
 	 * Create a new menu entry
 	 * @param idx the index to create the menu entry at. Accepts negative indexes eg. -1 inserts at the end.
 	 * @return the newly created menu entry
+	 * @see #getMenu()
+	 * @see Menu#createMenuEntry(int)
 	 */
+	@Deprecated
 	MenuEntry createMenuEntry(int idx);
 
 	/**
@@ -609,7 +618,10 @@ public interface Client extends OAuthApi, GameEngine
 	 * clicked and activated.
 	 *
 	 * @return array of open menu entries
+	 * @see #getMenu()
+	 * @see Menu#getMenuEntries()
 	 */
+	@Deprecated
 	MenuEntry[] getMenuEntries();
 
 	/**
@@ -619,7 +631,10 @@ public interface Client extends OAuthApi, GameEngine
 	 * event, since setting the menu entries will be overwritten the next frame
 	 *
 	 * @param entries new array of open menu entries
+	 * @see #getMenu()
+	 * @see Menu#setMenuEntries(MenuEntry[])
 	 */
+	@Deprecated
 	void setMenuEntries(MenuEntry[] entries);
 
 	/**
@@ -651,28 +666,36 @@ public interface Client extends OAuthApi, GameEngine
 	 * Get the menu x location. Only valid if the menu is open.
 	 *
 	 * @return the menu x location
+	 * @see Menu#getMenuX()
 	 */
+	@Deprecated
 	int getMenuX();
 
 	/**
 	 * Get the menu y location. Only valid if the menu is open.
 	 *
 	 * @return the menu y location
+	 * @see Menu#getMenuY()
 	 */
+	@Deprecated
 	int getMenuY();
 
 	/**
 	 * Get the menu height. Only valid if the menu is open.
 	 *
 	 * @return the menu height
+	 * @see Menu#getMenuHeight()
 	 */
+	@Deprecated
 	int getMenuHeight();
 
 	/**
 	 * Get the menu width. Only valid if the menu is open.
 	 *
 	 * @return the menu width
+	 * @see Menu#getMenuWidth()
 	 */
+	@Deprecated
 	int getMenuWidth();
 
 	/**
@@ -2047,6 +2070,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * @return the array of instance template chunks
 	 * @see Constants#CHUNK_SIZE
 	 * @see InstanceTemplates
+	 * @see WorldView#getInstanceTemplateChunks()
 	 */
 	@Deprecated
 	int[][][] getInstanceTemplateChunks();
@@ -2068,6 +2092,7 @@ public interface Client extends OAuthApi, GameEngine
 
 	/**
 	 * Checks whether the scene is in an instanced region.
+	 * @see WorldView#isInstance()
 	 */
 	@Deprecated
 	boolean isInInstancedRegion();
@@ -2082,6 +2107,7 @@ public interface Client extends OAuthApi, GameEngine
 
 	/**
 	 * Gets the current scene
+	 * @see WorldView#getScene()
 	 */
 	@Deprecated
 	default Scene getScene()
@@ -2094,6 +2120,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * Gets a list of all valid players from the player cache.
 	 *
 	 * @return a list of all players
+	 * @see WorldView#players()
 	 */
 	@Deprecated
 	default List<Player> getPlayers()
@@ -2108,6 +2135,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * Gets a list of all valid NPCs from the NPC cache.
 	 *
 	 * @return a list of all NPCs
+	 * @see WorldView#npcs()
 	 */
 	@Deprecated
 	default List<NPC> getNpcs()
@@ -2122,6 +2150,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * Gets an array of all cached NPCs.
 	 *
 	 * @return cached NPCs
+	 * @see WorldView#npcs()
 	 */
 	@Deprecated
 	default NPC[] getCachedNPCs()
@@ -2134,6 +2163,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * Gets an array of all cached players.
 	 *
 	 * @return cached players
+	 * @see WorldView#players()
 	 */
 	@Deprecated
 	default Player[] getCachedPlayers()
@@ -2148,6 +2178,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * The index into the array is the plane/z-axis coordinate.
 	 *
 	 * @return the collision data
+	 * @see WorldView#getCollisionMaps()
 	 */
 	@Nullable
 	@Deprecated
@@ -2167,6 +2198,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * level use a tile offset and are still considered plane 0 by the game.
 	 *
 	 * @return the plane
+	 * @see WorldView#getPlane()
 	 */
 	@Deprecated
 	default int getPlane()
@@ -2179,6 +2211,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * current scene.
 	 *
 	 * @return the tile heights
+	 * @see WorldView#getTileHeights()
 	 */
 	@Deprecated
 	default int[][][] getTileHeights()
@@ -2191,6 +2224,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * current scene.
 	 *
 	 * @return the tile settings
+	 * @see WorldView#getTileSettings()
 	 */
 	@Deprecated
 	default byte[][][] getTileSettings()
@@ -2205,6 +2239,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * the current scene (ie. the bottom-left most coordinates in the scene).
 	 *
 	 * @return the base x-axis coordinate
+	 * @see WorldView#getBaseX()
 	 */
 	@Deprecated
 	default int getBaseX()
@@ -2220,6 +2255,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * the current scene (ie. the bottom-left most coordinates in the scene).
 	 *
 	 * @return the base y-axis coordinate
+	 * @see WorldView#getBaseY()
 	 */
 	@Deprecated
 	default int getBaseY()
@@ -2244,6 +2280,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * @param targetX target x - if an actor target is supplied should be the target x
 	 * @param targetY target y - if an actor target is supplied should be the target y
 	 * @return the new projectile
+	 * @see WorldView#createProjectile(int, int, int, int, int, int, int, int, int, int, Actor, int, int)
 	 */
 	@Deprecated
 	default Projectile createProjectile(int id, int plane, int startX, int startY, int startZ, int startCycle, int endCycle,
@@ -2256,6 +2293,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * Gets a list of all projectiles currently spawned.
 	 *
 	 * @return all projectiles
+	 * @see WorldView#getProjectiles()
 	 */
 	@Deprecated
 	default Deque<Projectile> getProjectiles()
@@ -2267,6 +2305,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * Gets a list of all graphics objects currently drawn.
 	 *
 	 * @return all graphics objects
+	 * @see WorldView#getGraphicsObjects()
 	 */
 	@Deprecated
 	default Deque<GraphicsObject> getGraphicsObjects()
@@ -2278,6 +2317,7 @@ public interface Client extends OAuthApi, GameEngine
 	 * Gets the currently selected tile. (ie. last right clicked tile)
 	 *
 	 * @return the selected tile
+	 * @see WorldView#getSelectedSceneTile()
 	 */
 	@Deprecated
 	@Nullable
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 b72e63f66..6f316e984 100644
--- a/runelite-api/src/main/java/net/runelite/api/ItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ItemID.java
@@ -4192,7 +4192,7 @@ public final class ItemID
 	public static final int BOTTLED_WATER = 6953;
 	public static final int REDBERRY_JUICE = 6954;
 	public static final int PINK_DYE = 6955;
-	public static final int ROSE_TINTED_LENS = 6956;
+	public static final int ROSETINTED_LENS = 6956;
 	public static final int WIZARDS_HEAD = 6957;
 	public static final int SAND = 6958;
 	public static final int PINK_CAPE = 6959;
@@ -7610,7 +7610,7 @@ public final class ItemID
 	public static final int BIG_PIRATE_HAT = 12355;
 	public static final int KATANA = 12357;
 	public static final int LEPRECHAUN_HAT = 12359;
-	public static final int CAT_MASK = 12361;
+	public static final int CAT_H = 12361;
 	public static final int BRONZE_DRAGON_MASK = 12363;
 	public static final int IRON_DRAGON_MASK = 12365;
 	public static final int STEEL_DRAGON_MASK = 12367;
@@ -14113,5 +14113,53 @@ public final class ItemID
 	public static final int SIGIL_OF_GUNSLINGER_29676 = 29676;
 	public static final int SIGIL_OF_ARCANE_SWIFTNESS = 29678;
 	public static final int SIGIL_OF_ARCANE_SWIFTNESS_29679 = 29679;
+	public static final int GUTHIXIAN_TEMPLE_TELEPORT = 29684;
+	public static final int DNI23_TORSO_LIGHTBUTTONS = 29686;
+	public static final int DNI23_TORSO_DARKBUTTONS = 29688;
+	public static final int DNI23_TORSO_DARKBUTTONS_29690 = 29690;
+	public static final int DNI23_TORSO_SHIRT = 29692;
+	public static final int DNI23_TORSO_STITCHING = 29694;
+	public static final int DNI23_TORSO_TWOTONED = 29696;
+	public static final int DNI23_TORSO_PRINCELY = 29698;
+	public static final int DNI23_TORSO_RIPPEDWESKIT = 29700;
+	public static final int DNI23_TORSO_RIPPEDWESKIT_29702 = 29702;
+	public static final int DNI23_TORSO_CROPTOPS = 29704;
+	public static final int DNI23_TORSO_POLONECK = 29706;
+	public static final int DNI23_TORSO_SIMPLE = 29708;
+	public static final int DNI23_TORSO_FRILLY = 29710;
+	public static final int DNI23_TORSO_CORSETRY = 29712;
+	public static final int DNI23_TORSO_BODICE = 29714;
+	public static final int DNI23_ARMS_THIN = 29716;
+	public static final int DNI23_ARMS_SHOULDERPADS = 29718;
+	public static final int DNI23_ARMS_THICKSTRIPE = 29720;
+	public static final int DNI23_ARMS_LOOSESLEEVES = 29722;
+	public static final int DNI23_ARMS_PRINCELY = 29724;
+	public static final int DNI23_ARMS_TATTYLONG = 29726;
+	public static final int DNI23_ARMS_RIPPED = 29728;
+	public static final int DNI23_ARMS_BARE = 29730;
+	public static final int DNI23_ARMS_FRILLY = 29732;
+	public static final int DNI23_ARMS_TATTYSHORT = 29734;
+	public static final int DNI23_ARMS_BARESHOULDERS = 29736;
+	public static final int DNI23_LEGS_SHORTS = 29738;
+	public static final int DNI23_LEGS_BEACH = 29740;
+	public static final int DNI23_LEGS_PRINCELY = 29742;
+	public static final int DNI23_LEGS_LEGGINGS = 29744;
+	public static final int DNI23_LEGS_SIDESTRIPES = 29746;
+	public static final int DNI23_LEGS_RIPPED = 29748;
+	public static final int DNI23_LEGS_PATCHED = 29750;
+	public static final int DNI23_LEGS_SKIRT = 29752;
+	public static final int DNI23_LEGS_LONGSKIRT = 29754;
+	public static final int DNI23_LEGS_LONGNARROWSKIRT = 29756;
+	public static final int DNI23_LEGS_SHORTSKIRT = 29758;
+	public static final int DNI23_LEGS_LAYERED = 29760;
+	public static final int DNI23_LEGS_SASHDOTS = 29762;
+	public static final int DNI23_LEGS_BIGHEM = 29764;
+	public static final int DNI23_LEGS_SASHTROUSERS = 29766;
+	public static final int DNI23_LEGS_PATTERNED = 29768;
+	public static final int DNI23_LEGS_TORNSKIRT = 29770;
+	public static final int DNI23_LEGS_PATCHEDSKIRT = 29772;
+	public static final int AMYS_SAW_OFFHAND = 29774;
+	public static final int IMCANDO_HAMMER_OFFHAND = 29775;
+	public static final int BRUMA_TORCH_OFFHAND = 29777;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/Menu.java b/runelite-api/src/main/java/net/runelite/api/Menu.java
new file mode 100644
index 000000000..74d7562ae
--- /dev/null
+++ b/runelite-api/src/main/java/net/runelite/api/Menu.java
@@ -0,0 +1,89 @@
+/*
+ * 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.api;
+
+/**
+ * The client minimenu.
+ */
+public interface Menu
+{
+	/**
+	 * Create a new menu entry
+	 * @param idx the index to create the menu entry at. Accepts negative indexes eg. -1 inserts at the end.
+	 * @return the newly created menu entry
+	 */
+	MenuEntry createMenuEntry(int idx);
+
+	/**
+	 * Gets the current mini menu entries.
+	 *
+	 * @return array of menu entries
+	 */
+	MenuEntry[] getMenuEntries();
+
+	/**
+	 * Sets the array of menu entries.
+	 * <p>
+	 * This method should typically be used in the context of the {@link net.runelite.api.events.MenuOpened}
+	 * event, since setting the menu entries will be overwritten the next frame
+	 *
+	 * @param entries new array of open menu entries
+	 */
+	void setMenuEntries(MenuEntry[] entries);
+
+	/**
+	 * Remove a menu entry
+	 * @param entry the menu entry
+	 */
+	void removeMenuEntry(MenuEntry entry);
+
+	/**
+	 * Get the menu x location. Only valid if the menu is open.
+	 *
+	 * @return the menu x location
+	 */
+	int getMenuX();
+
+	/**
+	 * Get the menu y location. Only valid if the menu is open.
+	 *
+	 * @return the menu y location
+	 */
+	int getMenuY();
+
+	/**
+	 * Get the menu width. Only valid if the menu is open.
+	 *
+	 * @return the menu width
+	 */
+	int getMenuWidth();
+
+	/**
+	 * Get the menu height. Only valid if the menu is open.
+	 *
+	 * @return the menu height
+	 */
+	int getMenuHeight();
+}
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 41bb1af3f..739325518 100644
--- a/runelite-api/src/main/java/net/runelite/api/MenuAction.java
+++ b/runelite-api/src/main/java/net/runelite/api/MenuAction.java
@@ -245,10 +245,10 @@ public enum MenuAction
 	WIDGET_TARGET_ON_WIDGET(58),
 
 	/**
-	 * Submenu parent that is also a widget.
-	 * see {@link #RUNELITE_SUBMENU}
+	 * RuneLite menu that is a widge.
+	 * @see MenuEntry#getWidget()
 	 */
-	RUNELITE_SUBMENU_WIDGET(998),
+	RUNELITE_WIDGET(998),
 
 	/**
 	 * Menu action for high priority runelite options
@@ -303,9 +303,9 @@ public enum MenuAction
 	 */
 	RUNELITE_INFOBOX(1504),
 	/**
-	 * Parent menu of submenus
+	 * Like {@link #RUNELITE}, except clicking always forces the menu open.
 	 */
-	RUNELITE_SUBMENU(1505),
+	RUNELITE_LOW_PRIORITY(1506),
 
 	/**
 	 * Menu action triggered when the id is not defined in this class.
diff --git a/runelite-api/src/main/java/net/runelite/api/MenuEntry.java b/runelite-api/src/main/java/net/runelite/api/MenuEntry.java
index a207a814b..83e227cb1 100644
--- a/runelite-api/src/main/java/net/runelite/api/MenuEntry.java
+++ b/runelite-api/src/main/java/net/runelite/api/MenuEntry.java
@@ -25,6 +25,7 @@
 package net.runelite.api;
 
 import java.util.function.Consumer;
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import net.runelite.api.widgets.Widget;
 
@@ -99,18 +100,10 @@ public interface MenuEntry
 	MenuEntry onClick(Consumer<MenuEntry> callback);
 
 	/**
-	 * Set the parent for the menu entry. This makes this menu entry part of the sub-menu off of the parent.
-	 * @param parent
+	 * Get the callback called when the menu option is clicked
 	 * @return
 	 */
-	MenuEntry setParent(MenuEntry parent);
-
-	/**
-	 * Get the parent for the menu entry.
-	 * @return
-	 */
-	@Nullable
-	MenuEntry getParent();
+	Consumer<MenuEntry> onClick();
 
 	/**
 	 * Test if this menu entry is an item op. "Use" and "Examine" are not considered item ops.
@@ -167,4 +160,25 @@ public interface MenuEntry
 	 */
 	@Nullable
 	Actor getActor();
+
+	/**
+	 * Get the submenu for this menu entry.
+	 * @return the submenu, or null if one doesn't exist
+	 * @see #createSubMenu()
+	 */
+	@Nullable
+	Menu getSubMenu();
+
+	/**
+	 * Create a new submenu for this menu entry.
+	 * This will erase any previous submenu.
+	 * @return the new submenu
+	 */
+	@Nonnull
+	Menu createSubMenu();
+
+	/**
+	 * Remove the submenu from this menu entry.
+	 */
+	void deleteSubMenu();
 }
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 d2f3362b9..f73d19887 100644
--- a/runelite-api/src/main/java/net/runelite/api/NpcID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NpcID.java
@@ -1008,7 +1008,7 @@ public final class NpcID
 	public static final int CAVE_HORROR_1050 = 1050;
 	public static final int CAVE_HORROR_1051 = 1051;
 	public static final int CAVEY_DAVEY = 1052;
-	public static final int PATCHY = 1053;
+	public static final int MAC = 1053;
 	public static final int LAUNA = 1054;
 	public static final int LAUNA_1055 = 1055;
 	public static final int BRANA = 1056;
@@ -5442,7 +5442,7 @@ public final class NpcID
 	public static final int SUMMONED_SOUL = 5867;
 	public static final int SUMMONED_SOUL_5868 = 5868;
 	public static final int SUMMONED_SOUL_5869 = 5869;
-	public static final int KEY_MASTER = 5870;
+	public static final int PATCHY = 5870;
 	public static final int KING_ARTHUR_5871 = 5871;
 	public static final int BABY_GREEN_DRAGON_5872 = 5872;
 	public static final int BABY_GREEN_DRAGON_5873 = 5873;
@@ -5859,7 +5859,7 @@ public final class NpcID
 	public static final int RUFUS_6478 = 6478;
 	public static final int EYE = 6479;
 	public static final int EYE_6480 = 6480;
-	public static final int MAC = 6481;
+	public static final int JACK_FROST = 6481;
 	public static final int BOULDER_6482 = 6482;
 	public static final int MONKEY_6483 = 6483;
 	public static final int GELIN = 6484;
@@ -10552,7 +10552,7 @@ public final class NpcID
 	public static final int JALIMKOT_12594 = 12594;
 	public static final int GOOSE = 12597;
 	public static final int ANTISANTA = 12598;
-	public static final int JACK_FROST = 12599;
+	public static final int KEY_MASTER = 12599;
 	public static final int GUSS_MISTLETOAD = 12600;
 	public static final int SANTAS_SCRIBE = 12601;
 	public static final int SANTA_CLAUS = 12602;
@@ -11349,10 +11349,10 @@ public final class NpcID
 	public static final int HILL_GIANT_13502 = 13502;
 	public static final int HILL_GIANT_13503 = 13503;
 	public static final int HILL_GIANT_13504 = 13504;
-	public static final int EARTH_SURGE = 13505;
-	public static final int WATER_SURGE = 13506;
-	public static final int FIRE_SURGE = 13507;
-	public static final int WIND_SURGE = 13508;
+	public static final int EARTH_SPELL = 13505;
+	public static final int WATER_SPELL = 13506;
+	public static final int FIRE_SPELL = 13507;
+	public static final int WIND_SPELL = 13508;
 	public static final int GUARDIAN_OF_ARMADYL_13509 = 13509;
 	public static final int IVY_SOPHISTA = 13510;
 	public static final int THAERISK = 13511;
@@ -11474,5 +11474,6 @@ public final class NpcID
 	public static final int DURIAL321 = 13662;
 	public static final int MAGIC_MARK = 13663;
 	public static final int RANGING_RO = 13664;
+	public static final int BROAV_13665 = 13665;
 	/* This file is automatically generated. Do not edit. */
 }
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 783d20138..5e8e07a2d 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullItemID.java
@@ -15357,5 +15357,54 @@ public final class NullItemID
 	public static final int NULL_29674 = 29674;
 	public static final int NULL_29677 = 29677;
 	public static final int NULL_29680 = 29680;
+	public static final int NULL_29681 = 29681;
+	public static final int NULL_29682 = 29682;
+	public static final int NULL_29683 = 29683;
+	public static final int NULL_29685 = 29685;
+	public static final int NULL_29687 = 29687;
+	public static final int NULL_29689 = 29689;
+	public static final int NULL_29691 = 29691;
+	public static final int NULL_29693 = 29693;
+	public static final int NULL_29695 = 29695;
+	public static final int NULL_29697 = 29697;
+	public static final int NULL_29699 = 29699;
+	public static final int NULL_29701 = 29701;
+	public static final int NULL_29703 = 29703;
+	public static final int NULL_29705 = 29705;
+	public static final int NULL_29707 = 29707;
+	public static final int NULL_29709 = 29709;
+	public static final int NULL_29711 = 29711;
+	public static final int NULL_29713 = 29713;
+	public static final int NULL_29715 = 29715;
+	public static final int NULL_29717 = 29717;
+	public static final int NULL_29719 = 29719;
+	public static final int NULL_29721 = 29721;
+	public static final int NULL_29723 = 29723;
+	public static final int NULL_29725 = 29725;
+	public static final int NULL_29727 = 29727;
+	public static final int NULL_29729 = 29729;
+	public static final int NULL_29731 = 29731;
+	public static final int NULL_29733 = 29733;
+	public static final int NULL_29735 = 29735;
+	public static final int NULL_29737 = 29737;
+	public static final int NULL_29739 = 29739;
+	public static final int NULL_29741 = 29741;
+	public static final int NULL_29743 = 29743;
+	public static final int NULL_29745 = 29745;
+	public static final int NULL_29747 = 29747;
+	public static final int NULL_29749 = 29749;
+	public static final int NULL_29751 = 29751;
+	public static final int NULL_29753 = 29753;
+	public static final int NULL_29755 = 29755;
+	public static final int NULL_29757 = 29757;
+	public static final int NULL_29759 = 29759;
+	public static final int NULL_29761 = 29761;
+	public static final int NULL_29763 = 29763;
+	public static final int NULL_29765 = 29765;
+	public static final int NULL_29767 = 29767;
+	public static final int NULL_29769 = 29769;
+	public static final int NULL_29771 = 29771;
+	public static final int NULL_29773 = 29773;
+	public static final int NULL_29776 = 29776;
 	/* 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 347daa871..21f0ceef1 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
@@ -28083,5 +28083,14 @@ public final class NullObjectID
 	public static final int NULL_54111 = 54111;
 	public static final int NULL_54112 = 54112;
 	public static final int NULL_54113 = 54113;
+	public static final int NULL_54118 = 54118;
+	public static final int NULL_54119 = 54119;
+	public static final int NULL_54120 = 54120;
+	public static final int NULL_54121 = 54121;
+	public static final int NULL_54122 = 54122;
+	public static final int NULL_54123 = 54123;
+	public static final int NULL_54124 = 54124;
+	public static final int NULL_54125 = 54125;
+	public static final int NULL_54126 = 54126;
 	/* 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 0dccca9ea..90695fe1b 100644
--- a/runelite-api/src/main/java/net/runelite/api/ObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ObjectID.java
@@ -2254,18 +2254,18 @@ public final class ObjectID
 	public static final int FIRE_ALTAR = 4091;
 	public static final int BROKEN_FIRE_ALTAR = 4092;
 	public static final int FUNERAL_PYRE = 4093;
-	public static final int FUNERAL_PYRE_4094 = 4094;
-	public static final int FUNERAL_PYRE_4095 = 4095;
-	public static final int FUNERAL_PYRE_4096 = 4096;
-	public static final int FUNERAL_PYRE_4097 = 4097;
-	public static final int FUNERAL_PYRE_4098 = 4098;
-	public static final int FUNERAL_PYRE_4099 = 4099;
-	public static final int FUNERAL_PYRE_4100 = 4100;
-	public static final int FUNERAL_PYRE_4101 = 4101;
-	public static final int FUNERAL_PYRE_4102 = 4102;
-	public static final int FUNERAL_PYRE_4103 = 4103;
-	public static final int FUNERAL_PYRE_4104 = 4104;
-	public static final int FUNERAL_PYRE_4105 = 4105;
+	public static final int BASIC_PYRE = 4094;
+	public static final int OAK_PYRE = 4095;
+	public static final int WILLOW_PYRE = 4096;
+	public static final int MAPLE_PYRE = 4097;
+	public static final int YEW_PYRE = 4098;
+	public static final int MAGIC_PYRE = 4099;
+	public static final int BASIC_PYRE_4100 = 4100;
+	public static final int OAK_PYRE_4101 = 4101;
+	public static final int WILLOW_PYRE_4102 = 4102;
+	public static final int MAPLE_PYRE_4103 = 4103;
+	public static final int YEW_PYRE_4104 = 4104;
+	public static final int MAGIC_PYRE_4105 = 4105;
 	public static final int SOLID_BRONZE_DOOR = 4106;
 	public static final int SOLID_STEEL_DOOR = 4107;
 	public static final int SOLID_BLACK_DOOR = 4108;
@@ -4550,11 +4550,6 @@ public final class ObjectID
 	public static final int HERB_PATCH_8136 = 8136;
 	public static final int HERB_PATCH_8137 = 8137;
 	public static final int HERB_PATCH_8138 = 8138;
-	public static final int HERBS_8139 = 8139;
-	public static final int HERBS_8140 = 8140;
-	public static final int HERBS_8141 = 8141;
-	public static final int HERBS_8142 = 8142;
-	public static final int HERBS_8143 = 8143;
 	public static final int DISEASED_HERBS = 8144;
 	public static final int DISEASED_HERBS_8145 = 8145;
 	public static final int DISEASED_HERBS_8146 = 8146;
@@ -5255,10 +5250,10 @@ public final class ObjectID
 	public static final int NET_TRAP_9003 = 9003;
 	public static final int NET_TRAP_9004 = 9004;
 	public static final int NET_TRAP_9005 = 9005;
-	public static final int FUNERAL_PYRE_9006 = 9006;
-	public static final int FUNERAL_PYRE_9007 = 9007;
-	public static final int FUNERAL_PYRE_9008 = 9008;
-	public static final int FUNERAL_PYRE_9009 = 9009;
+	public static final int TEAK_PYRE = 9006;
+	public static final int MAHOGANY_PYRE = 9007;
+	public static final int TEAK_PYRE_9008 = 9008;
+	public static final int MAHOGANY_PYRE_9009 = 9009;
 	public static final int LIGHT_JUNGLE = 9010;
 	public static final int LIGHT_JUNGLE_9011 = 9011;
 	public static final int LIGHT_JUNGLE_9012 = 9012;
@@ -11640,8 +11635,8 @@ public final class ObjectID
 	public static final int URN_21268 = 21268;
 	public static final int URN_21269 = 21269;
 	public static final int URN_21270 = 21270;
-	public static final int FUNERAL_PYRE_21271 = 21271;
-	public static final int FUNERAL_PYRE_21272 = 21272;
+	public static final int ARCTIC_PINE_PYRE = 21271;
+	public static final int ARCTIC_PINE_PYRE_21272 = 21272;
 	public static final int URN_21273 = 21273;
 	public static final int TREE_STUMP_21274 = 21274;
 	public static final int URN_21275 = 21275;
@@ -15186,8 +15181,8 @@ public final class ObjectID
 	public static final int TABLE_28862 = 28862;
 	public static final int TABLE_28863 = 28863;
 	public static final int COUNTER_28864 = 28864;
-	public static final int FUNERAL_PYRE_28865 = 28865;
-	public static final int FUNERAL_PYRE_28866 = 28866;
+	public static final int REDWOOD_PYRE = 28865;
+	public static final int REDWOOD_PYRE_28866 = 28866;
 	public static final int ROCKS_28890 = 28890;
 	public static final int CRACK_28892 = 28892;
 	public static final int STONE_28893 = 28893;
@@ -26017,5 +26012,6 @@ public final class ObjectID
 	public static final int RUG_54114 = 54114;
 	public static final int RUG_54115 = 54115;
 	public static final int RUG_54116 = 54116;
+	public static final int CHEST_54117 = 54117;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/PlayerComposition.java b/runelite-api/src/main/java/net/runelite/api/PlayerComposition.java
index 5a9b26c3f..0ccf54ad9 100644
--- a/runelite-api/src/main/java/net/runelite/api/PlayerComposition.java
+++ b/runelite-api/src/main/java/net/runelite/api/PlayerComposition.java
@@ -33,6 +33,9 @@ import net.runelite.api.kit.KitType;
  */
 public interface PlayerComposition
 {
+	int KIT_OFFSET = 256;
+	int ITEM_OFFSET = 2048;
+
 	/**
 	 * Checks if the player is female.
 	 *
@@ -59,9 +62,8 @@ public interface PlayerComposition
 	/**
 	 * Gets an array of IDs related to equipment slots.
 	 * <p>
-	 * If the ID for a specific slot is between 256 and 512, subtracting
-	 * 256 will result in the kit ID. Values above 512 indicate an item
-	 * and can be converted to the item ID by subtracting 512.
+	 * IDs between {@link #KIT_OFFSET} and {@link #ITEM_OFFSET} are kits, offset by {@link #KIT_OFFSET}.
+	 * IDs greater than or equal to {@link #ITEM_OFFSET} are items, offset by {@link #ITEM_OFFSET}.
 	 *
 	 * @return the equipment IDs
 	 */
diff --git a/runelite-api/src/main/java/net/runelite/api/SpriteID.java b/runelite-api/src/main/java/net/runelite/api/SpriteID.java
index 3fb2f0eeb..1e1b4cfc4 100644
--- a/runelite-api/src/main/java/net/runelite/api/SpriteID.java
+++ b/runelite-api/src/main/java/net/runelite/api/SpriteID.java
@@ -1683,6 +1683,9 @@ public final class SpriteID
 	public static final int HEALTHBAR_PURPLE_BACK_140PX = 4725;
 	public static final int HEALTHBAR_PURPLE_FRONT_160PX = 4726;
 	public static final int HEALTHBAR_PURPLE_BACK_160PX = 4727;
+	/* Unmapped: 4728~4765 */
+	public static final int COLOSSEUM_DOOM = 4766;
+	/* Unmapped: 4767~4842 */
 	public static final int PRAYER_RP_ANCIENT_STRENGTH = 4843;
 	public static final int PRAYER_RP_ANCIENT_SIGHT = 4844;
 	public static final int PRAYER_RP_ANCIENT_WILL = 4845;
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 b72cc8b39..6ad752084 100644
--- a/runelite-api/src/main/java/net/runelite/api/Varbits.java
+++ b/runelite-api/src/main/java/net/runelite/api/Varbits.java
@@ -89,6 +89,15 @@ public final class Varbits
 	public static final int DIVINE_BASTION = 13664;
 	public static final int DIVINE_BATTLEMAGE = 13665;
 
+	/**
+	 * Moonlight potion timer.
+	 * When at least 70 herblore, the moonlight potion's defense effect will be removed when this timer runs out.
+	 * If the player drinks a dose of moonlight potion while already under its effects, desync between
+	 * Varbits.MOONLIGHT_POTION and Varbits.DIVINE_SUPER_DEFENCE can occur, with the latter being 1 tick greater.
+	 * In case of desync, the moonlight defence effect will be removed once Varbits.DIVINE_SUPER_DEFENCE becomes 0.
+	 */
+	public static final int MOONLIGHT_POTION = 10029;
+
 	/**
 	 * Ring of endurance effect timer, stamina duration extended from using the ring of endurance
 	 * Number of game ticks remaining on ring of endurance effect in intervals of 10; for a value X there are 10 * X game ticks remaining.
@@ -892,4 +901,19 @@ public final class Varbits
 
 	public static final int SPELLBOOK = 4070;
 	public static final int SPELLBOOK_SUBMENU = 9730;
+
+	/**
+	 * The amount of Curse of the Moons stacks received when fighting the Blue Moon or Eclipse Moon.
+	 * The varbit value remains 0 when fighting the Blood Moon.
+	 * When fighting the Blue Moon, the player's joints will lock up at 18 stacks, which causes their next attack to be
+	 * canceled and 18 stacks to be removed.
+	 * When fighting the Eclipse Moon, the stacks increase the chance of a player's attack glancing off the shield of
+	 * the Eclipse Moon. Glancing attacks reduce the player's max hit by two times the flat armour of the Eclipse Moon.
+	 */
+	public static final int CURSE_OF_THE_MOONS = 9853;
+
+	/**
+	 * The amount of Doom stacks received in the Fortis Colosseum.
+	 */
+	public static final int COLOSSEUM_DOOM = 9801;
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java b/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java
index 97511a09b..6ed585fcd 100644
--- a/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java
+++ b/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java
@@ -25,8 +25,10 @@
 package net.runelite.api.coords;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import lombok.Getter;
 import net.runelite.api.CollisionData;
 import net.runelite.api.CollisionDataFlag;
@@ -250,8 +252,8 @@ public class WorldArea
 	 * with some actors. However, using the {@code extraCondition} param
 	 * it is possible to implement this check manually.
 	 *
-	 * @param dx the x-axis direction to travel (-1, 0, or 1)
-	 * @param dy the y-axis direction to travel (-1, 0, or 1)
+	 * @param dx             the x-axis direction to travel (-1, 0, or 1)
+	 * @param dy             the y-axis direction to travel (-1, 0, or 1)
 	 * @param extraCondition an additional condition to perform when checking valid tiles,
 	 *                       such as performing a check for un-passable actors
 	 * @return true if the area can travel in the specified direction
@@ -488,80 +490,34 @@ public class WorldArea
 			return false;
 		}
 
-		LocalPoint sourceLp = LocalPoint.fromWorld(wv, x, y);
-		LocalPoint targetLp = LocalPoint.fromWorld(wv, other.getX(), other.getY());
-		if (sourceLp == null || targetLp == null)
-		{
-			return false;
-		}
-
-		int thisX = sourceLp.getSceneX();
-		int thisY = sourceLp.getSceneY();
-		int otherX = targetLp.getSceneX();
-		int otherY = targetLp.getSceneY();
-
-		int cmpThisX, cmpThisY, cmpOtherX, cmpOtherY;
-
-		// Determine which position to compare with for this NPC
-		if (otherX <= thisX)
-		{
-			cmpThisX = thisX;
-		}
-		else if (otherX >= thisX + width - 1)
-		{
-			cmpThisX = thisX + width - 1;
-		}
-		else
-		{
-			cmpThisX = otherX;
-		}
-		if (otherY <= thisY)
-		{
-			cmpThisY = thisY;
-		}
-		else if (otherY >= thisY + height - 1)
-		{
-			cmpThisY = thisY + height - 1;
-		}
-		else
+		List<WorldPoint> fromEdges = other.getVisibleCandidates(this);
+		List<WorldPoint> toEdges = this.getVisibleCandidates(other);
+		Tile[][][] tiles = wv.getScene().getTiles();
+		for (WorldPoint fromPoint : fromEdges)
 		{
-			cmpThisY = otherY;
-		}
+			for (WorldPoint toPoint : toEdges)
+			{
+				LocalPoint fromLp = LocalPoint.fromWorld(wv, fromPoint);
+				LocalPoint toLp = LocalPoint.fromWorld(wv, toPoint);
 
-		// Determine which position to compare for the other actor
-		if (thisX <= otherX)
-		{
-			cmpOtherX = otherX;
-		}
-		else if (thisX >= otherX + other.getWidth() - 1)
-		{
-			cmpOtherX = otherX + other.getWidth() - 1;
-		}
-		else
-		{
-			cmpOtherX = thisX;
-		}
-		if (thisY <= otherY)
-		{
-			cmpOtherY = otherY;
-		}
-		else if (thisY >= otherY + other.getHeight() - 1)
-		{
-			cmpOtherY = otherY + other.getHeight() - 1;
-		}
-		else
-		{
-			cmpOtherY = thisY;
-		}
+				if (fromLp == null || toLp == null)
+				{
+					continue;
+				}
 
-		Tile[][][] tiles = wv.getScene().getTiles();
-		Tile sourceTile = tiles[plane][cmpThisX][cmpThisY];
-		Tile targetTile = tiles[other.getPlane()][cmpOtherX][cmpOtherY];
-		if (sourceTile == null || targetTile == null)
-		{
-			return false;
+				Tile sourceTile = tiles[plane][fromLp.getSceneX()][fromLp.getSceneY()];
+				Tile targetTile = tiles[other.getPlane()][toLp.getSceneX()][toLp.getSceneY()];
+				if (sourceTile == null || targetTile == null)
+				{
+					continue;
+				}
+				if (hasLineOfSightTo(wv, sourceTile, targetTile))
+				{
+					return true;
+				}
+			}
 		}
-		return hasLineOfSightTo(wv, sourceTile, targetTile);
+		return false;
 	}
 
 	private static boolean hasLineOfSightTo(WorldView wv, Tile from, Tile to)
@@ -692,6 +648,99 @@ public class WorldArea
 		return hasLineOfSightTo(wv, other.toWorldArea());
 	}
 
+	/**
+	 * Gets the points of another worldArea that may be viewable by this WorldArea
+	 * The points are sorted by their distance to the closest point in this WorldArea
+	 *
+	 * @param other the other WorldArea
+	 * @return the list of potentially visible points
+	 */
+	private List<WorldPoint> getVisibleCandidates(WorldArea other)
+	{
+		Point compPoint = this.getComparisonPoint(other);
+		Comparator<WorldPoint> byDistance = Comparator.comparingInt((p) -> p.distanceTo(
+			new WorldPoint(compPoint.getX(), compPoint.getY(), this.getPlane())));
+
+		return other.toWorldPointList()
+			.stream()
+			.filter(p -> isEdgePoint(other, p))
+			.filter(p -> isVisibleCandidate(other, p))
+			.sorted(byDistance)
+			.collect(Collectors.toList());
+	}
+
+	/**
+	 * Checks if the given point is on the edge of the provided WorldArea
+	 *
+	 * @param p Point to test
+	 * @return true if on the edge, false otherwise
+	 */
+	private static boolean isEdgePoint(WorldArea wa, WorldPoint p)
+	{
+		return p.getX() == wa.getX()
+			|| p.getX() == wa.getX() + wa.getWidth() - 1
+			|| p.getY() == wa.getY()
+			|| p.getY() == wa.getY() + wa.getHeight() - 1;
+	}
+
+	/**
+	 * Prunes potentially visible WorldArea points in the other WorldArea by checking
+	 * if the given point is on a visible edge.
+	 *
+	 * @param other The other WorldArea
+	 * @param p     Test point in other WorldArea
+	 * @return true if the point is on a potentially visible edge, false otherwise
+	 */
+	private boolean isVisibleCandidate(WorldArea other, WorldPoint p)
+	{
+		if (this.intersectsWith(other))
+		{
+			return false;
+		}
+		// This WorldArea is east of other
+		if (this.getX() + this.getWidth() - 1 > other.getX() + other.getWidth() - 1)
+		{
+			// Southeast
+			if (this.getY() < other.getY())
+			{
+				return p.getX() == other.getX() + other.getWidth() - 1 || p.getY() == other.getY();
+			}
+			// Northeast
+			if (this.getY() + this.getHeight() - 1 > other.getY() + other.getHeight() - 1)
+			{
+				return p.getX() == other.getX() + other.getWidth() - 1 || p.getY() == other.getY() + other.getHeight() - 1;
+			}
+			return p.getX() == other.getX() + other.getWidth() - 1;
+		}
+		// This WorldArea is west of other
+		else if (this.getX() < other.getX())
+		{
+			// Southwest
+			if (this.getY() < other.getY())
+			{
+				return p.getX() == other.getX() || p.getY() == other.getY();
+			}
+			// Northwest
+			if (this.getY() + this.getHeight() - 1 > other.getY() + other.getHeight() - 1)
+			{
+				return p.getX() == other.getX() || p.getY() == other.getY() + other.getHeight() - 1;
+			}
+			// Straight west
+			return p.getX() == other.getX();
+		}
+		// This WorldArea is north
+		else if (this.getY() > other.getY() + other.getHeight() - 1)
+		{
+			return p.getY() == other.getY() + other.getHeight() - 1;
+		}
+		// This WorldArea is south
+		else if (this.getY() < other.getY())
+		{
+			return p.getY() == other.getY();
+		}
+		return false;
+	}
+
 	/**
 	 * Retrieves the southwestern most point of this WorldArea.
 	 *
diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml
index f9e6d2f0c..194fae556 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.36-SNAPSHOT</version>
+		<version>1.10.37-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>client</artifactId>
diff --git a/runelite-client/src/main/java/net/runelite/client/game/ItemMapping.java b/runelite-client/src/main/java/net/runelite/client/game/ItemMapping.java
index c534c8994..f31a1d59d 100644
--- a/runelite-client/src/main/java/net/runelite/client/game/ItemMapping.java
+++ b/runelite-client/src/main/java/net/runelite/client/game/ItemMapping.java
@@ -126,7 +126,7 @@ public enum ItemMapping
 	ITEM_RUNE_CROSSBOW(RUNE_CROSSBOW, RUNE_CROSSBOW_OR),
 
 	// Godsword ornament kits
-	ITEM_ARMADYL_GODSWORD(ARMADYL_GODSWORD, ARMADYL_GODSWORD_OR),
+	ITEM_ARMADYL_GODSWORD(ARMADYL_GODSWORD, ARMADYL_GODSWORD_OR, ARMADYL_GODSWORD_DEADMAN),
 	ITEM_ARMADYL_GODSWORD_ORNAMENT_KIT(ARMADYL_GODSWORD_ORNAMENT_KIT, ARMADYL_GODSWORD_OR),
 	ITEM_BANDOS_GODSWORD(BANDOS_GODSWORD, BANDOS_GODSWORD_OR),
 	ITEM_BANDOS_GODSWORD_ORNAMENT_KIT(BANDOS_GODSWORD_ORNAMENT_KIT, BANDOS_GODSWORD_OR),
@@ -286,7 +286,7 @@ public enum ItemMapping
 	ITEM_INFINITY_HAT_DARK_COLOUR_KIT(DARK_INFINITY_COLOUR_KIT, DARK_INFINITY_HAT),
 
 	// Miscellaneous ornament kits
-	ITEM_DARK_BOW(DARK_BOW, DARK_BOW_12765, DARK_BOW_12766, DARK_BOW_12767, DARK_BOW_12768, DARK_BOW_20408, DARK_BOW_BH),
+	ITEM_DARK_BOW(DARK_BOW, DARK_BOW_12765, DARK_BOW_12766, DARK_BOW_12767, DARK_BOW_12768, DARK_BOW_20408, DARK_BOW_BH, DARK_BOW_DEADMAN),
 	ITEM_ODIUM_WARD(ODIUM_WARD, ODIUM_WARD_12807),
 	ITEM_MALEDICTION_WARD(MALEDICTION_WARD, MALEDICTION_WARD_12806),
 	ITEM_STEAM_BATTLESTAFF(STEAM_BATTLESTAFF, STEAM_BATTLESTAFF_12795),
@@ -294,6 +294,7 @@ public enum ItemMapping
 	ITEM_TZHAARKETOM(TZHAARKETOM, TZHAARKETOM_T),
 	ITEM_TZHAARKETOM_ORNAMENT_KIT(TZHAARKETOM_ORNAMENT_KIT, TZHAARKETOM_T),
 	ITEM_DRAGON_HUNTER_CROSSBOW(DRAGON_HUNTER_CROSSBOW, DRAGON_HUNTER_CROSSBOW_B, DRAGON_HUNTER_CROSSBOW_T),
+	ITEM_VOIDWAKER(VOIDWAKER, VOIDWAKER_DEADMAN),
 
 	// Slayer helm/black mask
 	ITEM_BLACK_MASK(BLACK_MASK, true, 1L, BLACK_MASK, SLAYER_HELMET),
@@ -308,8 +309,8 @@ public enum ItemMapping
 	ITEM_TWISTED_HORNS(TWISTED_HORNS, TWISTED_SLAYER_HELMET, TWISTED_SLAYER_HELMET_I, TWISTED_SLAYER_HELMET_I_25191, TWISTED_SLAYER_HELMET_I_26681),
 	ITEM_ELDRITCH_ORB(ELDRITCH_ORB, ELDRITCH_NIGHTMARE_STAFF),
 	ITEM_HARMONISED_ORB(HARMONISED_ORB, HARMONISED_NIGHTMARE_STAFF),
-	ITEM_VOLATILE_ORB(VOLATILE_ORB, VOLATILE_NIGHTMARE_STAFF),
-	ITEM_NIGHTMARE_STAFF(NIGHTMARE_STAFF, ELDRITCH_NIGHTMARE_STAFF, HARMONISED_NIGHTMARE_STAFF, VOLATILE_NIGHTMARE_STAFF),
+	ITEM_VOLATILE_ORB(VOLATILE_ORB, VOLATILE_NIGHTMARE_STAFF, VOLATILE_NIGHTMARE_STAFF_DEADMAN),
+	ITEM_NIGHTMARE_STAFF(NIGHTMARE_STAFF, ELDRITCH_NIGHTMARE_STAFF, HARMONISED_NIGHTMARE_STAFF, VOLATILE_NIGHTMARE_STAFF, VOLATILE_NIGHTMARE_STAFF_DEADMAN),
 	ITEM_GHARZI_RAPIER(GHRAZI_RAPIER, HOLY_GHRAZI_RAPIER),
 	ITEM_MASTER_SCROLL_BOOK(MASTER_SCROLL_BOOK_EMPTY, MASTER_SCROLL_BOOK),
 	ITEM_ARCANE_SIGIL(ARCANE_SIGIL, ELIDINIS_WARD_F, ELIDINIS_WARD_OR),
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/animsmoothing/AnimationSmoothingPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/animsmoothing/AnimationSmoothingPlugin.java
index 74d168eda..ede1be560 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/animsmoothing/AnimationSmoothingPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/animsmoothing/AnimationSmoothingPlugin.java
@@ -77,7 +77,12 @@ public class AnimationSmoothingPlugin extends Plugin
 			case AnimationID.VIGGORAS_CHAINMACE_IDLE:
 
 			case AnimationID.MLM_WATER_WHEEL_SPINNING:
+			case AnimationID.HARMONY_ISLAND_WINDMILL_SPINNING:
+			case AnimationID.GWENITH_WINDMILL_SPINNING:
+			case AnimationID.LITHKREN_GENERATOR_SPINNING:
 			case AnimationID.GIANTS_FOUNDRY_WATER_WHEEL_SPINNING:
+
+			case AnimationID.MAGIC_ARCEUUS_DEMONBANE:
 				return false;
 
 			default:
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
index b84ba1ea0..d5a75275d 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
@@ -803,14 +803,15 @@ public class TabInterface
 			&& event.getActionParam1() == ComponentID.BANK_ITEM_CONTAINER
 			&& event.getOption().equals(DUPLICATE_ITEM))
 		{
-			event.getMenuEntry().setType(MenuAction.RUNELITE);
+			// use RUNELITE_LOW_PRIORITY to avoid sending the op to the server, but also keep it right-click only
+			event.getMenuEntry().setType(MenuAction.RUNELITE_LOW_PRIORITY);
 			event.getMenuEntry().onClick(this::opDuplicateItem);
 		}
 		if (activeTab != null
 			&& event.getActionParam1() == ComponentID.BANK_ITEM_CONTAINER
 			&& event.getOption().equals(REMOVE_LAYOUT))
 		{
-			event.getMenuEntry().setType(MenuAction.RUNELITE);
+			event.getMenuEntry().setType(MenuAction.RUNELITE_LOW_PRIORITY);
 			event.getMenuEntry().onClick(this::opRemoveLayout);
 		}
 		else if (event.getActionParam1() == ComponentID.BANK_DEPOSIT_INVENTORY
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 69570d467..8524914f4 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,7 +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 Pattern HUNTER_RUMOUR_KC_PATTERN = Pattern.compile("You have completed <col=[0-9a-f]{6}>([0-9,]+)</col> rumours? for the Hunter Guild.");
 
 	private static final String TOTAL_LEVEL_COMMAND_STRING = "!total";
 	private static final String PRICE_COMMAND_STRING = "!price";
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/EmoteClue.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/EmoteClue.java
index a45f49d3e..fa12ef0de 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/EmoteClue.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/EmoteClue.java
@@ -180,7 +180,7 @@ public class EmoteClue extends ClueScroll implements LocationClueScroll
 		new EmoteClue("Jump for joy in the TzHaar sword shop. Shrug before you talk to me. Equip a Steel longsword, Blue D'hide body and blue mystic gloves.", "Tzhaar weapon store", TZHAAR_WEAPONS_STORE, new WorldPoint(2477, 5146, 0), JUMP_FOR_JOY, SHRUG, item(STEEL_LONGSWORD), item(BLUE_DHIDE_BODY), item(MYSTIC_GLOVES)),
 		new EmoteClue("Jump for joy in the Ancient Cavern. Equip a granite shield, splitbark body and any rune heraldic helm.", "Ancient cavern", ENTRANCE_OF_THE_CAVERN_UNDER_THE_WHIRLPOOL, new WorldPoint(1768, 5366, 1), JUMP_FOR_JOY, item(GRANITE_SHIELD), item(SPLITBARK_BODY), range("Any rune heraldic helm", RUNE_HELM_H1, RUNE_HELM_H5)),
 		new EmoteClue("Jump for joy at the Neitiznot rune rock. Equip Rune boots, a proselyte hauberk and a dragonstone ring.", "Fremennik Isles", NEAR_A_RUNITE_ROCK_IN_THE_FREMENNIK_ISLES, new WorldPoint(2375, 3850, 0), JUMP_FOR_JOY, item(RUNE_BOOTS), item(PROSELYTE_HAUBERK), item(DRAGONSTONE_RING)),
-		new EmoteClue("Jump for joy in the centre of Zul-Andra. Beware of double agents! Equip a dragon 2h sword, bandos boots and an obsidian cape.", "Zul-Andra", NEAR_THE_PIER_IN_ZULANDRA, new WorldPoint(2199, 3056, 0), DOUBLE_AGENT_141, JUMP_FOR_JOY, item(DRAGON_2H_SWORD), any("Bandos boots", item(BANDOS_BOOTS), item(GUARDIAN_BOOTS)), item(OBSIDIAN_CAPE)),
+		new EmoteClue("Jump for joy in the centre of Zul-Andra. Beware of double agents! Equip a dragon 2h sword, bandos boots and an obsidian cape.", "Zul-Andra", NEAR_THE_PIER_IN_ZULANDRA, new WorldPoint(2199, 3056, 0), DOUBLE_AGENT_141, JUMP_FOR_JOY, item(DRAGON_2H_SWORD), any("Bandos boots", item(BANDOS_BOOTS), item(GUARDIAN_BOOTS), item(ECHO_BOOTS)), item(OBSIDIAN_CAPE)),
 		new EmoteClue("Laugh by the fountain of heroes. Equip splitbark legs, dragon boots and a Rune longsword.", "Fountain of heroes", FOUNTAIN_OF_HEROES, new WorldPoint(2920, 9893, 0), LAUGH, item(SPLITBARK_LEGS), any("Dragon boots", item(DRAGON_BOOTS), item(DRAGON_BOOTS_G), item(PRIMORDIAL_BOOTS)), item(RUNE_LONGSWORD)),
 		new EmoteClue("Laugh in Jokul's tent in the Mountain Camp. Beware of double agents! Equip a rune full helmet, blue dragonhide chaps and a fire battlestaff.", "Mountain Camp", MOUNTAIN_CAMP_GOAT_ENCLOSURE, new WorldPoint(2812, 3681, 0), DOUBLE_AGENT_108, LAUGH, item(RUNE_FULL_HELM), item(BLUE_DHIDE_CHAPS), item(FIRE_BATTLESTAFF)),
 		new EmoteClue("Laugh at the crossroads south of the Sinclair Mansion. Equip a cowl, a blue wizard robe top and an iron scimitar.", "Sinclair Mansion", ROAD_JUNCTION_SOUTH_OF_SINCLAIR_MANSION, new WorldPoint(2741, 3536, 0), LAUGH, item(LEATHER_COWL), item(BLUE_WIZARD_ROBE), item(IRON_SCIMITAR)),
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java
index 42b878bb5..a2e42b8f2 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java
@@ -51,10 +51,12 @@ import net.runelite.api.Client;
 import net.runelite.api.Experience;
 import net.runelite.api.IndexedSprite;
 import net.runelite.api.ItemID;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.NPC;
 import net.runelite.api.Player;
+import net.runelite.api.PlayerComposition;
 import net.runelite.api.Skill;
 import net.runelite.api.VarbitComposition;
 import net.runelite.api.coords.WorldPoint;
@@ -457,28 +459,28 @@ public class DevToolsPlugin extends Plugin
 				int slot = Integer.parseInt(args[0]);
 				int id = Integer.parseInt(args[1]);
 				Player player = client.getLocalPlayer();
-				player.getPlayerComposition().getEquipmentIds()[slot] = id + 512;
+				player.getPlayerComposition().getEquipmentIds()[slot] = id + PlayerComposition.ITEM_OFFSET;
 				player.getPlayerComposition().setHash();
 				break;
 			}
 			case "tex":
 			{
 				Player player = client.getLocalPlayer();
-				player.getPlayerComposition().getEquipmentIds()[KitType.CAPE.getIndex()] = ItemID.FIRE_CAPE + 512;
-				player.getPlayerComposition().getEquipmentIds()[KitType.SHIELD.getIndex()] = ItemID.MIRROR_SHIELD + 512;
+				player.getPlayerComposition().getEquipmentIds()[KitType.CAPE.getIndex()] = ItemID.FIRE_CAPE + PlayerComposition.ITEM_OFFSET;
+				player.getPlayerComposition().getEquipmentIds()[KitType.SHIELD.getIndex()] = ItemID.MIRROR_SHIELD + PlayerComposition.ITEM_OFFSET;
 				player.getPlayerComposition().setHash();
 				break;
 			}
 			case "alpha":
 			{
 				Player player = client.getLocalPlayer();
-				player.getPlayerComposition().getEquipmentIds()[KitType.HEAD.getIndex()] = ItemID.GHOSTLY_HOOD + 512;
-				player.getPlayerComposition().getEquipmentIds()[KitType.AMULET.getIndex()] = ItemID.AMULET_OF_TORTURE_OR + 512;
-				player.getPlayerComposition().getEquipmentIds()[KitType.CAPE.getIndex()] = ItemID.GHOSTLY_CLOAK + 512;
-				player.getPlayerComposition().getEquipmentIds()[KitType.TORSO.getIndex()] = ItemID.GHOSTLY_ROBE + 512;
-				player.getPlayerComposition().getEquipmentIds()[KitType.SHIELD.getIndex()] = ItemID.ELYSIAN_SPIRIT_SHIELD + 512;
+				player.getPlayerComposition().getEquipmentIds()[KitType.HEAD.getIndex()] = ItemID.GHOSTLY_HOOD + PlayerComposition.ITEM_OFFSET;
+				player.getPlayerComposition().getEquipmentIds()[KitType.AMULET.getIndex()] = ItemID.AMULET_OF_TORTURE_OR + PlayerComposition.ITEM_OFFSET;
+				player.getPlayerComposition().getEquipmentIds()[KitType.CAPE.getIndex()] = ItemID.GHOSTLY_CLOAK + PlayerComposition.ITEM_OFFSET;
+				player.getPlayerComposition().getEquipmentIds()[KitType.TORSO.getIndex()] = ItemID.GHOSTLY_ROBE + PlayerComposition.ITEM_OFFSET;
+				player.getPlayerComposition().getEquipmentIds()[KitType.SHIELD.getIndex()] = ItemID.ELYSIAN_SPIRIT_SHIELD + PlayerComposition.ITEM_OFFSET;
 				player.getPlayerComposition().getEquipmentIds()[KitType.ARMS.getIndex()] = -1;
-				player.getPlayerComposition().getEquipmentIds()[KitType.LEGS.getIndex()] = ItemID.GHOSTLY_ROBE_6108 + 512;
+				player.getPlayerComposition().getEquipmentIds()[KitType.LEGS.getIndex()] = ItemID.GHOSTLY_ROBE_6108 + PlayerComposition.ITEM_OFFSET;
 				player.getPlayerComposition().getEquipmentIds()[KitType.HAIR.getIndex()] = -1;
 				player.getPlayerComposition().getEquipmentIds()[KitType.HANDS.getIndex()] = ItemID.GHOSTLY_GLOVES;
 				player.getPlayerComposition().getEquipmentIds()[KitType.BOOTS.getIndex()] = ItemID.GHOSTLY_BOOTS;
@@ -621,17 +623,17 @@ public class DevToolsPlugin extends Plugin
 				{
 					MenuEntry parent = client.createMenuEntry(1)
 						.setOption("pmenu" + i)
-						.setTarget("devtools")
-						.setType(MenuAction.RUNELITE_SUBMENU);
+						.setTarget(i % 60 == 0 ? "devtools devtools devtools devtools" : "devtools")
+						.setType(MenuAction.RUNELITE);
+					Menu submenu = parent.createSubMenu();
 
 					for (int j = 0; j < 4; ++j)
 					{
 						final int j_ = j;
-						client.createMenuEntry(1)
+						submenu.createMenuEntry(0)
 							.setOption("submenu" + j)
 							.setTarget("devtools")
 							.setType(MenuAction.RUNELITE)
-							.setParent(parent)
 							.onClick(c -> client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", "menu " + i_ + " sub " + j_, null));
 					}
 					continue;
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 991e3a15a..ee62ad4b7 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
@@ -62,6 +62,7 @@ import net.runelite.api.GameState;
 import net.runelite.api.ItemComposition;
 import net.runelite.api.ItemID;
 import net.runelite.api.KeyCode;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.Tile;
@@ -537,23 +538,22 @@ public class GroundItemsPlugin extends Plugin
 			MenuEntry parent = client.createMenuEntry(-1)
 				.setOption("Color")
 				.setTarget(event.getTarget())
-				.setType(MenuAction.RUNELITE_SUBMENU);
+				.setType(MenuAction.RUNELITE);
+			Menu submenu = parent.createSubMenu();
 			final int itemId = event.getIdentifier();
 			Color color = getItemColor(itemId);
 
 			if (color != null)
 			{
-				client.createMenuEntry(-1)
+				submenu.createMenuEntry(-1)
 					.setOption("Reset")
 					.setType(MenuAction.RUNELITE)
-					.setParent(parent)
 					.onClick(e -> unsetItemColor(itemId));
 			}
 
-			client.createMenuEntry(-1)
+			submenu.createMenuEntry(-1)
 				.setOption("Pick")
 				.setType(MenuAction.RUNELITE)
-				.setParent(parent)
 				.onClick(e ->
 					SwingUtilities.invokeLater(() ->
 					{
@@ -576,10 +576,9 @@ public class GroundItemsPlugin extends Plugin
 
 			colors.stream()
 				.filter(c -> !c.equals(color))
-				.forEach(c -> client.createMenuEntry(-1)
+				.forEach(c -> submenu.createMenuEntry(-1)
 					.setOption(ColorUtil.prependColorTag("Color", c))
 					.setType(MenuAction.RUNELITE)
-					.setParent(parent)
 					.onClick(e -> setItemColor(itemId, c)));
 		}
 	}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/groundmarkers/GroundMarkerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/groundmarkers/GroundMarkerPlugin.java
index 57b99343f..67d05cccf 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/groundmarkers/GroundMarkerPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/groundmarkers/GroundMarkerPlugin.java
@@ -46,6 +46,7 @@ import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.Client;
 import net.runelite.api.GameState;
 import net.runelite.api.KeyCode;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.Tile;
@@ -280,14 +281,14 @@ public class GroundMarkerPlugin extends Plugin
 				MenuEntry menuColor = client.createMenuEntry(-3)
 					.setOption("Color")
 					.setTarget("Tile")
-					.setType(MenuAction.RUNELITE_SUBMENU);
+					.setType(MenuAction.RUNELITE);
+				Menu submenu = menuColor.createSubMenu();
 
 				if (regionPoints.size() > 1)
 				{
-					client.createMenuEntry(-3)
+					submenu.createMenuEntry(-1)
 						.setOption("Reset all")
 						.setType(MenuAction.RUNELITE)
-						.setParent(menuColor)
 						.onClick(e ->
 							chatboxPanelManager.openTextMenuInput("Are you sure you want to reset the color of " + regionPoints.size() + " tiles?")
 								.option("Yes", () ->
@@ -302,10 +303,9 @@ public class GroundMarkerPlugin extends Plugin
 								.build());
 				}
 
-				client.createMenuEntry(-3)
+				submenu.createMenuEntry(-1)
 					.setOption("Pick")
 					.setType(MenuAction.RUNELITE)
-					.setParent(menuColor)
 					.onClick(e ->
 					{
 						Color color = existing.getColor();
@@ -326,10 +326,9 @@ public class GroundMarkerPlugin extends Plugin
 				{
 					if (!color.equals(existing.getColor()))
 					{
-						client.createMenuEntry(-3)
+						submenu.createMenuEntry(-1)
 							.setOption(ColorUtil.prependColorTag("Color", color))
 							.setType(MenuAction.RUNELITE)
-							.setParent(menuColor)
 							.onClick(e -> colorTile(existing, color));
 					}
 				}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/interfacestyles/InterfaceStylesPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/interfacestyles/InterfaceStylesPlugin.java
index 61c65ef02..db60386c4 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/interfacestyles/InterfaceStylesPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/interfacestyles/InterfaceStylesPlugin.java
@@ -28,11 +28,13 @@ package net.runelite.client.plugins.interfacestyles;
 
 import com.google.inject.Provides;
 import java.awt.image.BufferedImage;
+import java.util.Arrays;
 import javax.inject.Inject;
 import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.Client;
 import net.runelite.api.GameState;
 import net.runelite.api.HealthBarConfig;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.Player;
@@ -146,42 +148,58 @@ public class InterfaceStylesPlugin extends Plugin
 	private void condensePlayerOptions()
 	{
 		MenuEntry[] menuEntries = client.getMenuEntries();
-		MenuEntry parentMenu = null;
+		MenuEntry[] newMenus = new MenuEntry[menuEntries.length];
+		int newIdx = 0;
+
+		Menu submenu = null;
 		Player prev = null;
-		for (int i = menuEntries.length - 1; i >= 0; --i)
+		boolean changed = false;
+
+		for (MenuEntry menuEntry : menuEntries)
 		{
-			MenuEntry menuEntry = menuEntries[i];
 			MenuAction type = menuEntry.getType();
 
 			Player player = menuEntry.getPlayer();
 			if (player != null && type != MenuAction.ITEM_USE_ON_PLAYER && type != MenuAction.WIDGET_TARGET_ON_PLAYER)
 			{
+				String option = menuEntry.getOption();
+				boolean deprioritized = menuEntry.isDeprioritized();
+
 				if (prev != player)
 				{
-					// This works by making the top most player menu the submenu, then adding a new
-					// menu with a copy of what this one was.
-					MenuEntry copy = client.createMenuEntry(-1)
-						.setIdentifier(menuEntry.getIdentifier())
-						.setOption(menuEntry.getOption())
-						.setTarget(menuEntry.getTarget())
-						.setType(menuEntry.getType())
-						.setParam0(menuEntry.getParam0())
-						.setParam1(menuEntry.getParam1())
-						.setDeprioritized(menuEntry.isDeprioritized());
-
+					// Change this menu to be the submenu parent
 					menuEntry.setOption("");
-					menuEntry.setType(MenuAction.RUNELITE_SUBMENU);
+					menuEntry.setType(MenuAction.RUNELITE);
 					menuEntry.setDeprioritized(false);
+					submenu = menuEntry.createSubMenu();
 
-					parentMenu = menuEntry;
-					menuEntry = copy;
+					newMenus[newIdx++] = menuEntry;
 				}
 
-				menuEntry.setParent(parentMenu);
+				// Add this menu to the submenu
+				assert submenu != null;
+				submenu.createMenuEntry(-1)
+					.setIdentifier(menuEntry.getIdentifier())
+					.setOption(option)
+					.setTarget(menuEntry.getTarget())
+					.setType(type)
+					.setParam0(menuEntry.getParam0())
+					.setParam1(menuEntry.getParam1())
+					.setDeprioritized(deprioritized);
+				changed = true;
+			}
+			else
+			{
+				newMenus[newIdx++] = menuEntry;
 			}
 
 			prev = player;
 		}
+
+		if (changed)
+		{
+			client.setMenuEntries(Arrays.copyOf(newMenus, newIdx));
+		}
 	}
 
 	@Subscribe
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/inventorytags/InventoryTagsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/inventorytags/InventoryTagsPlugin.java
index 46962a1d4..831a30a65 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/inventorytags/InventoryTagsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/inventorytags/InventoryTagsPlugin.java
@@ -42,6 +42,7 @@ import static net.runelite.api.InventoryID.INVENTORY;
 import net.runelite.api.Item;
 import net.runelite.api.ItemContainer;
 import net.runelite.api.KeyCode;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.events.MenuOpened;
@@ -185,7 +186,8 @@ public class InventoryTagsPlugin extends Plugin
 				final MenuEntry parent = client.createMenuEntry(idx)
 					.setOption("Inventory tag")
 					.setTarget(entry.getTarget())
-					.setType(MenuAction.RUNELITE_SUBMENU);
+					.setType(MenuAction.RUNELITE);
+				final Menu submenu = parent.createSubMenu();
 
 				Set<Color> invEquipmentColors = new HashSet<>();
 				invEquipmentColors.addAll(getColorsFromItemContainer(INVENTORY));
@@ -194,10 +196,9 @@ public class InventoryTagsPlugin extends Plugin
 				{
 					if (tag == null || !tag.color.equals(color))
 					{
-						client.createMenuEntry(idx)
+						submenu.createMenuEntry(0)
 							.setOption(ColorUtil.prependColorTag("Color", color))
 							.setType(MenuAction.RUNELITE)
-							.setParent(parent)
 							.onClick(e ->
 							{
 								Tag t = new Tag();
@@ -207,10 +208,9 @@ public class InventoryTagsPlugin extends Plugin
 					}
 				}
 
-				client.createMenuEntry(idx)
+				submenu.createMenuEntry(0)
 					.setOption("Pick")
 					.setType(MenuAction.RUNELITE)
-					.setParent(parent)
 					.onClick(e ->
 					{
 						Color color = tag == null ? Color.WHITE : tag.color;
@@ -230,10 +230,9 @@ public class InventoryTagsPlugin extends Plugin
 
 				if (tag != null)
 				{
-					client.createMenuEntry(idx)
+					submenu.createMenuEntry(0)
 						.setOption("Reset")
 						.setType(MenuAction.RUNELITE)
-						.setParent(parent)
 						.onClick(e -> unsetTag(itemId));
 				}
 			}
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 83fa34f70..e169b09a2 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
@@ -38,7 +38,6 @@ import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Multimap;
 import com.google.inject.Provides;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -55,6 +54,7 @@ import net.runelite.api.Client;
 import net.runelite.api.ItemComposition;
 import net.runelite.api.ItemID;
 import net.runelite.api.KeyCode;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.NPC;
@@ -162,7 +162,8 @@ public class MenuEntrySwapperPlugin extends Plugin
 	private NpcUtil npcUtil;
 
 	private final Multimap<String, Swap> swaps = LinkedHashMultimap.create();
-	private final ArrayListMultimap<String, Integer> optionIndexes = ArrayListMultimap.create();
+	private final ArrayListMultimap<String, Integer> cacheOptionIndexes = ArrayListMultimap.create();
+	private Menu cacheOptionMenu;
 	private final Multimap<Integer, TeleportSwap> teleportSwaps = HashMultimap.create();
 	private boolean lastShift, curShift;
 
@@ -506,8 +507,16 @@ public class MenuEntrySwapperPlugin extends Plugin
 				final MenuAction currentShiftAction = shiftSwapConfig == null ? defaultAction(composition) :
 					(shiftSwapConfig == -1 ? MenuAction.WALK : OBJECT_MENU_TYPES.get(shiftSwapConfig));
 
-				List<MenuEntry> leftClickMenus = new ArrayList<>(actions.length + 2);
-				List<MenuEntry> shiftClickMenus = new ArrayList<>(actions.length + 2);
+				MenuEntry swapLeftClick = client.createMenuEntry(idx)
+					.setOption("Swap left click")
+					.setTarget(entry.getTarget())
+					.setType(MenuAction.RUNELITE);
+				MenuEntry swapShiftClick = client.createMenuEntry(idx)
+					.setOption("Swap shift click")
+					.setTarget(entry.getTarget())
+					.setType(MenuAction.RUNELITE);
+				Menu subLeft = swapLeftClick.createSubMenu();
+				Menu subShift = swapShiftClick.createSubMenu();
 
 				for (int actionIdx = 0; actionIdx < OBJECT_MENU_TYPES.size(); ++actionIdx)
 				{
@@ -519,71 +528,53 @@ public class MenuEntrySwapperPlugin extends Plugin
 					final MenuAction menuAction = OBJECT_MENU_TYPES.get(actionIdx);
 					if (menuAction != currentAction)
 					{
-						leftClickMenus.add(client.createMenuEntry(idx)
+						subLeft.createMenuEntry(0)
 							.setOption(actions[actionIdx])
 							.setType(MenuAction.RUNELITE)
-							.onClick(objectConsumer(composition, actions, actionIdx, menuAction, false)));
+							.onClick(objectConsumer(composition, actions, actionIdx, menuAction, false));
 					}
 
 					if (menuAction != currentShiftAction && menuAction != currentAction)
 					{
-						shiftClickMenus.add(client.createMenuEntry(idx)
+						subShift.createMenuEntry(0)
 							.setOption(actions[actionIdx])
 							.setType(MenuAction.RUNELITE)
-							.onClick(objectConsumer(composition, actions, actionIdx, menuAction, true)));
+							.onClick(objectConsumer(composition, actions, actionIdx, menuAction, true));
 					}
 				}
 
 				// Walk here
 				if (currentAction != MenuAction.WALK)
 				{
-					leftClickMenus.add(client.createMenuEntry(idx)
+					subLeft.createMenuEntry(0)
 						.setOption("Walk here")
 						.setType(MenuAction.RUNELITE)
-						.onClick(walkHereConsumer(false, composition)));
+						.onClick(walkHereConsumer(false, composition));
 				}
 
 				if (currentShiftAction != MenuAction.WALK)
 				{
-					shiftClickMenus.add(client.createMenuEntry(idx)
+					subShift.createMenuEntry(0)
 						.setOption("Walk here")
 						.setType(MenuAction.RUNELITE)
-						.onClick(walkHereConsumer(true, composition)));
+						.onClick(walkHereConsumer(true, composition));
 				}
 
 				// Reset
 				if (swapConfig != null)
 				{
-					leftClickMenus.add(client.createMenuEntry(idx)
+					subLeft.createMenuEntry(0)
 						.setOption("Reset")
 						.setType(MenuAction.RUNELITE)
-						.onClick(objectResetConsumer(composition, false)));
+						.onClick(objectResetConsumer(composition, false));
 				}
 
 				if (shiftSwapConfig != null)
 				{
-					shiftClickMenus.add(client.createMenuEntry(idx)
+					subShift.createMenuEntry(0)
 						.setOption("Reset")
 						.setType(MenuAction.RUNELITE)
-						.onClick(objectResetConsumer(composition, true)));
-				}
-
-				if (!leftClickMenus.isEmpty())
-				{
-					MenuEntry sub = client.createMenuEntry(idx)
-						.setOption("Swap left click")
-						.setTarget(entry.getTarget())
-						.setType(MenuAction.RUNELITE_SUBMENU);
-					leftClickMenus.forEach(menu -> menu.setParent(sub));
-				}
-
-				if (!shiftClickMenus.isEmpty())
-				{
-					MenuEntry sub = client.createMenuEntry(idx)
-						.setOption("Swap shift click")
-						.setTarget(entry.getTarget())
-						.setType(MenuAction.RUNELITE_SUBMENU);
-					shiftClickMenus.forEach(menu -> menu.setParent(sub));
+						.onClick(objectResetConsumer(composition, true));
 				}
 			}
 		}
@@ -705,8 +696,16 @@ public class MenuEntrySwapperPlugin extends Plugin
 					(hasAttack ? null : defaultAction(composition)) :
 					(shiftSwapConfig == -1 ? MenuAction.WALK : NPC_MENU_TYPES.get(shiftSwapConfig));
 
-				List<MenuEntry> leftClickMenus = new ArrayList<>(actions.length + 2);
-				List<MenuEntry> shiftClickMenus = new ArrayList<>(actions.length + 2);
+				MenuEntry swapLeftClick = client.createMenuEntry(idx)
+					.setOption("Swap left click")
+					.setTarget(entry.getTarget())
+					.setType(MenuAction.RUNELITE);
+				MenuEntry swapShiftClick = client.createMenuEntry(idx)
+					.setOption("Swap shift click")
+					.setTarget(entry.getTarget())
+					.setType(MenuAction.RUNELITE);
+				Menu subLeft = swapLeftClick.createSubMenu();
+				Menu subShift = swapShiftClick.createSubMenu();
 
 				for (int actionIdx = 0; actionIdx < NPC_MENU_TYPES.size(); ++actionIdx)
 				{
@@ -728,64 +727,46 @@ public class MenuEntrySwapperPlugin extends Plugin
 					final MenuAction menuAction = NPC_MENU_TYPES.get(actionIdx);
 					if (menuAction != currentAction)
 					{
-						leftClickMenus.add(client.createMenuEntry(idx)
+						subLeft.createMenuEntry(0)
 							.setOption(actions[actionIdx])
 							.setType(MenuAction.RUNELITE)
-							.onClick(npcConsumer(composition, actions, actionIdx, menuAction, false)));
+							.onClick(npcConsumer(composition, actions, actionIdx, menuAction, false));
 					}
 
 					if (menuAction != currentShiftAction)
 					{
-						shiftClickMenus.add(client.createMenuEntry(idx)
+						subShift.createMenuEntry(0)
 							.setOption(actions[actionIdx])
 							.setType(MenuAction.RUNELITE)
-							.onClick(npcConsumer(composition, actions, actionIdx, menuAction, true)));
+							.onClick(npcConsumer(composition, actions, actionIdx, menuAction, true));
 					}
 				}
 
 				// Walk here swap
-				leftClickMenus.add(client.createMenuEntry(idx)
+				subLeft.createMenuEntry(0)
 					.setOption("Walk here")
 					.setType(MenuAction.RUNELITE)
-					.onClick(walkHereConsumer(false, composition)));
+					.onClick(walkHereConsumer(false, composition));
 
-				shiftClickMenus.add(client.createMenuEntry(idx)
+				subShift.createMenuEntry(0)
 					.setOption("Walk here")
 					.setType(MenuAction.RUNELITE)
-					.onClick(walkHereConsumer(true, composition)));
+					.onClick(walkHereConsumer(true, composition));
 
 				if (getNpcSwapConfig(false, composition.getId()) != null)
 				{
-					leftClickMenus.add(client.createMenuEntry(idx)
+					subLeft.createMenuEntry(0)
 						.setOption("Reset")
 						.setType(MenuAction.RUNELITE)
-						.onClick(npcResetConsumer(composition, false)));
+						.onClick(npcResetConsumer(composition, false));
 				}
 
 				if (getNpcSwapConfig(true, composition.getId()) != null)
 				{
-					shiftClickMenus.add(client.createMenuEntry(idx)
+					subShift.createMenuEntry(0)
 						.setOption("Reset")
 						.setType(MenuAction.RUNELITE)
-						.onClick(npcResetConsumer(composition, true)));
-				}
-
-				if (!leftClickMenus.isEmpty())
-				{
-					MenuEntry sub = client.createMenuEntry(idx)
-						.setOption("Swap left click")
-						.setTarget(entry.getTarget())
-						.setType(MenuAction.RUNELITE_SUBMENU);
-					leftClickMenus.forEach(menu -> menu.setParent(sub));
-				}
-
-				if (!shiftClickMenus.isEmpty())
-				{
-					MenuEntry sub = client.createMenuEntry(idx)
-						.setOption("Swap shift click")
-						.setTarget(entry.getTarget())
-						.setType(MenuAction.RUNELITE_SUBMENU);
-					shiftClickMenus.forEach(menu -> menu.setParent(sub));
+						.onClick(npcResetConsumer(composition, true));
 				}
 			}
 		}
@@ -854,8 +835,16 @@ public class MenuEntrySwapperPlugin extends Plugin
 					final Integer leftClickOp = getWornItemSwapConfig(false, itemComposition.getId());
 					final Integer shiftClickOp = getWornItemSwapConfig(true, itemComposition.getId());
 
-					List<MenuEntry> leftClickMenus = new ArrayList<>();
-					List<MenuEntry> shiftClickMenus = new ArrayList<>();
+					MenuEntry swapLeftClick = client.createMenuEntry(idx)
+						.setOption("Swap left click")
+						.setTarget(entry.getTarget())
+						.setType(MenuAction.RUNELITE);
+					MenuEntry swapShiftClick = client.createMenuEntry(idx)
+						.setOption("Swap shift click")
+						.setTarget(entry.getTarget())
+						.setType(MenuAction.RUNELITE);
+					Menu subLeft = swapLeftClick.createSubMenu();
+					Menu subShift = swapShiftClick.createSubMenu();
 
 					for (int paramId = ParamID.OC_ITEM_OP1, opId = 2; paramId <= ParamID.OC_ITEM_OP8; ++paramId, ++opId)
 					{
@@ -864,17 +853,17 @@ public class MenuEntrySwapperPlugin extends Plugin
 						{
 							if (leftClickOp == null || leftClickOp != opId)
 							{
-								leftClickMenus.add(client.createMenuEntry(idx)
+								subLeft.createMenuEntry(0)
 									.setOption(opName)
 									.setType(MenuAction.RUNELITE)
-									.onClick(wornItemConsumer(itemComposition, opName, opId, false)));
+									.onClick(wornItemConsumer(itemComposition, opName, opId, false));
 							}
 							if (shiftClickOp == null || shiftClickOp != opId)
 							{
-								shiftClickMenus.add(client.createMenuEntry(idx)
+								subShift.createMenuEntry(0)
 									.setOption(opName)
 									.setType(MenuAction.RUNELITE)
-									.onClick(wornItemConsumer(itemComposition, opName, opId, true)));
+									.onClick(wornItemConsumer(itemComposition, opName, opId, true));
 							}
 						}
 					}
@@ -891,17 +880,17 @@ public class MenuEntrySwapperPlugin extends Plugin
 							{
 								if (leftClickOp == null || leftClickOp != sub.option.hashCode())
 								{
-									leftClickMenus.add(client.createMenuEntry(idx)
+									subLeft.createMenuEntry(idx)
 										.setOption(sub.option)
 										.setType(MenuAction.RUNELITE)
-										.onClick(wornItemConsumer(itemComposition, sub.option, sub.option.hashCode(), false)));
+										.onClick(wornItemConsumer(itemComposition, sub.option, sub.option.hashCode(), false));
 								}
 								if (shiftClickOp == null || shiftClickOp != sub.option.hashCode())
 								{
-									shiftClickMenus.add(client.createMenuEntry(idx)
+									subShift.createMenuEntry(idx)
 										.setOption(sub.option)
 										.setType(MenuAction.RUNELITE)
-										.onClick(wornItemConsumer(itemComposition, sub.option, sub.option.hashCode(), true)));
+										.onClick(wornItemConsumer(itemComposition, sub.option, sub.option.hashCode(), true));
 								}
 							}
 						}
@@ -909,7 +898,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 
 					if (leftClickOp != null)
 					{
-						leftClickMenus.add(client.createMenuEntry(idx)
+						subLeft.createMenuEntry(0)
 							.setOption("Reset")
 							.setType(MenuAction.RUNELITE)
 							.onClick(e ->
@@ -926,11 +915,11 @@ public class MenuEntrySwapperPlugin extends Plugin
 
 								log.debug("Unset worn item left swap for {}", itemComposition.getMembersName());
 								unsetWornItemSwapConfig(false, itemComposition.getId());
-							}));
+							});
 					}
 					if (shiftClickOp != null)
 					{
-						shiftClickMenus.add(client.createMenuEntry(idx)
+						subShift.createMenuEntry(0)
 							.setOption("Reset")
 							.setType(MenuAction.RUNELITE)
 							.onClick(e ->
@@ -947,25 +936,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 
 								log.debug("Unset worn item shift swap for {}", itemComposition.getMembersName());
 								unsetWornItemSwapConfig(true, itemComposition.getId());
-							}));
-					}
-
-					if (!leftClickMenus.isEmpty())
-					{
-						MenuEntry sub = client.createMenuEntry(idx)
-							.setOption("Swap left click")
-							.setTarget(entry.getTarget())
-							.setType(MenuAction.RUNELITE_SUBMENU);
-						leftClickMenus.forEach(menu -> menu.setParent(sub));
-					}
-
-					if (!shiftClickMenus.isEmpty())
-					{
-						MenuEntry sub = client.createMenuEntry(idx)
-							.setOption("Swap shift click")
-							.setTarget(entry.getTarget())
-							.setType(MenuAction.RUNELITE_SUBMENU);
-						shiftClickMenus.forEach(menu -> menu.setParent(sub));
+							});
 					}
 				}
 				break;
@@ -1015,8 +986,16 @@ public class MenuEntrySwapperPlugin extends Plugin
 				final int defaultLeftClickOp = defaultOp(itemComposition, false);
 				final int defaultShiftClickOp = defaultOp(itemComposition, true);
 
-				List<MenuEntry> leftClickMenus = new ArrayList<>(actions.length + 2);
-				List<MenuEntry> shiftClickMenus = new ArrayList<>(actions.length + 2);
+				MenuEntry swapLeftClick = client.createMenuEntry(idx)
+					.setOption("Swap left click")
+					.setTarget(entry.getTarget())
+					.setType(MenuAction.RUNELITE);
+				MenuEntry swapShiftClick = client.createMenuEntry(idx)
+					.setOption("Swap shift click")
+					.setTarget(entry.getTarget())
+					.setType(MenuAction.RUNELITE);
+				Menu subLeft = swapLeftClick.createSubMenu();
+				Menu subShift = swapShiftClick.createSubMenu();
 
 				for (int actionIdx = 0; actionIdx < actions.length; ++actionIdx)
 				{
@@ -1027,20 +1006,20 @@ public class MenuEntrySwapperPlugin extends Plugin
 						{
 							if (defaultLeftClickOp != actionIdx && (leftClickOp == null || leftClickOp != actionIdx))
 							{
-								leftClickMenus.add(client.createMenuEntry(idx)
+								subLeft.createMenuEntry(0)
 									.setOption(opName)
 									.setType(MenuAction.RUNELITE)
-									.onClick(heldItemConsumer(itemComposition, opName, actionIdx, false)));
+									.onClick(heldItemConsumer(itemComposition, opName, actionIdx, false));
 							}
 						}
 						if (config.shiftClickCustomization())
 						{
 							if (defaultShiftClickOp != actionIdx && (shiftClickOp == null || shiftClickOp != actionIdx))
 							{
-								shiftClickMenus.add(client.createMenuEntry(idx)
+								subShift.createMenuEntry(0)
 									.setOption(opName)
 									.setType(MenuAction.RUNELITE)
-									.onClick(heldItemConsumer(itemComposition, opName, actionIdx, true)));
+									.onClick(heldItemConsumer(itemComposition, opName, actionIdx, true));
 							}
 						}
 					}
@@ -1050,17 +1029,17 @@ public class MenuEntrySwapperPlugin extends Plugin
 						// Use
 						if (defaultLeftClickOp != -1 && config.leftClickCustomization())
 						{
-							leftClickMenus.add(client.createMenuEntry(idx)
+							subLeft.createMenuEntry(0)
 								.setOption("Use")
 								.setType(MenuAction.RUNELITE)
-								.onClick(heldItemConsumer(itemComposition, "Use", -1, false)));
+								.onClick(heldItemConsumer(itemComposition, "Use", -1, false));
 						}
 						if (defaultShiftClickOp != -1 && config.shiftClickCustomization())
 						{
-							shiftClickMenus.add(client.createMenuEntry(idx)
+							subShift.createMenuEntry(0)
 								.setOption("Use")
 								.setType(MenuAction.RUNELITE)
-								.onClick(heldItemConsumer(itemComposition, "Use", -1, true)));
+								.onClick(heldItemConsumer(itemComposition, "Use", -1, true));
 						}
 					}
 				}
@@ -1077,17 +1056,17 @@ public class MenuEntrySwapperPlugin extends Plugin
 						{
 							if (leftClickOp == null || leftClickOp != sub.option.hashCode())
 							{
-								leftClickMenus.add(client.createMenuEntry(idx)
+								subLeft.createMenuEntry(0)
 									.setOption(sub.option)
 									.setType(MenuAction.RUNELITE)
-									.onClick(heldItemConsumer(itemComposition, sub.option, sub.option.hashCode(), false)));
+									.onClick(heldItemConsumer(itemComposition, sub.option, sub.option.hashCode(), false));
 							}
 							if (shiftClickOp == null || shiftClickOp != sub.option.hashCode())
 							{
-								shiftClickMenus.add(client.createMenuEntry(idx)
+								subShift.createMenuEntry(0)
 									.setOption(sub.option)
 									.setType(MenuAction.RUNELITE)
-									.onClick(heldItemConsumer(itemComposition, sub.option, sub.option.hashCode(), true)));
+									.onClick(heldItemConsumer(itemComposition, sub.option, sub.option.hashCode(), true));
 							}
 						}
 					}
@@ -1095,7 +1074,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 
 				if (leftClickOp != null && config.leftClickCustomization())
 				{
-					leftClickMenus.add(client.createMenuEntry(idx)
+					subLeft.createMenuEntry(0)
 						.setOption("Reset")
 						.setType(MenuAction.RUNELITE)
 						.onClick(e ->
@@ -1112,11 +1091,11 @@ public class MenuEntrySwapperPlugin extends Plugin
 
 							log.debug("Unset held item left swap for {}", itemComposition.getMembersName());
 							unsetItemSwapConfig(false, itemComposition.getId());
-						}));
+						});
 				}
 				if (shiftClickOp != null && config.shiftClickCustomization())
 				{
-					shiftClickMenus.add(client.createMenuEntry(idx)
+					subShift.createMenuEntry(0)
 						.setOption("Reset")
 						.setType(MenuAction.RUNELITE)
 						.onClick(e ->
@@ -1133,26 +1112,9 @@ public class MenuEntrySwapperPlugin extends Plugin
 
 							log.debug("Unset held item shift swap for {}", itemComposition.getMembersName());
 							unsetItemSwapConfig(true, itemComposition.getId());
-						}));
-				}
-
-				if (!leftClickMenus.isEmpty())
-				{
-					MenuEntry sub = client.createMenuEntry(idx)
-						.setOption("Swap left click")
-						.setTarget(entry.getTarget())
-						.setType(MenuAction.RUNELITE_SUBMENU);
-					leftClickMenus.forEach(menu -> menu.setParent(sub));
+						});
 				}
 
-				if (!shiftClickMenus.isEmpty())
-				{
-					MenuEntry sub = client.createMenuEntry(idx)
-						.setOption("Swap shift click")
-						.setTarget(entry.getTarget())
-						.setType(MenuAction.RUNELITE_SUBMENU);
-					shiftClickMenus.forEach(menu -> menu.setParent(sub));
-				}
 				break;
 			}
 		}
@@ -1186,8 +1148,9 @@ public class MenuEntrySwapperPlugin extends Plugin
 
 		final MenuEntry[] entries = event.getMenuEntries();
 
-		List<MenuEntry> leftClickMenus = new ArrayList<>();
-		List<MenuEntry> shiftClickMenus = new ArrayList<>();
+		MenuEntry swapLeftClick = null, swapShiftClick = null;
+		Menu subLeft = null, subShift = null;
+		boolean initialized = false;
 
 		for (int idx = entries.length - 1; idx >= 0; --idx)
 		{
@@ -1237,27 +1200,40 @@ public class MenuEntrySwapperPlugin extends Plugin
 						highestOp = opEntry.getIdentifier();
 					}
 
+					if (!initialized)
+					{
+						initialized = true;
+						swapLeftClick = client.createMenuEntry(2)
+							.setOption("Swap left click")
+							.setType(MenuAction.RUNELITE);
+						swapShiftClick = client.createMenuEntry(2)
+							.setOption("Swap shift click")
+							.setType(MenuAction.RUNELITE);
+						subLeft = swapLeftClick.createSubMenu();
+						subShift = swapShiftClick.createSubMenu();
+					}
+
 					if (identifier != lowestOp && (leftClick == null || leftClick != identifier))
 					{
-						leftClickMenus.add(client.createMenuEntry(1)
+						subLeft.createMenuEntry(0)
 							.setOption(entry.getOption())
 							.setType(MenuAction.RUNELITE)
-							.onClick(uiConsumer(entry.getOption(), entry.getTarget(), false, componentId, itemId, identifier)));
+							.onClick(uiConsumer(entry.getOption(), entry.getTarget(), false, componentId, itemId, identifier));
 					}
 
 					if (identifier != lowestOp && (shiftClick == null || shiftClick != identifier))
 					{
-						shiftClickMenus.add(client.createMenuEntry(1)
+						subShift.createMenuEntry(0)
 							.setOption(entry.getOption())
 							.setType(MenuAction.RUNELITE)
-							.onClick(uiConsumer(entry.getOption(), entry.getTarget(), true, componentId, itemId, identifier)));
+							.onClick(uiConsumer(entry.getOption(), entry.getTarget(), true, componentId, itemId, identifier));
 					}
 
 					if (identifier == highestOp)
 					{
 						if (leftClick != null)
 						{
-							leftClickMenus.add(client.createMenuEntry(1)
+							subLeft.createMenuEntry(0)
 								.setOption("Reset")
 								.setType(MenuAction.RUNELITE)
 								.onClick(menuEntry ->
@@ -1275,12 +1251,12 @@ public class MenuEntrySwapperPlugin extends Plugin
 									log.debug("Unset ui left swap for {}/{}", componentId, menuEntry.getTarget());
 
 									unsetUiSwapConfig(false, componentId, itemId);
-								}));
+								});
 						}
 
 						if (shiftClick != null)
 						{
-							shiftClickMenus.add(client.createMenuEntry(1)
+							subShift.createMenuEntry(0)
 								.setOption("Reset")
 								.setType(MenuAction.RUNELITE)
 								.onClick(menuEntry ->
@@ -1298,26 +1274,11 @@ public class MenuEntrySwapperPlugin extends Plugin
 									log.debug("Unset ui shift swap for {}/{}", componentId, menuEntry.getTarget());
 
 									unsetUiSwapConfig(true, componentId, itemId);
-								}));
-						}
-
-						if (!leftClickMenus.isEmpty())
-						{
-							MenuEntry sub = client.createMenuEntry(2)
-								.setOption("Swap left click")
-								.setTarget(entry.getTarget())
-								.setType(MenuAction.RUNELITE_SUBMENU);
-							leftClickMenus.forEach(menu -> menu.setParent(sub));
+								});
 						}
 
-						if (!shiftClickMenus.isEmpty())
-						{
-							MenuEntry sub = client.createMenuEntry(1)
-								.setOption("Swap shift click")
-								.setTarget(entry.getTarget())
-								.setType(MenuAction.RUNELITE_SUBMENU);
-							shiftClickMenus.forEach(menu -> menu.setParent(sub));
-						}
+						swapLeftClick.setTarget(entry.getTarget());
+						swapShiftClick.setTarget(entry.getTarget());
 					}
 				}
 			}
@@ -1377,7 +1338,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 		};
 	}
 
-	private boolean swapBank(MenuEntry menuEntry, MenuAction type)
+	private boolean swapBank(Menu menu, MenuEntry menuEntry, MenuAction type)
 	{
 		if (type != MenuAction.CC_OP && type != MenuAction.CC_OP_LOW_PRIORITY)
 		{
@@ -1402,7 +1363,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 				: isGroupStoragePlayerInventory ? shiftDepositMode.getIdentifierGroupStorage()
 				: shiftDepositMode.getIdentifier();
 			final MenuAction action = opId >= 6 ? MenuAction.CC_OP_LOW_PRIORITY : MenuAction.CC_OP;
-			bankModeSwap(action, opId);
+			bankModeSwap(menu, action, opId);
 			return true;
 		}
 
@@ -1425,16 +1386,16 @@ public class MenuEntrySwapperPlugin extends Plugin
 				action = shiftWithdrawMode.getMenuAction();
 				opId = shiftWithdrawMode.getIdentifier();
 			}
-			bankModeSwap(action, opId);
+			bankModeSwap(menu, action, opId);
 			return true;
 		}
 
 		return false;
 	}
 
-	private void bankModeSwap(MenuAction entryType, int entryIdentifier)
+	private void bankModeSwap(Menu menu, MenuAction entryType, int entryIdentifier)
 	{
-		MenuEntry[] menuEntries = client.getMenuEntries();
+		MenuEntry[] menuEntries = menu.getMenuEntries();
 
 		for (int i = menuEntries.length - 1; i >= 0; --i)
 		{
@@ -1448,20 +1409,31 @@ public class MenuEntrySwapperPlugin extends Plugin
 				menuEntries[i] = menuEntries[menuEntries.length - 1];
 				menuEntries[menuEntries.length - 1] = entry;
 
-				client.setMenuEntries(menuEntries);
+				menu.setMenuEntries(menuEntries);
 				break;
 			}
 		}
 	}
 
-	private void swapMenuEntry(MenuEntry[] menuEntries, int index, MenuEntry menuEntry)
+	private void swapMenuEntry(MenuEntry parent, Menu menu, MenuEntry[] menuEntries, int index, MenuEntry menuEntry)
 	{
+		Menu sub = menuEntry.getSubMenu();
+		if (sub != null)
+		{
+			int subidx = 0;
+			MenuEntry[] subEntries = sub.getMenuEntries();
+			for (MenuEntry subEntry : subEntries)
+			{
+				swapMenuEntry(menuEntry, sub, subEntries, subidx++, subEntry);
+			}
+		}
+
 		final int eventId = menuEntry.getIdentifier();
 		final MenuAction menuAction = menuEntry.getType();
 		final String option = Text.removeTags(menuEntry.getOption()).toLowerCase();
 		final String target = Text.removeTags(menuEntry.getTarget()).toLowerCase();
 
-		final Widget w = menuEntry.getParent() != null ? menuEntry.getParent().getWidget() : menuEntry.getWidget();
+		final Widget w = parent != null ? parent.getWidget() : menuEntry.getWidget();
 		// Custom item swap
 		if (w != null && WidgetUtil.componentToInterface(w.getId()) == InterfaceID.INVENTORY
 			&& (lastShift ? config.shiftClickCustomization() : config.leftClickCustomization()))
@@ -1471,13 +1443,20 @@ public class MenuEntrySwapperPlugin extends Plugin
 			{
 				if (swapIndex == -1)
 				{
-					swap(menuEntries, "use", target, index, true);
+					swap(menu, menuEntries, "use", target, index, true);
+				}
+				else if (swapIndex + 1 == menuEntry.getItemOp())
+				{
+					swap(menu, menuEntries, index, menuEntries.length - 1);
 				}
-				else if (swapIndex + 1 == menuEntry.getItemOp()
-					|| (menuEntry.getParent() != null && menuEntry.getOption().hashCode() == swapIndex))
+				// Submenu swap. The swapIndex is actually the option hashCode.
+				else if (parent != null && menuEntry.getOption().hashCode() == swapIndex)
 				{
-					swap(optionIndexes, menuEntries, index, menuEntries.length - 1);
-					menuEntry.setParent(null);
+					// Since it isn't possible to reparent the menu to the top level, just copy it
+					client.createMenuEntry(-1)
+						.setOption(menuEntry.getOption())
+						.setTarget(menuEntry.getTarget())
+						.onClick(menuEntry.onClick());
 				}
 				return;
 			}
@@ -1492,11 +1471,18 @@ public class MenuEntrySwapperPlugin extends Plugin
 				final Integer wornItemSwapConfig = getWornItemSwapConfig(shiftModifier(), child.getItemId());
 				if (wornItemSwapConfig != null)
 				{
-					if (wornItemSwapConfig == menuEntry.getIdentifier()
-						|| (menuEntry.getParent() != null && menuEntry.getOption().hashCode() == wornItemSwapConfig))
+					if (wornItemSwapConfig == menuEntry.getIdentifier())
 					{
-						swap(optionIndexes, menuEntries, index, menuEntries.length - 1);
-						menuEntry.setParent(null); // Move from a submenu to a top level menu
+						swap(menu, menuEntries, index, menuEntries.length - 1);
+					}
+					// Submenu swap.
+					else if (parent != null && menuEntry.getOption().hashCode() == wornItemSwapConfig)
+					{
+						// Since it isn't possible to reparent the menu to the top level, just copy it
+						client.createMenuEntry(-1)
+							.setOption(menuEntry.getOption())
+							.setTarget(menuEntry.getTarget())
+							.onClick(menuEntry.onClick());
 					}
 					return;
 				}
@@ -1520,7 +1506,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 				MenuAction swapAction = OBJECT_MENU_TYPES.get(customOption);
 				if (swapAction == menuAction)
 				{
-					swap(optionIndexes, menuEntries, index, menuEntries.length - 1);
+					swap(menu, menuEntries, index, menuEntries.length - 1);
 					return;
 				}
 			}
@@ -1546,7 +1532,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 						++i;
 					}
 
-					swap(optionIndexes, menuEntries, index, i);
+					swap(menu, menuEntries, index, i);
 					return;
 				}
 			}
@@ -1568,13 +1554,13 @@ public class MenuEntrySwapperPlugin extends Plugin
 				final Integer op = getUiSwapConfig(shiftModifier(), componentId, itemId);
 				if (op != null && op == menuEntry.getIdentifier())
 				{
-					swap(optionIndexes, menuEntries, index, menuEntries.length - 1);
+					swap(menu, menuEntries, index, menuEntries.length - 1);
 					return;
 				}
 			}
 		}
 
-		if (swapBank(menuEntry, menuAction))
+		if (swapBank(menu, menuEntry, menuAction))
 		{
 			return;
 		}
@@ -1595,7 +1581,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 		{
 			if (swap.getTargetPredicate().test(target) && swap.getEnabled().get())
 			{
-				if (swap(menuEntries, swap.getSwappedOption(), target, index, swap.isStrict()))
+				if (swap(menu, menuEntries, swap.getSwappedOption(), target, index, swap.isStrict()))
 				{
 					break;
 				}
@@ -1684,28 +1670,24 @@ public class MenuEntrySwapperPlugin extends Plugin
 			return;
 		}
 
-		MenuEntry[] menuEntries = client.getMenuEntries();
-
-		// Build option map for quick lookup in findIndex
-		int idx = 0;
-		optionIndexes.clear();
-		for (MenuEntry entry : menuEntries)
-		{
-			String option = Text.removeTags(entry.getOption()).toLowerCase();
-			optionIndexes.put(option, idx++);
-		}
+		Menu root = client.getMenu();
+		MenuEntry[] menuEntries = root.getMenuEntries();
 
 		// Perform swaps
-		idx = 0;
+		int idx = 0;
 		for (MenuEntry entry : menuEntries)
 		{
-			swapMenuEntry(menuEntries, idx++, entry);
+			swapMenuEntry(null, root, menuEntries, idx++, entry);
 		}
 
 		if (config.removeDeadNpcMenus())
 		{
 			removeDeadNpcs();
 		}
+
+		// invalidate option index cache
+		cacheOptionIndexes.clear();
+		cacheOptionMenu = null;
 	}
 
 	private void removeDeadNpcs()
@@ -1724,25 +1706,25 @@ public class MenuEntrySwapperPlugin extends Plugin
 		}
 	}
 
-	private boolean swap(MenuEntry[] menuEntries, String option, String target, int index, boolean strict)
+	private boolean swap(Menu menu, MenuEntry[] menuEntries, String option, String target, int index, boolean strict)
 	{
 		// find option to swap with
-		int optionIdx = findIndex(menuEntries, index, option, target, strict);
+		int optionIdx = findIndex(menu, menuEntries, index, option, target, strict);
 
 		if (optionIdx >= 0)
 		{
-			swap(optionIndexes, menuEntries, optionIdx, index);
+			swap(menu, menuEntries, optionIdx, index);
 			return true;
 		}
 
 		return false;
 	}
 
-	private int findIndex(MenuEntry[] entries, int limit, String option, String target, boolean strict)
+	private int findIndex(Menu menu, MenuEntry[] entries, int limit, String option, String target, boolean strict)
 	{
 		if (strict)
 		{
-			List<Integer> indexes = optionIndexes.get(option);
+			List<Integer> indexes = findOptionIndex(menu, option);
 
 			// We want the last index which matches the target, as that is what is top-most
 			// on the menu
@@ -1779,7 +1761,24 @@ public class MenuEntrySwapperPlugin extends Plugin
 		return -1;
 	}
 
-	private void swap(ArrayListMultimap<String, Integer> optionIndexes, MenuEntry[] entries, int index1, int index2)
+	private List<Integer> findOptionIndex(Menu menu, String option)
+	{
+		if (cacheOptionMenu == null || cacheOptionIndexes.isEmpty())
+		{
+			int idx = 0;
+			cacheOptionMenu = menu;
+			cacheOptionIndexes.clear();
+			for (MenuEntry entry : menu.getMenuEntries())
+			{
+				String opt = Text.removeTags(entry.getOption()).toLowerCase();
+				cacheOptionIndexes.put(opt, idx++);
+			}
+			log.trace("[{}] Rebuilt option index cache with {} entries", client.getGameCycle(), idx);
+		}
+		return cacheOptionIndexes.get(option);
+	}
+
+	private void swap(Menu menu, MenuEntry[] entries, int index1, int index2)
 	{
 		if (index1 == index2)
 		{
@@ -1803,21 +1802,26 @@ public class MenuEntrySwapperPlugin extends Plugin
 			entry2.setType(MenuAction.CC_OP);
 		}
 
-		client.setMenuEntries(entries);
+		menu.setMenuEntries(entries);
 
 		// Update optionIndexes
-		String option1 = Text.removeTags(entry1.getOption()).toLowerCase(),
-			option2 = Text.removeTags(entry2.getOption()).toLowerCase();
+		if (cacheOptionMenu == menu)
+		{
+			String option1 = Text.removeTags(entry1.getOption()).toLowerCase(),
+				option2 = Text.removeTags(entry2.getOption()).toLowerCase();
 
-		List<Integer> list1 = optionIndexes.get(option1),
-			list2 = optionIndexes.get(option2);
+			List<Integer> list1 = cacheOptionIndexes.get(option1),
+				list2 = cacheOptionIndexes.get(option2);
 
-		// call remove(Object) instead of remove(int)
-		list1.remove((Integer) index1);
-		list2.remove((Integer) index2);
+			// call remove(Object) instead of remove(int)
+			list1.remove((Integer) index1);
+			list2.remove((Integer) index2);
 
-		sortedInsert(list1, index2);
-		sortedInsert(list2, index1);
+			sortedInsert(list1, index2);
+			sortedInsert(list2, index1);
+
+			log.trace("Swapped option index {} <-> {}", index1, index2);
+		}
 	}
 
 	private static <T extends Comparable<? super T>> void sortedInsert(List<T> list, T value)
@@ -2134,11 +2138,11 @@ public class MenuEntrySwapperPlugin extends Plugin
 				.orElse(null);
 			if (swap != null && config.teleportSubmenus())
 			{
-				me.setType(MenuAction.RUNELITE_SUBMENU_WIDGET);
+				me.setType(MenuAction.RUNELITE_WIDGET);
 				me.onClick(e -> client.menuAction(e.getParam0(), e.getParam1(), MenuAction.CC_OP,
 					e.getIdentifier(), e.getItemId(), e.getOption(), e.getTarget()));
+				Menu submenu = me.createSubMenu();
 
-				int off = 0;
 				final int p0 = me.getParam0();
 				final int p1 = me.getParam1();
 				final int id = me.getIdentifier();
@@ -2147,13 +2151,12 @@ public class MenuEntrySwapperPlugin extends Plugin
 				final String target = me.getTarget();
 				for (TeleportSub sub : swap.subs)
 				{
-					client.createMenuEntry(-2 - off++)
+					submenu.createMenuEntry(0)
 						.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);
@@ -2172,11 +2175,11 @@ public class MenuEntrySwapperPlugin extends Plugin
 				.orElse(null);
 			if (swap != null && config.teleportSubmenus())
 			{
-				me.setType(MenuAction.RUNELITE_SUBMENU_WIDGET);
+				me.setType(MenuAction.RUNELITE_WIDGET);
 				me.onClick(e -> client.menuAction(e.getParam0(), e.getParam1(), MenuAction.CC_OP,
 					e.getIdentifier(), e.getItemId(), e.getOption(), e.getTarget()));
+				Menu submenu = me.createSubMenu();
 
-				int off = 0;
 				final int p0 = me.getParam0();
 				final int p1 = me.getParam1();
 				final int id = me.getIdentifier();
@@ -2185,13 +2188,12 @@ public class MenuEntrySwapperPlugin extends Plugin
 				final String target = me.getTarget();
 				for (TeleportSub sub : swap.subs)
 				{
-					client.createMenuEntry(-2 - off++)
+					submenu.createMenuEntry(0)
 						.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);
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java
index 0d511d251..1e7d100ba 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java
@@ -49,6 +49,7 @@ import net.runelite.api.GameState;
 import net.runelite.api.GraphicID;
 import net.runelite.api.GraphicsObject;
 import net.runelite.api.KeyCode;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.NPC;
@@ -342,14 +343,14 @@ public class NpcIndicatorsPlugin extends Plugin
 		MenuEntry parent = client.createMenuEntry(idx--)
 			.setOption("Tag color")
 			.setTarget(target)
-			.setType(MenuAction.RUNELITE_SUBMENU);
+			.setType(MenuAction.RUNELITE);
+		Menu submenu = parent.createSubMenu();
 
 		for (final Color c : colors)
 		{
-			client.createMenuEntry(idx--)
+			submenu.createMenuEntry(0)
 				.setOption(ColorUtil.prependColorTag("Set color", c))
 				.setType(MenuAction.RUNELITE)
-				.setParent(parent)
 				.onClick(e ->
 				{
 					setNpcHighlightColor(npc.getId(), c);
@@ -357,10 +358,9 @@ public class NpcIndicatorsPlugin extends Plugin
 				});
 		}
 
-		client.createMenuEntry(idx--)
+		submenu.createMenuEntry(0)
 			.setOption("Pick color")
 			.setType(MenuAction.RUNELITE)
-			.setParent(parent)
 			.onClick(e -> SwingUtilities.invokeLater(() ->
 			{
 				RuneliteColorPicker colorPicker = colorPickerManager.create(SwingUtilities.windowForComponent((Applet) client),
@@ -375,10 +375,9 @@ public class NpcIndicatorsPlugin extends Plugin
 
 		if (getNpcHighlightColor(npc.getId()) != null)
 		{
-			client.createMenuEntry(idx--)
+			submenu.createMenuEntry(0)
 				.setOption("Reset")
 				.setType(MenuAction.RUNELITE)
-				.setParent(parent)
 				.onClick(e ->
 				{
 					unsetNpcHighlightColor(npc.getId());
@@ -394,7 +393,8 @@ public class NpcIndicatorsPlugin extends Plugin
 		MenuEntry parent = client.createMenuEntry(idx--)
 			.setOption("Tag style")
 			.setTarget(target)
-			.setType(MenuAction.RUNELITE_SUBMENU);
+			.setType(MenuAction.RUNELITE);
+		Menu submenu = parent.createSubMenu();
 
 		String[] names = {"Hull", "Tile", "True tile", "South-west tile", "South-west true tile", "Outline"};
 		String[] styles = {STYLE_HULL, STYLE_TILE, STYLE_TRUE_TILE, STYLE_SW_TILE, STYLE_SW_TRUE_TILE, STYLE_OUTLINE};
@@ -402,10 +402,9 @@ public class NpcIndicatorsPlugin extends Plugin
 		for (int i = 0; i < names.length; ++i)
 		{
 			final String style = styles[i];
-			client.createMenuEntry(idx--)
+			submenu.createMenuEntry(0)
 				.setOption(names[i])
 				.setType(MenuAction.RUNELITE)
-				.setParent(parent)
 				.onClick(e ->
 				{
 					setNpcTagStyle(npc.getId(), style);
@@ -415,10 +414,9 @@ public class NpcIndicatorsPlugin extends Plugin
 
 		if (getNpcTagStyle(npc.getId()) != null)
 		{
-			client.createMenuEntry(idx--)
+			submenu.createMenuEntry(0)
 				.setOption("Reset")
 				.setType(MenuAction.RUNELITE)
-				.setParent(parent)
 				.onClick(e ->
 				{
 					unsetNpcTagStyle(npc.getId());
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsPlugin.java
index b16373f15..757d98958 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/objectindicators/ObjectIndicatorsPlugin.java
@@ -54,6 +54,7 @@ import net.runelite.api.GameObject;
 import net.runelite.api.GameState;
 import net.runelite.api.GroundObject;
 import net.runelite.api.KeyCode;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.ObjectComposition;
@@ -273,21 +274,20 @@ public class ObjectIndicatorsPlugin extends Plugin
 		MenuEntry parent = client.createMenuEntry(idx--)
 			.setOption("Mark border color")
 			.setTarget(target)
-			.setType(MenuAction.RUNELITE_SUBMENU);
+			.setType(MenuAction.RUNELITE);
+		Menu submenu = parent.createSubMenu();
 
 		for (final Color c : colors)
 		{
-			client.createMenuEntry(idx--)
+			submenu.createMenuEntry(0)
 				.setOption(ColorUtil.prependColorTag("Set color", c))
 				.setType(MenuAction.RUNELITE)
-				.setParent(parent)
 				.onClick(e -> updateObjectConfig(object, p -> p.setBorderColor(c)));
 		}
 
-		client.createMenuEntry(idx--)
+		submenu.createMenuEntry(0)
 			.setOption("Pick color")
 			.setType(MenuAction.RUNELITE)
-			.setParent(parent)
 			.onClick(e -> SwingUtilities.invokeLater(() ->
 			{
 				RuneliteColorPicker colorPicker = colorPickerManager.create(SwingUtilities.windowForComponent((Applet) client),
@@ -317,21 +317,20 @@ public class ObjectIndicatorsPlugin extends Plugin
 		MenuEntry parent = client.createMenuEntry(idx--)
 			.setOption("Mark fill color")
 			.setTarget(target)
-			.setType(MenuAction.RUNELITE_SUBMENU);
+			.setType(MenuAction.RUNELITE);
+		Menu submenu = parent.createSubMenu();
 
 		for (final Color c : colors)
 		{
-			client.createMenuEntry(idx--)
+			submenu.createMenuEntry(0)
 				.setOption(ColorUtil.prependColorTag("Set color", c))
 				.setType(MenuAction.RUNELITE)
-				.setParent(parent)
 				.onClick(e -> updateObjectConfig(object, p -> p.setFillColor(c)));
 		}
 
-		client.createMenuEntry(idx--)
+		submenu.createMenuEntry(0)
 			.setOption("Pick color")
 			.setType(MenuAction.RUNELITE)
-			.setParent(parent)
 			.onClick(e -> SwingUtilities.invokeLater(() ->
 			{
 				// default fill color depends on the highlight type. just use a=50 from hull fill.
@@ -345,10 +344,9 @@ public class ObjectIndicatorsPlugin extends Plugin
 				colorPicker.setVisible(true);
 			}));
 
-		client.createMenuEntry(idx--)
+		submenu.createMenuEntry(0)
 			.setOption("Reset")
 			.setType(MenuAction.RUNELITE)
-			.setParent(parent)
 			.onClick(e -> updateObjectConfig(object, p -> p.setFillColor(null)));
 
 		return idx;
@@ -359,36 +357,32 @@ public class ObjectIndicatorsPlugin extends Plugin
 		MenuEntry parent = client.createMenuEntry(idx--)
 			.setOption("Mark style")
 			.setTarget(target)
-			.setType(MenuAction.RUNELITE_SUBMENU);
+			.setType(MenuAction.RUNELITE);
+		Menu submenu = parent.createSubMenu();
 
-		client.createMenuEntry(idx--)
+		submenu.createMenuEntry(0)
 			.setOption("Hull")
 			.setType(MenuAction.RUNELITE)
-			.setParent(parent)
 			.onClick(e -> updateObjectConfig(object, c -> c.setHull(c.getHull() != Boolean.TRUE)));
 
-		client.createMenuEntry(idx--)
+		submenu.createMenuEntry(0)
 			.setOption("Outline")
 			.setType(MenuAction.RUNELITE)
-			.setParent(parent)
 			.onClick(e -> updateObjectConfig(object, c -> c.setOutline(c.getOutline() != Boolean.TRUE)));
 
-		client.createMenuEntry(idx--)
+		submenu.createMenuEntry(0)
 			.setOption("Clickbox")
 			.setType(MenuAction.RUNELITE)
-			.setParent(parent)
 			.onClick(e -> updateObjectConfig(object, c -> c.setClickbox(c.getClickbox() != Boolean.TRUE)));
 
-		client.createMenuEntry(idx--)
+		submenu.createMenuEntry(0)
 			.setOption("Tile")
 			.setType(MenuAction.RUNELITE)
-			.setParent(parent)
 			.onClick(e -> updateObjectConfig(object, c -> c.setTile(c.getTile() != Boolean.TRUE)));
 
-		client.createMenuEntry(idx--)
+		submenu.createMenuEntry(0)
 			.setOption("Reset")
 			.setType(MenuAction.RUNELITE)
-			.setParent(parent)
 			.onClick(e ->
 				updateObjectConfig(object, c ->
 				{
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java
index 3cf59bdc1..8cb8c8cf3 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java
@@ -493,7 +493,8 @@ public class SlayerPlugin extends Plugin
 	@Subscribe
 	public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked)
 	{
-		if (menuOptionClicked.getMenuAction() == MenuAction.CC_OP && menuOptionClicked.getMenuOption().equals("Check"))
+		if ((menuOptionClicked.getMenuAction() == MenuAction.CC_OP || menuOptionClicked.getMenuAction() == MenuAction.CC_OP_LOW_PRIORITY)
+			&& menuOptionClicked.getMenuOption().equals("Check"))
 		{
 			Widget w = client.getWidget(menuOptionClicked.getParam1());
 			if (w == null)
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/Task.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/Task.java
index 2193e1e7b..bdba82747 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/Task.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/Task.java
@@ -38,24 +38,24 @@ enum Task
 {
 	//<editor-fold desc="Enums">
 	ABERRANT_SPECTRES("Aberrant spectres", ItemID.ABERRANT_SPECTRE, "Spectre"),
-	ABYSSAL_DEMONS("Abyssal demons", ItemID.ABYSSAL_DEMON),
+	ABYSSAL_DEMONS("Abyssal demons", ItemID.ABYSSAL_DEMON, "Abyssal Sire"),
 	ABYSSAL_SIRE("The Abyssal Sire", ItemID.ABYSSAL_ORPHAN),
 	ADAMANT_DRAGONS("Adamant dragons", ItemID.ADAMANT_DRAGON_MASK),
 	ALCHEMICAL_HYDRA("The Alchemical Hydra", ItemID.IKKLE_HYDRA),
 	ANKOU("Ankou", ItemID.ANKOU_MASK),
-	AVIANSIES("Aviansies", ItemID.ENSOULED_AVIANSIE_HEAD),
+	AVIANSIES("Aviansies", ItemID.ENSOULED_AVIANSIE_HEAD, "Kree'arra", "Flight Kilisa", "Flockleader Geerin", "Wingman Skree"),
 	BANDITS("Bandits", ItemID.BANDIT, "Bandit", "Black Heather", "Donny the Lad", "Speedy Keith"),
 	BANSHEES("Banshees", ItemID.BANSHEE),
 	BARROWS_BROTHERS("Barrows Brothers", ItemID.KARILS_COIF),
 	BASILISKS("Basilisks", ItemID.BASILISK),
 	BATS("Bats", ItemID.GIRAL_BAT_2, "Death wing"),
-	BEARS("Bears", ItemID.ENSOULED_BEAR_HEAD),
-	BIRDS("Birds", ItemID.FEATHER, "Chicken", "Rooster", "Terrorbird", "Seagull", "Vulture", "Duck", "Penguin"),
-	BLACK_DEMONS("Black demons", ItemID.BLACK_DEMON_MASK),
-	BLACK_DRAGONS("Black dragons", ItemID.BLACK_DRAGON_MASK, "Baby black dragon"),
+	BEARS("Bears", ItemID.ENSOULED_BEAR_HEAD, "Callisto", "Artio"),
+	BIRDS("Birds", ItemID.FEATHER, "Chicken", "Rooster", "Terrorbird", "Seagull", "Vulture", "Duck", "Penguin", "Baby Roc"),
+	BLACK_DEMONS("Black demons", ItemID.BLACK_DEMON_MASK, "Demonic gorilla", "Balfrug Kreeyath", "Skotizo", "Porazdir"),
+	BLACK_DRAGONS("Black dragons", ItemID.BLACK_DRAGON_MASK),
 	BLACK_KNIGHTS("Black Knights", ItemID.BLACK_FULL_HELM, "Black Knight"),
 	BLOODVELD("Bloodveld", ItemID.BLOODVELD),
-	BLUE_DRAGONS("Blue dragons", ItemID.BLUE_DRAGON_MASK, "Baby blue dragon"),
+	BLUE_DRAGONS("Blue dragons", ItemID.BLUE_DRAGON_MASK, "Vorkath"),
 	BRINE_RATS("Brine rats", ItemID.BRINE_RAT),
 	BRONZE_DRAGONS("Bronze dragons", ItemID.BRONZE_DRAGON_MASK),
 	CALLISTO("Callisto", ItemID.CALLISTO_CUB),
@@ -63,46 +63,46 @@ enum Task
 	CAVE_BUGS("Cave bugs", ItemID.SWAMP_CAVE_BUG),
 	CAVE_CRAWLERS("Cave crawlers", ItemID.CAVE_CRAWLER, "Chasm crawler"),
 	CAVE_HORRORS("Cave horrors", ItemID.CAVE_HORROR, "Cave abomination"),
-	CAVE_KRAKEN("Cave kraken", ItemID.CAVE_KRAKEN),
+	CAVE_KRAKEN("Cave kraken", ItemID.CAVE_KRAKEN, "Kraken"),
 	CAVE_SLIMES("Cave slimes", ItemID.SWAMP_CAVE_SLIME),
 	CERBERUS("Cerberus", ItemID.HELLPUPPY),
-	CHAOS_DRUIDS("Chaos druids", ItemID.ELDER_CHAOS_HOOD, "Elder Chaos druid", "Chaos druid"),
+	CHAOS_DRUIDS("Chaos druids", ItemID.ELDER_CHAOS_HOOD),
 	CHAOS_ELEMENTAL("The Chaos Elemental", ItemID.PET_CHAOS_ELEMENTAL),
 	CHAOS_FANATIC("The Chaos Fanatic", ItemID.ANCIENT_STAFF),
 	COCKATRICE("Cockatrice", ItemID.COCKATRICE, "Cockathrice"),
 	COWS("Cows", ItemID.COW_MASK),
 	CRAWLING_HANDS("Crawling hands", ItemID.CRAWLING_HAND, "Crushing hand"),
-	CRAZY_ARCHAEOLOGIST("Crazy Archaeologists", ItemID.FEDORA, "Crazy Archaeologist"),
+	CRAZY_ARCHAEOLOGIST("Crazy Archaeologists", ItemID.FEDORA),
 	CROCODILES("Crocodiles", ItemID.SWAMP_LIZARD),
 	DAGANNOTH("Dagannoth", ItemID.DAGANNOTH),
 	DAGANNOTH_KINGS("Dagannoth Kings", ItemID.PET_DAGANNOTH_PRIME),
 	DARK_BEASTS("Dark beasts", ItemID.DARK_BEAST, "Night beast"),
 	DARK_WARRIORS("Dark warriors", ItemID.BLACK_MED_HELM, "Dark warrior"),
 	DERANGED_ARCHAEOLOGIST("Deranged Archaeologist", ItemID.ARCHAEOLOGISTS_DIARY),
-	DOGS("Dogs", ItemID.GUARD_DOG, "Jackal"),
+	DOGS("Dogs", ItemID.GUARD_DOG, "Jackal", "Temple Guardian"),
 	DRAKES("Drakes", ItemID.DRAKE),
 	DUKE_SUCELLUS("Duke Sucellus", ItemID.BARON),
 	DUST_DEVILS("Dust devils", ItemID.DUST_DEVIL, "Choke devil"),
 	DWARVES("Dwarves", ItemID.DWARVEN_HELMET, "Dwarf", "Black Guard"),
 	EARTH_WARRIORS("Earth warriors", ItemID.BRONZE_FULL_HELM_T),
 	ELVES("Elves", ItemID.ELF, "Elf", "Iorwerth Warrior", "Iorwerth Archer"),
-	ENTS("Ents", ItemID.NICE_TREE, "Ent"),
+	ENTS("Ents", ItemID.NICE_TREE),
 	FEVER_SPIDERS("Fever spiders", ItemID.FEVER_SPIDER),
 	FIRE_GIANTS("Fire giants", ItemID.FIRE_BATTLESTAFF),
 	FLESH_CRAWLERS("Fleshcrawlers", ItemID.ENSOULED_SCORPION_HEAD, "Flesh crawler"),
 	FOSSIL_ISLAND_WYVERNS("Fossil island wyverns", ItemID.FOSSIL_ISLAND_WYVERN, "Ancient wyvern", "Long-tailed wyvern", "Spitting wyvern", "Taloned wyvern"),
-	GARGOYLES("Gargoyles", ItemID.GARGOYLE, 9, ItemID.ROCK_HAMMER),
+	GARGOYLES("Gargoyles", ItemID.GARGOYLE, 9, ItemID.ROCK_HAMMER, "Dusk", "Dawn"),
 	GENERAL_GRAARDOR("General Graardor", ItemID.PET_GENERAL_GRAARDOR),
 	GHOSTS("Ghosts", ItemID.GHOSTSPEAK_AMULET, "Death wing", "Tortured soul", "Forgotten Soul", "Revenant"),
 	GHOULS("Ghouls", ItemID.ZOMBIE_HEAD),
 	GIANT_MOLE("The Giant Mole", ItemID.BABY_MOLE),
-	GOBLINS("Goblins", ItemID.ENSOULED_GOBLIN_HEAD),
-	GREATER_DEMONS("Greater demons", ItemID.GREATER_DEMON_MASK),
-	GREEN_DRAGONS("Green dragons", ItemID.GREEN_DRAGON_MASK, "Baby green dragon", "Elvarg"),
+	GOBLINS("Goblins", ItemID.ENSOULED_GOBLIN_HEAD, "Sergeant Strongstack", "Sergeant Grimspike", "Sergeant Steelwill"),
+	GREATER_DEMONS("Greater demons", ItemID.GREATER_DEMON_MASK, "K'ril Tsutsaroth", "Tstanon Karlak", "Skotizo", "Tormented Demon"),
+	GREEN_DRAGONS("Green dragons", ItemID.GREEN_DRAGON_MASK, "Elvarg"),
 	GROTESQUE_GUARDIANS("The Grotesque Guardians", ItemID.MIDNIGHT, 0, ItemID.ROCK_HAMMER, "Dusk", "Dawn"),
 	HARPIE_BUG_SWARMS("Harpie bug swarms", ItemID.SWARM),
-	HELLHOUNDS("Hellhounds", ItemID.HELLHOUND),
-	HILL_GIANTS("Hill giants", ItemID.ENSOULED_GIANT_HEAD, "Cyclops"),
+	HELLHOUNDS("Hellhounds", ItemID.HELLHOUND, "Cerberus"),
+	HILL_GIANTS("Hill giants", ItemID.ENSOULED_GIANT_HEAD, "Cyclops", "Reanimated giant", "Obor"),
 	HOBGOBLINS("Hobgoblins", ItemID.HOBGOBLIN_GUARD),
 	HYDRAS("Hydras", ItemID.HYDRA),
 	ICEFIENDS("Icefiends", ItemID.ICE_DIAMOND),
@@ -122,43 +122,43 @@ enum Task
 	KRIL_TSUTSAROTH("K'ril Tsutsaroth", ItemID.PET_KRIL_TSUTSAROTH),
 	KURASK("Kurask", ItemID.KURASK),
 	LAVA_DRAGONS("Lava Dragons", ItemID.LAVA_SCALE, "Lava dragon"),
-	LESSER_DEMONS("Lesser demons", ItemID.LESSER_DEMON_MASK),
+	LESSER_DEMONS("Lesser demons", ItemID.LESSER_DEMON_MASK, "Zakl'n Gritch"),
 	LESSER_NAGUA("Lesser Nagua", ItemID.LESSER_NAGUA, "Sulphur Nagua"),
 	LIZARDMEN("Lizardmen", ItemID.LIZARDMAN_FANG, "Lizardman"),
-	LIZARDS("Lizards", ItemID.DESERT_LIZARD, "Desert lizard", "Sulphur lizard", "Small lizard", "Lizard"),
-	MAGIC_AXES("Magic axes", ItemID.IRON_BATTLEAXE, "Magic axe"),
-	MAMMOTHS("Mammoths", ItemID.ATTACKER_HORN, "Mammoth"),
+	LIZARDS("Lizards", ItemID.DESERT_LIZARD),
+	MAGIC_AXES("Magic axes", ItemID.IRON_BATTLEAXE),
+	MAMMOTHS("Mammoths", ItemID.ATTACKER_HORN),
 	MINIONS_OF_SCABARAS("Minions of scabaras", ItemID.GOLDEN_SCARAB, "Scarab swarm", "Locust rider", "Scarab mage"),
 	MINOTAURS("Minotaurs", ItemID.ENSOULED_MINOTAUR_HEAD),
 	MITHRIL_DRAGONS("Mithril dragons", ItemID.MITHRIL_DRAGON_MASK),
 	MOGRES("Mogres", ItemID.MOGRE),
 	MOLANISKS("Molanisks", ItemID.MOLANISK),
-	MONKEYS("Monkeys", ItemID.ENSOULED_MONKEY_HEAD, "Tortured gorilla"),
-	MOSS_GIANTS("Moss giants", ItemID.MOSSY_KEY),
+	MONKEYS("Monkeys", ItemID.ENSOULED_MONKEY_HEAD, "Tortured gorilla", "Demonic gorilla", "Padulah"),
+	MOSS_GIANTS("Moss giants", ItemID.MOSSY_KEY, "Bryophyta"),
 	MUTATED_ZYGOMITES("Mutated zygomites", ItemID.MUTATED_ZYGOMITE, 7, ItemID.FUNGICIDE_SPRAY_0, "Zygomite", "Fungi"),
 	NECHRYAEL("Nechryael", ItemID.NECHRYAEL, "Nechryarch"),
-	OGRES("Ogres", ItemID.ENSOULED_OGRE_HEAD, "Mogre", "Ogress", "Skogre", "Zogre"),
+	OGRES("Ogres", ItemID.ENSOULED_OGRE_HEAD, "Enclave guard", "Mogre", "Ogress", "Skogre", "Zogre"),
 	OTHERWORLDLY_BEING("Otherworldly beings", ItemID.GHOSTLY_HOOD),
 	PHANTOM_MUSPAH("The Phantom Muspah", ItemID.MUPHIN),
 	PIRATES("Pirates", ItemID.PIRATE_HAT, "Pirate"),
 	PYREFIENDS("Pyrefiends", ItemID.PYREFIEND, "Flaming pyrelord"),
 	RATS("Rats", ItemID.RATS_TAIL),
-	RED_DRAGONS("Red dragons", ItemID.BABY_RED_DRAGON, "Baby red dragon"),
-	REVENANTS("Revenants", ItemID.BRACELET_OF_ETHEREUM, "Revenant imp", "Revenant goblin", "Revenant pyrefiend", "Revenant hobgoblin", "Revenant cyclops", "Revenant hellhound", "Revenant demon", "Revenant ork", "Revenant dark beast", "Revenant knight", "Revenant dragon"),
+	RED_DRAGONS("Red dragons", ItemID.BABY_RED_DRAGON),
+	REVENANTS("Revenants", ItemID.BRACELET_OF_ETHEREUM),
 	ROCKSLUGS("Rockslugs", ItemID.ROCKSLUG, 4, ItemID.BAG_OF_SALT),
-	ROGUES("Rogues", ItemID.ROGUE_MASK, "Rogue"),
+	ROGUES("Rogues", ItemID.ROGUE_MASK),
 	RUNE_DRAGONS("Rune dragons", ItemID.RUNE_DRAGON_MASK),
 	SARACHNIS("Sarachnis", ItemID.SRARACHA),
 	SCORPIA("Scorpia", ItemID.SCORPIAS_OFFSPRING),
-	SCORPIONS("Scorpions", ItemID.ENSOULED_SCORPION_HEAD),
+	SCORPIONS("Scorpions", ItemID.ENSOULED_SCORPION_HEAD, "Scorpia", "Lobstrosity"),
 	SEA_SNAKES("Sea snakes", ItemID.SNAKE_CORPSE),
-	SHADES("Shades", ItemID.SHADE_ROBE_TOP, "Loar Shadow", "Loar Shade", "Phrin Shadow", "Phrin Shade", "Riyl Shadow", "Riyl Shade", "Asyn Shadow", "Asyn Shade", "Fiyr Shadow", "Fiyr Shade"),
+	SHADES("Shades", ItemID.SHADE_ROBE_TOP, "Loar", "Phrin", "Riyl", "Asyn", "Fiyr", "Urium"),
 	SHADOW_WARRIORS("Shadow warriors", ItemID.BLACK_FULL_HELM),
 	SKELETAL_WYVERNS("Skeletal wyverns", ItemID.SKELETAL_WYVERN),
-	SKELETONS("Skeletons", ItemID.SKELETON_GUARD),
+	SKELETONS("Skeletons", ItemID.SKELETON_GUARD, "Vet'ion", "Calvar'ion"),
 	SMOKE_DEVILS("Smoke devils", ItemID.SMOKE_DEVIL),
 	SOURHOGS("Sourhogs", ItemID.SOURHOG_FOOT),
-	SPIDERS("Spiders", ItemID.HUGE_SPIDER),
+	SPIDERS("Spiders", ItemID.HUGE_SPIDER, "Kalrag", "Sarachnis", "Venenatis", "Spindel"),
 	SPIRITUAL_CREATURES("Spiritual creatures", ItemID.DRAGON_BOOTS, "Spiritual ranger", "Spiritual mage", "Spiritual warrior"),
 	STEEL_DRAGONS("Steel dragons", ItemID.STEEL_DRAGON),
 	SUQAHS("Suqahs", ItemID.SUQAH_TOOTH),
@@ -166,10 +166,10 @@ enum Task
 	THE_LEVIATHAN("The Leviathan", ItemID.LILVIATHAN),
 	THE_WHISPERER("The Whisperer", ItemID.WISP),
 	THERMONUCLEAR_SMOKE_DEVIL("The Thermonuclear Smoke Devil", ItemID.PET_SMOKE_DEVIL),
-	TROLLS("Trolls", ItemID.TROLL_GUARD, "Dad", "Arrg"),
+	TROLLS("Trolls", ItemID.TROLL_GUARD, "Dad", "Arrg", "Stick", "Kraka", "Pee Hat", "Rock", "Twig", "Berry"),
 	TUROTH("Turoth", ItemID.TUROTH),
-	TZHAAR("Tzhaar", ItemID.ENSOULED_TZHAAR_HEAD),
-	VAMPYRES("Vampyres", ItemID.STAKE, "Vyrewatch", "Vampire"),
+	TZHAAR("Tzhaar", ItemID.ENSOULED_TZHAAR_HEAD, "TzTok-Jad", "TzKal-Zuk"),
+	VAMPYRES("Vampyres", ItemID.STAKE, "Vyrewatch"),
 	VARDORVIS("Vardorvis", ItemID.BUTCH),
 	VENENATIS("Venenatis", ItemID.VENENATIS_SPIDERLING),
 	VETION("Vet'ion", ItemID.VETION_JR),
@@ -181,7 +181,7 @@ enum Task
 	WOLVES("Wolves", ItemID.GREY_WOLF_FUR, "Wolf"),
 	WYRMS("Wyrms", ItemID.WYRM),
 	ZILYANA("Commander Zilyana", ItemID.PET_ZILYANA),
-	ZOMBIES("Zombies", ItemID.ZOMBIE_HEAD, "Undead"),
+	ZOMBIES("Zombies", ItemID.ZOMBIE_HEAD, "Undead", "Vorkath", "Zogre"),
 	ZUK("TzKal-Zuk", ItemID.TZREKZUK),
 	ZULRAH("Zulrah", ItemID.PET_SNAKELING);
 	//</editor-fold>
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameIndicator.java b/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameIndicator.java
deleted file mode 100644
index 55151e340..000000000
--- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameIndicator.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (c) 2018, Tyler <https://github.com/tylerthardy>
- * 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.timers;
-
-import java.awt.Color;
-import lombok.AccessLevel;
-import lombok.Getter;
-import net.runelite.api.SpriteID;
-
-@Getter(AccessLevel.PACKAGE)
-enum GameIndicator
-{
-	VENGEANCE_ACTIVE(SpriteID.SPELL_VENGEANCE_OTHER, GameTimerImageType.SPRITE, "Vengeance active");
-
-	private final String description;
-	private String text;
-	private Color textColor;
-	private final int imageId;
-	private final GameTimerImageType imageType;
-
-	GameIndicator(int imageId, GameTimerImageType idType, String description, String text, Color textColor)
-	{
-		this.imageId = imageId;
-		this.imageType = idType;
-		this.description = description;
-		this.text = text;
-		this.textColor = textColor;
-	}
-
-	GameIndicator(int imageId, GameTimerImageType idType, String description)
-	{
-		this(imageId, idType, description, "", null);
-	}
-}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/IndicatorIndicator.java b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/BuffCounter.java
similarity index 66%
rename from runelite-client/src/main/java/net/runelite/client/plugins/timers/IndicatorIndicator.java
rename to runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/BuffCounter.java
index 03990ce5e..9204f4d0a 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/IndicatorIndicator.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/BuffCounter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018, Tyler <https://github.com/tylerthardy>
+ * Copyright (c) 2024, YvesW <https://github.com/YvesW>
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -22,35 +22,38 @@
  * (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.timers;
+package net.runelite.client.plugins.timersandbuffs;
 
 import java.awt.Color;
+import lombok.AccessLevel;
 import lombok.Getter;
-import net.runelite.client.plugins.Plugin;
-import net.runelite.client.ui.overlay.infobox.InfoBox;
-import net.runelite.client.ui.overlay.infobox.InfoBoxPriority;
+import net.runelite.client.ui.overlay.infobox.Counter;
 
-public class IndicatorIndicator extends InfoBox
+@Getter(AccessLevel.PACKAGE)
+class BuffCounter extends Counter
 {
-	@Getter
-	private final GameIndicator indicator;
+	private final TimersAndBuffsPlugin plugin;
+	private final GameCounter gameCounter;
 
-	IndicatorIndicator(GameIndicator indicator, Plugin plugin)
+	BuffCounter(
+		TimersAndBuffsPlugin plugin,
+		GameCounter gameCounter,
+		int count)
 	{
-		super(null, plugin);
-		this.indicator = indicator;
-		setPriority(InfoBoxPriority.MED);
+		super(null, plugin, count);
+		this.plugin = plugin;
+		this.gameCounter = gameCounter;
 	}
 
 	@Override
 	public String getText()
 	{
-		return indicator.getText();
+		return gameCounter.isShouldDisplayCount() ? Integer.toString(getCount()) : "";
 	}
 
 	@Override
 	public Color getTextColor()
 	{
-		return indicator.getTextColor();
+		return gameCounter.getColorBoundaryType().shouldRecolor(getCount(), gameCounter.getBoundary()) ? gameCounter.getColor() : Color.WHITE;
 	}
-}
+}
\ No newline at end of file
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/ElapsedTimer.java b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/ElapsedTimer.java
similarity index 94%
rename from runelite-client/src/main/java/net/runelite/client/plugins/timers/ElapsedTimer.java
rename to runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/ElapsedTimer.java
index b9571ae11..0482e9d61 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/ElapsedTimer.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/ElapsedTimer.java
@@ -22,7 +22,7 @@
  * (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.timers;
+package net.runelite.client.plugins.timersandbuffs;
 
 import lombok.Getter;
 import net.runelite.client.ui.overlay.infobox.InfoBox;
@@ -39,7 +39,7 @@ class ElapsedTimer extends InfoBox
 	private final Instant lastTime;
 
 	// Creates a timer that counts up if lastTime is null, or a paused timer if lastTime is defined
-	ElapsedTimer(BufferedImage image, TimersPlugin plugin, Instant startTime, Instant lastTime)
+	ElapsedTimer(BufferedImage image, TimersAndBuffsPlugin plugin, Instant startTime, Instant lastTime)
 	{
 		super(image, plugin);
 		this.startTime = startTime;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/GameCounter.java b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/GameCounter.java
new file mode 100644
index 000000000..f49302607
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/GameCounter.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2024, YvesW <https://github.com/YvesW>
+ * 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.timersandbuffs;
+
+import java.awt.Color;
+import java.util.function.BiPredicate;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import net.runelite.api.ItemID;
+import net.runelite.api.SpriteID;
+
+@Getter(AccessLevel.PACKAGE)
+@AllArgsConstructor
+enum GameCounter
+{
+	COLOSSEUM_DOOM(SpriteID.COLOSSEUM_DOOM, GameTimerImageType.SPRITE, "Doom"),
+	CURSE_OF_THE_MOONS_BLUE(ItemID.BLUE_MOON_HELM, GameTimerImageType.ITEM, "Curse of the Moons (Blue Moon)", ColorBoundaryType.GREATER_THAN_EQUAL_TO, 18, Color.RED),
+	CURSE_OF_THE_MOONS_ECLIPSE(ItemID.ECLIPSE_MOON_HELM, GameTimerImageType.ITEM, "Curse of the Moons (Eclipse Moon)"),
+	VENGEANCE_ACTIVE(SpriteID.SPELL_VENGEANCE_OTHER, GameTimerImageType.SPRITE, "Vengeance active", false),
+	;
+
+	private final int imageId;
+	private final GameTimerImageType imageType;
+	private final String description;
+	private final ColorBoundaryType colorBoundaryType;
+	private final int boundary;
+	private final Color color;
+	private final boolean shouldDisplayCount;
+
+	GameCounter(int imageId, GameTimerImageType idType, String description, ColorBoundaryType colorBoundaryType, int boundary, Color color)
+	{
+		this(imageId, idType, description, colorBoundaryType, boundary, color, true);
+	}
+
+	GameCounter(int imageId, GameTimerImageType idType, String description, boolean shouldDisplayCount)
+	{
+		this(imageId, idType, description, ColorBoundaryType.NO_BOUNDARY, 0, Color.WHITE, shouldDisplayCount);
+	}
+
+	GameCounter(int imageId, GameTimerImageType idType, String description)
+	{
+		this(imageId, idType, description, ColorBoundaryType.NO_BOUNDARY, 0, Color.WHITE, true);
+	}
+
+	@AllArgsConstructor
+	enum ColorBoundaryType
+	{
+		GREATER_THAN_EQUAL_TO((count, boundary) -> count >= boundary),
+		LESS_THAN_EQUAL_TO((count, boundary) -> count <= boundary),
+		NO_BOUNDARY((count, boundary) -> false);
+
+		final BiPredicate<Integer, Integer> shouldRecolorPredicate;
+
+		boolean shouldRecolor(int count, int boundary)
+		{
+			return shouldRecolorPredicate.test(count, boundary);
+		}
+	}
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameTimer.java b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/GameTimer.java
similarity index 98%
rename from runelite-client/src/main/java/net/runelite/client/plugins/timers/GameTimer.java
rename to runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/GameTimer.java
index 02e9c5a47..377e02ec8 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameTimer.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/GameTimer.java
@@ -24,7 +24,7 @@
  * (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.timers;
+package net.runelite.client.plugins.timersandbuffs;
 
 import java.time.Duration;
 import java.time.temporal.ChronoUnit;
@@ -74,6 +74,7 @@ enum GameTimer
 	DIVINE_MAGIC(ItemID.DIVINE_MAGIC_POTION4, GameTimerImageType.ITEM, "Divine Magic", false),
 	DIVINE_BASTION(ItemID.DIVINE_BASTION_POTION4, GameTimerImageType.ITEM, "Divine Bastion", false),
 	DIVINE_BATTLEMAGE(ItemID.DIVINE_BATTLEMAGE_POTION4, GameTimerImageType.ITEM, "Divine Battlemage", false),
+	MOONLIGHT_POTION(ItemID.MOONLIGHT_POTION4, GameTimerImageType.ITEM, "Moonlight potion", false),
 	ANTIPOISON(ItemID.ANTIPOISON4, GameTimerImageType.ITEM, "Antipoison", false),
 	ANTIVENOM(ItemID.ANTIVENOM4, GameTimerImageType.ITEM, "Anti-venom", false),
 	TELEBLOCK(SpriteID.SPELL_TELE_BLOCK, GameTimerImageType.SPRITE, "Teleblock", false),
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameTimerImageType.java b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/GameTimerImageType.java
similarity index 96%
rename from runelite-client/src/main/java/net/runelite/client/plugins/timers/GameTimerImageType.java
rename to runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/GameTimerImageType.java
index 1d51d05e3..9c5c3766c 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/GameTimerImageType.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/GameTimerImageType.java
@@ -22,7 +22,7 @@
  * (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.timers;
+package net.runelite.client.plugins.timersandbuffs;
 
 enum GameTimerImageType
 {
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimerTimer.java b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimerTimer.java
similarity index 97%
rename from runelite-client/src/main/java/net/runelite/client/plugins/timers/TimerTimer.java
rename to runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimerTimer.java
index 8dfb1ce36..bc41a6d0d 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimerTimer.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimerTimer.java
@@ -22,7 +22,7 @@
  * (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.timers;
+package net.runelite.client.plugins.timersandbuffs;
 
 import java.time.Duration;
 import java.time.temporal.ChronoUnit;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsConfig.java
similarity index 72%
rename from runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersConfig.java
rename to runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsConfig.java
index daf383090..2502e5891 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsConfig.java
@@ -22,22 +22,52 @@
  * (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.timers;
+package net.runelite.client.plugins.timersandbuffs;
 
 import net.runelite.client.config.Config;
 import net.runelite.client.config.ConfigGroup;
 import net.runelite.client.config.ConfigItem;
+import net.runelite.client.config.ConfigSection;
 import java.time.Instant;
 
-@ConfigGroup(TimersConfig.GROUP)
-public interface TimersConfig extends Config
+@ConfigGroup(TimersAndBuffsConfig.GROUP)
+public interface TimersAndBuffsConfig extends Config
 {
 	String GROUP = "timers";
 
+	@ConfigSection(
+		name = "Bosses",
+		description = "Timers and buffs related to bosses",
+		position = 0
+	)
+	String bossesSection = "bossesSection";
+
+	@ConfigSection(
+		name = "Potions & Consumables",
+		description = "Timers and buffs related to potions/consumables/boosts",
+		position = 1
+	)
+	String consumablesSection = "consumablesSection";
+
+	@ConfigSection(
+		name = "Spells",
+		description = "Timers and buffs related to spells you cast",
+		position = 2
+	)
+	String spellsSection = "spellsSection";
+
+	@ConfigSection(
+		name = "Miscellaneous",
+		description = "Timers and buffs related to miscellaneous items or activities",
+		position = 3
+	)
+	String miscellaneousSection = "miscellaneousSection";
+
 	@ConfigItem(
 		keyName = "showHomeMinigameTeleports",
 		name = "Teleport cooldown timers",
-		description = "Configures whether timers for home and minigame teleport cooldowns are displayed"
+		description = "Configures whether timers for home and minigame teleport cooldowns are displayed",
+		section = miscellaneousSection
 	)
 	default boolean showHomeMinigameTeleports()
 	{
@@ -47,7 +77,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showAntipoison",
 		name = "Antipoison/Venom timers",
-		description = "Configures whether timers for poison and venom protection are displayed"
+		description = "Configures whether timers for poison and venom protection are displayed",
+		section = consumablesSection
 	)
 	default boolean showAntiPoison()
 	{
@@ -57,7 +88,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showAntiFire",
 		name = "Antifire timer",
-		description = "Configures whether antifire timer is displayed"
+		description = "Configures whether antifire timer is displayed",
+		section = consumablesSection
 	)
 	default boolean showAntiFire()
 	{
@@ -67,7 +99,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showStamina",
 		name = "Stamina timer",
-		description = "Configures whether stamina timer is displayed"
+		description = "Configures whether stamina timer is displayed",
+		section = consumablesSection
 	)
 	default boolean showStamina()
 	{
@@ -77,7 +110,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showOverload",
 		name = "Overload timer",
-		description = "Configures whether overload timer is displayed"
+		description = "Configures whether overload timer is displayed",
+		section = consumablesSection
 	)
 	default boolean showOverload()
 	{
@@ -87,7 +121,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showLiquidAdrenaline",
 		name = "Liquid adrenaline timer",
-		description = "Configures whether liquid adrenaline timer is displayed"
+		description = "Configures whether liquid adrenaline timer is displayed",
+		section = consumablesSection
 	)
 	default boolean showLiquidAdrenaline()
 	{
@@ -97,7 +132,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showMenaphiteRemedy",
 		name = "Menaphite remedy timer",
-		description = "Configures whether Menaphite remedy timer is displayed"
+		description = "Configures whether Menaphite remedy timer is displayed",
+		section = consumablesSection
 	)
 	default boolean showMenaphiteRemedy()
 	{
@@ -107,7 +143,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showSilkDressing",
 		name = "Silk dressing timer",
-		description = "Configures whether silk dressing timer is displayed"
+		description = "Configures whether silk dressing timer is displayed",
+		section = consumablesSection
 	)
 	default boolean showSilkDressing()
 	{
@@ -117,7 +154,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showBlessedCrystalScarab",
 		name = "Blessed crystal scarab timer",
-		description = "Configures whether blessed crystal scarab timer is displayed"
+		description = "Configures whether blessed crystal scarab timer is displayed",
+		section = consumablesSection
 	)
 	default boolean showBlessedCrystalScarab()
 	{
@@ -127,7 +165,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showPrayerEnhance",
 		name = "Prayer enhance timer",
-		description = "Configures whether prayer enhance timer is displayed"
+		description = "Configures whether prayer enhance timer is displayed",
+		section = consumablesSection
 	)
 	default boolean showPrayerEnhance()
 	{
@@ -137,7 +176,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showDivine",
 		name = "Divine potion timer",
-		description = "Configures whether divine potion timer is displayed"
+		description = "Configures whether divine potion timer is displayed",
+		section = consumablesSection
 	)
 	default boolean showDivine()
 	{
@@ -147,7 +187,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showCannon",
 		name = "Cannon timer",
-		description = "Configures whether cannon timer is displayed"
+		description = "Configures whether cannon timer is displayed",
+		section = miscellaneousSection
 	)
 	default boolean showCannon()
 	{
@@ -157,7 +198,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showMagicImbue",
 		name = "Magic imbue timer",
-		description = "Configures whether magic imbue timer is displayed"
+		description = "Configures whether magic imbue timer is displayed",
+		section = spellsSection
 	)
 	default boolean showMagicImbue()
 	{
@@ -167,7 +209,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showCharge",
 		name = "Charge timer",
-		description = "Configures whether to show a timer for the Charge spell"
+		description = "Configures whether to show a timer for the Charge spell",
+		section = spellsSection
 	)
 	default boolean showCharge()
 	{
@@ -177,7 +220,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showImbuedHeart",
 		name = "Imbued heart timer",
-		description = "Configures whether imbued heart timer is displayed"
+		description = "Configures whether imbued heart timer is displayed",
+		section = consumablesSection
 	)
 	default boolean showImbuedHeart()
 	{
@@ -187,7 +231,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showVengeance",
 		name = "Vengeance timer",
-		description = "Configures whether vengeance and vengeance other timer is displayed"
+		description = "Configures whether vengeance and vengeance other timer is displayed",
+		section = spellsSection
 	)
 	default boolean showVengeance()
 	{
@@ -197,7 +242,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showVengeanceActive",
 		name = "Vengeance active",
-		description = "Configures whether an indicator for vengeance being active is displayed"
+		description = "Configures whether an indicator for vengeance being active is displayed",
+		section = spellsSection
 	)
 	default boolean showVengeanceActive()
 	{
@@ -207,7 +253,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showHealGroup",
 		name = "Heal Group timer",
-		description = "Configures whether heal group timer is displayed"
+		description = "Configures whether heal group timer is displayed",
+		section = spellsSection
 	)
 	default boolean showHealGroup()
 	{
@@ -217,7 +264,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showTeleblock",
 		name = "Teleblock timer",
-		description = "Configures whether teleblock timer is displayed"
+		description = "Configures whether teleblock timer is displayed",
+		section = miscellaneousSection
 	)
 	default boolean showTeleblock()
 	{
@@ -227,7 +275,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showFreezes",
 		name = "Freeze timer",
-		description = "Configures whether freeze timer is displayed"
+		description = "Configures whether freeze timer is displayed",
+		section = miscellaneousSection
 	)
 	default boolean showFreezes()
 	{
@@ -237,7 +286,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showGodWarsAltar",
 		name = "God wars altar timer",
-		description = "Configures whether god wars altar timer is displayed"
+		description = "Configures whether god wars altar timer is displayed",
+		section = bossesSection
 	)
 	default boolean showGodWarsAltar()
 	{
@@ -247,7 +297,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showTzhaarTimers",
 		name = "Fight Caves and Inferno timers",
-		description = "Display elapsed time in the Fight Caves and Inferno"
+		description = "Display elapsed time in the Fight Caves and Inferno",
+		section = bossesSection
 	)
 	default boolean showTzhaarTimers()
 	{
@@ -287,7 +338,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showStaffOfTheDead",
 		name = "Staff of the Dead timer",
-		description = "Configures whether staff of the dead timer is displayed"
+		description = "Configures whether staff of the dead timer is displayed",
+		section = miscellaneousSection
 	)
 	default boolean showStaffOfTheDead()
 	{
@@ -297,7 +349,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showAbyssalSireStun",
 		name = "Abyssal Sire stun timer",
-		description = "Configures whether Abyssal Sire stun timer is displayed"
+		description = "Configures whether Abyssal Sire stun timer is displayed",
+		section = bossesSection
 	)
 	default boolean showAbyssalSireStun()
 	{
@@ -307,7 +360,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showDfsSpecial",
 		name = "Dragonfire Shield special timer",
-		description = "Configures whether the special attack cooldown timer for the Dragonfire Shield is displayed"
+		description = "Configures whether the special attack cooldown timer for the Dragonfire Shield is displayed",
+		section = miscellaneousSection
 	)
 	default boolean showDFSSpecial()
 	{
@@ -317,7 +371,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showArceuus",
 		name = "Arceuus spells duration",
-		description = "Whether to show Arceuus spellbook spell timers"
+		description = "Whether to show Arceuus spellbook spell timers",
+		section = spellsSection
 	)
 	default boolean showArceuus()
 	{
@@ -327,7 +382,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showArceuusCooldown",
 		name = "Arceuus spells cooldown",
-		description = "Whether to show cooldown timers for Arceuus spellbook spells"
+		description = "Whether to show cooldown timers for Arceuus spellbook spells",
+		section = spellsSection
 	)
 	default boolean showArceuusCooldown()
 	{
@@ -337,7 +393,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showPickpocketStun",
 		name = "Pickpocket stun timer",
-		description = "Configures whether pickpocket stun timer is displayed"
+		description = "Configures whether pickpocket stun timer is displayed",
+		section = miscellaneousSection
 	)
 	default boolean showPickpocketStun()
 	{
@@ -347,7 +404,8 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showFarmersAffinity",
 		name = "Farmer's Affinity",
-		description = "Configures whether Farmer's Affinity (Puro-Puro) timer is displayed"
+		description = "Configures whether Farmer's Affinity (Puro-Puro) timer is displayed",
+		section = miscellaneousSection
 	)
 	default boolean showFarmersAffinity()
 	{
@@ -357,10 +415,44 @@ public interface TimersConfig extends Config
 	@ConfigItem(
 		keyName = "showSpellbookSwap",
 		name = "Spellbook Swap timer",
-		description = "Configures whether Spellbook Swap timer is displayed"
+		description = "Configures whether Spellbook Swap timer is displayed",
+		section = spellsSection
 	)
 	default boolean showSpellbookSwap()
 	{
 		return true;
 	}
+
+	@ConfigItem(
+		keyName = "showCurseOfTheMoons",
+		name = "Curse of the Moons buff",
+		description = "Configures whether Curse of the Moons buff is displayed",
+		section = bossesSection
+	)
+	default boolean showCurseOfTheMoons()
+	{
+		return true;
+	}
+
+	@ConfigItem(
+		keyName = "showColosseumDoom",
+		name = "Colosseum Doom buff",
+		description = "Configures whether Fortis Colosseum Doom buff is displayed",
+		section = bossesSection
+	)
+	default boolean showColosseumDoom()
+	{
+		return true;
+	}
+
+	@ConfigItem(
+		keyName = "showMoonlightPotion",
+		name = "Moonlight potion timer",
+		description = "Configures whether Moonlight potion timer is displayed",
+		section = consumablesSection
+	)
+	default boolean showMoonlightPotion()
+	{
+		return true;
+	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsPlugin.java
similarity index 89%
rename from runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java
rename to runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsPlugin.java
index 09cbc7cde..4ce09737d 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/timers/TimersPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsPlugin.java
@@ -24,7 +24,7 @@
  * (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.timers;
+package net.runelite.client.plugins.timersandbuffs;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
@@ -76,19 +76,20 @@ import net.runelite.client.game.ItemVariationMapping;
 import net.runelite.client.game.SpriteManager;
 import net.runelite.client.plugins.Plugin;
 import net.runelite.client.plugins.PluginDescriptor;
-import static net.runelite.client.plugins.timers.GameIndicator.VENGEANCE_ACTIVE;
-import static net.runelite.client.plugins.timers.GameTimer.*;
+import static net.runelite.client.plugins.timersandbuffs.GameCounter.*;
+import static net.runelite.client.plugins.timersandbuffs.GameTimer.*;
 import net.runelite.client.ui.overlay.infobox.InfoBoxManager;
 import net.runelite.client.util.RSTimeUnit;
 import org.apache.commons.lang3.ArrayUtils;
 
 @PluginDescriptor(
-	name = "Timers",
-	description = "Show various timers in an infobox",
-	tags = {"combat", "items", "magic", "potions", "prayer", "overlay", "abyssal", "sire", "inferno", "fight", "caves", "cape", "timer", "tzhaar", "thieving", "pickpocket", "hunter", "impling", "puro"}
+	name = "Timers & Buffs",
+	configName = "TimersPlugin",
+	description = "Show various timers and buffs in an infobox",
+	tags = {"combat", "items", "magic", "potions", "prayer", "overlay", "abyssal", "sire", "inferno", "fight", "caves", "cape", "timer", "tzhaar", "thieving", "pickpocket", "hunter", "impling", "puro", "buff"}
 )
 @Slf4j
-public class TimersPlugin extends Plugin
+public class TimersAndBuffsPlugin extends Plugin
 {
 	private static final String ABYSSAL_SIRE_STUN_MESSAGE = "The Sire has been disorientated temporarily.";
 	private static final String CANNON_BASE_MESSAGE = "You place the cannon base on the ground.";
@@ -144,6 +145,9 @@ public class TimersPlugin extends Plugin
 	private WorldPoint lastPoint;
 	private ElapsedTimer tzhaarTimer;
 
+	private final Map<GameCounter, BuffCounter> varCounters = new EnumMap<>(GameCounter.class);
+	private static final int ECLIPSE_MOON_REGION_ID = 6038;
+
 	@Inject
 	private ItemManager itemManager;
 
@@ -154,15 +158,15 @@ public class TimersPlugin extends Plugin
 	private Client client;
 
 	@Inject
-	private TimersConfig config;
+	private TimersAndBuffsConfig config;
 
 	@Inject
 	private InfoBoxManager infoBoxManager;
 
 	@Provides
-	TimersConfig getConfig(ConfigManager configManager)
+	TimersAndBuffsConfig getConfig(ConfigManager configManager)
 	{
-		return configManager.getConfig(TimersConfig.class);
+		return configManager.getConfig(TimersAndBuffsConfig.class);
 	}
 
 	@Override
@@ -186,6 +190,8 @@ public class TimersPlugin extends Plugin
 		nextSuperAntifireTick = 0;
 		removeTzhaarTimer();
 		varTimers.clear();
+		infoBoxManager.removeIf(buffCounter -> buffCounter instanceof BuffCounter);
+		varCounters.clear();
 	}
 
 	@Subscribe
@@ -295,14 +301,7 @@ public class TimersPlugin extends Plugin
 
 		if (event.getVarbitId() == Varbits.VENGEANCE_ACTIVE && config.showVengeanceActive())
 		{
-			if (event.getValue() == 1)
-			{
-				createGameIndicator(VENGEANCE_ACTIVE);
-			}
-			else
-			{
-				removeGameIndicator(VENGEANCE_ACTIVE);
-			}
+			updateVarCounter(VENGEANCE_ACTIVE, event.getValue());
 		}
 
 		if (event.getVarbitId() == Varbits.DEATH_CHARGE && config.showArceuus())
@@ -477,11 +476,20 @@ public class TimersPlugin extends Plugin
 		{
 			if (client.getVarbitValue(Varbits.DIVINE_SUPER_COMBAT) > event.getValue()
 				|| client.getVarbitValue(Varbits.DIVINE_BASTION) > event.getValue()
-				|| client.getVarbitValue(Varbits.DIVINE_BATTLEMAGE) > event.getValue())
+				|| client.getVarbitValue(Varbits.DIVINE_BATTLEMAGE) > event.getValue()
+				// When drinking a dose of moonlight potion while already under its effects, desync between
+				// Varbits.MOONLIGHT_POTION and Varbits.DIVINE_SUPER_DEFENCE can occur, with the latter being 1 tick
+				// greater
+				|| client.getVarbitValue(Varbits.MOONLIGHT_POTION) >= event.getValue())
 			{
 				return;
 			}
 
+			if (client.getVarbitValue(Varbits.MOONLIGHT_POTION) < event.getValue())
+			{
+				removeVarTimer(MOONLIGHT_POTION);
+			}
+
 			updateVarTimer(DIVINE_SUPER_DEFENCE, event.getValue(), IntUnaryOperator.identity());
 		}
 
@@ -575,12 +583,43 @@ public class TimersPlugin extends Plugin
 		{
 			updateVarTimer(GOD_WARS_ALTAR, event.getValue(), i -> i * 100);
 		}
+
+		if (event.getVarbitId() == Varbits.CURSE_OF_THE_MOONS && config.showCurseOfTheMoons())
+		{
+			final int regionID = WorldPoint.fromLocal(client, client.getLocalPlayer().getLocalLocation()).getRegionID();
+			if (regionID == ECLIPSE_MOON_REGION_ID)
+			{
+				updateVarCounter(CURSE_OF_THE_MOONS_ECLIPSE, event.getValue());
+			}
+			else
+			{
+				updateVarCounter(CURSE_OF_THE_MOONS_BLUE, event.getValue());
+			}
+		}
+
+		if (event.getVarbitId() == Varbits.COLOSSEUM_DOOM && config.showColosseumDoom())
+		{
+			updateVarCounter(COLOSSEUM_DOOM, event.getValue());
+		}
+
+		if (event.getVarbitId() == Varbits.MOONLIGHT_POTION && config.showMoonlightPotion())
+		{
+			int moonlightValue = event.getValue();
+			// Increase the timer by 1 tick in case of desync due to drinking a dose of moonlight potion while already
+			// under its effects. Otherwise, the timer would be 1 tick shorter than it is meant to be.
+			if (client.getVarbitValue(Varbits.DIVINE_SUPER_DEFENCE) == moonlightValue + 1)
+			{
+				moonlightValue++;
+			}
+
+			updateVarTimer(MOONLIGHT_POTION, moonlightValue, IntUnaryOperator.identity());
+		}
 	}
 
 	@Subscribe
 	public void onConfigChanged(ConfigChanged event)
 	{
-		if (!event.getGroup().equals(TimersConfig.GROUP))
+		if (!event.getGroup().equals(TimersAndBuffsConfig.GROUP))
 		{
 			return;
 		}
@@ -674,7 +713,7 @@ public class TimersPlugin extends Plugin
 
 		if (!config.showVengeanceActive())
 		{
-			removeGameIndicator(VENGEANCE_ACTIVE);
+			removeVarCounter(VENGEANCE_ACTIVE);
 		}
 
 		if (!config.showTeleblock())
@@ -771,6 +810,22 @@ public class TimersPlugin extends Plugin
 		{
 			removeGameTimer(SPELLBOOK_SWAP);
 		}
+
+		if (!config.showCurseOfTheMoons())
+		{
+			removeVarCounter(CURSE_OF_THE_MOONS_BLUE);
+			removeVarCounter(CURSE_OF_THE_MOONS_ECLIPSE);
+		}
+
+		if (!config.showColosseumDoom())
+		{
+			removeVarCounter(COLOSSEUM_DOOM);
+		}
+
+		if (!config.showMoonlightPotion())
+		{
+			removeVarTimer(MOONLIGHT_POTION);
+		}
 	}
 
 	@Subscribe
@@ -1059,6 +1114,8 @@ public class TimersPlugin extends Plugin
 					config.tzhaarStartTime(null);
 					config.tzhaarLastTime(null);
 				}
+				// Varbits.COLOSSEUM_DOOM is not set to 0 when teleporting out of the Colosseum.
+				removeVarCounter(COLOSSEUM_DOOM);
 				break;
 			case LOGIN_SCREEN:
 				// fall through
@@ -1076,7 +1133,6 @@ public class TimersPlugin extends Plugin
 		}
 	}
 
-
 	@Subscribe
 	public void onGraphicChanged(GraphicChanged event)
 	{
@@ -1142,7 +1198,7 @@ public class TimersPlugin extends Plugin
 		ItemContainer container = itemContainerChanged.getItemContainer();
 
 		Item weapon = container.getItem(EquipmentInventorySlot.WEAPON.getSlotIdx());
-		if (weapon == null || STAVES_OF_THE_DEAD.contains(weapon.getId()))
+		if (weapon == null || !STAVES_OF_THE_DEAD.contains(weapon.getId()))
 		{
 			// remove sotd timer if the staff has been unwielded
 			removeGameTimer(STAFF_OF_THE_DEAD);
@@ -1211,31 +1267,6 @@ public class TimersPlugin extends Plugin
 		infoBoxManager.removeIf(t -> t instanceof TimerTimer && ((TimerTimer) t).getTimer() == timer);
 	}
 
-	private IndicatorIndicator createGameIndicator(GameIndicator gameIndicator)
-	{
-		removeGameIndicator(gameIndicator);
-
-		IndicatorIndicator indicator = new IndicatorIndicator(gameIndicator, this);
-		switch (gameIndicator.getImageType())
-		{
-			case SPRITE:
-				spriteManager.getSpriteAsync(gameIndicator.getImageId(), 0, indicator);
-				break;
-			case ITEM:
-				indicator.setImage(itemManager.getImage(gameIndicator.getImageId()));
-				break;
-		}
-		indicator.setTooltip(gameIndicator.getDescription());
-		infoBoxManager.addInfoBox(indicator);
-
-		return indicator;
-	}
-
-	private void removeGameIndicator(GameIndicator indicator)
-	{
-		infoBoxManager.removeIf(t -> t instanceof IndicatorIndicator && ((IndicatorIndicator) t).getIndicator() == indicator);
-	}
-
 	private void updateVarTimer(final GameTimer gameTimer, final int varValue, final IntUnaryOperator tickDuration)
 	{
 		updateVarTimer(gameTimer, varValue, i -> i == 0, tickDuration);
@@ -1270,4 +1301,53 @@ public class TimersPlugin extends Plugin
 		removeGameTimer(gameTimer);
 		varTimers.remove(gameTimer);
 	}
+
+	private void updateVarCounter(final GameCounter gameCounter, final int varValue)
+	{
+		BuffCounter buffCounter = varCounters.get(gameCounter);
+
+		if (varValue == 0)
+		{
+			removeVarCounter(gameCounter);
+		}
+		else if (buffCounter == null)
+		{
+			buffCounter = createBuffCounter(gameCounter, varValue);
+			varCounters.put(gameCounter, buffCounter);
+		}
+		else
+		{
+			buffCounter.setCount(varValue);
+		}
+	}
+
+	private BuffCounter createBuffCounter(GameCounter gameCounter, int count)
+	{
+		removeBuffCounter(gameCounter);
+
+		BuffCounter buffCounter = new BuffCounter(this, gameCounter, count);
+		switch (gameCounter.getImageType())
+		{
+			case SPRITE:
+				spriteManager.getSpriteAsync(gameCounter.getImageId(), 0, buffCounter);
+				break;
+			case ITEM:
+				buffCounter.setImage(itemManager.getImage(gameCounter.getImageId()));
+				break;
+		}
+		buffCounter.setTooltip(gameCounter.getDescription());
+		infoBoxManager.addInfoBox(buffCounter);
+		return buffCounter;
+	}
+
+	private void removeVarCounter(final GameCounter gameCounter)
+	{
+		removeBuffCounter(gameCounter);
+		varCounters.remove(gameCounter);
+	}
+
+	private void removeBuffCounter(GameCounter gameCounter)
+	{
+		infoBoxManager.removeIf(b -> b instanceof BuffCounter && ((BuffCounter) b).getGameCounter() == gameCounter);
+	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/tithefarm/TitheFarmPlantOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/tithefarm/TitheFarmPlantOverlay.java
index 7394fed37..91a0143ad 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/tithefarm/TitheFarmPlantOverlay.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/tithefarm/TitheFarmPlantOverlay.java
@@ -75,11 +75,6 @@ public class TitheFarmPlantOverlay extends Overlay
 		final Color colorWatered = ColorUtil.colorWithAlpha(colorWateredBorder, (int) (colorWateredBorder.getAlpha() / 2.5));
 		borders.put(TitheFarmPlantState.WATERED, colorWateredBorder);
 		fills.put(TitheFarmPlantState.WATERED, colorWatered);
-
-		final Color colorGrownBorder = config.getColorGrown();
-		final Color colorGrown = ColorUtil.colorWithAlpha(colorGrownBorder, (int) (colorGrownBorder.getAlpha() / 2.5));
-		borders.put(TitheFarmPlantState.GROWN, colorGrownBorder);
-		fills.put(TitheFarmPlantState.GROWN, colorGrown);
 	}
 
 	@Override
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/tithefarm/TitheFarmPluginConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/tithefarm/TitheFarmPluginConfig.java
index f9ee9a26d..bbf591461 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/tithefarm/TitheFarmPluginConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/tithefarm/TitheFarmPluginConfig.java
@@ -56,16 +56,4 @@ public interface TitheFarmPluginConfig extends Config
 	{
 		return new Color(0, 153, 255);
 	}
-
-	@Alpha
-	@ConfigItem(
-		position = 3,
-		keyName = "hexColorGrown",
-		name = "Grown plant",
-		description = "Color of grown plant timer"
-	)
-	default Color getColorGrown()
-	{
-		return new Color(0, 217, 0);
-	}
 }
diff --git a/runelite-client/src/main/resources/item_variations.json b/runelite-client/src/main/resources/item_variations.json
index d3b7fe12f..1a61f23df 100644
--- a/runelite-client/src/main/resources/item_variations.json
+++ b/runelite-client/src/main/resources/item_variations.json
@@ -9078,6 +9078,10 @@
     20716,
     27358
   ],
+  "bruma torch": [
+    20720,
+    29777
+  ],
   "manor key": [
     20766,
     21052
@@ -10451,6 +10455,10 @@
     24824,
     24840
   ],
+  "amys saw": [
+    24880,
+    29774
+  ],
   "plank sack": [
     24882,
     25629
@@ -10702,7 +10710,8 @@
   ],
   "imcando hammer": [
     25633,
-    25644
+    25644,
+    29775
   ],
   "simple lockbox": [
     25646,
@@ -12091,5 +12100,13 @@
   "sigil of arcane swiftness": [
     29678,
     29679
+  ],
+  "dnitorso darkbuttons": [
+    29688,
+    29690
+  ],
+  "dnitorso rippedweskit": [
+    29700,
+    29702
   ]
 }
\ No newline at end of file
diff --git a/runelite-jshell/pom.xml b/runelite-jshell/pom.xml
index ea8165545..1665933b3 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.36-SNAPSHOT</version>
+        <version>1.10.37-SNAPSHOT</version>
     </parent>
 
     <artifactId>jshell</artifactId>
diff --git a/runelite-maven-plugin/pom.xml b/runelite-maven-plugin/pom.xml
index c6dae7644..bb95b465d 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.36-SNAPSHOT</version>
+		<version>1.10.37-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-maven-plugin</artifactId>


Test patch:
diff --git a/runelite-client/src/test/java/net/runelite/client/menus/TestMenuEntry.java b/runelite-client/src/test/java/net/runelite/client/menus/TestMenuEntry.java
index 55b11076f..01f182d33 100644
--- a/runelite-client/src/test/java/net/runelite/client/menus/TestMenuEntry.java
+++ b/runelite-client/src/test/java/net/runelite/client/menus/TestMenuEntry.java
@@ -30,6 +30,7 @@ import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
 import net.runelite.api.Actor;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.NPC;
@@ -190,13 +191,7 @@ public class TestMenuEntry implements MenuEntry
 	}
 
 	@Override
-	public MenuEntry setParent(MenuEntry parent)
-	{
-		return this;
-	}
-
-	@Override
-	public MenuEntry getParent()
+	public Consumer<MenuEntry> onClick()
 	{
 		return null;
 	}
@@ -253,4 +248,22 @@ public class TestMenuEntry implements MenuEntry
 	{
 		return actor;
 	}
+
+	@Override
+	public Menu getSubMenu()
+	{
+		return null;
+	}
+
+	@Override
+	public Menu createSubMenu()
+	{
+		return null;
+	}
+
+	@Override
+	public void deleteSubMenu()
+	{
+
+	}
 }
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 c4082ac28..4697843f5 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
@@ -1275,19 +1275,21 @@ public class ChatCommandsPluginTest
 	@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);
+		testHunterRumourChatMessage("You have completed <col=ff3045>77</col> rumours for the Hunter Guild.", 77);
+		// single kc has no s.
+		testHunterRumourChatMessage("You have completed <col=ff3045>1</col> rumour for the Hunter Guild.", 1);
+		// opaque chatbox has different color
+		testHunterRumourChatMessage("You have completed <col=e00a19>2</col> rumours for the Hunter Guild.", 2);
+		// with comma in number
+		testHunterRumourChatMessage("You have completed <col=ff3045>1,032</col> rumours for the Hunter Guild.", 1032);
 	}
 
-	@Test
-	public void testHunterRumoursOver1k()
+	private void testHunterRumourChatMessage(String message, int kc)
 	{
-		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", "You have completed <col=ff3045>1,032</col> rumours for the Hunter Guild.", null, 0);
+		ChatMessage chatMessageEvent = new ChatMessage(null, GAMEMESSAGE, "", message, null, 0);
 		chatCommandsPlugin.onChatMessage(chatMessageEvent);
 
-		verify(configManager).setRSProfileConfiguration("killcount", "hunter rumours", 1032);
+		verify(configManager).setRSProfileConfiguration("killcount", "hunter rumours", kc);
 	}
 
 	@Test
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 6d2cc692c..13e440a3b 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
@@ -31,6 +31,7 @@ import com.google.inject.testing.fieldbinder.BoundFieldModule;
 import java.util.Arrays;
 import net.runelite.api.Client;
 import net.runelite.api.KeyCode;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.NPC;
@@ -85,6 +86,7 @@ public class MenuEntrySwapperPluginTest
 	@Inject
 	MenuEntrySwapperPlugin menuEntrySwapperPlugin;
 
+	private Menu menu;
 	private NPC npc;
 	private MenuEntry[] entries;
 
@@ -99,7 +101,10 @@ public class MenuEntrySwapperPluginTest
 		NPCComposition composition = mock(NPCComposition.class);
 		when(npc.getTransformedComposition()).thenReturn(composition);
 
-		when(client.getMenuEntries()).thenAnswer((Answer<MenuEntry[]>) invocationOnMock ->
+		menu = mock(Menu.class);
+		when(client.getMenu()).thenReturn(menu);
+
+		when(menu.getMenuEntries()).thenAnswer((Answer<MenuEntry[]>) invocationOnMock ->
 		{
 			// The menu implementation returns a copy of the array, which causes swap() to not
 			// modify the same array being iterated in onClientTick
@@ -110,7 +115,7 @@ public class MenuEntrySwapperPluginTest
 			Object argument = invocationOnMock.getArguments()[0];
 			entries = (MenuEntry[]) argument;
 			return null;
-		}).when(client).setMenuEntries(any(MenuEntry[].class));
+		}).when(menu).setMenuEntries(any(MenuEntry[].class));
 
 		menuEntrySwapperPlugin.startUp();
 	}
@@ -153,7 +158,7 @@ public class MenuEntrySwapperPluginTest
 		menuEntrySwapperPlugin.onPostMenuSort(new PostMenuSort());
 
 		ArgumentCaptor<MenuEntry[]> argumentCaptor = ArgumentCaptor.forClass(MenuEntry[].class);
-		verify(client).setMenuEntries(argumentCaptor.capture());
+		verify(menu).setMenuEntries(argumentCaptor.capture());
 
 		// check the assignment swap is hit first instead of trade
 		assertArrayEquals(new MenuEntry[]{
@@ -190,7 +195,7 @@ public class MenuEntrySwapperPluginTest
 		menuEntrySwapperPlugin.onPostMenuSort(new PostMenuSort());
 
 		ArgumentCaptor<MenuEntry[]> argumentCaptor = ArgumentCaptor.forClass(MenuEntry[].class);
-		verify(client, times(2)).setMenuEntries(argumentCaptor.capture());
+		verify(menu, times(2)).setMenuEntries(argumentCaptor.capture());
 
 		assertArrayEquals(new MenuEntry[]{
 			menu("Cancel", "", MenuAction.CANCEL),
@@ -228,7 +233,7 @@ public class MenuEntrySwapperPluginTest
 		menuEntrySwapperPlugin.onPostMenuSort(new PostMenuSort());
 
 		ArgumentCaptor<MenuEntry[]> argumentCaptor = ArgumentCaptor.forClass(MenuEntry[].class);
-		verify(client).setMenuEntries(argumentCaptor.capture());
+		verify(menu).setMenuEntries(argumentCaptor.capture());
 
 		assertArrayEquals(new MenuEntry[]{
 			menu("Cancel", "", MenuAction.CANCEL),
@@ -259,7 +264,7 @@ public class MenuEntrySwapperPluginTest
 		menuEntrySwapperPlugin.onPostMenuSort(new PostMenuSort());
 
 		ArgumentCaptor<MenuEntry[]> argumentCaptor = ArgumentCaptor.forClass(MenuEntry[].class);
-		verify(client).setMenuEntries(argumentCaptor.capture());
+		verify(menu).setMenuEntries(argumentCaptor.capture());
 
 		assertArrayEquals(new MenuEntry[]{
 			menu("Cancel", "", MenuAction.CANCEL),
@@ -286,7 +291,7 @@ public class MenuEntrySwapperPluginTest
 		menuEntrySwapperPlugin.onPostMenuSort(new PostMenuSort());
 
 		ArgumentCaptor<MenuEntry[]> argumentCaptor = ArgumentCaptor.forClass(MenuEntry[].class);
-		verify(client).setMenuEntries(argumentCaptor.capture());
+		verify(menu).setMenuEntries(argumentCaptor.capture());
 
 		assertArrayEquals(new MenuEntry[]{
 			menu("Cancel", "", MenuAction.CANCEL),
@@ -311,7 +316,7 @@ public class MenuEntrySwapperPluginTest
 		menuEntrySwapperPlugin.onPostMenuSort(new PostMenuSort());
 
 		ArgumentCaptor<MenuEntry[]> argumentCaptor = ArgumentCaptor.forClass(MenuEntry[].class);
-		verify(client).setMenuEntries(argumentCaptor.capture());
+		verify(menu).setMenuEntries(argumentCaptor.capture());
 
 		assertArrayEquals(new MenuEntry[]{
 			menu("Cancel", "", MenuAction.CANCEL),
@@ -339,7 +344,7 @@ public class MenuEntrySwapperPluginTest
 		menuEntrySwapperPlugin.onPostMenuSort(new PostMenuSort());
 
 		ArgumentCaptor<MenuEntry[]> argumentCaptor = ArgumentCaptor.forClass(MenuEntry[].class);
-		verify(client).setMenuEntries(argumentCaptor.capture());
+		verify(menu).setMenuEntries(argumentCaptor.capture());
 
 		assertArrayEquals(new MenuEntry[]{
 			menu("Cancel", "", MenuAction.CANCEL),
@@ -369,7 +374,7 @@ public class MenuEntrySwapperPluginTest
 		menuEntrySwapperPlugin.onPostMenuSort(new PostMenuSort());
 
 		ArgumentCaptor<MenuEntry[]> argumentCaptor = ArgumentCaptor.forClass(MenuEntry[].class);
-		verify(client).setMenuEntries(argumentCaptor.capture());
+		verify(menu).setMenuEntries(argumentCaptor.capture());
 
 		assertArrayEquals(new MenuEntry[]{
 			menu("Cancel", "", MenuAction.CANCEL),
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java
index 7f36e52e4..90c8f5ee8 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java
@@ -272,11 +272,19 @@ public class SlayerPluginTest
 	@Test
 	public void npcMatching()
 	{
+		assertTrue(matches("Abyssal Sire", Task.ABYSSAL_DEMONS));
 		assertTrue(matches("Abyssal demon", Task.ABYSSAL_DEMONS));
 		assertTrue(matches("Baby blue dragon", Task.BLUE_DRAGONS));
+		assertTrue(matches("Vorkath", Task.BLUE_DRAGONS));
+		assertTrue(matches("Brutal red dragon", Task.RED_DRAGONS));
 		assertTrue(matches("Duck", Task.BIRDS));
 		assertTrue(matches("Donny the Lad", Task.BANDITS));
 		assertTrue(matches("Revenant Imp", Task.GHOSTS));
+		assertTrue(matches("Sulphur lizard", Task.LIZARDS));
+		assertTrue(matches("Spiked Turoth", Task.TUROTH));
+		assertTrue(matches("Loar shade", Task.SHADES));
+		assertTrue(matches("Loar shadow", Task.SHADES));
+		assertTrue(matches("Urium shadow", Task.SHADES));
 
 		assertFalse(matches("Rat", Task.PIRATES));
 		assertFalse(matches("Wolf", Task.WEREWOLVES));
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/timers/ElapsedTimerTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/timersandbuffs/ElapsedTimerTest.java
similarity index 98%
rename from runelite-client/src/test/java/net/runelite/client/plugins/timers/ElapsedTimerTest.java
rename to runelite-client/src/test/java/net/runelite/client/plugins/timersandbuffs/ElapsedTimerTest.java
index 2b3c1e461..8851811e3 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/timers/ElapsedTimerTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/timersandbuffs/ElapsedTimerTest.java
@@ -22,7 +22,7 @@
  * (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.timers;
+package net.runelite.client.plugins.timersandbuffs;
 
 import java.time.Instant;
 import static org.junit.Assert.assertEquals;
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsPluginTest.java
similarity index 79%
rename from runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java
rename to runelite-client/src/test/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsPluginTest.java
index 5499078c2..0b336adff 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/timers/TimersPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/timersandbuffs/TimersAndBuffsPluginTest.java
@@ -22,7 +22,7 @@
  * (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.timers;
+package net.runelite.client.plugins.timersandbuffs;
 
 import com.google.inject.Guice;
 import com.google.inject.Inject;
@@ -66,14 +66,14 @@ import org.mockito.junit.MockitoJUnitRunner;
 import org.mockito.stubbing.Answer;
 
 @RunWith(MockitoJUnitRunner.class)
-public class TimersPluginTest
+public class TimersAndBuffsPluginTest
 {
 	@Inject
-	private TimersPlugin timersPlugin;
+	private TimersAndBuffsPlugin timersAndBuffsPlugin;
 
 	@Mock
 	@Bind
-	private TimersConfig timersConfig;
+	private TimersAndBuffsConfig timersAndBuffsConfig;
 
 	@Mock
 	@Bind
@@ -100,27 +100,27 @@ public class TimersPluginTest
 	@Test
 	public void removeTimersOnConfigChanged()
 	{
-		timersPlugin = spy(timersPlugin);
+		timersAndBuffsPlugin = spy(timersAndBuffsPlugin);
 		final ConfigChanged configChanged = new ConfigChanged();
-		configChanged.setGroup(TimersConfig.GROUP);
+		configChanged.setGroup(TimersAndBuffsConfig.GROUP);
 
-		timersPlugin.onConfigChanged(configChanged);
+		timersAndBuffsPlugin.onConfigChanged(configChanged);
 
 		for (GameTimer timer : GameTimer.values())
 		{
-			verify(timersPlugin).removeGameTimer(timer);
+			verify(timersAndBuffsPlugin).removeGameTimer(timer);
 		}
 	}
 
 	@Test
 	public void testDivineRanging()
 	{
-		when(timersConfig.showDivine()).thenReturn(true);
+		when(timersAndBuffsConfig.showDivine()).thenReturn(true);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.DIVINE_RANGING);
 		varbitChanged.setValue(500);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -131,31 +131,31 @@ public class TimersPluginTest
 	@Test
 	public void testDivineBastion()
 	{
-		when(timersConfig.showDivine()).thenReturn(true);
+		when(timersAndBuffsConfig.showDivine()).thenReturn(true);
 
 		VarbitChanged rangingVarbitChanged = new VarbitChanged();
 		rangingVarbitChanged.setVarbitId(Varbits.DIVINE_RANGING);
 		rangingVarbitChanged.setValue(500);
-		timersPlugin.onVarbitChanged(rangingVarbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(rangingVarbitChanged);
 		when(client.getVarbitValue(Varbits.DIVINE_RANGING)).thenReturn(500);
 
 		VarbitChanged superDefenceVarbitChanged = new VarbitChanged();
 		superDefenceVarbitChanged.setVarbitId(Varbits.DIVINE_SUPER_DEFENCE);
 		superDefenceVarbitChanged.setValue(500);
-		timersPlugin.onVarbitChanged(superDefenceVarbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(superDefenceVarbitChanged);
 		when(client.getVarbitValue(Varbits.DIVINE_SUPER_DEFENCE)).thenReturn(500);
 
 		VarbitChanged bastionVarbitChanged = new VarbitChanged();
 		bastionVarbitChanged.setVarbitId(Varbits.DIVINE_BASTION);
 		bastionVarbitChanged.setValue(500);
-		timersPlugin.onVarbitChanged(bastionVarbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(bastionVarbitChanged);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager, times(3)).addInfoBox(captor.capture());
 		List<InfoBox> infoBoxes = captor.getAllValues();
 
 		ArgumentCaptor<Predicate<InfoBox>> prcaptor = ArgumentCaptor.forClass(Predicate.class);
-		verify(infoBoxManager, times(5)).removeIf(prcaptor.capture());
+		verify(infoBoxManager, times(6)).removeIf(prcaptor.capture());
 		List<Predicate<InfoBox>> filters = prcaptor.getAllValues();
 
 		// test defence, ranging, and bastion infoboxes added
@@ -165,22 +165,24 @@ public class TimersPluginTest
 
 		// test ranging and defence infoboxes removed
 		assertTrue(filters.get(0).test(infoBoxes.get(0)));  // divine ranging infobox added
-		assertTrue(filters.get(1).test(infoBoxes.get(1)));  // divine super defence infobox added
-		assertTrue(filters.get(2).test(infoBoxes.get(0)));  // divine ranging infobox removed
-		assertTrue(filters.get(3).test(infoBoxes.get(1)));  // divine super defence infobox removed
-		assertTrue(filters.get(4).test(infoBoxes.get(2)));  // divine bastion infobox added
+		// filters.get(1) tries to remove a non-existent moonlight potion timer since Varbits.DIVINE_SUPER_DEFENCE
+		// was changed
+		assertTrue(filters.get(2).test(infoBoxes.get(1)));  // divine super defence infobox added
+		assertTrue(filters.get(3).test(infoBoxes.get(0)));  // divine ranging infobox removed
+		assertTrue(filters.get(4).test(infoBoxes.get(1)));  // divine super defence infobox removed
+		assertTrue(filters.get(5).test(infoBoxes.get(2)));  // divine bastion infobox added
 	}
 
 	@Test
 	public void testDivineRangingAfterBastion()
 	{
-		when(timersConfig.showDivine()).thenReturn(true);
+		when(timersAndBuffsConfig.showDivine()).thenReturn(true);
 		when(client.getVarbitValue(Varbits.DIVINE_BASTION)).thenReturn(400);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.DIVINE_RANGING);
 		varbitChanged.setValue(500);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -191,14 +193,14 @@ public class TimersPluginTest
 	@Test
 	public void testStamina()
 	{
-		when(timersConfig.showStamina()).thenReturn(true);
+		when(timersAndBuffsConfig.showStamina()).thenReturn(true);
 		when(client.getVarbitValue(Varbits.RUN_SLOWED_DEPLETION_ACTIVE)).thenReturn(1);
 		when(client.getVarbitValue(Varbits.STAMINA_EFFECT)).thenReturn(20);
 		when(client.getVarbitValue(Varbits.RING_OF_ENDURANCE_EFFECT)).thenReturn(0);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.RUN_SLOWED_DEPLETION_ACTIVE); // just has to be one of the vars
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -210,9 +212,9 @@ public class TimersPluginTest
 	@Test
 	public void testSireStunTimer()
 	{
-		when(timersConfig.showAbyssalSireStun()).thenReturn(true);
+		when(timersAndBuffsConfig.showAbyssalSireStun()).thenReturn(true);
 		ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "The Sire has been disorientated temporarily.", "", 0);
-		timersPlugin.onChatMessage(chatMessage);
+		timersAndBuffsPlugin.onChatMessage(chatMessage);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -224,14 +226,14 @@ public class TimersPluginTest
 	@Test
 	public void testEndurance()
 	{
-		when(timersConfig.showStamina()).thenReturn(true);
+		when(timersAndBuffsConfig.showStamina()).thenReturn(true);
 		when(client.getVarbitValue(Varbits.RUN_SLOWED_DEPLETION_ACTIVE)).thenReturn(1);
 		when(client.getVarbitValue(Varbits.STAMINA_EFFECT)).thenReturn(20);
 		when(client.getVarbitValue(Varbits.RING_OF_ENDURANCE_EFFECT)).thenReturn(20);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.RUN_SLOWED_DEPLETION_ACTIVE); // just has to be one of the vars
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -241,7 +243,7 @@ public class TimersPluginTest
 
 		// unwield ring
 		when(client.getVarbitValue(Varbits.RING_OF_ENDURANCE_EFFECT)).thenReturn(0);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 		int mins = (int) infoBox.getDuration().toMinutes();
 		assertEquals(2, mins);
 	}
@@ -249,8 +251,8 @@ public class TimersPluginTest
 	@Test
 	public void testTzhaarTimer()
 	{
-		when(timersConfig.showTzhaarTimers()).thenReturn(true);
-		when(client.getMapRegions()).thenReturn(new int[]{TimersPlugin.FIGHT_CAVES_REGION_ID});
+		when(timersAndBuffsConfig.showTzhaarTimers()).thenReturn(true);
+		when(client.getMapRegions()).thenReturn(new int[]{TimersAndBuffsPlugin.FIGHT_CAVES_REGION_ID});
 
 		class InstantRef
 		{
@@ -258,33 +260,33 @@ public class TimersPluginTest
 		}
 
 		InstantRef startTime = new InstantRef();
-		when(timersConfig.tzhaarStartTime()).then(a -> startTime.i);
+		when(timersAndBuffsConfig.tzhaarStartTime()).then(a -> startTime.i);
 		doAnswer((Answer<Void>) invocationOnMock ->
 		{
 			Object argument = invocationOnMock.getArguments()[0];
 			startTime.i = (Instant) argument;
 			return null;
-		}).when(timersConfig).tzhaarStartTime(nullable(Instant.class));
+		}).when(timersAndBuffsConfig).tzhaarStartTime(nullable(Instant.class));
 
 		InstantRef lastTime = new InstantRef();
-		when(timersConfig.tzhaarLastTime()).then(a -> lastTime.i);
+		when(timersAndBuffsConfig.tzhaarLastTime()).then(a -> lastTime.i);
 		doAnswer((Answer<Void>) invocationOnMock ->
 		{
 			Object argument = invocationOnMock.getArguments()[0];
 			lastTime.i = (Instant) argument;
 			return null;
-		}).when(timersConfig).tzhaarLastTime(nullable(Instant.class));
+		}).when(timersAndBuffsConfig).tzhaarLastTime(nullable(Instant.class));
 
 		// test timer creation: verify the infobox was added and that it is an ElapsedTimer
 		ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "<col=ef1020>Wave: 1</col>", "", 0);
-		timersPlugin.onChatMessage(chatMessage);
+		timersAndBuffsPlugin.onChatMessage(chatMessage);
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager, times(1)).addInfoBox(captor.capture());
 		assertTrue(captor.getValue() instanceof ElapsedTimer);
 
 		// test timer pause: verify the added ElapsedTimer has a non-null lastTime
 		chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "<col=ef1020>The Inferno has been paused. You may now log out.", "", 0);
-		timersPlugin.onChatMessage(chatMessage);
+		timersAndBuffsPlugin.onChatMessage(chatMessage);
 		verify(infoBoxManager, times(1)).removeInfoBox(captor.capture());
 		verify(infoBoxManager, times(2)).addInfoBox(captor.capture());
 		assertTrue(captor.getValue() instanceof ElapsedTimer);
@@ -294,7 +296,7 @@ public class TimersPluginTest
 
 		// test timer unpause: verify the last time is null after being unpaused
 		chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "<col=ef1020>Wave: 2</col>", "", 0);
-		timersPlugin.onChatMessage(chatMessage);
+		timersAndBuffsPlugin.onChatMessage(chatMessage);
 		verify(infoBoxManager, times(2)).removeInfoBox(captor.capture());
 		verify(infoBoxManager, times(3)).addInfoBox(captor.capture());
 		assertTrue(captor.getValue() instanceof ElapsedTimer);
@@ -306,8 +308,8 @@ public class TimersPluginTest
 		final GameStateChanged gameStateChanged = new GameStateChanged();
 		gameStateChanged.setGameState(GameState.LOADING);
 		when(client.getMapRegions()).thenReturn(new int[0]);
-		timersPlugin.onChatMessage(chatMessage);
-		timersPlugin.onGameStateChanged(gameStateChanged);
+		timersAndBuffsPlugin.onChatMessage(chatMessage);
+		timersAndBuffsPlugin.onGameStateChanged(gameStateChanged);
 		verify(infoBoxManager, times(3)).removeInfoBox(captor.capture());
 		verify(infoBoxManager, times(3)).addInfoBox(captor.capture());
 	}
@@ -315,8 +317,8 @@ public class TimersPluginTest
 	@Test
 	public void testInfernoTimerStartOffset()
 	{
-		when(timersConfig.showTzhaarTimers()).thenReturn(true);
-		when(client.getMapRegions()).thenReturn(new int[]{TimersPlugin.INFERNO_REGION_ID});
+		when(timersAndBuffsConfig.showTzhaarTimers()).thenReturn(true);
+		when(client.getMapRegions()).thenReturn(new int[]{TimersAndBuffsPlugin.INFERNO_REGION_ID});
 
 		class InstantRef
 		{
@@ -324,16 +326,16 @@ public class TimersPluginTest
 		}
 
 		InstantRef startTime = new InstantRef();
-		when(timersConfig.tzhaarStartTime()).then(a -> startTime.i);
+		when(timersAndBuffsConfig.tzhaarStartTime()).then(a -> startTime.i);
 		doAnswer((Answer<Void>) invocationOnMock ->
 		{
 			Object argument = invocationOnMock.getArguments()[0];
 			startTime.i = (Instant) argument;
 			return null;
-		}).when(timersConfig).tzhaarStartTime(nullable(Instant.class));
+		}).when(timersAndBuffsConfig).tzhaarStartTime(nullable(Instant.class));
 
 		ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "<col=ef1020>Wave: 1</col>", "", 0);
-		timersPlugin.onChatMessage(chatMessage);
+		timersAndBuffsPlugin.onChatMessage(chatMessage);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager, times(1)).addInfoBox(captor.capture());
@@ -346,12 +348,12 @@ public class TimersPluginTest
 	@Test
 	public void testDeathChargeCast()
 	{
-		when(timersConfig.showArceuus()).thenReturn(true);
+		when(timersAndBuffsConfig.showArceuus()).thenReturn(true);
 		when(client.getRealSkillLevel(Skill.MAGIC)).thenReturn(50);
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.DEATH_CHARGE);
 		varbitChanged.setValue(1);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> ibcaptor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(ibcaptor.capture());
@@ -363,12 +365,12 @@ public class TimersPluginTest
 	@Test
 	public void testDeathChargeCooldown()
 	{
-		when(timersConfig.showArceuusCooldown()).thenReturn(true);
+		when(timersAndBuffsConfig.showArceuusCooldown()).thenReturn(true);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.DEATH_CHARGE_COOLDOWN);
 		varbitChanged.setValue(1);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> ibcaptor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(ibcaptor.capture());
@@ -379,10 +381,10 @@ public class TimersPluginTest
 	@Test
 	public void testArceuusWard()
 	{
-		when(timersConfig.showArceuus()).thenReturn(true);
+		when(timersAndBuffsConfig.showArceuus()).thenReturn(true);
 		when(client.getRealSkillLevel(Skill.MAGIC)).thenReturn(57);
 		ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "<col=0000b2>Your defence against Arceuus magic has been strengthened.</col>", "", 0);
-		timersPlugin.onChatMessage(chatMessage);
+		timersAndBuffsPlugin.onChatMessage(chatMessage);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -394,12 +396,12 @@ public class TimersPluginTest
 	@Test
 	public void testArceuusWardCooldown()
 	{
-		when(timersConfig.showArceuusCooldown()).thenReturn(true);
+		when(timersAndBuffsConfig.showArceuusCooldown()).thenReturn(true);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.WARD_OF_ARCEUUS_COOLDOWN);
 		varbitChanged.setValue(1);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -410,12 +412,12 @@ public class TimersPluginTest
 	@Test
 	public void testCorruptionCooldown()
 	{
-		when(timersConfig.showArceuusCooldown()).thenReturn(true);
+		when(timersAndBuffsConfig.showArceuusCooldown()).thenReturn(true);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.CORRUPTION_COOLDOWN);
 		varbitChanged.setValue(1);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -426,11 +428,11 @@ public class TimersPluginTest
 	@Test
 	public void testShadowVeil()
 	{
-		when(timersConfig.showArceuus()).thenReturn(true);
+		when(timersAndBuffsConfig.showArceuus()).thenReturn(true);
 		when(client.getRealSkillLevel(Skill.MAGIC)).thenReturn(57);
 
 		ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "<col=6800bf>Your thieving abilities have been enhanced.</col>", "", 0);
-		timersPlugin.onChatMessage(chatMessage);
+		timersAndBuffsPlugin.onChatMessage(chatMessage);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -441,12 +443,12 @@ public class TimersPluginTest
 	@Test
 	public void testShadowVeilCooldown()
 	{
-		when(timersConfig.showArceuusCooldown()).thenReturn(true);
+		when(timersAndBuffsConfig.showArceuusCooldown()).thenReturn(true);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.SHADOW_VEIL_COOLDOWN);
 		varbitChanged.setValue(1);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -457,11 +459,11 @@ public class TimersPluginTest
 	@Test
 	public void testThrall()
 	{
-		when(timersConfig.showArceuus()).thenReturn(true);
+		when(timersAndBuffsConfig.showArceuus()).thenReturn(true);
 		when(client.getBoostedSkillLevel(Skill.MAGIC)).thenReturn(60);
 
 		ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "<col=ef0083>You resurrect a greater zombified thrall.</col>", "", 0);
-		timersPlugin.onChatMessage(chatMessage);
+		timersAndBuffsPlugin.onChatMessage(chatMessage);
 
 		ArgumentCaptor<InfoBox> ibcaptor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(ibcaptor.capture());
@@ -473,12 +475,12 @@ public class TimersPluginTest
 	@Test
 	public void testThrallCooldown()
 	{
-		when(timersConfig.showArceuusCooldown()).thenReturn(true);
+		when(timersAndBuffsConfig.showArceuusCooldown()).thenReturn(true);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.RESURRECT_THRALL_COOLDOWN);
 		varbitChanged.setValue(1);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> ibcaptor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(ibcaptor.capture());
@@ -490,12 +492,12 @@ public class TimersPluginTest
 	@Test
 	public void testImbuedHeartStart()
 	{
-		when(timersConfig.showImbuedHeart()).thenReturn(true);
+		when(timersAndBuffsConfig.showImbuedHeart()).thenReturn(true);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.IMBUED_HEART_COOLDOWN);
 		varbitChanged.setValue(70);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -507,15 +509,15 @@ public class TimersPluginTest
 	@Test
 	public void testImbuedHeartEnd()
 	{
-		when(timersConfig.showImbuedHeart()).thenReturn(true);
+		when(timersAndBuffsConfig.showImbuedHeart()).thenReturn(true);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.IMBUED_HEART_COOLDOWN);
 		varbitChanged.setValue(70);
-		timersPlugin.onVarbitChanged(varbitChanged); // Calls removeIf once (on createGameTimer)
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged); // Calls removeIf once (on createGameTimer)
 
 		ArgumentCaptor<Predicate<InfoBox>> prcaptor = ArgumentCaptor.forClass(Predicate.class);
-		TimerTimer imbuedHeartInfoBox = new TimerTimer(GameTimer.IMBUEDHEART, Duration.ofSeconds(420), timersPlugin);
+		TimerTimer imbuedHeartInfoBox = new TimerTimer(GameTimer.IMBUEDHEART, Duration.ofSeconds(420), timersAndBuffsPlugin);
 		verify(infoBoxManager, times (1)).addInfoBox(any());
 		verify(infoBoxManager, times(1)).removeIf(prcaptor.capture());
 		Predicate<InfoBox> pred = prcaptor.getValue();
@@ -524,7 +526,7 @@ public class TimersPluginTest
 		varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.IMBUED_HEART_COOLDOWN);
 		varbitChanged.setValue(0);
-		timersPlugin.onVarbitChanged(varbitChanged); // Calls removeIf once
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged); // Calls removeIf once
 
 		verify(infoBoxManager, times(1)).addInfoBox(any());
 		verify(infoBoxManager, times(2)).removeIf(prcaptor.capture());
@@ -535,9 +537,9 @@ public class TimersPluginTest
 	@Test
 	public void testMartinPickpocket()
 	{
-		when(timersConfig.showPickpocketStun()).thenReturn(true);
+		when(timersAndBuffsConfig.showPickpocketStun()).thenReturn(true);
 		ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You fail to pick Martin's pocket.", "", 0);
-		timersPlugin.onChatMessage(chatMessage);
+		timersAndBuffsPlugin.onChatMessage(chatMessage);
 
 		ArgumentCaptor<InfoBox> captor = ArgumentCaptor.forClass(InfoBox.class);
 		verify(infoBoxManager).addInfoBox(captor.capture());
@@ -549,16 +551,16 @@ public class TimersPluginTest
 	@Test
 	public void testCoXOverload()
 	{
-		when(timersConfig.showOverload()).thenReturn(true);
+		when(timersAndBuffsConfig.showOverload()).thenReturn(true);
 		ArgumentCaptor<Predicate<InfoBox>> prcaptor = ArgumentCaptor.forClass(Predicate.class);
 
 		when(client.getVarbitValue(Varbits.IN_RAID)).thenReturn(1);
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.COX_OVERLOAD_REFRESHES_REMAINING);
 		varbitChanged.setValue(15);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
-		TimerTimer overloadInfobox = new TimerTimer(GameTimer.OVERLOAD_RAID, Duration.ofSeconds(225), timersPlugin);
+		TimerTimer overloadInfobox = new TimerTimer(GameTimer.OVERLOAD_RAID, Duration.ofSeconds(225), timersAndBuffsPlugin);
 		verify(infoBoxManager).addInfoBox(any());
 		verify(infoBoxManager).removeIf(prcaptor.capture());
 		Predicate<InfoBox> pred = prcaptor.getValue();
@@ -566,7 +568,7 @@ public class TimersPluginTest
 
 		// Remove on running out
 		varbitChanged.setValue(0);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		verify(infoBoxManager).addInfoBox(any());
 		verify(infoBoxManager, times(2)).removeIf(any());
@@ -575,15 +577,15 @@ public class TimersPluginTest
 	@Test
 	public void testNMZOverload()
 	{
-		when(timersConfig.showOverload()).thenReturn(true);
+		when(timersAndBuffsConfig.showOverload()).thenReturn(true);
 		ArgumentCaptor<Predicate<InfoBox>> prcaptor = ArgumentCaptor.forClass(Predicate.class);
 
 		VarbitChanged varbitChanged = new VarbitChanged();
 		varbitChanged.setVarbitId(Varbits.NMZ_OVERLOAD_REFRESHES_REMAINING);
 		varbitChanged.setValue(9);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
-		TimerTimer overloadInfobox = new TimerTimer(GameTimer.OVERLOAD, Duration.ofSeconds(135), timersPlugin);
+		TimerTimer overloadInfobox = new TimerTimer(GameTimer.OVERLOAD, Duration.ofSeconds(135), timersAndBuffsPlugin);
 		verify(infoBoxManager).addInfoBox(any());
 		verify(infoBoxManager).removeIf(prcaptor.capture());
 		Predicate<InfoBox> pred = prcaptor.getValue();
@@ -591,7 +593,7 @@ public class TimersPluginTest
 
 		// Remove on running out
 		varbitChanged.setValue(0);
-		timersPlugin.onVarbitChanged(varbitChanged);
+		timersAndBuffsPlugin.onVarbitChanged(varbitChanged);
 
 		verify(infoBoxManager).addInfoBox(any());
 		verify(infoBoxManager, times(2)).removeIf(any());