Repository: runelite/runelite
Pull request #18455 (2024-10-20 10:32:41)

Add boosts for a few Aldarin drinks not in ItemStats
Adds boosts for Metztonalli White, Blackbird Red, and Steamforge Brew


Patch:
diff --git a/cache/pom.xml b/cache/pom.xml
index 1a9645ac8..70c61be2a 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.42-SNAPSHOT</version>
+		<version>1.10.43-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cache</artifactId>
diff --git a/pom.xml b/pom.xml
index c1873b74d..4663571c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
 
 	<groupId>net.runelite</groupId>
 	<artifactId>runelite-parent</artifactId>
-	<version>1.10.42-SNAPSHOT</version>
+	<version>1.10.43-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>RuneLite</name>
diff --git a/runelite-api/pom.xml b/runelite-api/pom.xml
index 978ce3837..efbc160ee 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.42-SNAPSHOT</version>
+		<version>1.10.43-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 8c70ec7ab..6f036f1a1 100644
--- a/runelite-api/src/main/java/net/runelite/api/Client.java
+++ b/runelite-api/src/main/java/net/runelite/api/Client.java
@@ -2051,6 +2051,38 @@ public interface Client extends OAuthApi, GameEngine
 	 */
 	void setCameraShakeDisabled(boolean disabled);
 
+	/**
+	 * Draw all 2D extras. This is the default.
+	 */
+	int DRAW_2D_ALL = ~0;
+	/**
+	 * Hide all 2D extras.
+	 */
+	int DRAW_2D_NONE = 0;
+	/**
+	 * Render overhead text.
+	 */
+	int DRAW_2D_OVERHEAD_TEXT = 1;
+	/**
+	 * Render elements not otherwise specified in this bitflag.
+	 */
+	int DRAW_2D_OTHERS = 1 << 30;
+
+	/**
+	 * Gets the current draw2D mask. 
+	 * @return the current mask
+	 * @see Client#setDraw2DMask(int)
+	 */
+	@MagicConstant(intValues = {DRAW_2D_NONE, DRAW_2D_ALL, DRAW_2D_OVERHEAD_TEXT, DRAW_2D_OTHERS})
+	int getDraw2DMask();
+
+	/**
+	 * Sets the current draw2D mask.
+	 * Use bit operations on the value returned by {@link Client#getDraw2DMask()} to modify specific features.
+	 * @param mask The new mask.
+	 */
+	void setDraw2DMask(@MagicConstant(intValues = {DRAW_2D_NONE, DRAW_2D_ALL, DRAW_2D_OVERHEAD_TEXT, DRAW_2D_OTHERS}) int mask);
+
 	/**
 	 * Contains a 3D array of template chunks for instanced areas.
 	 * <p>
diff --git a/runelite-api/src/main/java/net/runelite/api/EquipmentInventorySlot.java b/runelite-api/src/main/java/net/runelite/api/EquipmentInventorySlot.java
index 6c4064549..c2c46f3e8 100644
--- a/runelite-api/src/main/java/net/runelite/api/EquipmentInventorySlot.java
+++ b/runelite-api/src/main/java/net/runelite/api/EquipmentInventorySlot.java
@@ -42,9 +42,12 @@ public enum EquipmentInventorySlot
 	WEAPON(3),
 	BODY(4),
 	SHIELD(5),
+	ARMS(6),
 	LEGS(7),
+	HAIR(8),
 	GLOVES(9),
 	BOOTS(10),
+	JAW(11),
 	RING(12),
 	AMMO(13);
 
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 353a54a24..2e44b36ce 100644
--- a/runelite-api/src/main/java/net/runelite/api/ItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ItemID.java
@@ -14517,5 +14517,9 @@ public final class ItemID
 	public static final int POWDER_GREY_PUMPKIN_SAD_30299 = 30299;
 	public static final int POWDER_GREY_PUMPKIN_HAPPY_30300 = 30300;
 	public static final int POWDER_GREY_PUMPKIN_LAUGHING_30301 = 30301;
+	public static final int TORVA_FULL_HELM_30302 = 30302;
+	public static final int TORVA_PLATEBODY_30303 = 30303;
+	public static final int TORVA_PLATELEGS_30304 = 30304;
+	public static final int ARCLIGHT_INACTIVE = 30305;
 	/* 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 68e69ae98..33aa75d57 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullItemID.java
@@ -15574,5 +15574,6 @@ public final class NullItemID
 	public static final int NULL_30235 = 30235;
 	public static final int NULL_30238 = 30238;
 	public static final int NULL_30240 = 30240;
+	public static final int NULL_30306 = 30306;
 	/* 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 a263073be..8e6342a3e 100644
--- a/runelite-api/src/main/java/net/runelite/api/ObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ObjectID.java
@@ -26918,5 +26918,6 @@ public final class ObjectID
 	public static final int SCARECROW_55768 = 55768;
 	public static final int SCARECROW_55769 = 55769;
 	public static final int SCARECROW_55770 = 55770;
+	public static final int MURAL_55775 = 55775;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java
index 8242b60f5..fb16cbf64 100644
--- a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java
+++ b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java
@@ -181,6 +181,9 @@ public final class VarPlayer
 	 */
 	public static final int HP_HUD_NPC_ID = 1683;
 
+	public static final int CLOG_LOGGED = 2943;
+	public static final int CLOG_TOTAL = 2944;
+
 	/**
 	 * Colors for chat messages
 	 */
diff --git a/runelite-api/src/main/java/net/runelite/api/Varbits.java b/runelite-api/src/main/java/net/runelite/api/Varbits.java
index de6790707..847eb84d4 100644
--- a/runelite-api/src/main/java/net/runelite/api/Varbits.java
+++ b/runelite-api/src/main/java/net/runelite/api/Varbits.java
@@ -936,4 +936,11 @@ public final class Varbits
 	 * </p>
 	 */
 	public static final int COLOSSAL_WYRM_COURSE_ADVANCED = 11293;
+
+	public static final int COMBAT_TASK_EASY = 12885;
+	public static final int COMBAT_TASK_MEDIUM = 12886;
+	public static final int COMBAT_TASK_HARD = 12887;
+	public static final int COMBAT_TASK_ELITE = 12888;
+	public static final int COMBAT_TASK_MASTER = 12889;
+	public static final int COMBAT_TASK_GRANDMASTER = 12890;
 }
diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml
index d041b9f36..ef85d7476 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.42-SNAPSHOT</version>
+		<version>1.10.43-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>client</artifactId>
diff --git a/runelite-client/src/main/java/net/runelite/client/config/RuneScapeProfileType.java b/runelite-client/src/main/java/net/runelite/client/config/RuneScapeProfileType.java
index e17590cdb..25ccb26d1 100644
--- a/runelite-client/src/main/java/net/runelite/client/config/RuneScapeProfileType.java
+++ b/runelite-client/src/main/java/net/runelite/client/config/RuneScapeProfileType.java
@@ -44,7 +44,8 @@ public enum RuneScapeProfileType
 	TRAILBLAZER_LEAGUE,
 	DEADMAN_REBORN,
 	SHATTERED_RELICS_LEAGUE,
-	TRAILBLAZER_RELOADED_LEAGUE(client -> client.getWorldType().contains(WorldType.SEASONAL)),
+	TRAILBLAZER_RELOADED_LEAGUE,
+	RAGING_ECHOES_LEAGUE(client -> client.getWorldType().contains(WorldType.SEASONAL)),
 	;
 
 	private final Predicate<Client> test;
diff --git a/runelite-client/src/main/java/net/runelite/client/game/ItemClient.java b/runelite-client/src/main/java/net/runelite/client/game/ItemClient.java
index 6c2a28831..2da536f99 100644
--- a/runelite-client/src/main/java/net/runelite/client/game/ItemClient.java
+++ b/runelite-client/src/main/java/net/runelite/client/game/ItemClient.java
@@ -37,7 +37,6 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import lombok.extern.slf4j.Slf4j;
 import net.runelite.http.api.item.ItemPrice;
-import net.runelite.http.api.item.ItemStats;
 import okhttp3.HttpUrl;
 import okhttp3.OkHttpClient;
 import okhttp3.Request;
@@ -98,7 +97,6 @@ public class ItemClient
 	{
 		HttpUrl.Builder urlBuilder = staticBase.newBuilder()
 			.addPathSegment("item")
-			// TODO: Change this to stats.min.json later after release is undeployed
 			.addPathSegment("stats.ids.min.json");
 
 		HttpUrl url = urlBuilder.build();
diff --git a/runelite-client/src/main/java/net/runelite/client/game/ItemEquipmentStats.java b/runelite-client/src/main/java/net/runelite/client/game/ItemEquipmentStats.java
new file mode 100644
index 000000000..dd5755b5e
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/game/ItemEquipmentStats.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.client.game;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Builder;
+import lombok.Value;
+
+@Value
+@Builder
+public class ItemEquipmentStats
+{
+	int slot;
+
+	@SerializedName("is2h")
+	boolean isTwoHanded;
+
+	int astab;
+	int aslash;
+	int acrush;
+	int amagic;
+	int arange;
+
+	int dstab;
+	int dslash;
+	int dcrush;
+	int dmagic;
+	int drange;
+
+	int str;
+	int rstr;
+	float mdmg;
+	int prayer;
+	int aspeed;
+
+	net.runelite.http.api.item.ItemEquipmentStats toHttpApiFormat()
+	{
+		return net.runelite.http.api.item.ItemEquipmentStats.builder()
+			.slot(this.slot)
+			.isTwoHanded(this.isTwoHanded)
+			.astab(this.astab)
+			.aslash(this.aslash)
+			.acrush(this.acrush)
+			.amagic(this.amagic)
+			.arange(this.arange)
+			.dstab(this.dstab)
+			.dslash(this.dslash)
+			.dcrush(this.dcrush)
+			.dmagic(this.dmagic)
+			.drange(this.drange)
+			.str(this.str)
+			.rstr(this.rstr)
+			.mdmg((int) this.mdmg)
+			.prayer(this.prayer)
+			.aspeed(this.aspeed)
+			.build();
+	}
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java b/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java
index 54e85a67e..1b22e42ef 100644
--- a/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java
+++ b/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java
@@ -58,7 +58,6 @@ import net.runelite.client.callback.ClientThread;
 import net.runelite.client.config.RuneLiteConfig;
 import net.runelite.client.util.AsyncBufferedImage;
 import net.runelite.http.api.item.ItemPrice;
-import net.runelite.http.api.item.ItemStats;
 
 @Singleton
 @Slf4j
@@ -351,11 +350,11 @@ public class ItemManager
 	 * @return item stats
 	 */
 	@Nullable
-	public ItemStats getItemStats(int itemId, boolean allowNote)
+	public ItemStats getItemStats(int itemId)
 	{
 		ItemComposition itemComposition = getItemComposition(itemId);
 
-		if (itemComposition == null || itemComposition.getName() == null || (!allowNote && itemComposition.getNote() != -1))
+		if (itemComposition.getName() == null || itemComposition.getNote() != -1)
 		{
 			return null;
 		}
@@ -363,6 +362,28 @@ public class ItemManager
 		return itemStats.get(canonicalize(itemId));
 	}
 
+	/**
+	 * Look up an item's stats
+	 *
+	 * @param itemId item id
+	 * @return item stats
+	 * @deprecated See {@link #getItemStats(int)}
+	 */
+	@Nullable
+	@Deprecated
+	public net.runelite.http.api.item.ItemStats getItemStats(int itemId, boolean allowNote)
+	{
+		ItemComposition itemComposition = getItemComposition(itemId);
+
+		if (itemComposition.getName() == null || (!allowNote && itemComposition.getNote() != -1))
+		{
+			return null;
+		}
+
+		var stats = itemStats.get(canonicalize(itemId));
+		return stats != null ? stats.toHttpApiFormat() : null;
+	}
+
 	/**
 	 * Search for tradeable items based on item name
 	 *
diff --git a/runelite-client/src/main/java/net/runelite/client/game/ItemStats.java b/runelite-client/src/main/java/net/runelite/client/game/ItemStats.java
new file mode 100644
index 000000000..1357879c1
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/game/ItemStats.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2018, Tomas Slusny <slusnucky@gmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.client.game;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Value;
+
+@Value
+public class ItemStats
+{
+	boolean equipable;
+	double weight;
+	@SerializedName("ge_limit")
+	int geLimit;
+
+	ItemEquipmentStats equipment;
+
+	net.runelite.http.api.item.ItemStats toHttpApiFormat()
+	{
+		var equipment = this.equipment == null ? null : this.equipment.toHttpApiFormat();
+		return new net.runelite.http.api.item.ItemStats(this.equipable, this.weight, this.geLimit, equipment);
+	}
+}
+
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTag.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTag.java
index 064a51fd2..a0a56fede 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTag.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTag.java
@@ -25,17 +25,14 @@
  */
 package net.runelite.client.plugins.banktags;
 
-import net.runelite.client.plugins.banktags.tabs.Layout;
-
 /**
  * A bank tag. Plugins may implement this interface to define custom bank tags.
  * You may register a BankTag with {@link TagManager#registerTag(String, BankTag)} to
- * make it searchable in the bank UI and tab interface. You may also set the active bank tag
- * via {@link BankTagsService#openBankTag(BankTag)}, regardless of if the banktag is registered.
+ * make it searchable in the bank UI and tab interface.
  *
  * @see TagManager#registerTag(String, BankTag)
  * @see TagManager#unregisterTag(String)
- * @see BankTagsService#openBankTag(BankTag)
+ * @see BankTagsService#openBankTag(String, int)
  */
 public interface BankTag
 {
@@ -45,13 +42,4 @@ public interface BankTag
 	 * @return
 	 */
 	boolean contains(int itemId);
-
-	/**
-	 * The tag layout
-	 * @return the layout for the tag, or null for no layout
-	 */
-	default Layout layout()
-	{
-		return null;
-	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java
index c70ba0cc2..d75f620c4 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java
@@ -68,7 +68,6 @@ import net.runelite.client.plugins.banktags.tabs.LayoutManager;
 import net.runelite.client.plugins.banktags.tabs.TabInterface;
 import static net.runelite.client.plugins.banktags.tabs.TabInterface.FILTERED_CHARS;
 import net.runelite.client.plugins.banktags.tabs.TabSprites;
-import net.runelite.client.plugins.banktags.tabs.TagTab;
 import net.runelite.client.util.Text;
 
 @PluginDescriptor(
@@ -141,7 +140,16 @@ public class BankTagsPlugin extends Plugin implements BankTagsService
 	private BankTagsConfig config;
 
 	@Getter
-	private BankTag activeTag;
+	private String activeTag;
+
+	@Getter
+	private BankTag activeBankTag;
+
+	@Getter
+	private Layout activeLayout;
+
+	@Getter
+	private int options;
 
 	@Override
 	public void configure(Binder binder)
@@ -323,7 +331,7 @@ public class BankTagsPlugin extends Plugin implements BankTagsService
 				final int itemId = intStack[intStackSize - 1];
 				String searchfilter = stringStack[stringStackSize - 1];
 
-				BankTag tag = activeTag;
+				BankTag tag = activeBankTag;
 				boolean tagSearch = true;
 				// Shared storage uses ~bankmain_filteritem too. Allow using tag searches in it but don't
 				// apply the tag search from the active tab.
@@ -345,7 +353,7 @@ public class BankTagsPlugin extends Plugin implements BankTagsService
 					tag = buildSearchFilterBankTag(searchfilter);
 				}
 
-				if (itemId == -1 && tag.layout() != null)
+				if (itemId == -1 && activeLayout != null)
 				{
 					// item -1 always passes on a laid out tab so items can be dragged to it
 					return;
@@ -364,12 +372,12 @@ public class BankTagsPlugin extends Plugin implements BankTagsService
 				}
 				break;
 			case "getSearchingTagTab":
-				intStack[intStackSize - 1] = activeTag != null ? 1 : 0;
+				intStack[intStackSize - 1] = activeBankTag != null ? 1 : 0;
 				break;
 			case "bankBuildTab":
 				// Use the per-tab view when we want to hide the separators to avoid having to reposition items &
 				// recomputing the scroll height.
-				if (activeTag != null && (tabInterface.isTagTabActive() || config.removeSeparators() || activeTag.layout() != null))
+				if (activeBankTag != null && (tabInterface.isTagTabActive() || config.removeSeparators() || activeLayout != null))
 				{
 					var stack = client.getIntStack();
 					var sz = client.getIntStackSize();
@@ -455,32 +463,28 @@ public class BankTagsPlugin extends Plugin implements BankTagsService
 		}
 	}
 
-	public void open(TagTab tab)
+	public void openTag(String tag, Layout layout)
 	{
-		if (tab == null)
+		openTag(tag, layout, OPTION_ALLOW_MODIFICATIONS);
+	}
+
+	public void openTag(String tag, Layout layout, int options)
+	{
+		if (tag == null)
 		{
-			activeTag = null;
+			this.activeTag = null;
+			this.activeBankTag = null;
+			this.activeLayout = null;
+			this.options = 0;
 			return;
 		}
 
-		// custom tags are combined with the tab
-		final BankTag custom = tagManager.findTag(tab.getTag());
+		this.activeTag = tag;
+		this.activeBankTag = buildSearchFilterBankTag(tag);
+		this.activeLayout = layout;
+		this.options = options;
 
-		activeTag = new BankTag()
-		{
-			@Override
-			public boolean contains(int itemId)
-			{
-				return tagManager.findTag(itemId, tab.getTag())
-					|| (custom != null && custom.contains(itemId));
-			}
-
-			@Override
-			public Layout layout()
-			{
-				return tab.getLayout();
-			}
-		};
+		tabInterface.openTag(tag, layout, options, true);
 	}
 
 	private BankTag buildSearchFilterBankTag(String tag)
@@ -491,19 +495,26 @@ public class BankTagsPlugin extends Plugin implements BankTagsService
 			|| (custom != null && custom.contains(itemId));
 	}
 
+	public void openBankTag(String name)
+	{
+		openBankTag(name, OPTION_ALLOW_MODIFICATIONS);
+	}
+
 	@Override
-	public void openTagTab(TagTab tagTab)
+	public void openBankTag(String name, int options)
 	{
-		tabInterface.closeTag(false);
-		open(tagTab);
-		bankSearch.layoutBank();
+		Layout layout = layoutManager.loadLayout(name);
+		openTag(name, layout, options);
 	}
 
 	@Override
-	public void openBankTag(BankTag bankTag)
+	public void closeBankTag()
 	{
 		tabInterface.closeTag(false);
-		activeTag = bankTag;
+		this.activeTag = null;
+		this.activeBankTag = null;
+		this.activeLayout = null;
+		this.options = 0;
 		bankSearch.layoutBank();
 	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsService.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsService.java
index da41171e1..641a7701c 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsService.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsService.java
@@ -25,19 +25,62 @@
  */
 package net.runelite.client.plugins.banktags;
 
-import net.runelite.client.plugins.banktags.tabs.TagTab;
+import javax.annotation.Nullable;
+import net.runelite.client.plugins.banktags.tabs.Layout;
 
+/**
+ * API for the bank tags plugin
+ *
+ * @see TagManager
+ * @see net.runelite.client.plugins.banktags.tabs.TabManager
+ * @see net.runelite.client.plugins.banktags.tabs.LayoutManager
+ */
 public interface BankTagsService
 {
 	/**
-	 * Open the given tag tab.
-	 * @param tagTab
+	 * Bank tag is allowed to be modified
+	 */
+	int OPTION_ALLOW_MODIFICATIONS = 1;
+	/**
+	 * Option to hide the tag name from the "Remove-tag" menu option.
+	 */
+	int OPTION_HIDE_REMOVE_TAG_NAME = 2;
+
+	/**
+	 * Open the given bank tag. The tag may have an associated {@link net.runelite.client.plugins.banktags.tabs.TagTab},
+	 * but this isn't required. If the tag has an associated {@link net.runelite.client.plugins.banktags.tabs.Layout},
+	 * the layout will be applied.
+	 *
+	 * @param tag the tag name
+	 * @param options
+	 * @see #OPTION_ALLOW_MODIFICATIONS
+	 * @see #OPTION_HIDE_REMOVE_TAG_NAME
+	 */
+	void openBankTag(String tag, int options);
+
+	/**
+	 * Close the currently open {@link BankTag}.
+	 */
+	void closeBankTag();
+
+	/**
+	 * Get the currently active bank tag
+	 * @return
+	 */
+	@Nullable
+	String getActiveTag();
+
+	/**
+	 * Get the currently open {@link BankTag}
+	 * @return
 	 */
-	void openTagTab(TagTab tagTab);
+	@Nullable
+	BankTag getActiveBankTag();
 
 	/**
-	 * Open the given bank tag.
-	 * @param bankTag
+	 * Get the currently active {@link Layout}
+	 * @return
 	 */
-	void openBankTag(BankTag bankTag);
+	@Nullable
+	Layout getActiveLayout();
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/AutoLayout.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/AutoLayout.java
index b511b7672..4dfe7af9c 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/AutoLayout.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/AutoLayout.java
@@ -27,5 +27,5 @@ package net.runelite.client.plugins.banktags.tabs;
 
 public interface AutoLayout
 {
-	Layout generateLayout(TagTab tab);
+	Layout generateLayout(Layout previous);
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/Layout.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/Layout.java
index f4cab6500..1ac4778e7 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/Layout.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/Layout.java
@@ -26,25 +26,30 @@
 package net.runelite.client.plugins.banktags.tabs;
 
 import java.util.Arrays;
+import lombok.Getter;
 import lombok.NonNull;
 
 public class Layout
 {
+	@Getter
+	private final String tag;
 	private int[] layout;
-	boolean dirty = true;
 
-	public Layout()
+	public Layout(String tag)
 	{
+		this.tag = tag;
 		this.layout = new int[0];
 	}
 
-	public Layout(@NonNull int[] layout)
+	public Layout(String tag, @NonNull int[] layout)
 	{
+		this.tag = tag;
 		this.layout = layout;
 	}
 
 	public Layout(Layout other)
 	{
+		tag = other.tag;
 		layout = other.layout.clone();
 	}
 
@@ -83,7 +88,6 @@ public class Layout
 		}
 
 		layout[pos] = itemId;
-		dirty = true;
 	}
 
 	public void addItem(int itemId)
@@ -93,8 +97,6 @@ public class Layout
 
 	public void addItemAfter(int itemId, int pos)
 	{
-		dirty = true;
-
 		int i;
 		for (i = pos; i < layout.length; ++i)
 		{
@@ -116,7 +118,6 @@ public class Layout
 			if (layout[i] == itemId)
 			{
 				layout[i] = -1;
-				dirty = true;
 			}
 		}
 	}
@@ -129,7 +130,6 @@ public class Layout
 		}
 
 		layout[pos] = -1;
-		dirty = true;
 	}
 
 	void swap(int sidx, int tidx)
@@ -137,7 +137,6 @@ public class Layout
 		int sid = layout[sidx];
 		layout[sidx] = layout[tidx];
 		layout[tidx] = sid;
-		dirty = true;
 	}
 
 	void insert(int sidx, int tidx)
@@ -169,7 +168,6 @@ public class Layout
 			System.arraycopy(layout, tidx, layout, tidx + 1, i - tidx);
 			layout[tidx] = sid;
 		}
-		dirty = true;
 	}
 
 	public int count(int itemId)
@@ -198,6 +196,5 @@ public class Layout
 			Arrays.fill(n, layout.length, size, -1);
 		}
 		layout = n;
-		dirty = true;
 	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/LayoutManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/LayoutManager.java
index 808a60f1a..5afe3a3dd 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/LayoutManager.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/LayoutManager.java
@@ -25,7 +25,6 @@
  */
 package net.runelite.client.plugins.banktags.tabs;
 
-import com.google.common.util.concurrent.Runnables;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -34,6 +33,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import lombok.NonNull;
@@ -65,6 +65,7 @@ import net.runelite.api.widgets.JavaScriptCallback;
 import net.runelite.api.widgets.Widget;
 import net.runelite.client.chat.ChatMessageManager;
 import net.runelite.client.chat.QueuedMessage;
+import net.runelite.client.config.ConfigManager;
 import net.runelite.client.eventbus.EventBus;
 import net.runelite.client.eventbus.Subscribe;
 import net.runelite.client.game.ItemManager;
@@ -80,6 +81,9 @@ import static net.runelite.client.plugins.banktags.BankTagsPlugin.BANK_ITEM_STAR
 import static net.runelite.client.plugins.banktags.BankTagsPlugin.BANK_ITEM_WIDTH;
 import static net.runelite.client.plugins.banktags.BankTagsPlugin.BANK_ITEM_X_PADDING;
 import static net.runelite.client.plugins.banktags.BankTagsPlugin.BANK_ITEM_Y_PADDING;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.CONFIG_GROUP;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.TAG_LAYOUT_PREFIX;
+import net.runelite.client.plugins.banktags.BankTagsService;
 import static net.runelite.client.plugins.banktags.tabs.TabInterface.DUPLICATE_ITEM;
 import static net.runelite.client.plugins.banktags.tabs.TabInterface.REMOVE_LAYOUT;
 import net.runelite.client.util.Text;
@@ -93,29 +97,27 @@ public class LayoutManager
 	private final BankTagsPlugin plugin;
 	private final ChatboxPanelManager chatboxPanelManager;
 	private final BankSearch bankSearch;
-	private final TabManager tabManager;
-	private final TabInterface tabInterface;
 	private final ChatMessageManager chatMessageManager;
 	private final PotionStorage potionStorage;
 	private final EventBus eventBus;
+	private final ConfigManager configManager;
 
 	private final List<PluginAutoLayout> autoLayouts = new ArrayList<>();
 
 	@Inject
 	LayoutManager(Client client, ItemManager itemManager, BankTagsPlugin plugin, ChatboxPanelManager chatboxPanelManager,
-		BankSearch bankSearch, TabManager tabManager, TabInterface tabInterface, ChatMessageManager chatMessageManager,
-		PotionStorage potionStorage, EventBus eventBus)
+		BankSearch bankSearch, ChatMessageManager chatMessageManager,
+		PotionStorage potionStorage, EventBus eventBus, ConfigManager configManager)
 	{
 		this.client = client;
 		this.itemManager = itemManager;
 		this.plugin = plugin;
 		this.chatboxPanelManager = chatboxPanelManager;
 		this.bankSearch = bankSearch;
-		this.tabManager = tabManager;
-		this.tabInterface = tabInterface;
 		this.chatMessageManager = chatMessageManager;
 		this.potionStorage = potionStorage;
 		this.eventBus = eventBus;
+		this.configManager = configManager;
 
 		registerAutoLayout(plugin, "Default", new DefaultLayout());
 	}
@@ -132,6 +134,45 @@ public class LayoutManager
 		eventBus.unregister(potionStorage);
 	}
 
+	@Nullable
+	public Layout loadLayout(String tag)
+	{
+		String layoutStr = configManager.getConfiguration(CONFIG_GROUP, TAG_LAYOUT_PREFIX + Text.standardize(tag));
+		if (layoutStr != null)
+		{
+			List<String> layoutList = Text.fromCSV(layoutStr);
+			int[] layout = new int[layoutList.size()];
+			for (int i = 0; i < layoutList.size(); ++i)
+			{
+				layout[i] = Integer.parseInt(layoutList.get(i));
+			}
+
+			return new Layout(tag, layout);
+		}
+		return null;
+	}
+
+	public void saveLayout(Layout layout)
+	{
+		String tag = layout.getTag();
+		int[] l = layout.getLayout();
+		StringBuilder sb = new StringBuilder(l.length * 5);
+		for (int i = 0; i < l.length; ++i)
+		{
+			if (i > 0)
+			{
+				sb.append(',');
+			}
+			sb.append(l[i]);
+		}
+		configManager.setConfiguration(CONFIG_GROUP, TAG_LAYOUT_PREFIX + Text.standardize(tag), sb.toString());
+	}
+
+	public void removeLayout(String tag)
+	{
+		configManager.unsetConfiguration(CONFIG_GROUP, TAG_LAYOUT_PREFIX + Text.standardize(tag));
+	}
+
 	private void layout(Layout l)
 	{
 		ItemContainer bank = client.getItemContainer(InventoryID.BANK);
@@ -283,7 +324,7 @@ public class LayoutManager
 
 		if (modified)
 		{
-			tabManager.save();
+			saveLayout(l);
 		}
 	}
 
@@ -326,9 +367,9 @@ public class LayoutManager
 				c.setItemQuantity(Integer.MAX_VALUE);
 				c.setItemQuantityMode(ItemQuantityMode.NEVER);
 
-				// TabInterface rewrites these to RUNELITE types and adds handlers
-				if (tabInterface.isActive())
+				if ((plugin.getOptions() & BankTagsService.OPTION_ALLOW_MODIFICATIONS) != 0)
 				{
+					// TabInterface rewrites these to RUNELITE types and adds handlers
 					c.setAction(7 - 1, DUPLICATE_ITEM);
 					c.setAction(8 - 1, REMOVE_LAYOUT);
 				}
@@ -443,7 +484,7 @@ public class LayoutManager
 			l.insert(sidx, tidx);
 		}
 
-		tabManager.save();
+		saveLayout(l);
 		bankSearch.layoutBank();
 	}
 
@@ -545,7 +586,7 @@ public class LayoutManager
 		{
 			resetWidgets();
 
-			BankTag activeTag = plugin.getActiveTag();
+			BankTag activeTag = plugin.getActiveBankTag();
 			if (activeTag != null)
 			{
 				// Since the script vm isn't reentrant, we can't call into POTIONSTORE_DOSES/POTIONSTORE_WITHDRAW_DOSES
@@ -554,7 +595,7 @@ public class LayoutManager
 				// them by the time the inv transmit listener runs.
 				potionStorage.cachePotions = true;
 
-				Layout layout = activeTag.layout();
+				Layout layout = plugin.getActiveLayout();
 				if (layout != null)
 				{
 					layout(layout);
@@ -587,8 +628,7 @@ public class LayoutManager
 		}
 	}
 
-	@Subscribe
-	private void onMenuEntryAdded(MenuEntryAdded event)
+	void onMenuEntryAdded(MenuEntryAdded event, TabInterface tabInterface)
 	{
 		if (event.getActionParam1() == ComponentID.BANK_CONTENT_CONTAINER && event.getOption().equals(TabInterface.DISABLE_LAYOUT))
 		{
@@ -601,8 +641,8 @@ public class LayoutManager
 					.setType(MenuAction.RUNELITE_HIGH_PRIORITY)
 					.onClick(e ->
 					{
-						TagTab tab = tabManager.find(Text.removeTags(e.getTarget()));
-						if (tab != tabInterface.getActiveTab())
+						String tag = Text.standardize(e.getTarget());
+						if (!tag.equals(tabInterface.getActiveTag()))
 						{
 							chatMessageManager.queue(QueuedMessage.builder()
 								.type(ChatMessageType.CONSOLE)
@@ -611,20 +651,15 @@ public class LayoutManager
 							return;
 						}
 
-						Layout old = tab.getLayout();
-						Layout new_ = autoLayout.autoLayout.generateLayout(tab);
-						tab.setLayout(new_);
-						bankSearch.layoutBank();
-						tab.setLayout(old);
-						tabManager.save();
+						Layout old = plugin.getActiveLayout();
+						Layout new_ = autoLayout.autoLayout.generateLayout(old);
+						plugin.openTag(tag, new_);
 
 						chatboxPanelManager.openTextMenuInput("Tab laid out using the '" + autoLayout.getName() + "' layout.")
 							.option("1. Keep", () ->
-							{
-								tab.setLayout(new_);
-								tabManager.save();
-							})
-							.option("2. Undo", Runnables.doNothing())
+								saveLayout(new_))
+							.option("2. Undo", () ->
+								plugin.openTag(tag, old))
 							.onClose(bankSearch::layoutBank)
 							.build();
 					});
@@ -632,13 +667,10 @@ public class LayoutManager
 		}
 	}
 
-	@Subscribe(priority = -1)
-	private void onMenuOptionClicked(MenuOptionClicked event)
+	void onMenuOptionClicked(MenuOptionClicked event)
 	{
 		// Update widget index of the menu so withdraws work in laid out tabs.
-		BankTag activeTag = plugin.getActiveTag();
-		if (event.getParam1() == ComponentID.BANK_ITEM_CONTAINER
-			&& activeTag != null && !tabInterface.isTagTabActive() && activeTag.layout() != null)
+		if (event.getParam1() == ComponentID.BANK_ITEM_CONTAINER && plugin.getActiveLayout() != null)
 		{
 			MenuEntry menu = event.getMenuEntry();
 			Widget w = menu.getWidget();
@@ -695,9 +727,9 @@ public class LayoutManager
 	private class DefaultLayout implements AutoLayout
 	{
 		@Override
-		public Layout generateLayout(TagTab tab)
+		public Layout generateLayout(Layout previous)
 		{
-			Layout l = new Layout(tab.getLayout());
+			Layout l = new Layout(previous);
 			List<Integer> removed = new ArrayList<>();
 
 			// Equipment
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/PotionStorage.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/PotionStorage.java
index 532ec72eb..fd0237836 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/PotionStorage.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/PotionStorage.java
@@ -95,7 +95,7 @@ class PotionStorage
 			if (layout)
 			{
 				layout = false;
-				BankTag activeTag = plugin.getActiveTag();
+				BankTag activeTag = plugin.getActiveBankTag();
 				if (activeTag != null)
 				{
 					bankSearch.layoutBank();
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 d66e683a9..328ed842c 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
@@ -88,6 +88,7 @@ import net.runelite.client.plugins.banktags.BankTagsConfig;
 import net.runelite.client.plugins.banktags.BankTagsPlugin;
 import static net.runelite.client.plugins.banktags.BankTagsPlugin.TAG_SEARCH;
 import static net.runelite.client.plugins.banktags.BankTagsPlugin.VAR_TAG_SUFFIX;
+import net.runelite.client.plugins.banktags.BankTagsService;
 import net.runelite.client.plugins.banktags.TagManager;
 import net.runelite.client.ui.JagexColors;
 import net.runelite.client.util.ColorUtil;
@@ -112,7 +113,7 @@ public class TabInterface
 	private static final String REMOVE_TAG = "Remove-tag";
 	private static final String TAG_GEAR = "Tag-equipment";
 	private static final String TAG_INVENTORY = "Tag-inventory";
-	private static final String TAB_MENU_KEY = "tagtabs";
+	private static final String TAGTABS = "tagtabs";
 	private static final String OPEN_TAB_MENU = "View tag tabs";
 	static final String ENABLE_LAYOUT = "Enable layout";
 	static final String DISABLE_LAYOUT = "Disable layout";
@@ -150,6 +151,7 @@ public class TabInterface
 	private final ItemManager itemManager;
 	private final TagManager tagManager;
 	private final TabManager tabManager;
+	private final LayoutManager layoutManager;
 	private final ChatboxPanelManager chatboxPanelManager;
 	private final BankTagsConfig config;
 	private final BankSearch bankSearch;
@@ -158,7 +160,9 @@ public class TabInterface
 
 	private boolean enabled;
 	@Getter
-	private TagTab activeTab;
+	private String activeTag;
+	private Layout activeLayout;
+	private int activeOptions;
 	@Getter
 	private boolean tagTabActive;
 	private int tagTabFirstChildIdx = -1;
@@ -180,6 +184,7 @@ public class TabInterface
 		final ItemManager itemManager,
 		final TagManager tagManager,
 		final TabManager tabManager,
+		final LayoutManager layoutManager,
 		final ChatboxPanelManager chatboxPanelManager,
 		final BankTagsConfig config,
 		final BankSearch bankSearch,
@@ -193,6 +198,7 @@ public class TabInterface
 		this.itemManager = itemManager;
 		this.tagManager = tagManager;
 		this.tabManager = tabManager;
+		this.layoutManager = layoutManager;
 		this.chatboxPanelManager = chatboxPanelManager;
 		this.config = config;
 		this.bankSearch = bankSearch;
@@ -200,11 +206,6 @@ public class TabInterface
 		this.chatMessageManager = chatMessageManager;
 	}
 
-	public boolean isActive()
-	{
-		return activeTab != null;
-	}
-
 	@Subscribe
 	public void onScriptPreFired(ScriptPreFired event)
 	{
@@ -246,7 +247,7 @@ public class TabInterface
 		}
 		else if (event.getScriptId() == ScriptID.BANKMAIN_SEARCH_TOGGLE)
 		{
-			if (activeTab != null || tagTabActive)
+			if (activeTag != null || tagTabActive)
 			{
 				// close the active tab when search is pressed
 				closeTag(false);
@@ -270,10 +271,10 @@ public class TabInterface
 				Widget bankTitle = client.getWidget(ComponentID.BANK_TITLE_BAR);
 				bankTitle.setText("Tag tab tab");
 			}
-			else if (activeTab != null)
+			else if (activeTag != null)
 			{
 				Widget bankTitle = client.getWidget(ComponentID.BANK_TITLE_BAR);
-				bankTitle.setText("Tag tab <col=ff0000>" + activeTab.getTag() + "</col>");
+				bankTitle.setText("Tag tab <col=ff0000>" + activeTag + "</col>");
 			}
 
 			// Recompute scroll size. Only required for tag tab tab since it doesn't show real items.
@@ -295,8 +296,10 @@ public class TabInterface
 		{
 			enabled = false;
 			upButton = downButton = newTab = scrollComponent = parent = null;
-			activeTab = null;
-			plugin.open(null);
+			activeTag = null;
+			activeLayout = null;
+			activeOptions = 0;
+			plugin.openTag(null, null);
 			tagTabActive = false;
 			tagTabFirstChildIdx = -1;
 		}
@@ -346,9 +349,8 @@ public class TabInterface
 			// the server will resync the last opened vanilla tab when the bank is opened
 			client.setVarbit(Varbits.CURRENT_BANK_TAB, 0);
 			var tab = config.tab();
-			activeTab = tabManager.find(tab);
-			tagTabActive = TAB_MENU_KEY.equals(tab);
-			plugin.open(activeTab);
+			var layout = layoutManager.loadLayout(tab);
+			plugin.openTag(tab, layout);
 		}
 
 		// Move equipment button to the titlebar
@@ -391,8 +393,10 @@ public class TabInterface
 	public void deinit()
 	{
 		enabled = false;
-		activeTab = null;
-		plugin.open(null);
+		activeTag = null;
+		activeLayout = null;
+		activeOptions = 0;
+		plugin.openTag(null, null);
 
 		upButton = downButton = newTab = scrollComponent = null;
 
@@ -422,11 +426,11 @@ public class TabInterface
 
 		if (!Strings.isNullOrEmpty(event.getMenuTarget()))
 		{
-			if (activeTab != null && Text.removeTags(event.getMenuTarget()).equals(activeTab.getTag()))
+			if (activeTag != null && Text.removeTags(event.getMenuTarget()).equals(activeTag))
 			{
 				for (Integer item : items)
 				{
-					tagManager.addTag(item, activeTab.getTag(), false);
+					tagManager.addTag(item, activeTag, false);
 				}
 
 				reloadActiveTab();
@@ -498,7 +502,7 @@ public class TabInterface
 					rebuildTabs();
 					rebuildTagTabTab();
 
-					if (activeTab != null && tab.getTag().equals(activeTab.getTag()))
+					if (tab.getTag().equals(activeTag))
 					{
 						bankSearch.reset(true);
 					}
@@ -513,7 +517,7 @@ public class TabInterface
 				break;
 			case NEWTAB_OP_OPEN_TAB_MENU:
 				client.setVarbit(Varbits.CURRENT_BANK_TAB, 0);
-				openNamedTag(TAB_MENU_KEY, true);
+				openTag(TAGTABS, null, 0, true);
 				break;
 		}
 	}
@@ -559,7 +563,7 @@ public class TabInterface
 
 		if (dataIter.hasNext())
 		{
-			Layout l = new Layout();
+			Layout l = new Layout(name);
 			while (dataIter.hasNext())
 			{
 				final int idx = Integer.parseInt(dataIter.next());
@@ -568,7 +572,7 @@ public class TabInterface
 				tagManager.addTag(itemId, name, false);
 			}
 
-			tab.setLayout(l);
+			layoutManager.saveLayout(l);
 		}
 
 		return tab;
@@ -596,8 +600,7 @@ public class TabInterface
 		name = sb.toString();
 
 		TagTab tab = tabManager.load(name);
-		Layout l = new Layout();
-		tab.setLayout(l);
+		Layout l = new Layout(name);
 
 		while (dataIter.hasNext())
 		{
@@ -622,6 +625,8 @@ public class TabInterface
 			tagManager.addTag(itemId, name, itemId < 0);
 		}
 
+		layoutManager.saveLayout(l);
+
 		return tab;
 	}
 
@@ -642,15 +647,16 @@ public class TabInterface
 
 				Widget clicked = event.getSource();
 
-				TagTab tab = tabManager.find(Text.removeTags(clicked.getName()));
+				String tag = Text.removeTags(clicked.getName());
 
-				if (tab.equals(activeTab))
+				if (tag.equals(activeTag))
 				{
 					closeTag(true);
 				}
 				else
 				{
-					openTag(tab, true);
+					Layout layout = layoutManager.loadLayout(tag);
+					plugin.openTag(tag, layout);
 				}
 
 				client.playSoundEffect(SoundEffectID.UI_BOOP);
@@ -677,23 +683,26 @@ public class TabInterface
 			}
 			case TAB_OP_LAYOUT:
 			{
-				TagTab tab = tabManager.find(Text.removeTags(event.getSource().getName()));
-				if (tab == null)
-				{
-					return;
-				}
+				String tag = Text.removeTags(event.getSource().getName());
+				Layout layout = layoutManager.loadLayout(tag);
 
-				if (!tab.hasLayout())
+				if (layout == null)
 				{
-					tab.setLayout(new Layout());
-					sendChatMessage("Tag tab '" + tab.getTag() + "' is now in layout mode. You may reorder the items without changing their order in the bank.");
+					layout = new Layout(tag);
+					layoutManager.saveLayout(layout);
+					sendChatMessage("Tag tab '" + tag + "' is now in layout mode. You may reorder the items without changing their order in the bank.");
 				}
 				else
 				{
-					tab.setLayout(null);
-					sendChatMessage("Tag tab '" + tab.getTag() + "' is no longer in layout mode");
+					layoutManager.removeLayout(tag);
+					layout = null;
+					sendChatMessage("Tag tab '" + tag + "' is no longer in layout mode");
+				}
+
+				if (tag.equals(activeTag))
+				{
+					plugin.openTag(tag, layout);
 				}
-				tabManager.save();
 
 				bankSearch.layoutBank();
 				break;
@@ -714,7 +723,9 @@ public class TabInterface
 				break;
 			case TAB_OP_EXPORT_TAB:
 				final List<String> data = new ArrayList<>();
-				final TagTab tagTab = tabManager.find(Text.removeTags(event.getOpbase()));
+				final String tag = Text.removeTags(event.getOpbase());
+				final TagTab tagTab = tabManager.find(tag);
+				final Layout layout = layoutManager.loadLayout(tag);
 				data.add("banktags");
 				data.add("1");
 				data.add(tagTab.getTag());
@@ -722,22 +733,22 @@ public class TabInterface
 
 				for (Integer item : tagManager.getItemsForTag(tagTab.getTag()))
 				{
-					if (!tagTab.hasLayout() || tagTab.getLayout().count(item) == 0)
+					if (layout == null || layout.count(item) == 0)
 					{
 						data.add(String.valueOf(item));
 					}
 				}
 
-				if (tagTab.hasLayout())
+				if (layout != null)
 				{
 					data.add("layout");
-					int[] layout = tagTab.getLayout().getLayout();
-					for (int idx = 0; idx < layout.length; ++idx)
+					int[] l = layout.getLayout();
+					for (int idx = 0; idx < l.length; ++idx)
 					{
-						if (layout[idx] != -1)
+						if (l[idx] != -1)
 						{
 							data.add(String.valueOf(idx));
-							data.add(String.valueOf(layout[idx]));
+							data.add(String.valueOf(l[idx]));
 						}
 					}
 				}
@@ -756,11 +767,11 @@ public class TabInterface
 	@Subscribe
 	private void onMenuEntryAdded(MenuEntryAdded event)
 	{
-		if (activeTab != null
+		if ((activeOptions & BankTagsService.OPTION_ALLOW_MODIFICATIONS) != 0
 			&& event.getActionParam1() == ComponentID.BANK_ITEM_CONTAINER
 			&& event.getOption().equals("Examine"))
 		{
-			if (activeTab.hasLayout())
+			if (activeLayout != null)
 			{
 				client.createMenuEntry(-1)
 					.setParam0(event.getActionParam0())
@@ -773,7 +784,7 @@ public class TabInterface
 					.onClick(this::opDuplicateItem);
 			}
 
-			if (activeTab.hasLayout() && activeTab.getLayout().count(itemManager.canonicalize(event.getItemId())) > 1)
+			if (activeLayout != null && activeLayout.count(itemManager.canonicalize(event.getItemId())) > 1)
 			{
 				client.createMenuEntry(-1)
 					.setParam0(event.getActionParam0())
@@ -790,25 +801,25 @@ public class TabInterface
 					.setParam0(event.getActionParam0())
 					.setParam1(event.getActionParam1())
 					.setTarget(event.getTarget())
-					.setOption(REMOVE_TAG + " (" + activeTab.getTag() + ")")
+					.setOption((activeOptions & BankTagsService.OPTION_HIDE_REMOVE_TAG_NAME) == 0 ? REMOVE_TAG + " (" + activeTag + ")" : REMOVE_TAG)
 					.setType(MenuAction.RUNELITE)
 					.setIdentifier(event.getIdentifier())
 					.setItemId(event.getItemId())
 					.onClick(e ->
 					{
 						final int itemId = e.getItemId();
-						if (activeTab.hasLayout())
+						if (activeLayout != null)
 						{
-							activeTab.getLayout().removeItem(itemId);
-							tabManager.save();
+							activeLayout.removeItem(itemId);
+							layoutManager.saveLayout(activeLayout);
 						}
-						tagManager.removeTag(itemId, activeTab.getTag());
+						tagManager.removeTag(itemId, activeTag);
 						bankSearch.layoutBank(); // re-layout to filter the removed item out
 					});
 			}
 		}
 		// Duplicate/Remove on layout placeholders
-		else if (activeTab != null
+		else if (activeTag != null
 			&& event.getActionParam1() == ComponentID.BANK_ITEM_CONTAINER
 			&& event.getOption().equals(DUPLICATE_ITEM))
 		{
@@ -816,7 +827,7 @@ public class TabInterface
 			event.getMenuEntry().setType(MenuAction.RUNELITE_LOW_PRIORITY);
 			event.getMenuEntry().onClick(this::opDuplicateItem);
 		}
-		if (activeTab != null
+		if (activeTag != null
 			&& event.getActionParam1() == ComponentID.BANK_ITEM_CONTAINER
 			&& event.getOption().equals(REMOVE_LAYOUT))
 		{
@@ -828,9 +839,9 @@ public class TabInterface
 		{
 			createMenuEntry(event, TAG_INVENTORY, event.getTarget());
 
-			if (activeTab != null)
+			if (activeTag != null && (activeOptions & BankTagsService.OPTION_ALLOW_MODIFICATIONS) != 0)
 			{
-				createMenuEntry(event, TAG_INVENTORY, ColorUtil.wrapWithColorTag(activeTab.getTag(), HILIGHT_COLOR));
+				createMenuEntry(event, TAG_INVENTORY, ColorUtil.wrapWithColorTag(activeTag, HILIGHT_COLOR));
 			}
 		}
 		else if (event.getActionParam1() == ComponentID.BANK_DEPOSIT_EQUIPMENT
@@ -838,30 +849,32 @@ public class TabInterface
 		{
 			createMenuEntry(event, TAG_GEAR, event.getTarget());
 
-			if (activeTab != null)
+			if (activeTag != null && (activeOptions & BankTagsService.OPTION_ALLOW_MODIFICATIONS) != 0)
 			{
-				createMenuEntry(event, TAG_GEAR, ColorUtil.wrapWithColorTag(activeTab.getTag(), HILIGHT_COLOR));
+				createMenuEntry(event, TAG_GEAR, ColorUtil.wrapWithColorTag(activeTag, HILIGHT_COLOR));
 			}
 		}
+
+		layoutManager.onMenuEntryAdded(event, this);
 	}
 
 	private void opDuplicateItem(MenuEntry e)
 	{
 		int id = itemManager.canonicalize(e.getItemId());
 		log.debug("Duplicate item {} at {}", itemManager.getItemComposition(id).getName(), e.getParam0());
-		activeTab.getLayout().addItemAfter(id, e.getParam0());
-		tabManager.save();
+		activeLayout.addItemAfter(id, e.getParam0());
+		layoutManager.saveLayout(activeLayout);
 		bankSearch.layoutBank();
 	}
 
 	private void opRemoveLayout(MenuEntry e)
 	{
-		activeTab.getLayout().removeItemAtPos(e.getParam0());
-		tabManager.save();
+		activeLayout.removeItemAtPos(e.getParam0());
+		layoutManager.saveLayout(activeLayout);
 		bankSearch.layoutBank();
 	}
 
-	@Subscribe
+	@Subscribe(priority = -1)
 	public void onMenuOptionClicked(MenuOptionClicked event)
 	{
 		// Close the chatbox input when clicking on things in the bank, to mimic how actions like withdrawing
@@ -888,6 +901,8 @@ public class TabInterface
 		{
 			handleDeposit(event, event.getParam1() == ComponentID.BANK_DEPOSIT_INVENTORY);
 		}
+
+		layoutManager.onMenuOptionClicked(event);
 	}
 
 	@Subscribe
@@ -903,8 +918,9 @@ public class TabInterface
 
 		// Returning early or nulling the drag release listener has no effect. Hence, we need to
 		// null the draggedOnWidget instead.
-		if (draggedWidget.getId() == ComponentID.BANK_ITEM_CONTAINER && activeTab != null && !activeTab.hasLayout()
-			&& config.preventTagTabDrags())
+		if (draggedWidget.getId() == ComponentID.BANK_ITEM_CONTAINER && activeTag != null
+			&& (activeLayout == null && config.preventTagTabDrags()
+			|| (activeOptions & BankTagsService.OPTION_ALLOW_MODIFICATIONS) == 0))
 		{
 			client.setDraggedOnWidget(null);
 		}
@@ -994,9 +1010,9 @@ public class TabInterface
 	{
 		w.setAction(TAB_OP_OPEN_TAG, VIEW_TAB);
 		w.setAction(TAB_OP_CHANGE_ICON, CHANGE_ICON);
-		if (!TAB_MENU_KEY.equals(tab.getTag()))
+		if (!TAGTABS.equals(tab.getTag()))
 		{
-			w.setAction(TAB_OP_LAYOUT, tab.hasLayout() ? DISABLE_LAYOUT : ENABLE_LAYOUT);
+			w.setAction(TAB_OP_LAYOUT, activeLayout != null ? DISABLE_LAYOUT : ENABLE_LAYOUT);
 		}
 		w.setAction(TAB_OP_EXPORT_TAB, EXPORT_TAB);
 		w.setAction(TAB_OP_RENAME_TAB, RENAME_TAB);
@@ -1025,7 +1041,7 @@ public class TabInterface
 
 	private void deleteTab(String tag)
 	{
-		if (activeTab != null && activeTab.getTag().equals(tag))
+		if (tag.equals(activeTag))
 		{
 			closeTag(true);
 		}
@@ -1033,6 +1049,8 @@ public class TabInterface
 		tabManager.remove(tag);
 		tabManager.save();
 
+		layoutManager.removeLayout(tag);
+
 		repositionButtons();
 		rebuildTabs();
 		rebuildTagTabTab();
@@ -1050,12 +1068,27 @@ public class TabInterface
 					if (tabManager.find(newTag) == null)
 					{
 						TagTab tagTab = tabManager.find(oldTag);
+						Layout layout = layoutManager.loadLayout(oldTag);
+
+						tabManager.remove(oldTag); // remove the icon
+						layoutManager.removeLayout(oldTag); // remove the layout
 
-						tabManager.remove(oldTag); // remove the icon/layout of the old tag
 						tagTab.setTag(newTag);
+
 						tabManager.add(tagTab);
 						tabManager.save();
 
+						if (activeTag.equals(oldTag))
+						{
+							activeTag = newTag;
+						}
+
+						if (layout != null)
+						{
+							Layout newLayout = new Layout(newTag, layout.getLayout());
+							layoutManager.saveLayout(newLayout);
+						}
+
 						tagManager.renameTag(oldTag, newTag); // rename tag on items
 
 						rebuildTabs();
@@ -1070,12 +1103,11 @@ public class TabInterface
 								clientThread.invoke(() ->
 								{
 									tagManager.renameTag(oldTag, newTag);
-									final String activeTag = activeTab != null ? activeTab.getTag() : "";
 									deleteTab(oldTag);
 
-									if (activeTag.equals(oldTag))
+									if (oldTag.equals(activeTag))
 									{
-										openNamedTag(newTag, true);
+										plugin.openBankTag(newTag);
 									}
 									else
 									{
@@ -1124,25 +1156,13 @@ public class TabInterface
 		layoutTabs();
 	}
 
-	private void openNamedTag(String name, boolean relayout)
+	public void openTag(String tag, Layout layout, int options, boolean relayout)
 	{
-		activeTab = tabManager.find(name);
-		tagTabActive = TAB_MENU_KEY.equals(name);
-		plugin.open(activeTab);
-		config.tab(name);
-
-		if (relayout)
-		{
-			bankSearch.reset(true);
-		}
-	}
-
-	private void openTag(TagTab tab, boolean relayout)
-	{
-		activeTab = tab;
-		tagTabActive = tab != null && TAB_MENU_KEY.equals(tab.getTag());
-		plugin.open(activeTab);
-		config.tab(tab != null ? tab.getTag() : "");
+		activeTag = tag;
+		activeLayout = layout;
+		activeOptions = options;
+		tagTabActive = TAGTABS.equals(tag);
+		config.tab(tag);
 
 		if (relayout)
 		{
@@ -1152,9 +1172,11 @@ public class TabInterface
 
 	public void closeTag(boolean relayout)
 	{
-		activeTab = null;
+		activeTag = null;
+		activeLayout = null;
+		activeOptions = 0;
 		tagTabActive = false;
-		plugin.open(null);
+		plugin.openTag(null, null);
 		config.tab("");
 
 		if (relayout)
@@ -1165,9 +1187,9 @@ public class TabInterface
 
 	public void reloadActiveTab()
 	{
-		if (activeTab != null)
+		if (activeTag != null)
 		{
-			bankSearch.reset(true);
+			plugin.openBankTag(activeTag);
 		}
 	}
 
@@ -1235,7 +1257,7 @@ public class TabInterface
 		for (TagTab tab : tabs)
 		{
 			Widget background = createGraphic(parent, ColorUtil.wrapWithColorTag(tab.getTag(), HILIGHT_COLOR),
-				(activeTab == tab ? TabSprites.TAB_BACKGROUND_ACTIVE : TabSprites.TAB_BACKGROUND).getSpriteId(),
+				(tab.getTag().equals(activeTag) ? TabSprites.TAB_BACKGROUND_ACTIVE : TabSprites.TAB_BACKGROUND).getSpriteId(),
 				-1, TAB_WIDTH, TAB_HEIGHT, MARGIN, -1);
 			addTabActions(tab, background);
 
@@ -1270,7 +1292,9 @@ public class TabInterface
 		int y = scrollComponent.getOriginalY();
 		y += MARGIN;
 
-		for (int i = tabScrollOffset; i < tabScrollOffset + tabCount && i * 2 + 1 < children.length - TAGTAB_CHILD_OFFSET; ++i)
+		for (int i = tabScrollOffset;
+			i < tabScrollOffset + tabCount && i * 2 + 1 < children.length - TAGTAB_CHILD_OFFSET && children[TAGTAB_CHILD_OFFSET + i * 2] != null;
+			++i)
 		{
 			Widget background = children[TAGTAB_CHILD_OFFSET + i * 2];
 			background.setOriginalY(y);
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabManager.java
index 59359aa4e..63272535f 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabManager.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabManager.java
@@ -38,7 +38,6 @@ import net.runelite.api.ItemID;
 import net.runelite.client.config.ConfigManager;
 import static net.runelite.client.plugins.banktags.BankTagsPlugin.CONFIG_GROUP;
 import static net.runelite.client.plugins.banktags.BankTagsPlugin.TAG_ICON_PREFIX;
-import static net.runelite.client.plugins.banktags.BankTagsPlugin.TAG_LAYOUT_PREFIX;
 import static net.runelite.client.plugins.banktags.BankTagsPlugin.TAG_TABS_CONFIG;
 import net.runelite.client.util.Text;
 import org.apache.commons.lang3.math.NumberUtils;
@@ -61,10 +60,6 @@ public class TabManager
 		if (!contains(tagTab.getTag()))
 		{
 			tabs.add(tagTab);
-			if (tagTab.hasLayout())
-			{
-				tagTab.getLayout().dirty = true;
-			}
 		}
 	}
 
@@ -94,19 +89,6 @@ public class TabManager
 			String item = configManager.getConfiguration(CONFIG_GROUP, TAG_ICON_PREFIX + tag);
 			int itemid = NumberUtils.toInt(item, ItemID.SPADE);
 			tagTab = new TagTab(itemid, tag);
-
-			String layoutStr = configManager.getConfiguration(CONFIG_GROUP, TAG_LAYOUT_PREFIX + tag);
-			if (layoutStr != null)
-			{
-				List<String> layoutList = Text.fromCSV(layoutStr);
-				int[] layout = new int[layoutList.size()];
-				for (int i = 0; i < layoutList.size(); ++i)
-				{
-					layout[i] = Integer.parseInt(layoutList.get(i));
-				}
-
-				tagTab.setLayout(new Layout(layout));
-			}
 		}
 
 		return tagTab;
@@ -115,19 +97,6 @@ public class TabManager
 	private void save(TagTab tab)
 	{
 		setIcon(tab.getTag(), tab.getIconItemId());
-
-		if (tab.hasLayout())
-		{
-			Layout layout = tab.getLayout();
-			if (layout.dirty)
-			{
-				setLayout(tab.getTag(), layout.getLayout());
-			}
-		}
-		else
-		{
-			removeLayout(tab.getTag());
-		}
 	}
 
 	void swap(String tagToMove, String tagDestination)
@@ -160,7 +129,6 @@ public class TabManager
 		{
 			tabs.remove(tagTab);
 			removeIcon(tag);
-			removeLayout(tag);
 		}
 	}
 
@@ -185,25 +153,6 @@ public class TabManager
 		configManager.setConfiguration(CONFIG_GROUP, TAG_ICON_PREFIX + Text.standardize(tag), itemId);
 	}
 
-	private void removeLayout(String tag)
-	{
-		configManager.unsetConfiguration(CONFIG_GROUP, TAG_LAYOUT_PREFIX + Text.standardize(tag));
-	}
-
-	private void setLayout(String tag, int[] layout)
-	{
-		StringBuilder sb = new StringBuilder(layout.length * 5);
-		for (int i = 0; i < layout.length; ++i)
-		{
-			if (i > 0)
-			{
-				sb.append(',');
-			}
-			sb.append(layout[i]);
-		}
-		configManager.setConfiguration(CONFIG_GROUP, TAG_LAYOUT_PREFIX + Text.standardize(tag), sb.toString());
-	}
-
 	int size()
 	{
 		return tabs.size();
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TagTab.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TagTab.java
index f85af47ba..7755924a3 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TagTab.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TagTab.java
@@ -30,7 +30,7 @@ import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 
 /**
- * A bank tag tab.
+ * A bank tag tab. This represents one of the tabs on the left hand side of the bank.
  *
  * @see TabManager#add(TagTab)
  * @see TabManager#remove(String)
@@ -42,16 +42,10 @@ public class TagTab
 {
 	private String tag;
 	private int iconItemId;
-	private Layout layout;
 
 	TagTab(int iconItemId, String tag)
 	{
 		this.iconItemId = iconItemId;
 		this.tag = tag;
 	}
-
-	public boolean hasLayout()
-	{
-		return layout != null;
-	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsConfig.java
index 4ed53b575..ed389c593 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsConfig.java
@@ -189,6 +189,28 @@ public interface ChatCommandsConfig extends Config
 		return true;
 	}
 
+	@ConfigItem(
+		position = 14,
+		keyName = "ca",
+		name = "CA Command",
+		description = "Configures whether the Combat Achievements command is enabled<br> !ca"
+	)
+	default boolean ca()
+	{
+		return true;
+	}
+
+	@ConfigItem(
+		position = 15,
+		keyName = "clog",
+		name = "CLOG Command",
+		description = "Configures whether the Collection Log command is enabled<br> !clog"
+	)
+	default boolean clog()
+	{
+		return true;
+	}
+
 	@ConfigItem(
 		position = 20,
 		keyName = "clearSingleWord",
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 178e83e96..88596b367 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
@@ -142,6 +142,8 @@ public class ChatCommandsPlugin extends Plugin
 	private static final String LEAGUE_POINTS_COMMAND = "!lp";
 	private static final String SOUL_WARS_ZEAL_COMMAND = "!sw";
 	private static final String PET_LIST_COMMAND = "!pets";
+	private static final String CA_COMMAND = "!ca";
+	private static final String CLOG_COMMAND = "!clog";
 
 	@VisibleForTesting
 	static final int ADV_LOG_EXPLOITS_TEXT_INDEX = 1;
@@ -223,6 +225,8 @@ public class ChatCommandsPlugin extends Plugin
 		chatCommandManager.registerCommandAsync(DUEL_ARENA_COMMAND, this::duelArenaLookup, this::duelArenaSubmit);
 		chatCommandManager.registerCommandAsync(SOUL_WARS_ZEAL_COMMAND, this::soulWarsZealLookup);
 		chatCommandManager.registerCommandAsync(PET_LIST_COMMAND, this::petListLookup, this::petListSubmit);
+		chatCommandManager.registerCommandAsync(CA_COMMAND, this::caLookup, this::caSubmit);
+		chatCommandManager.registerCommandAsync(CLOG_COMMAND, this::clogLookup, this::clogSubmit);
 
 		clientThread.invoke(() ->
 		{
@@ -261,6 +265,8 @@ public class ChatCommandsPlugin extends Plugin
 		chatCommandManager.unregisterCommand(DUEL_ARENA_COMMAND);
 		chatCommandManager.unregisterCommand(SOUL_WARS_ZEAL_COMMAND);
 		chatCommandManager.unregisterCommand(PET_LIST_COMMAND);
+		chatCommandManager.unregisterCommand(CA_COMMAND);
+		chatCommandManager.unregisterCommand(CLOG_COMMAND);
 	}
 
 	@Provides
@@ -1390,6 +1396,145 @@ public class ChatCommandsPlugin extends Plugin
 		return true;
 	}
 
+	private void caLookup(ChatMessage chatMessage, String message)
+	{
+		if (!config.ca())
+		{
+			return;
+		}
+
+		ChatMessageType type = chatMessage.getType();
+
+		final String player;
+		if (type.equals(ChatMessageType.PRIVATECHATOUT))
+		{
+			player = client.getLocalPlayer().getName();
+		}
+		else
+		{
+			player = Text.sanitize(chatMessage.getName());
+		}
+
+		int num;
+		try
+		{
+			num = chatClient.getKc(player, "Combat Achievements");
+		}
+		catch (IOException ex)
+		{
+			log.debug("unable to lookup combat achievements");
+			return;
+		}
+
+		String response = new ChatMessageBuilder()
+			.append(ChatColorType.NORMAL)
+			.append("Combat Achievements: ")
+			.append(ChatColorType.HIGHLIGHT)
+			.append(Integer.toString(num))
+			.build();
+
+		log.debug("Setting response {}", response);
+		final MessageNode messageNode = chatMessage.getMessageNode();
+		messageNode.setRuneLiteFormatMessage(response);
+		client.refreshChat();
+	}
+
+	private boolean caSubmit(ChatInput chatInput, String value)
+	{
+		final int tasks = client.getVarbitValue(Varbits.COMBAT_TASK_EASY) +
+			client.getVarbitValue(Varbits.COMBAT_TASK_MEDIUM) +
+			client.getVarbitValue(Varbits.COMBAT_TASK_HARD) +
+			client.getVarbitValue(Varbits.COMBAT_TASK_ELITE) +
+			client.getVarbitValue(Varbits.COMBAT_TASK_MASTER) +
+			client.getVarbitValue(Varbits.COMBAT_TASK_GRANDMASTER);
+		final String playerName = client.getLocalPlayer().getName();
+
+		executor.execute(() ->
+		{
+			try
+			{
+				chatClient.submitKc(playerName, "Combat Achievements", tasks);
+			}
+			catch (Exception ex)
+			{
+				log.warn("unable to submit combat achievements", ex);
+			}
+			finally
+			{
+				chatInput.resume();
+			}
+		});
+
+		return true;
+	}
+
+	private void clogLookup(ChatMessage chatMessage, String message)
+	{
+		if (!config.clog())
+		{
+			return;
+		}
+
+		ChatMessageType type = chatMessage.getType();
+
+		final String player;
+		if (type.equals(ChatMessageType.PRIVATECHATOUT))
+		{
+			player = client.getLocalPlayer().getName();
+		}
+		else
+		{
+			player = Text.sanitize(chatMessage.getName());
+		}
+
+		int num;
+		try
+		{
+			num = chatClient.getKc(player, "Collections Logged");
+		}
+		catch (IOException ex)
+		{
+			log.debug("unable to lookup clog");
+			return;
+		}
+
+		String response = new ChatMessageBuilder()
+			.append(ChatColorType.NORMAL)
+			.append("Collections Logged: ")
+			.append(ChatColorType.HIGHLIGHT)
+			.append(Integer.toString(num))
+			.build();
+
+		log.debug("Setting response {}", response);
+		final MessageNode messageNode = chatMessage.getMessageNode();
+		messageNode.setRuneLiteFormatMessage(response);
+		client.refreshChat();
+	}
+
+	private boolean clogSubmit(ChatInput chatInput, String value)
+	{
+		final int clog = client.getVarpValue(VarPlayer.CLOG_LOGGED);
+		final String playerName = client.getLocalPlayer().getName();
+
+		executor.execute(() ->
+		{
+			try
+			{
+				chatClient.submitKc(playerName, "Collections Logged", clog);
+			}
+			catch (Exception ex)
+			{
+				log.warn("unable to submit clog", ex);
+			}
+			finally
+			{
+				chatInput.resume();
+			}
+		});
+
+		return true;
+	}
+
 	/**
 	 * Looks up the item price and changes the original message to the
 	 * response.
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 3d5a1a269..2e708e1ed 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
@@ -35,6 +35,7 @@ import javax.annotation.Nullable;
 import lombok.AccessLevel;
 import lombok.Getter;
 import net.runelite.api.Client;
+import net.runelite.api.EquipmentInventorySlot;
 import static net.runelite.api.EquipmentInventorySlot.AMMO;
 import static net.runelite.api.EquipmentInventorySlot.AMULET;
 import static net.runelite.api.EquipmentInventorySlot.BODY;
@@ -125,9 +126,9 @@ public class EmoteClue extends ClueScroll implements LocationClueScroll
 		new EmoteClue("Headbang in the Fight Arena pub. Equip a pirate bandana, a dragonstone necklace and and a magic longbow.", "Fight Arena pub", OUTSIDE_THE_BAR_BY_THE_FIGHT_ARENA, new WorldPoint(2568, 3149, 0), HEADBANG, any("Any pirate bandana", item(PIRATE_BANDANA), item(PIRATE_BANDANA_7124), item(PIRATE_BANDANA_7130), item(PIRATE_BANDANA_7136)), item(DRAGON_NECKLACE), item(MAGIC_LONGBOW)),
 		new EmoteClue("Do a jig at the barrows chest. Beware of double agents! Equip any full barrows set.", "Barrows chest", BARROWS_CHEST, new WorldPoint(3551, 9694, 0), DOUBLE_AGENT_141, JIG, any("Any full barrows set", all(any("Ahrim's hood", item(AHRIMS_HOOD), range(AHRIMS_HOOD_100, AHRIMS_HOOD_0)), any("Ahrim's staff", item(AHRIMS_STAFF), range(AHRIMS_STAFF_100, AHRIMS_STAFF_0)), any("Ahrim's robetop", item(AHRIMS_ROBETOP), range(AHRIMS_ROBETOP_100, AHRIMS_ROBETOP_0)), any("Ahrim's robeskirt", item(AHRIMS_ROBESKIRT), range(AHRIMS_ROBESKIRT_100, AHRIMS_ROBESKIRT_0))), all(any("Dharok's helm", item(DHAROKS_HELM), range(DHAROKS_HELM_100, DHAROKS_HELM_0)), any("Dharok's greataxe", item(DHAROKS_GREATAXE), range(DHAROKS_GREATAXE_100, DHAROKS_GREATAXE_0)), any("Dharok's platebody", item(DHAROKS_PLATEBODY), range(DHAROKS_PLATEBODY_100, DHAROKS_PLATEBODY_0)), any("Dharok's platelegs", item(DHAROKS_PLATELEGS), range(DHAROKS_PLATELEGS_100, DHAROKS_PLATELEGS_0))), all(any("Guthan's helm", item(GUTHANS_HELM), range(GUTHANS_HELM_100, GUTHANS_HELM_0)), any("Guthan's warspear", item(GUTHANS_WARSPEAR), range(GUTHANS_WARSPEAR_100, GUTHANS_WARSPEAR_0)), any("Guthan's platebody", item(GUTHANS_PLATEBODY), range(GUTHANS_PLATEBODY_100, GUTHANS_PLATEBODY_0)), any("Guthan's chainskirt", item(GUTHANS_CHAINSKIRT), range(GUTHANS_CHAINSKIRT_100, GUTHANS_CHAINSKIRT_0))), all(any("Karil's coif", item(KARILS_COIF), range(KARILS_COIF_100, KARILS_COIF_0)), any("Karil's crossbow", item(KARILS_CROSSBOW), range(KARILS_CROSSBOW_100, KARILS_CROSSBOW_0)), any("Karil's leathertop", item(KARILS_LEATHERTOP), range(KARILS_LEATHERTOP_100, KARILS_LEATHERTOP_0)), any("Karil's leatherskirt", item(KARILS_LEATHERSKIRT), range(KARILS_LEATHERSKIRT_100, KARILS_LEATHERSKIRT_0))), all(any("Torag's helm", item(TORAGS_HELM), range(TORAGS_HELM_100, TORAGS_HELM_0)), any("Torag's hammers", item(TORAGS_HAMMERS), range(TORAGS_HAMMERS_100, TORAGS_HAMMERS_0)), any("Torag's platebody", item(TORAGS_PLATEBODY), range(TORAGS_PLATEBODY_100, TORAGS_PLATEBODY_0)), any("Torag's platelegs", item(TORAGS_PLATELEGS), range(TORAGS_PLATELEGS_100, TORAGS_PLATELEGS_0))), all(any("Verac's helm", item(VERACS_HELM), range(VERACS_HELM_100, VERACS_HELM_0)), any("Verac's flail", item(VERACS_FLAIL), range(VERACS_FLAIL_100, VERACS_FLAIL_0)), any("Verac's brassard", item(VERACS_BRASSARD), range(VERACS_BRASSARD_100, VERACS_BRASSARD_0)), any("Verac's plateskirt", item(VERACS_PLATESKIRT), range(VERACS_PLATESKIRT_100, VERACS_PLATESKIRT_0))))),
 		new EmoteClue("Jig at Jiggig. Beware of double agents! Equip a Rune spear, rune platelegs and any rune heraldic helm.", "Jiggig", IN_THE_MIDDLE_OF_JIGGIG, new WorldPoint(2477, 3047, 0), DOUBLE_AGENT_108, JIG, range("Any rune heraldic helm", RUNE_HELM_H1, RUNE_HELM_H5), item(RUNE_SPEAR), item(RUNE_PLATELEGS)),
-		new EmoteClue("Cheer at the games room. Have nothing equipped at all when you do.", "Burthorpe Games Room", null, new WorldPoint(2207, 4952, 0), CHEER, emptySlot("Nothing at all", HEAD, CAPE, AMULET, WEAPON, BODY, SHIELD, LEGS, GLOVES, BOOTS, RING, AMMO)),
-		new EmoteClue("Panic on the pier where you catch the Fishing trawler. Have nothing equipped at all when you do.", "Fishing trawler", null, new WorldPoint(2676, 3169, 0), PANIC, emptySlot("Nothing at all", HEAD, CAPE, AMULET, WEAPON, BODY, SHIELD, LEGS, GLOVES, BOOTS, RING, AMMO)),
-		new EmoteClue("Panic in the heart of the Haunted Woods. Beware of double agents! Have no items equipped when you do.", "Haunted Woods (ALQ)", null, new WorldPoint(3611, 3492, 0), DOUBLE_AGENT_108, PANIC, emptySlot("Nothing at all", HEAD, CAPE, AMULET, WEAPON, BODY, SHIELD, LEGS, GLOVES, BOOTS, RING, AMMO)),
+		new EmoteClue("Cheer at the games room. Have nothing equipped at all when you do.", "Burthorpe Games Room", null, new WorldPoint(2207, 4952, 0), CHEER, emptySlot("Nothing at all", EquipmentInventorySlot.values())),
+		new EmoteClue("Panic on the pier where you catch the Fishing trawler. Have nothing equipped at all when you do.", "Fishing trawler", null, new WorldPoint(2676, 3169, 0), PANIC, emptySlot("Nothing at all", EquipmentInventorySlot.values())),
+		new EmoteClue("Panic in the heart of the Haunted Woods. Beware of double agents! Have no items equipped when you do.", "Haunted Woods (ALQ)", null, new WorldPoint(3611, 3492, 0), DOUBLE_AGENT_108, PANIC, emptySlot("Nothing at all", EquipmentInventorySlot.values())),
 		new EmoteClue("Show your anger towards the Statue of Saradomin in Ellamaria's garden. Beware of double agents! Equip a zamorak godsword.", "Varrock Castle", BY_THE_BEAR_CAGE_IN_VARROCK_PALACE_GARDENS, new WorldPoint(3230, 3478, 0), DOUBLE_AGENT_141, ANGRY, any("Zamorak godsword", item(ZAMORAK_GODSWORD), item(ZAMORAK_GODSWORD_OR))),
 		new EmoteClue("Show your anger at the Wise old man. Beware of double agents! Equip an abyssal whip, a legend's cape and some spined chaps.", "Draynor Village", BEHIND_MISS_SCHISM_IN_DRAYNOR_VILLAGE, new WorldPoint(3088, 3254, 0), DOUBLE_AGENT_141, ANGRY, any("Abyssal whip", item(ABYSSAL_WHIP), item(VOLCANIC_ABYSSAL_WHIP), item(FROZEN_ABYSSAL_WHIP), item(ABYSSAL_WHIP_OR), item(ABYSSAL_TENTACLE), item(ABYSSAL_TENTACLE_OR)), item(CAPE_OF_LEGENDS), item(SPINED_CHAPS)),
 		new EmoteClue("Beckon by a collection of crystalline maple trees. Beware of double agents! Equip Bryophyta's staff and a nature tiara.", "North of Prifddinas", CRYSTALLINE_MAPLE_TREES, new WorldPoint(2211, 3427, 0), DOUBLE_AGENT_141, BECKON, range("Bryophyta's staff", BRYOPHYTAS_STAFF_UNCHARGED, BRYOPHYTAS_STAFF), item(NATURE_TIARA)),
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/SceneOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/SceneOverlay.java
index 2c089ba73..06b1f7e21 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/SceneOverlay.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/SceneOverlay.java
@@ -136,9 +136,8 @@ public class SceneOverlay extends Overlay
 			boolean first = true;
 			for (int z = startZ; z <= endZ; z += Constants.CHUNK_SIZE)
 			{
-				Point p = Perspective.localToCanvas(client,
-					x << Perspective.LOCAL_COORD_BITS, z << Perspective.LOCAL_COORD_BITS,
-					client.getPlane());
+				LocalPoint lp = new LocalPoint(x << Perspective.LOCAL_COORD_BITS, z << Perspective.LOCAL_COORD_BITS);
+				Point p = Perspective.localToCanvas(client, lp, client.getPlane());
 				if (p != null)
 				{
 					if (first)
@@ -158,9 +157,8 @@ public class SceneOverlay extends Overlay
 			boolean first = true;
 			for (int x = startX; x <= endX; x += Constants.CHUNK_SIZE)
 			{
-				Point p = Perspective.localToCanvas(client,
-					x << Perspective.LOCAL_COORD_BITS, z << Perspective.LOCAL_COORD_BITS,
-					client.getPlane());
+				LocalPoint lp = new LocalPoint(x << Perspective.LOCAL_COORD_BITS, z << Perspective.LOCAL_COORD_BITS);
+				Point p = Perspective.localToCanvas(client, lp, client.getPlane());
 				if (p != null)
 				{
 					if (first)
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java
index b5f9d3bc9..cdca494a9 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java
@@ -93,6 +93,7 @@ import net.runelite.client.events.ConfigChanged;
 import net.runelite.client.events.SessionClose;
 import net.runelite.client.events.SessionOpen;
 import net.runelite.client.game.ItemManager;
+import net.runelite.client.game.ItemStats;
 import net.runelite.client.input.KeyManager;
 import net.runelite.client.input.MouseManager;
 import net.runelite.client.plugins.Plugin;
@@ -106,7 +107,6 @@ import net.runelite.client.util.OSType;
 import net.runelite.client.util.QuantityFormatter;
 import net.runelite.client.util.Text;
 import net.runelite.http.api.ge.GrandExchangeTrade;
-import net.runelite.http.api.item.ItemStats;
 import net.runelite.http.api.worlds.WorldType;
 import org.apache.commons.lang3.time.DurationFormatUtils;
 
@@ -858,7 +858,7 @@ public class GrandExchangePlugin extends Plugin
 
 		if (buy && config.enableGELimits())
 		{
-			final ItemStats itemStats = itemManager.getItemStats(itemId, false);
+			final ItemStats itemStats = itemManager.getItemStats(itemId);
 
 			// If we have item buy limit, append it
 			if (itemStats != null && itemStats.getGeLimit() > 0)
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java
index 2a4e7c5dd..dfa96efc3 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java
@@ -43,12 +43,12 @@ import net.runelite.api.ItemComposition;
 import net.runelite.client.callback.ClientThread;
 import net.runelite.client.config.RuneLiteConfig;
 import net.runelite.client.game.ItemManager;
+import net.runelite.client.game.ItemStats;
 import net.runelite.client.ui.ColorScheme;
 import net.runelite.client.ui.components.IconTextField;
 import net.runelite.client.ui.components.PluginErrorPanel;
 import net.runelite.client.util.AsyncBufferedImage;
 import net.runelite.http.api.item.ItemPrice;
-import net.runelite.http.api.item.ItemStats;
 
 /**
  * This panel holds the search section of the Grand Exchange Plugin.
@@ -215,7 +215,7 @@ class GrandExchangeSearchPanel extends JPanel
 				break;
 			}
 
-			ItemStats itemStats = itemManager.getItemStats(itemId, false);
+			ItemStats itemStats = itemManager.getItemStats(itemId);
 
 			int itemPrice = useActivelyTradedPrice ? itemManager.getWikiPrice(item) : item.getPrice();
 			int itemLimit = itemStats != null ? itemStats.getGeLimit() : 0;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java
index 244423082..cb5aaf3c4 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/hiscore/HiscorePanel.java
@@ -121,7 +121,7 @@ public class HiscorePanel extends PluginPanel
 
 	private static final HiscoreEndpoint[] ENDPOINTS = {
 		HiscoreEndpoint.NORMAL, HiscoreEndpoint.IRONMAN, HiscoreEndpoint.HARDCORE_IRONMAN, HiscoreEndpoint.ULTIMATE_IRONMAN,
-		HiscoreEndpoint.DEADMAN, HiscoreEndpoint.PURE, HiscoreEndpoint.LEVEL_3_SKILLER, HiscoreEndpoint.TOURNAMENT
+		HiscoreEndpoint.DEADMAN, HiscoreEndpoint.PURE, HiscoreEndpoint.LEVEL_3_SKILLER, HiscoreEndpoint.LEAGUE
 	};
 
 	private final HiscorePlugin plugin;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatChanges.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatChanges.java
index eab1a0a39..d1aac0702 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatChanges.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatChanges.java
@@ -187,6 +187,9 @@ public class ItemStatChanges
 		add(combo(food(5), boost(STRENGTH, perc(.05, 1)), new BoostedStatBoost(ATTACK, false, perc(0.02, -3))), SUNSHINE);
 		add(combo(food(5), boost(STRENGTH, 5), new BoostedStatBoost(ATTACK, false, perc(.02, -3))), RUM_28896);
 		add(combo(food(16), boost(WOODCUTTING, 1), heal(ATTACK, -5), heal(FLETCHING, -1)), ECLIPSE_RED);
+		add(combo(food(1), boost(MAGIC, 1), boost(MINING, 1), new BoostedStatBoost(ATTACK, false, perc(.05, -3)), new BoostedStatBoost(DEFENCE, false, perc(.05, -3))), STEAMFORGE_BREW);
+		add(combo(food(16), boost(HUNTER, 1), dec(ATTACK, 5), dec(SLAYER, 1)), BLACKBIRD_RED);
+		add(combo(food(16), boost(RUNECRAFT, 1), dec(ATTACK, 5), dec(FIREMAKING, 1)), METZTONALLI_WHITE);
 
 		// Sq'irk Juice
 		add(heal(RUN_ENERGY, 5), WINTER_SQIRKJUICE);
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java
index 117744335..d0c1e5171 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatOverlay.java
@@ -40,7 +40,9 @@ import net.runelite.api.widgets.ComponentID;
 import net.runelite.api.widgets.InterfaceID;
 import net.runelite.api.widgets.Widget;
 import net.runelite.api.widgets.WidgetUtil;
+import net.runelite.client.game.ItemEquipmentStats;
 import net.runelite.client.game.ItemManager;
+import net.runelite.client.game.ItemStats;
 import net.runelite.client.plugins.itemstats.potions.PotionDuration;
 import net.runelite.client.ui.JagexColors;
 import net.runelite.client.ui.overlay.Overlay;
@@ -48,8 +50,6 @@ import net.runelite.client.ui.overlay.tooltip.Tooltip;
 import net.runelite.client.ui.overlay.tooltip.TooltipManager;
 import net.runelite.client.util.ColorUtil;
 import net.runelite.client.util.QuantityFormatter;
-import net.runelite.http.api.item.ItemEquipmentStats;
-import net.runelite.http.api.item.ItemStats;
 import org.apache.commons.lang3.time.DurationFormatUtils;
 
 public class ItemStatOverlay extends Overlay
@@ -188,7 +188,7 @@ public class ItemStatOverlay extends Overlay
 
 		if (config.equipmentStats())
 		{
-			final ItemStats stats = itemManager.getItemStats(itemId, false);
+			final ItemStats stats = itemManager.getItemStats(itemId);
 
 			if (stats != null)
 			{
@@ -275,7 +275,7 @@ public class ItemStatOverlay extends Overlay
 	private ItemStats getItemStatsFromContainer(ItemContainer container, int slotID)
 	{
 		final Item item = container.getItem(slotID);
-		return item != null ? itemManager.getItemStats(item.getId(), false) : null;
+		return item != null ? itemManager.getItemStats(item.getId()) : null;
 	}
 
 	@VisibleForTesting
@@ -303,7 +303,7 @@ public class ItemStatOverlay extends Overlay
 					{
 						// Account for speed change when two handed weapon gets removed
 						// shield - (2h - unarmed) == shield - 2h + unarmed
-						other = otherEquip.isTwoHanded() ? other.subtract(UNARMED) : null;
+						other = otherEquip.isTwoHanded() ? subtract(other, UNARMED) : null;
 					}
 				}
 			}
@@ -323,7 +323,7 @@ public class ItemStatOverlay extends Overlay
 			}
 		}
 
-		final ItemStats subtracted = s.subtract(other).subtract(offHand);
+		final ItemStats subtracted = subtract(subtract(s, other), offHand);
 		final ItemEquipmentStats e = subtracted.getEquipment();
 
 		final StringBuilder b = new StringBuilder();
@@ -370,6 +370,49 @@ public class ItemStatOverlay extends Overlay
 		return b.toString();
 	}
 
+	private static ItemStats subtract(ItemStats one, ItemStats two)
+	{
+		if (two == null)
+		{
+			return one;
+		}
+
+		final double newWeight = one.getWeight() - two.getWeight();
+		final ItemEquipmentStats newEquipment;
+
+		if (two.getEquipment() != null)
+		{
+			final ItemEquipmentStats equipment = one.getEquipment() != null
+				? one.getEquipment()
+				: ItemEquipmentStats.builder().build();
+
+			newEquipment = ItemEquipmentStats.builder()
+				.slot(equipment.getSlot())
+				.astab(equipment.getAstab() - two.getEquipment().getAstab())
+				.aslash(equipment.getAslash() - two.getEquipment().getAslash())
+				.acrush(equipment.getAcrush() - two.getEquipment().getAcrush())
+				.amagic(equipment.getAmagic() - two.getEquipment().getAmagic())
+				.arange(equipment.getArange() - two.getEquipment().getArange())
+				.dstab(equipment.getDstab() - two.getEquipment().getDstab())
+				.dslash(equipment.getDslash() - two.getEquipment().getDslash())
+				.dcrush(equipment.getDcrush() - two.getEquipment().getDcrush())
+				.dmagic(equipment.getDmagic() - two.getEquipment().getDmagic())
+				.drange(equipment.getDrange() - two.getEquipment().getDrange())
+				.str(equipment.getStr() - two.getEquipment().getStr())
+				.rstr(equipment.getRstr() - two.getEquipment().getRstr())
+				.mdmg(equipment.getMdmg() - two.getEquipment().getMdmg())
+				.prayer(equipment.getPrayer() - two.getEquipment().getPrayer())
+				.aspeed(equipment.getAspeed() - two.getEquipment().getAspeed())
+				.build();
+		}
+		else
+		{
+			newEquipment = one.getEquipment();
+		}
+
+		return new ItemStats(one.isEquipable(), newWeight, 0, newEquipment);
+	}
+
 	private String buildStatChangeString(StatChange c)
 	{
 		StringBuilder b = new StringBuilder();
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatPlugin.java
index a7b8fbad2..4a9d7fad0 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/ItemStatPlugin.java
@@ -56,15 +56,15 @@ import net.runelite.client.callback.ClientThread;
 import net.runelite.client.config.ConfigManager;
 import net.runelite.client.eventbus.Subscribe;
 import net.runelite.client.events.ConfigChanged;
+import net.runelite.client.game.ItemEquipmentStats;
 import net.runelite.client.game.ItemManager;
+import net.runelite.client.game.ItemStats;
 import net.runelite.client.plugins.Plugin;
 import net.runelite.client.plugins.PluginDescriptor;
 import net.runelite.client.ui.FontManager;
 import net.runelite.client.ui.JagexColors;
 import net.runelite.client.ui.overlay.OverlayManager;
 import net.runelite.client.util.QuantityFormatter;
-import net.runelite.http.api.item.ItemEquipmentStats;
-import net.runelite.http.api.item.ItemStats;
 
 @PluginDescriptor(
 	name = "Item Stats",
@@ -166,7 +166,7 @@ public class ItemStatPlugin extends Plugin
 
 	private void createItemInformation(int id)
 	{
-		final ItemStats itemStats = itemManager.getItemStats(id, false);
+		final ItemStats itemStats = itemManager.getItemStats(id);
 
 		if (itemStats == null || !itemStats.isEquipable())
 		{
@@ -314,14 +314,14 @@ public class ItemStatPlugin extends Plugin
 
 		yPos += TEXT_HEIGHT + 8;
 
-		final Map<String, Integer> miscStats = ImmutableMap.of(
+		final Map<String, Object> miscStats = ImmutableMap.of(
 			"Strength", equipmentStats.getStr(),
 			"Ranged Strength", equipmentStats.getRstr(),
 			"Magic Damage", equipmentStats.getMdmg(),
 			"Prayer Bonus", equipmentStats.getPrayer()
 		);
 
-		for (final Map.Entry<String, Integer> miscStat : miscStats.entrySet())
+		for (final Map.Entry<String, Object> miscStat : miscStats.entrySet())
 		{
 			final String name = miscStat.getKey();
 			final String value = miscStat.getValue().toString();
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java
index 317a5a0e4..a86d85b8a 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java
@@ -274,6 +274,9 @@ public class LootTrackerPlugin extends Plugin
 	private static final String ORE_PACK_VM_EVENT = "Ore Pack (Volcanic Mine)";
 
 	private static final String WINTERTODT_SUPPLY_CRATE_EVENT = "Supply crate (Wintertodt)";
+	private static final String WINTERTODT_REWARD_CART_EVENT = "Reward cart (Wintertodt)";
+	private static final String WINTERTODT_LOOT_STRING = "You found some loot: ";
+	private static final int WINTERTODT_REGION = 6461;
 
 	private static final String BAG_FULL_OF_GEMS_PERCY_EVENT = "Bag full of gems (Percy)";
 	private static final String BAG_FULL_OF_GEMS_BELONA_EVENT = "Bag full of gems (Belona)";
@@ -1154,6 +1157,12 @@ public class LootTrackerPlugin extends Plugin
 			return;
 		}
 
+		if (regionID == WINTERTODT_REGION && message.startsWith(WINTERTODT_LOOT_STRING))
+		{
+			onInvChange(collectInvItems(LootRecordType.EVENT, WINTERTODT_REWARD_CART_EVENT, client.getBoostedSkillLevel(Skill.FIREMAKING)));
+			return;
+		}
+
 		if (message.equals(IMPLING_CATCH_MESSAGE))
 		{
 			onInvChange(collectInvItems(LootRecordType.EVENT, client.getLocalPlayer().getInteracting().getName()));
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/prayer/PrayerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/prayer/PrayerPlugin.java
index fb2ed7643..729fcfedb 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/prayer/PrayerPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/prayer/PrayerPlugin.java
@@ -51,12 +51,12 @@ import net.runelite.client.eventbus.EventBus;
 import net.runelite.client.eventbus.Subscribe;
 import net.runelite.client.events.ConfigChanged;
 import net.runelite.client.game.ItemManager;
+import net.runelite.client.game.ItemStats;
 import net.runelite.client.game.SpriteManager;
 import net.runelite.client.plugins.Plugin;
 import net.runelite.client.plugins.PluginDescriptor;
 import net.runelite.client.ui.overlay.OverlayManager;
 import net.runelite.client.ui.overlay.infobox.InfoBoxManager;
-import net.runelite.http.api.item.ItemStats;
 
 @PluginDescriptor(
 	name = "Prayer",
@@ -238,7 +238,7 @@ public class PrayerPlugin extends Plugin
 		int total = 0;
 		for (Item item : items)
 		{
-			ItemStats is = itemManager.getItemStats(item.getId(), false);
+			ItemStats is = itemManager.getItemStats(item.getId());
 			if (is != null && is.getEquipment() != null)
 			{
 				total += is.getEquipment().getPrayer();
diff --git a/runelite-client/src/main/resources/item_variations.json b/runelite-client/src/main/resources/item_variations.json
index acd7b8fe3..d49e6a558 100644
--- a/runelite-client/src/main/resources/item_variations.json
+++ b/runelite-client/src/main/resources/item_variations.json
@@ -9001,6 +9001,10 @@
     19659,
     19662
   ],
+  "arclight": [
+    19675,
+    30305
+  ],
   "clue nest": [
     19712,
     19714,
@@ -11173,15 +11177,18 @@
   ],
   "torva full helm": [
     26376,
-    26382
+    26382,
+    30302
   ],
   "torva platebody": [
     26378,
-    26384
+    26384,
+    30303
   ],
   "torva platelegs": [
     26380,
-    26386
+    26386,
+    30304
   ],
   "blood essence": [
     26390,
diff --git a/runelite-jshell/pom.xml b/runelite-jshell/pom.xml
index d917ca76a..ec09e2887 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.42-SNAPSHOT</version>
+        <version>1.10.43-SNAPSHOT</version>
     </parent>
 
     <artifactId>jshell</artifactId>
diff --git a/runelite-maven-plugin/pom.xml b/runelite-maven-plugin/pom.xml
index 0bbded9a5..d0f7bcff4 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.42-SNAPSHOT</version>
+		<version>1.10.43-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>runelite-maven-plugin</artifactId>


Test patch:
diff --git a/runelite-client/src/test/java/net/runelite/client/game/ItemStatsTest.java b/runelite-client/src/test/java/net/runelite/client/game/ItemStatsTest.java
new file mode 100644
index 000000000..2e83aba6b
--- /dev/null
+++ b/runelite-client/src/test/java/net/runelite/client/game/ItemStatsTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2024, Adam <Adam@sigterm.info>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.client.game;
+
+import net.runelite.api.EquipmentInventorySlot;
+import org.junit.Test;
+
+public class ItemStatsTest
+{
+	private static final ItemStats WEAPON = new ItemStats(true, 0, 0,
+		ItemEquipmentStats.builder()
+			.slot(EquipmentInventorySlot.WEAPON.getSlotIdx())
+			.build());
+
+	@Test
+	public void testMigratedApiDoesNotThrow()
+	{
+		new ItemStats(true, 0, 0, null).toHttpApiFormat();
+		WEAPON.toHttpApiFormat();
+	}
+}
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/itemstats/ItemStatOverlayTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/itemstats/ItemStatOverlayTest.java
index f9bc39ba8..d9c9d2fbb 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/itemstats/ItemStatOverlayTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/itemstats/ItemStatOverlayTest.java
@@ -33,10 +33,10 @@ import net.runelite.api.Client;
 import net.runelite.api.EquipmentInventorySlot;
 import net.runelite.api.InventoryID;
 import net.runelite.api.ItemContainer;
+import net.runelite.client.game.ItemEquipmentStats;
 import net.runelite.client.game.ItemManager;
+import net.runelite.client.game.ItemStats;
 import net.runelite.client.util.Text;
-import net.runelite.http.api.item.ItemEquipmentStats;
-import net.runelite.http.api.item.ItemStats;
 import org.apache.commons.lang3.StringUtils;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/prayer/PrayerPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/prayer/PrayerPluginTest.java
index 81499a18f..641907d96 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/prayer/PrayerPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/prayer/PrayerPluginTest.java
@@ -38,16 +38,15 @@ import net.runelite.api.Skill;
 import net.runelite.api.events.ItemContainerChanged;
 import net.runelite.client.chat.ChatMessageManager;
 import net.runelite.client.config.ConfigManager;
+import net.runelite.client.game.ItemEquipmentStats;
 import net.runelite.client.game.ItemManager;
+import net.runelite.client.game.ItemStats;
 import net.runelite.client.ui.overlay.OverlayManager;
 import net.runelite.client.ui.overlay.infobox.InfoBoxManager;
-import net.runelite.http.api.item.ItemEquipmentStats;
-import net.runelite.http.api.item.ItemStats;
 import static org.junit.Assert.assertEquals;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import org.mockito.Mock;
 import static org.mockito.Mockito.mock;
@@ -106,7 +105,7 @@ public class PrayerPluginTest
 	{
 		ItemContainer itemContainer = mock(ItemContainer.class);
 		when(itemContainer.getItems()).thenReturn(new Item[]{new Item(-1, 1)});
-		when(itemManager.getItemStats(anyInt(), anyBoolean())).thenReturn(HIGH_PRAYER_BONUS_WEAPON);
+		when(itemManager.getItemStats(anyInt())).thenReturn(HIGH_PRAYER_BONUS_WEAPON);
 
 		when(client.isPrayerActive(Prayer.PRESERVE)).thenReturn(true);
 		when(client.getBoostedSkillLevel(Skill.PRAYER)).thenReturn(99);
@@ -149,7 +148,7 @@ public class PrayerPluginTest
 	{
 		ItemContainer itemContainer = mock(ItemContainer.class);
 		when(itemContainer.getItems()).thenReturn(new Item[]{new Item(-1, 1)});
-		when(itemManager.getItemStats(anyInt(), anyBoolean())).thenReturn(HIGH_PRAYER_BONUS_WEAPON);
+		when(itemManager.getItemStats(anyInt())).thenReturn(HIGH_PRAYER_BONUS_WEAPON);
 
 		when(client.isPrayerActive(Prayer.PRESERVE)).thenReturn(true);
 		when(client.getBoostedSkillLevel(Skill.PRAYER)).thenReturn(99);