Repository: runelite/runelite
Pull request #18122 (2024-08-03 14:36:33)

clues: add echo boots alternative
This PR adds Echo boots as an alternative to Bandos boots for the emote clue.

<details>
  <summary>For your convenience</summary>
   

Set clue step, in shell
```java
client.setVarcStrValue(335, "::clue Jump for joy in the centre of Zul-Andra. Beware of double agents! Equip a dragon 2h sword, bandos boots and an obsidian cape.");
```

Set boots to Echo boots
```
::wear 10 28945
```
  
</details>


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..e7c943968 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,18 @@ 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
 	 */
+	@Deprecated
 	MenuEntry createMenuEntry(int idx);
 
 	/**
@@ -610,6 +617,7 @@ public interface Client extends OAuthApi, GameEngine
 	 *
 	 * @return array of open menu entries
 	 */
+	@Deprecated
 	MenuEntry[] getMenuEntries();
 
 	/**
@@ -620,6 +628,7 @@ public interface Client extends OAuthApi, GameEngine
 	 *
 	 * @param entries new array of open menu entries
 	 */
+	@Deprecated
 	void setMenuEntries(MenuEntry[] entries);
 
 	/**
@@ -652,6 +661,7 @@ public interface Client extends OAuthApi, GameEngine
 	 *
 	 * @return the menu x location
 	 */
+	@Deprecated
 	int getMenuX();
 
 	/**
@@ -659,6 +669,7 @@ public interface Client extends OAuthApi, GameEngine
 	 *
 	 * @return the menu y location
 	 */
+	@Deprecated
 	int getMenuY();
 
 	/**
@@ -666,6 +677,7 @@ public interface Client extends OAuthApi, GameEngine
 	 *
 	 * @return the menu height
 	 */
+	@Deprecated
 	int getMenuHeight();
 
 	/**
@@ -673,6 +685,7 @@ public interface Client extends OAuthApi, GameEngine
 	 *
 	 * @return the menu width
 	 */
+	@Deprecated
 	int getMenuWidth();
 
 	/**
diff --git a/runelite-api/src/main/java/net/runelite/api/ItemID.java b/runelite-api/src/main/java/net/runelite/api/ItemID.java
index b72e63f66..6f316e984 100644
--- a/runelite-api/src/main/java/net/runelite/api/ItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ItemID.java
@@ -4192,7 +4192,7 @@ public final class ItemID
 	public static final int BOTTLED_WATER = 6953;
 	public static final int REDBERRY_JUICE = 6954;
 	public static final int PINK_DYE = 6955;
-	public static final int ROSE_TINTED_LENS = 6956;
+	public static final int ROSETINTED_LENS = 6956;
 	public static final int WIZARDS_HEAD = 6957;
 	public static final int SAND = 6958;
 	public static final int PINK_CAPE = 6959;
@@ -7610,7 +7610,7 @@ public final class ItemID
 	public static final int BIG_PIRATE_HAT = 12355;
 	public static final int KATANA = 12357;
 	public static final int LEPRECHAUN_HAT = 12359;
-	public static final int CAT_MASK = 12361;
+	public static final int CAT_H = 12361;
 	public static final int BRONZE_DRAGON_MASK = 12363;
 	public static final int IRON_DRAGON_MASK = 12365;
 	public static final int STEEL_DRAGON_MASK = 12367;
@@ -14113,5 +14113,53 @@ public final class ItemID
 	public static final int SIGIL_OF_GUNSLINGER_29676 = 29676;
 	public static final int SIGIL_OF_ARCANE_SWIFTNESS = 29678;
 	public static final int SIGIL_OF_ARCANE_SWIFTNESS_29679 = 29679;
+	public static final int GUTHIXIAN_TEMPLE_TELEPORT = 29684;
+	public static final int DNI23_TORSO_LIGHTBUTTONS = 29686;
+	public static final int DNI23_TORSO_DARKBUTTONS = 29688;
+	public static final int DNI23_TORSO_DARKBUTTONS_29690 = 29690;
+	public static final int DNI23_TORSO_SHIRT = 29692;
+	public static final int DNI23_TORSO_STITCHING = 29694;
+	public static final int DNI23_TORSO_TWOTONED = 29696;
+	public static final int DNI23_TORSO_PRINCELY = 29698;
+	public static final int DNI23_TORSO_RIPPEDWESKIT = 29700;
+	public static final int DNI23_TORSO_RIPPEDWESKIT_29702 = 29702;
+	public static final int DNI23_TORSO_CROPTOPS = 29704;
+	public static final int DNI23_TORSO_POLONECK = 29706;
+	public static final int DNI23_TORSO_SIMPLE = 29708;
+	public static final int DNI23_TORSO_FRILLY = 29710;
+	public static final int DNI23_TORSO_CORSETRY = 29712;
+	public static final int DNI23_TORSO_BODICE = 29714;
+	public static final int DNI23_ARMS_THIN = 29716;
+	public static final int DNI23_ARMS_SHOULDERPADS = 29718;
+	public static final int DNI23_ARMS_THICKSTRIPE = 29720;
+	public static final int DNI23_ARMS_LOOSESLEEVES = 29722;
+	public static final int DNI23_ARMS_PRINCELY = 29724;
+	public static final int DNI23_ARMS_TATTYLONG = 29726;
+	public static final int DNI23_ARMS_RIPPED = 29728;
+	public static final int DNI23_ARMS_BARE = 29730;
+	public static final int DNI23_ARMS_FRILLY = 29732;
+	public static final int DNI23_ARMS_TATTYSHORT = 29734;
+	public static final int DNI23_ARMS_BARESHOULDERS = 29736;
+	public static final int DNI23_LEGS_SHORTS = 29738;
+	public static final int DNI23_LEGS_BEACH = 29740;
+	public static final int DNI23_LEGS_PRINCELY = 29742;
+	public static final int DNI23_LEGS_LEGGINGS = 29744;
+	public static final int DNI23_LEGS_SIDESTRIPES = 29746;
+	public static final int DNI23_LEGS_RIPPED = 29748;
+	public static final int DNI23_LEGS_PATCHED = 29750;
+	public static final int DNI23_LEGS_SKIRT = 29752;
+	public static final int DNI23_LEGS_LONGSKIRT = 29754;
+	public static final int DNI23_LEGS_LONGNARROWSKIRT = 29756;
+	public static final int DNI23_LEGS_SHORTSKIRT = 29758;
+	public static final int DNI23_LEGS_LAYERED = 29760;
+	public static final int DNI23_LEGS_SASHDOTS = 29762;
+	public static final int DNI23_LEGS_BIGHEM = 29764;
+	public static final int DNI23_LEGS_SASHTROUSERS = 29766;
+	public static final int DNI23_LEGS_PATTERNED = 29768;
+	public static final int DNI23_LEGS_TORNSKIRT = 29770;
+	public static final int DNI23_LEGS_PATCHEDSKIRT = 29772;
+	public static final int AMYS_SAW_OFFHAND = 29774;
+	public static final int IMCANDO_HAMMER_OFFHAND = 29775;
+	public static final int BRUMA_TORCH_OFFHAND = 29777;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/Menu.java b/runelite-api/src/main/java/net/runelite/api/Menu.java
new file mode 100644
index 000000000..74d7562ae
--- /dev/null
+++ b/runelite-api/src/main/java/net/runelite/api/Menu.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2024, Adam <Adam@sigterm.info>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.api;
+
+/**
+ * The client minimenu.
+ */
+public interface Menu
+{
+	/**
+	 * Create a new menu entry
+	 * @param idx the index to create the menu entry at. Accepts negative indexes eg. -1 inserts at the end.
+	 * @return the newly created menu entry
+	 */
+	MenuEntry createMenuEntry(int idx);
+
+	/**
+	 * Gets the current mini menu entries.
+	 *
+	 * @return array of menu entries
+	 */
+	MenuEntry[] getMenuEntries();
+
+	/**
+	 * Sets the array of menu entries.
+	 * <p>
+	 * This method should typically be used in the context of the {@link net.runelite.api.events.MenuOpened}
+	 * event, since setting the menu entries will be overwritten the next frame
+	 *
+	 * @param entries new array of open menu entries
+	 */
+	void setMenuEntries(MenuEntry[] entries);
+
+	/**
+	 * Remove a menu entry
+	 * @param entry the menu entry
+	 */
+	void removeMenuEntry(MenuEntry entry);
+
+	/**
+	 * Get the menu x location. Only valid if the menu is open.
+	 *
+	 * @return the menu x location
+	 */
+	int getMenuX();
+
+	/**
+	 * Get the menu y location. Only valid if the menu is open.
+	 *
+	 * @return the menu y location
+	 */
+	int getMenuY();
+
+	/**
+	 * Get the menu width. Only valid if the menu is open.
+	 *
+	 * @return the menu width
+	 */
+	int getMenuWidth();
+
+	/**
+	 * Get the menu height. Only valid if the menu is open.
+	 *
+	 * @return the menu height
+	 */
+	int getMenuHeight();
+}
diff --git a/runelite-api/src/main/java/net/runelite/api/MenuAction.java b/runelite-api/src/main/java/net/runelite/api/MenuAction.java
index 41bb1af3f..739325518 100644
--- a/runelite-api/src/main/java/net/runelite/api/MenuAction.java
+++ b/runelite-api/src/main/java/net/runelite/api/MenuAction.java
@@ -245,10 +245,10 @@ public enum MenuAction
 	WIDGET_TARGET_ON_WIDGET(58),
 
 	/**
-	 * Submenu parent that is also a widget.
-	 * see {@link #RUNELITE_SUBMENU}
+	 * RuneLite menu that is a widge.
+	 * @see MenuEntry#getWidget()
 	 */
-	RUNELITE_SUBMENU_WIDGET(998),
+	RUNELITE_WIDGET(998),
 
 	/**
 	 * Menu action for high priority runelite options
@@ -303,9 +303,9 @@ public enum MenuAction
 	 */
 	RUNELITE_INFOBOX(1504),
 	/**
-	 * Parent menu of submenus
+	 * Like {@link #RUNELITE}, except clicking always forces the menu open.
 	 */
-	RUNELITE_SUBMENU(1505),
+	RUNELITE_LOW_PRIORITY(1506),
 
 	/**
 	 * Menu action triggered when the id is not defined in this class.
diff --git a/runelite-api/src/main/java/net/runelite/api/MenuEntry.java b/runelite-api/src/main/java/net/runelite/api/MenuEntry.java
index a207a814b..83e227cb1 100644
--- a/runelite-api/src/main/java/net/runelite/api/MenuEntry.java
+++ b/runelite-api/src/main/java/net/runelite/api/MenuEntry.java
@@ -25,6 +25,7 @@
 package net.runelite.api;
 
 import java.util.function.Consumer;
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import net.runelite.api.widgets.Widget;
 
@@ -99,18 +100,10 @@ public interface MenuEntry
 	MenuEntry onClick(Consumer<MenuEntry> callback);
 
 	/**
-	 * Set the parent for the menu entry. This makes this menu entry part of the sub-menu off of the parent.
-	 * @param parent
+	 * Get the callback called when the menu option is clicked
 	 * @return
 	 */
-	MenuEntry setParent(MenuEntry parent);
-
-	/**
-	 * Get the parent for the menu entry.
-	 * @return
-	 */
-	@Nullable
-	MenuEntry getParent();
+	Consumer<MenuEntry> onClick();
 
 	/**
 	 * Test if this menu entry is an item op. "Use" and "Examine" are not considered item ops.
@@ -167,4 +160,25 @@ public interface MenuEntry
 	 */
 	@Nullable
 	Actor getActor();
+
+	/**
+	 * Get the submenu for this menu entry.
+	 * @return the submenu, or null if one doesn't exist
+	 * @see #createSubMenu()
+	 */
+	@Nullable
+	Menu getSubMenu();
+
+	/**
+	 * Create a new submenu for this menu entry.
+	 * This will erase any previous submenu.
+	 * @return the new submenu
+	 */
+	@Nonnull
+	Menu createSubMenu();
+
+	/**
+	 * Remove the submenu from this menu entry.
+	 */
+	void deleteSubMenu();
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/NpcID.java b/runelite-api/src/main/java/net/runelite/api/NpcID.java
index d2f3362b9..f73d19887 100644
--- a/runelite-api/src/main/java/net/runelite/api/NpcID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NpcID.java
@@ -1008,7 +1008,7 @@ public final class NpcID
 	public static final int CAVE_HORROR_1050 = 1050;
 	public static final int CAVE_HORROR_1051 = 1051;
 	public static final int CAVEY_DAVEY = 1052;
-	public static final int PATCHY = 1053;
+	public static final int MAC = 1053;
 	public static final int LAUNA = 1054;
 	public static final int LAUNA_1055 = 1055;
 	public static final int BRANA = 1056;
@@ -5442,7 +5442,7 @@ public final class NpcID
 	public static final int SUMMONED_SOUL = 5867;
 	public static final int SUMMONED_SOUL_5868 = 5868;
 	public static final int SUMMONED_SOUL_5869 = 5869;
-	public static final int KEY_MASTER = 5870;
+	public static final int PATCHY = 5870;
 	public static final int KING_ARTHUR_5871 = 5871;
 	public static final int BABY_GREEN_DRAGON_5872 = 5872;
 	public static final int BABY_GREEN_DRAGON_5873 = 5873;
@@ -5859,7 +5859,7 @@ public final class NpcID
 	public static final int RUFUS_6478 = 6478;
 	public static final int EYE = 6479;
 	public static final int EYE_6480 = 6480;
-	public static final int MAC = 6481;
+	public static final int JACK_FROST = 6481;
 	public static final int BOULDER_6482 = 6482;
 	public static final int MONKEY_6483 = 6483;
 	public static final int GELIN = 6484;
@@ -10552,7 +10552,7 @@ public final class NpcID
 	public static final int JALIMKOT_12594 = 12594;
 	public static final int GOOSE = 12597;
 	public static final int ANTISANTA = 12598;
-	public static final int JACK_FROST = 12599;
+	public static final int KEY_MASTER = 12599;
 	public static final int GUSS_MISTLETOAD = 12600;
 	public static final int SANTAS_SCRIBE = 12601;
 	public static final int SANTA_CLAUS = 12602;
@@ -11349,10 +11349,10 @@ public final class NpcID
 	public static final int HILL_GIANT_13502 = 13502;
 	public static final int HILL_GIANT_13503 = 13503;
 	public static final int HILL_GIANT_13504 = 13504;
-	public static final int EARTH_SURGE = 13505;
-	public static final int WATER_SURGE = 13506;
-	public static final int FIRE_SURGE = 13507;
-	public static final int WIND_SURGE = 13508;
+	public static final int EARTH_SPELL = 13505;
+	public static final int WATER_SPELL = 13506;
+	public static final int FIRE_SPELL = 13507;
+	public static final int WIND_SPELL = 13508;
 	public static final int GUARDIAN_OF_ARMADYL_13509 = 13509;
 	public static final int IVY_SOPHISTA = 13510;
 	public static final int THAERISK = 13511;
@@ -11474,5 +11474,6 @@ public final class NpcID
 	public static final int DURIAL321 = 13662;
 	public static final int MAGIC_MARK = 13663;
 	public static final int RANGING_RO = 13664;
+	public static final int BROAV_13665 = 13665;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/NullItemID.java b/runelite-api/src/main/java/net/runelite/api/NullItemID.java
index 783d20138..5e8e07a2d 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullItemID.java
@@ -15357,5 +15357,54 @@ public final class NullItemID
 	public static final int NULL_29674 = 29674;
 	public static final int NULL_29677 = 29677;
 	public static final int NULL_29680 = 29680;
+	public static final int NULL_29681 = 29681;
+	public static final int NULL_29682 = 29682;
+	public static final int NULL_29683 = 29683;
+	public static final int NULL_29685 = 29685;
+	public static final int NULL_29687 = 29687;
+	public static final int NULL_29689 = 29689;
+	public static final int NULL_29691 = 29691;
+	public static final int NULL_29693 = 29693;
+	public static final int NULL_29695 = 29695;
+	public static final int NULL_29697 = 29697;
+	public static final int NULL_29699 = 29699;
+	public static final int NULL_29701 = 29701;
+	public static final int NULL_29703 = 29703;
+	public static final int NULL_29705 = 29705;
+	public static final int NULL_29707 = 29707;
+	public static final int NULL_29709 = 29709;
+	public static final int NULL_29711 = 29711;
+	public static final int NULL_29713 = 29713;
+	public static final int NULL_29715 = 29715;
+	public static final int NULL_29717 = 29717;
+	public static final int NULL_29719 = 29719;
+	public static final int NULL_29721 = 29721;
+	public static final int NULL_29723 = 29723;
+	public static final int NULL_29725 = 29725;
+	public static final int NULL_29727 = 29727;
+	public static final int NULL_29729 = 29729;
+	public static final int NULL_29731 = 29731;
+	public static final int NULL_29733 = 29733;
+	public static final int NULL_29735 = 29735;
+	public static final int NULL_29737 = 29737;
+	public static final int NULL_29739 = 29739;
+	public static final int NULL_29741 = 29741;
+	public static final int NULL_29743 = 29743;
+	public static final int NULL_29745 = 29745;
+	public static final int NULL_29747 = 29747;
+	public static final int NULL_29749 = 29749;
+	public static final int NULL_29751 = 29751;
+	public static final int NULL_29753 = 29753;
+	public static final int NULL_29755 = 29755;
+	public static final int NULL_29757 = 29757;
+	public static final int NULL_29759 = 29759;
+	public static final int NULL_29761 = 29761;
+	public static final int NULL_29763 = 29763;
+	public static final int NULL_29765 = 29765;
+	public static final int NULL_29767 = 29767;
+	public static final int NULL_29769 = 29769;
+	public static final int NULL_29771 = 29771;
+	public static final int NULL_29773 = 29773;
+	public static final int NULL_29776 = 29776;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/NullObjectID.java b/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
index 347daa871..21f0ceef1 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
@@ -28083,5 +28083,14 @@ public final class NullObjectID
 	public static final int NULL_54111 = 54111;
 	public static final int NULL_54112 = 54112;
 	public static final int NULL_54113 = 54113;
+	public static final int NULL_54118 = 54118;
+	public static final int NULL_54119 = 54119;
+	public static final int NULL_54120 = 54120;
+	public static final int NULL_54121 = 54121;
+	public static final int NULL_54122 = 54122;
+	public static final int NULL_54123 = 54123;
+	public static final int NULL_54124 = 54124;
+	public static final int NULL_54125 = 54125;
+	public static final int NULL_54126 = 54126;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/ObjectID.java b/runelite-api/src/main/java/net/runelite/api/ObjectID.java
index 0dccca9ea..90695fe1b 100644
--- a/runelite-api/src/main/java/net/runelite/api/ObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ObjectID.java
@@ -2254,18 +2254,18 @@ public final class ObjectID
 	public static final int FIRE_ALTAR = 4091;
 	public static final int BROKEN_FIRE_ALTAR = 4092;
 	public static final int FUNERAL_PYRE = 4093;
-	public static final int FUNERAL_PYRE_4094 = 4094;
-	public static final int FUNERAL_PYRE_4095 = 4095;
-	public static final int FUNERAL_PYRE_4096 = 4096;
-	public static final int FUNERAL_PYRE_4097 = 4097;
-	public static final int FUNERAL_PYRE_4098 = 4098;
-	public static final int FUNERAL_PYRE_4099 = 4099;
-	public static final int FUNERAL_PYRE_4100 = 4100;
-	public static final int FUNERAL_PYRE_4101 = 4101;
-	public static final int FUNERAL_PYRE_4102 = 4102;
-	public static final int FUNERAL_PYRE_4103 = 4103;
-	public static final int FUNERAL_PYRE_4104 = 4104;
-	public static final int FUNERAL_PYRE_4105 = 4105;
+	public static final int BASIC_PYRE = 4094;
+	public static final int OAK_PYRE = 4095;
+	public static final int WILLOW_PYRE = 4096;
+	public static final int MAPLE_PYRE = 4097;
+	public static final int YEW_PYRE = 4098;
+	public static final int MAGIC_PYRE = 4099;
+	public static final int BASIC_PYRE_4100 = 4100;
+	public static final int OAK_PYRE_4101 = 4101;
+	public static final int WILLOW_PYRE_4102 = 4102;
+	public static final int MAPLE_PYRE_4103 = 4103;
+	public static final int YEW_PYRE_4104 = 4104;
+	public static final int MAGIC_PYRE_4105 = 4105;
 	public static final int SOLID_BRONZE_DOOR = 4106;
 	public static final int SOLID_STEEL_DOOR = 4107;
 	public static final int SOLID_BLACK_DOOR = 4108;
@@ -4550,11 +4550,6 @@ public final class ObjectID
 	public static final int HERB_PATCH_8136 = 8136;
 	public static final int HERB_PATCH_8137 = 8137;
 	public static final int HERB_PATCH_8138 = 8138;
-	public static final int HERBS_8139 = 8139;
-	public static final int HERBS_8140 = 8140;
-	public static final int HERBS_8141 = 8141;
-	public static final int HERBS_8142 = 8142;
-	public static final int HERBS_8143 = 8143;
 	public static final int DISEASED_HERBS = 8144;
 	public static final int DISEASED_HERBS_8145 = 8145;
 	public static final int DISEASED_HERBS_8146 = 8146;
@@ -5255,10 +5250,10 @@ public final class ObjectID
 	public static final int NET_TRAP_9003 = 9003;
 	public static final int NET_TRAP_9004 = 9004;
 	public static final int NET_TRAP_9005 = 9005;
-	public static final int FUNERAL_PYRE_9006 = 9006;
-	public static final int FUNERAL_PYRE_9007 = 9007;
-	public static final int FUNERAL_PYRE_9008 = 9008;
-	public static final int FUNERAL_PYRE_9009 = 9009;
+	public static final int TEAK_PYRE = 9006;
+	public static final int MAHOGANY_PYRE = 9007;
+	public static final int TEAK_PYRE_9008 = 9008;
+	public static final int MAHOGANY_PYRE_9009 = 9009;
 	public static final int LIGHT_JUNGLE = 9010;
 	public static final int LIGHT_JUNGLE_9011 = 9011;
 	public static final int LIGHT_JUNGLE_9012 = 9012;
@@ -11640,8 +11635,8 @@ public final class ObjectID
 	public static final int URN_21268 = 21268;
 	public static final int URN_21269 = 21269;
 	public static final int URN_21270 = 21270;
-	public static final int FUNERAL_PYRE_21271 = 21271;
-	public static final int FUNERAL_PYRE_21272 = 21272;
+	public static final int ARCTIC_PINE_PYRE = 21271;
+	public static final int ARCTIC_PINE_PYRE_21272 = 21272;
 	public static final int URN_21273 = 21273;
 	public static final int TREE_STUMP_21274 = 21274;
 	public static final int URN_21275 = 21275;
@@ -15186,8 +15181,8 @@ public final class ObjectID
 	public static final int TABLE_28862 = 28862;
 	public static final int TABLE_28863 = 28863;
 	public static final int COUNTER_28864 = 28864;
-	public static final int FUNERAL_PYRE_28865 = 28865;
-	public static final int FUNERAL_PYRE_28866 = 28866;
+	public static final int REDWOOD_PYRE = 28865;
+	public static final int REDWOOD_PYRE_28866 = 28866;
 	public static final int ROCKS_28890 = 28890;
 	public static final int CRACK_28892 = 28892;
 	public static final int STONE_28893 = 28893;
@@ -26017,5 +26012,6 @@ public final class ObjectID
 	public static final int RUG_54114 = 54114;
 	public static final int RUG_54115 = 54115;
 	public static final int RUG_54116 = 54116;
+	public static final int CHEST_54117 = 54117;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/PlayerComposition.java b/runelite-api/src/main/java/net/runelite/api/PlayerComposition.java
index 5a9b26c3f..0ccf54ad9 100644
--- a/runelite-api/src/main/java/net/runelite/api/PlayerComposition.java
+++ b/runelite-api/src/main/java/net/runelite/api/PlayerComposition.java
@@ -33,6 +33,9 @@ import net.runelite.api.kit.KitType;
  */
 public interface PlayerComposition
 {
+	int KIT_OFFSET = 256;
+	int ITEM_OFFSET = 2048;
+
 	/**
 	 * Checks if the player is female.
 	 *
@@ -59,9 +62,8 @@ public interface PlayerComposition
 	/**
 	 * Gets an array of IDs related to equipment slots.
 	 * <p>
-	 * If the ID for a specific slot is between 256 and 512, subtracting
-	 * 256 will result in the kit ID. Values above 512 indicate an item
-	 * and can be converted to the item ID by subtracting 512.
+	 * IDs between {@link #KIT_OFFSET} and {@link #ITEM_OFFSET} are kits, offset by {@link #KIT_OFFSET}.
+	 * IDs greater than or equal to {@link #ITEM_OFFSET} are items, offset by {@link #ITEM_OFFSET}.
 	 *
 	 * @return the equipment IDs
 	 */
diff --git a/runelite-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/plugins/banktags/tabs/TabInterface.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
index b84ba1ea0..d5a75275d 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
@@ -803,14 +803,15 @@ public class TabInterface
 			&& event.getActionParam1() == ComponentID.BANK_ITEM_CONTAINER
 			&& event.getOption().equals(DUPLICATE_ITEM))
 		{
-			event.getMenuEntry().setType(MenuAction.RUNELITE);
+			// use RUNELITE_LOW_PRIORITY to avoid sending the op to the server, but also keep it right-click only
+			event.getMenuEntry().setType(MenuAction.RUNELITE_LOW_PRIORITY);
 			event.getMenuEntry().onClick(this::opDuplicateItem);
 		}
 		if (activeTab != null
 			&& event.getActionParam1() == ComponentID.BANK_ITEM_CONTAINER
 			&& event.getOption().equals(REMOVE_LAYOUT))
 		{
-			event.getMenuEntry().setType(MenuAction.RUNELITE);
+			event.getMenuEntry().setType(MenuAction.RUNELITE_LOW_PRIORITY);
 			event.getMenuEntry().onClick(this::opRemoveLayout);
 		}
 		else if (event.getActionParam1() == ComponentID.BANK_DEPOSIT_INVENTORY
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java
index 69570d467..8524914f4 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java
@@ -123,7 +123,7 @@ public class ChatCommandsPlugin extends Plugin
 	private static final Pattern HS_KC_GHC_PATTERN = Pattern.compile("You have opened the Grand Hallowed Coffin <col=ff0000>([0-9,]+)</col> times?!");
 	private static final Pattern COLLECTION_LOG_ITEM_PATTERN = Pattern.compile("New item added to your collection log: (.*)");
 	private static final Pattern GUARDIANS_OF_THE_RIFT_PATTERN = Pattern.compile("Amount of Rifts you have closed: <col=ff0000>([0-9,]+)</col>.", Pattern.CASE_INSENSITIVE);
-	private static final Pattern HUNTER_RUMOUR_KC_PATTERN = Pattern.compile("You have completed <col=ff3045>([0-9,]+)</col> rumours for the Hunter Guild.");
+	private static final Pattern HUNTER_RUMOUR_KC_PATTERN = Pattern.compile("You have completed <col=[0-9a-f]{6}>([0-9,]+)</col> rumours? for the Hunter Guild.");
 
 	private static final String TOTAL_LEVEL_COMMAND_STRING = "!total";
 	private static final String PRICE_COMMAND_STRING = "!price";
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/EmoteClue.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/EmoteClue.java
index e927ee972..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)),
@@ -204,7 +204,7 @@ public class EmoteClue extends ClueScroll implements LocationClueScroll
 		new EmoteClue("Shrug in Catherby bank. Yawn before you talk to me. Equip a maple longbow, green d'hide chaps and an iron med helm.", "Catherby", OUTSIDE_CATHERBY_BANK, new WorldPoint(2808, 3440, 0), SHRUG, YAWN, item(MAPLE_LONGBOW), item(GREEN_DHIDE_CHAPS), item(IRON_MED_HELM)),
 		new EmoteClue("Shrug in the woods east of the Level 19 Wilderness Obelisk. Beware of double agents! Equip rune platelegs, an iron platebody and blue dragonhide vambraces.", "East of the Level 19 Wilderness Obelisk", EAST_OF_THE_LEVEL_19_WILDERNESS_OBELISK, new WorldPoint(3241, 3672, 0), DOUBLE_AGENT_65, SHRUG, item(RUNE_PLATELEGS), item(IRON_PLATEBODY), item(BLUE_DHIDE_VAMBRACES)),
 		new EmoteClue("Shrug in the Shayzien war tent. Equip a blue mystic robe bottom, a rune kiteshield and any bob shirt.", "Shayzien war tent", SHAYZIEN_WAR_TENT, new WorldPoint(1487, 3635, 0), SHRUG, item(MYSTIC_ROBE_BOTTOM), item(RUNE_KITESHIELD), range("Any bob shirt", BOBS_RED_SHIRT, BOBS_PURPLE_SHIRT)),
-		new EmoteClue("Slap your head in the centre of the Kourend catacombs. Beware of double agents! Equip the arclight and the amulet of the damned.", "Kourend catacombs", CENTRE_OF_THE_CATACOMBS_OF_KOUREND, new WorldPoint(1663, 10045, 0), DOUBLE_AGENT_141, SLAP_HEAD, item(ARCLIGHT), any("Amulet of the damned", item(AMULET_OF_THE_DAMNED), item(AMULET_OF_THE_DAMNED_FULL))),
+		new EmoteClue("Slap your head in the centre of the Kourend catacombs. Beware of double agents! Equip arclight or emberlight along with the amulet of the damned.", "Kourend catacombs", CENTRE_OF_THE_CATACOMBS_OF_KOUREND, new WorldPoint(1663, 10045, 0), DOUBLE_AGENT_141, SLAP_HEAD, any("Arclight or Emberlight", item(ARCLIGHT), item(EMBERLIGHT)), any("Amulet of the damned", item(AMULET_OF_THE_DAMNED), item(AMULET_OF_THE_DAMNED_FULL))),
 		new EmoteClue("Spin at the crossroads north of Rimmington. Equip a green gnome hat, cream gnome top and leather chaps.", "Rimmington", ROAD_JUNCTION_NORTH_OF_RIMMINGTON, new WorldPoint(2981, 3276, 0), SPIN, item(GREEN_HAT), item(CREAM_ROBE_TOP), item(LEATHER_CHAPS)),
 		new EmoteClue("Spin in Draynor Manor by the fountain. Equip an iron platebody, studded leather chaps and a bronze full helmet.", "Draynor Manor", DRAYNOR_MANOR_BY_THE_FOUNTAIN, new WorldPoint(3088, 3336, 0), SPIN, item(IRON_PLATEBODY), item(STUDDED_CHAPS), item(BRONZE_FULL_HELM)),
 		new EmoteClue("Spin in front of the Soul altar. Beware of double agents! Equip a dragon pickaxe, helm of neitiznot and a pair of rune boots.", "Soul altar", SOUL_ALTAR, new WorldPoint(1815, 3856, 0), DOUBLE_AGENT_141, SPIN, any("Dragon or Crystal pickaxe", item(DRAGON_PICKAXE), item(DRAGON_PICKAXE_12797), item(INFERNAL_PICKAXE), item(INFERNAL_PICKAXE_UNCHARGED), item(DRAGON_PICKAXE_OR), item(DRAGON_PICKAXE_OR_25376), item(CRYSTAL_PICKAXE), item(CRYSTAL_PICKAXE_INACTIVE), item(INFERNAL_PICKAXE_OR), item(INFERNAL_PICKAXE_UNCHARGED_25369)), item(HELM_OF_NEITIZNOT), item(RUNE_BOOTS)),
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/grandexchange/FuzzySearchScorer.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/FuzzySearchScorer.java
index 47a8d2477..7471e61c7 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/FuzzySearchScorer.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/FuzzySearchScorer.java
@@ -29,6 +29,7 @@ import java.util.function.ToDoubleFunction;
 import javax.inject.Singleton;
 import net.runelite.api.ItemComposition;
 import org.apache.commons.text.similarity.JaroWinklerDistance;
+import org.apache.commons.text.similarity.LongestCommonSubsequence;
 import org.apache.commons.text.similarity.SimilarityScore;
 
 @Singleton
@@ -44,18 +45,24 @@ public class FuzzySearchScorer
 		query = query.toLowerCase().replace('-', ' ');
 		itemName = itemName.toLowerCase().replace('-', ' ');
 
-		// we raise the score for items that share a prefix with the query
-		int prefixLen = 0;
-		int maxLen = Math.min(query.length(), itemName.length());
-		while (prefixLen < maxLen && query.charAt(prefixLen) == itemName.charAt(prefixLen))
+		// we raise the score for longest substring of a word, scoring within [0,1]
+		String[] queryWords = query.split(" ");
+		String[] itemWords = itemName.split(" ");
+		double lcsScore = 0.0;
+		for (String queryWord : queryWords)
 		{
-			prefixLen++;
+			for (String itemWord : itemWords)
+			{
+				int lcsLen = new LongestCommonSubsequence().longestCommonSubsequence(queryWord, itemWord).length();
+				lcsScore = Math.max(lcsScore, ((double) lcsLen) / queryWord.length());
+			}
 		}
-		double prefixScore = ((double) prefixLen) / query.length() - 0.25;
 
-		// and also raise the score for string "closeness"
-		double proximityScore = baseAlgorithm.apply(query, itemName) - 0.25;
-		return prefixScore + proximityScore;
+		// and also raise the score for string "closeness", but strongly prefer high closeness, scoring within [-0.5,0.5]
+		double proximityScore = Math.log10(10 * baseAlgorithm.apply(query, itemName)) - 0.5;
+
+		// subtract 1.0 to filter out low-scoring results
+		return lcsScore + proximityScore - 1.0;
 	}
 
 	public ToDoubleFunction<ItemComposition> comparator(String query)
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..d631e6c75 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 left 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/specialcounter/SpecialCounterConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterConfig.java
index 20ed5e514..7be5ebbee 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialCounterConfig.java
@@ -144,6 +144,17 @@ public interface SpecialCounterConfig extends Config
 		return 0;
 	}
 
+	@ConfigItem(
+		position = 31,
+		keyName = "emberlightThreshold",
+		name = "Emberlight",
+		description = "Threshold for Emberlight (0 to disable)"
+	)
+	default int emberlightThreshold()
+	{
+		return 0;
+	}
+
 	@ConfigItem(
 		position = 40,
 		keyName = "bandosGodswordThreshold",
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialWeapon.java b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialWeapon.java
index 8d3060637..a40890e9a 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialWeapon.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/specialcounter/SpecialWeapon.java
@@ -112,7 +112,8 @@ public enum SpecialWeapon
 				return 0;
 			}
 		},
-	SEERCULL("Seercull", new int[]{ItemID.SEERCULL}, true, (d) -> 46 + (d * 5), (c) -> 0);
+	SEERCULL("Seercull", new int[]{ItemID.SEERCULL}, true, (d) -> 46 + (d * 5), (c) -> 0),
+	EMBERLIGHT("Emberlight", new int[]{ItemID.EMBERLIGHT}, false, SpecialCounterConfig::emberlightThreshold);
 
 	private static final Set<Integer> TEKTON_VARIANTS = ImmutableSet.of(
 		NpcID.TEKTON,
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