Repository: runelite/runelite
Pull request #18150 (2024-08-10 03:33:49)

Fix line of sight check for multi-tile `WorldArea`s
The existing `thisWorldArea.hasLineOfSightTo(otherWorldArea)` gets the closest single point between the two and checks if that comparison point has LoS to the other world area.

This runs into issues when source WorldArea is larger than one tile and there is partial obstacle obstruction between the two WorldAreas:

![image](https://github.com/user-attachments/assets/e41284c2-5f9c-4490-a55d-db67a20878c6)

The shaded corners would be the point used to check for LoS and the black line is an obstacle. The current LoS method returns false, but the blue line shows it should return true.

This PR should resolve this by performing an LoS check from each of the points on the potentially visible edges from each WorldArea. Since in most cases this likely isn't an issue, the points are sorted by distance to the target area to hopefully prevent unnecessary checks.

This shows which tiles will be checked for LoS (mocked my player to be size 3x3):

https://github.com/user-attachments/assets/0fbc9ac2-f853-4e8f-b8cf-9747d74fd3da

This shows LoS check. 
Target tile - Red when not seen, Green when seen by new LoS algo, Black is current behavior
Shaded tiles - Dark gray is visible from current player tile, light gray is from the mocked larger area (magenta tiles)

https://github.com/user-attachments/assets/161050ff-9d89-4e02-b271-b38b875f00ce


I side-loaded my local plugin to get the debug overlay more easily, let me know if there's something else I can provide to help verify this works.


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/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 b3405cc85..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
@@ -302,10 +302,6 @@ public enum MenuAction
 	 * Menu action for InfoBox menu entries
 	 */
 	RUNELITE_INFOBOX(1504),
-	/**
-	 * Parent menu of submenus
-	 */
-	RUNELITE_SUBMENU(1505),
 	/**
 	 * Like {@link #RUNELITE}, except clicking always forces the menu open.
 	 */
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/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..d06e5cdb6 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;
@@ -506,8 +506,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 +527,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 +695,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 +726,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 +834,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 +852,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 +879,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 +897,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 +914,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 +935,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 +985,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 +1005,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 +1028,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 +1055,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 +1073,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 +1090,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 +1111,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 +1147,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 +1199,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 +1250,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 +1273,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());
 					}
 				}
 			}
@@ -1454,14 +1414,25 @@ public class MenuEntrySwapperPlugin extends Plugin
 		}
 	}
 
-	private void swapMenuEntry(MenuEntry[] menuEntries, int index, MenuEntry menuEntry)
+	private void swapMenuEntry(MenuEntry parent, 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, 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()))
@@ -1473,11 +1444,18 @@ public class MenuEntrySwapperPlugin extends Plugin
 				{
 					swap(menuEntries, "use", target, index, true);
 				}
-				else if (swapIndex + 1 == menuEntry.getItemOp()
-					|| (menuEntry.getParent() != null && menuEntry.getOption().hashCode() == swapIndex))
+				else if (swapIndex + 1 == menuEntry.getItemOp())
 				{
 					swap(optionIndexes, menuEntries, index, menuEntries.length - 1);
-					menuEntry.setParent(null);
+				}
+				// Submenu swap. The swapIndex is actually the option hashCode.
+				else if (parent != null && menuEntry.getOption().hashCode() == swapIndex)
+				{
+					// 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 +1470,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
+					}
+					// 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;
 				}
@@ -1699,7 +1684,7 @@ public class MenuEntrySwapperPlugin extends Plugin
 		idx = 0;
 		for (MenuEntry entry : menuEntries)
 		{
-			swapMenuEntry(menuEntries, idx++, entry);
+			swapMenuEntry(null, menuEntries, idx++, entry);
 		}
 
 		if (config.removeDeadNpcMenus())
@@ -2134,11 +2119,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 +2132,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 +2156,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 +2169,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/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/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());