Repository: runelite/runelite
Pull request #18369 (2024-10-01 17:21:32)

NPC Indicators: Ignore impossibly long NPC respawn times
~~I didn't test this (yet).~~

Played with this for a few days now and seems to work alright.


Patch:
diff --git a/runelite-api/src/main/interfaces/interfaces.toml b/runelite-api/src/main/interfaces/interfaces.toml
index 04952b5af..f54f78c88 100644
--- a/runelite-api/src/main/interfaces/interfaces.toml
+++ b/runelite-api/src/main/interfaces/interfaces.toml
@@ -69,6 +69,7 @@ deposit_equipment=46
 potion_store=48
 incinerator=49
 incinerator_confirm=50
+potionstore_content=52
 equipment_parent=74
 equipment_set_bonus=115
 settings_button=119
diff --git a/runelite-api/src/main/java/net/runelite/api/AnimationID.java b/runelite-api/src/main/java/net/runelite/api/AnimationID.java
index 153e82525..7eb514c70 100644
--- a/runelite-api/src/main/java/net/runelite/api/AnimationID.java
+++ b/runelite-api/src/main/java/net/runelite/api/AnimationID.java
@@ -236,6 +236,10 @@ public final class AnimationID
 	public static final int DENSE_ESSENCE_CHIPPING = 7201;
 	public static final int DENSE_ESSENCE_CHISELING = 7202;
 	public static final int HERBLORE_POTIONMAKING = 363; //used for both herb and secondary
+	public static final int HERBLORE_MIXOLOGY_CONCENTRATE = 11644;
+	public static final int HERBLORE_MIXOLOGY_CRYSTALIZE = 11639;
+	public static final int HERBLORE_MIXOLOGY_HOMOGENIZE = 11634;
+	public static final int HERBLORE_MIXOLOGY_REFINER = 11604;
 	public static final int MAGIC_CHARGING_ORBS = 726;
 	public static final int MAGIC_MAKE_TABLET = 4068;
 	public static final int MAGIC_ENCHANTING_JEWELRY = 931;
@@ -301,6 +305,7 @@ public final class AnimationID
 	// NPC animations
 	public static final int TZTOK_JAD_MAGIC_ATTACK = 2656;
 	public static final int TZTOK_JAD_RANGE_ATTACK = 2652;
+	public static final int CYCLOPS_ATTACK = 4652;
 	public static final int HELLHOUND_DEFENCE = 6566;
 
 	// Farming
diff --git a/runelite-api/src/main/java/net/runelite/api/Constants.java b/runelite-api/src/main/java/net/runelite/api/Constants.java
index d1acb20c7..ef8dca4dc 100644
--- a/runelite-api/src/main/java/net/runelite/api/Constants.java
+++ b/runelite-api/src/main/java/net/runelite/api/Constants.java
@@ -88,6 +88,7 @@ public class Constants
 
 	public static final int TILE_FLAG_BRIDGE = 2;
 	public static final int TILE_FLAG_UNDER_ROOF = 4;
+	public static final int TILE_FLAG_VIS_BELOW = 8;
 
 	/**
 	 * Flag for roof removal to remove the roofs above the player's current position.
diff --git a/runelite-api/src/main/java/net/runelite/api/EnumID.java b/runelite-api/src/main/java/net/runelite/api/EnumID.java
index c8618f6d1..785435c43 100644
--- a/runelite-api/src/main/java/net/runelite/api/EnumID.java
+++ b/runelite-api/src/main/java/net/runelite/api/EnumID.java
@@ -93,4 +93,10 @@ public final class EnumID
 
 	public static final int SPELLBOOKS = 1981;
 	public static final int SPELLBOOKS_SUB = 5280;
+
+	/**
+	 * key: int id
+	 * val: enum potion
+	 */
+	public static final int POTIONSTORE_POTIONS = 4826;
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/FloatProjection.java b/runelite-api/src/main/java/net/runelite/api/FloatProjection.java
new file mode 100644
index 000000000..87c59e567
--- /dev/null
+++ b/runelite-api/src/main/java/net/runelite/api/FloatProjection.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2024, Adam <Adam@sigterm.info>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.api;
+
+public interface FloatProjection extends Projection
+{
+	/**
+	 * Get the projection as a 4x4 matrix
+	 */
+	float[] getProjection();
+}
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 3b28a9d46..3f261f613 100644
--- a/runelite-api/src/main/java/net/runelite/api/ItemID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ItemID.java
@@ -14291,8 +14291,8 @@ public final class ItemID
 	public static final int REAGENT_POUCH = 29996;
 	public static final int OPEN_REAGENT_POUCH = 29998;
 	public static final int POTION_STORAGE = 29999;
-	public static final int PREPOT_DEVICE = 30000;
-	public static final int PREPOT_DEVICE_DISASSEMBLED = 30002;
+	public static final int CHUGGING_BARREL = 30000;
+	public static final int CHUGGING_BARREL_DISASSEMBLED = 30002;
 	public static final int MOX_PASTE = 30005;
 	public static final int AGA_PASTE = 30007;
 	public static final int LYE_PASTE = 30009;
diff --git a/runelite-api/src/main/java/net/runelite/api/NullObjectID.java b/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
index e02cd34fe..33a6ad308 100644
--- a/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/NullObjectID.java
@@ -28808,5 +28808,8 @@ public final class NullObjectID
 	public static final int NULL_55399 = 55399;
 	public static final int NULL_55400 = 55400;
 	public static final int NULL_55401 = 55401;
+	public static final int NULL_55403 = 55403;
+	public static final int NULL_55404 = 55404;
+	public static final int NULL_55405 = 55405;
 	/* 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 38708e16e..dd7905b6d 100644
--- a/runelite-api/src/main/java/net/runelite/api/ObjectID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ObjectID.java
@@ -26207,8 +26207,8 @@ public final class ObjectID
 	public static final int SKELETON_54679 = 54679;
 	public static final int SKELETON_54680 = 54680;
 	public static final int SKELETON_54681 = 54681;
-	public static final int ICEY_WATER = 54682;
-	public static final int ICEY_WATER_54683 = 54683;
+	public static final int ICY_WATER = 54682;
+	public static final int ICY_WATER_54683 = 54683;
 	public static final int GATE_54689 = 54689;
 	public static final int CAVE_54690 = 54690;
 	public static final int CAVE_54691 = 54691;
@@ -26578,5 +26578,9 @@ public final class ObjectID
 	public static final int HOUSE_ADVERTISEMENT_55352 = 55352;
 	public static final int PORTAL_55353 = 55353;
 	public static final int HOPPER_CONTROLS_55402 = 55402;
+	public static final int SHELVES_55406 = 55406;
+	public static final int SHELVES_55407 = 55407;
+	public static final int SHELVES_55408 = 55408;
+	public static final int SHELVES_55409 = 55409;
 	/* This file is automatically generated. Do not edit. */
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/Scene.java b/runelite-api/src/main/java/net/runelite/api/Scene.java
index 886d70998..527f29e4e 100644
--- a/runelite-api/src/main/java/net/runelite/api/Scene.java
+++ b/runelite-api/src/main/java/net/runelite/api/Scene.java
@@ -81,10 +81,14 @@ public interface Scene extends Renderable
 	 */
 	void removeGameObject(GameObject gameObject);
 
-	void generateHouses();
+	void buildRoofs();
+
+	int[][][] getRoofs();
 
 	void setRoofRemovalMode(int flags);
 
+	int getRoofRemovalMode();
+
 	/**
 	 * Get the underlay ids for the scene. The value stored is id + 1, with 0 for no underlay.
 	 * @return
diff --git a/runelite-api/src/main/java/net/runelite/api/ScriptID.java b/runelite-api/src/main/java/net/runelite/api/ScriptID.java
index 0e8d831ba..14c18e6eb 100644
--- a/runelite-api/src/main/java/net/runelite/api/ScriptID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ScriptID.java
@@ -461,4 +461,10 @@ public final class ScriptID
 
 	@ScriptArguments(integer = 2)
 	public static final int MOTHERLODE_HUD_UPDATE = 1634;
+
+	@ScriptArguments(integer = 1)
+	public static final int POTIONSTORE_DOSES = 3750;
+
+	@ScriptArguments(integer = 1)
+	public static final int POTIONSTORE_WITHDRAW_DOSES = 4818;
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/WorldEntity.java b/runelite-api/src/main/java/net/runelite/api/WorldEntity.java
index c4de84c69..2e747e902 100644
--- a/runelite-api/src/main/java/net/runelite/api/WorldEntity.java
+++ b/runelite-api/src/main/java/net/runelite/api/WorldEntity.java
@@ -26,4 +26,5 @@ package net.runelite.api;
 
 public interface WorldEntity
 {
+	WorldView getWorldView();
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/WorldView.java b/runelite-api/src/main/java/net/runelite/api/WorldView.java
index 9037bf5eb..9bc7fee68 100644
--- a/runelite-api/src/main/java/net/runelite/api/WorldView.java
+++ b/runelite-api/src/main/java/net/runelite/api/WorldView.java
@@ -202,4 +202,11 @@ public interface WorldView
 	 * @see InstanceTemplates
 	 */
 	int[][][] getInstanceTemplateChunks();
+
+	/**
+	 * Gets an array of map region IDs that are currently loaded.
+	 *
+	 * @return the map regions
+	 */
+	int[] getMapRegions();
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/events/PreMapLoad.java b/runelite-api/src/main/java/net/runelite/api/events/PreMapLoad.java
new file mode 100644
index 000000000..2e52d70a9
--- /dev/null
+++ b/runelite-api/src/main/java/net/runelite/api/events/PreMapLoad.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2024, Adam <Adam@sigterm.info>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.api.events;
+
+import lombok.Value;
+import net.runelite.api.Scene;
+import net.runelite.api.WorldView;
+
+/**
+ * This event is run from the maploader thread prior to the map load completing.
+ * Most client operations can't be done from this thread safely.
+ * You probably don't want to use this event.
+ */
+@Value
+public class PreMapLoad
+{
+	WorldView worldView;
+	Scene scene;
+}
diff --git a/runelite-api/src/main/java/net/runelite/api/hooks/DrawCallbacks.java b/runelite-api/src/main/java/net/runelite/api/hooks/DrawCallbacks.java
index cbae2943b..4b5281912 100644
--- a/runelite-api/src/main/java/net/runelite/api/hooks/DrawCallbacks.java
+++ b/runelite-api/src/main/java/net/runelite/api/hooks/DrawCallbacks.java
@@ -24,6 +24,8 @@
  */
 package net.runelite.api.hooks;
 
+import java.util.Set;
+import net.runelite.api.GameObject;
 import net.runelite.api.Model;
 import net.runelite.api.Projection;
 import net.runelite.api.Renderable;
@@ -31,33 +33,48 @@ import net.runelite.api.Scene;
 import net.runelite.api.SceneTileModel;
 import net.runelite.api.SceneTilePaint;
 import net.runelite.api.Texture;
+import net.runelite.api.TileObject;
+import net.runelite.api.WorldView;
 
 public interface DrawCallbacks
 {
 	/**
 	 * GPU mode on.
 	 */
-	int GPU = 1;
+	int GPU = 0x1;
 	/**
 	 * GPU hillskew support. Enables the {@link Model#getUnskewedModel()}
 	 * API to get the unskewed model.
 	 */
-	int HILLSKEW = 2;
+	int HILLSKEW = 0x2;
 	/**
 	 * Requests normals be computed for models. Enables the {@link Model#getVertexNormalsX()}
 	 * {@link Model#getVertexNormalsY()} {@link Model#getVertexNormalsZ()} API.
 	 */
-	int NORMALS = 4;
+	int NORMALS = 0x4;
 	/**
 	 * Disable vertex snapping for animations
 	 */
-	int NO_VERTEX_SNAPPING = 8;
+	int NO_VERTEX_SNAPPING = 0x8;
+	/**
+	 * Enable zbuf renderer.
+	 */
+	int ZBUF = 0x10;
 
-	void draw(Projection projection, Scene scene, Renderable renderable, int orientation, int x, int y, int z, long hash);
+	int PASS_OPAQUE = 0;
+	int PASS_ALPHA = 1;
 
-	void drawScenePaint(Scene scene, SceneTilePaint paint, int plane, int tileX, int tileZ);
+	default void draw(Projection projection, Scene scene, Renderable renderable, int orientation, int x, int y, int z, long hash)
+	{
+	}
 
-	void drawSceneTileModel(Scene scene, SceneTileModel model, int tileX, int tileZ);
+	default void drawScenePaint(Scene scene, SceneTilePaint paint, int plane, int tileX, int tileZ)
+	{
+	}
+
+	default void drawSceneTileModel(Scene scene, SceneTileModel model, int tileX, int tileZ)
+	{
+	}
 
 	/**
 	 * Called when a frame should be drawn.
@@ -69,16 +86,24 @@ public interface DrawCallbacks
 	/**
 	 * Called before the scene is drawn
 	 */
-	void drawScene(double cameraX, double cameraY, double cameraZ, double cameraPitch, double cameraYaw, int plane);
+	default void drawScene(double cameraX, double cameraY, double cameraZ, double cameraPitch, double cameraYaw, int plane)
+	{
+	}
 
 	/**
 	 * Called after the scene has been drawn
 	 */
-	void postDrawScene();
+	default void postDrawScene()
+	{
+	}
 
-	void animate(Texture texture, int diff);
+	default void animate(Texture texture, int diff)
+	{
+	}
 
-	void loadScene(Scene scene);
+	default void loadScene(Scene scene)
+	{
+	}
 
 	void swapScene(Scene scene);
 
@@ -86,4 +111,39 @@ public interface DrawCallbacks
 	{
 		return true;
 	}
+
+	default void loadScene(WorldView worldView, Scene scene)
+	{
+	}
+
+	default void despawnWorldView(WorldView worldView)
+	{
+	}
+
+	default void prepareSceneDraw(
+		Scene scene,
+		float cameraX, float cameraY, float cameraZ, float cameraPitch, float cameraYaw,
+		int minLevel, int level, int maxLevel, Set<Integer> hideRoofIds)
+	{
+	}
+
+	default void drawPass(Projection projection, Scene scene, int pass)
+	{
+	}
+
+	default void drawZone(Projection projection, Scene scene, int pass, int zx, int zz)
+	{
+	}
+
+	default void drawDynamic(Scene scene, TileObject tileObject, Renderable r, Model m, int orient, int x, int y, int z)
+	{
+	}
+
+	default void drawTemp(Scene scene, GameObject gameObject, Model m)
+	{
+	}
+
+	default void invalidateZone(Scene scene, int zx, int zz)
+	{
+	}
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/Widget.java b/runelite-api/src/main/java/net/runelite/api/widgets/Widget.java
index 878dc756d..37578d6eb 100644
--- a/runelite-api/src/main/java/net/runelite/api/widgets/Widget.java
+++ b/runelite-api/src/main/java/net/runelite/api/widgets/Widget.java
@@ -993,6 +993,11 @@ public interface Widget
 	 */
 	void setNoScrollThrough(boolean noScrollThrough);
 
+	/**
+	 * {@link net.runelite.api.VarPlayer}s that triggers this widgets varTransmitListener
+	 */
+	int[] getVarTransmitTrigger();
+
 	/**
 	 * {@link net.runelite.api.VarPlayer}s that triggers this widgets varTransmitListener
 	 */
diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml
index 3b2282bd5..5f720d250 100644
--- a/runelite-client/pom.xml
+++ b/runelite-client/pom.xml
@@ -372,10 +372,21 @@
 							<shadedArtifactAttached>true</shadedArtifactAttached>
 							<shadedClassifierName>shaded</shadedClassifierName>
 							<createDependencyReducedPom>false</createDependencyReducedPom>
+							<filters>
+								<filter>
+									<artifact>*:*</artifact>
+									<excludes>
+										<exclude>META-INF/versions/**/module-info.class</exclude>
+									</excludes>
+								</filter>
+							</filters>
 
 							<transformers>
 								<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
-									<mainClass>net.runelite.client.RuneLite</mainClass>
+									<manifestEntries>
+										<Main-Class>net.runelite.client.RuneLite</Main-Class>
+										<Multi-Release>true</Multi-Release>
+									</manifestEntries>
 								</transformer>
 							</transformers>
 						</configuration>
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 8c39c5f22..54e85a67e 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
@@ -171,6 +171,12 @@ public class ItemManager
 		put(GRACEFUL_LEGS_27455, GRACEFUL_LEGS_27453).
 		put(GRACEFUL_GLOVES_27458, GRACEFUL_GLOVES_27456).
 		put(GRACEFUL_BOOTS_27461, GRACEFUL_BOOTS_27459).
+		put(GRACEFUL_HOOD_30047, GRACEFUL_HOOD_30045).
+		put(GRACEFUL_CAPE_30050, GRACEFUL_CAPE_30048).
+		put(GRACEFUL_TOP_30053, GRACEFUL_TOP_30051).
+		put(GRACEFUL_LEGS_30056, GRACEFUL_LEGS_30054).
+		put(GRACEFUL_GLOVES_30059, GRACEFUL_GLOVES_30057).
+		put(GRACEFUL_BOOTS_30062, GRACEFUL_BOOTS_30060).
 
 		put(MAX_CAPE_13342, MAX_CAPE).
 
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/animsmoothing/AnimationSmoothingPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/animsmoothing/AnimationSmoothingPlugin.java
index ede1be560..0616adaaa 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/animsmoothing/AnimationSmoothingPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/animsmoothing/AnimationSmoothingPlugin.java
@@ -69,6 +69,8 @@ public class AnimationSmoothingPlugin extends Plugin
 			case AnimationID.WYRM_ATTACK_MELEE:
 			case AnimationID.WYRM_ATTACK_MAGE:
 
+			case AnimationID.CYCLOPS_ATTACK:
+
 			case AnimationID.TREE_SPIRIT_IDLE:
 			case AnimationID.TREE_SPIRIT_WALK:
 
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/attackstyles/AttackStylesConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/attackstyles/AttackStylesConfig.java
index 7b40f7e6d..68226dd11 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/attackstyles/AttackStylesConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/attackstyles/AttackStylesConfig.java
@@ -27,6 +27,7 @@ package net.runelite.client.plugins.attackstyles;
 import net.runelite.client.config.Config;
 import net.runelite.client.config.ConfigGroup;
 import net.runelite.client.config.ConfigItem;
+import net.runelite.client.config.Notification;
 
 @ConfigGroup("attackIndicator")
 public interface AttackStylesConfig extends Config
@@ -129,4 +130,15 @@ public interface AttackStylesConfig extends Config
 	{
 		return true;
 	}
+
+	@ConfigItem(
+		keyName = "warningNotification",
+		name = "Warning notification",
+		description = "Notification for switching to an unwanted attack style",
+		position = 10
+	)
+	default Notification warningNotification()
+	{
+		return Notification.OFF;
+	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/attackstyles/AttackStylesPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/attackstyles/AttackStylesPlugin.java
index ea8cfd9a0..7f6e43959 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/attackstyles/AttackStylesPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/attackstyles/AttackStylesPlugin.java
@@ -47,6 +47,7 @@ import net.runelite.api.events.ScriptPostFired;
 import net.runelite.api.events.VarbitChanged;
 import net.runelite.api.widgets.ComponentID;
 import net.runelite.api.widgets.Widget;
+import net.runelite.client.Notifier;
 import net.runelite.client.callback.ClientThread;
 import net.runelite.client.chat.ChatColorType;
 import net.runelite.client.chat.ChatMessageBuilder;
@@ -95,6 +96,9 @@ public class AttackStylesPlugin extends Plugin
 	@Inject
 	private ChatMessageManager chatManager;
 
+	@Inject
+	private Notifier notifier;
+
 	@Provides
 	AttackStylesConfig provideConfig(ConfigManager configManager)
 	{
@@ -236,17 +240,22 @@ public class AttackStylesPlugin extends Plugin
 	@Subscribe
 	public void onGameTick(GameTick gameTick)
 	{
-		if (attackStyle != prevAttackStyle && warnedSkillSelected && config.showChatWarnings())
+		if (attackStyle != prevAttackStyle && warnedSkillSelected)
 		{
-			final String message = new ChatMessageBuilder()
-				.append(ChatColorType.HIGHLIGHT)
-				.append("Your attack style has been changed to " + attackStyle.getName())
-				.build();
-
-			chatManager.queue(QueuedMessage.builder()
-				.type(ChatMessageType.CONSOLE)
-				.runeLiteFormattedMessage(message)
-				.build());
+			if (config.showChatWarnings())
+			{
+				final String message = new ChatMessageBuilder()
+					.append(ChatColorType.HIGHLIGHT)
+					.append("Your attack style has been changed to " + attackStyle.getName())
+					.build();
+
+				chatManager.queue(QueuedMessage.builder()
+					.type(ChatMessageType.CONSOLE)
+					.runeLiteFormattedMessage(message)
+					.build());
+			}
+
+			notifier.notify(config.warningNotification(), "Attack style changed to " + attackStyle.getName() + "!");
 		}
 		prevAttackStyle = attackStyle;
 	}
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 1b7c2bd33..c70ba0cc2 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
@@ -187,7 +187,7 @@ public class BankTagsPlugin extends Plugin implements BankTagsService
 		cleanConfig();
 		spriteManager.addSpriteOverrides(TabSprites.values());
 		eventBus.register(tabInterface);
-		eventBus.register(layoutManager);
+		layoutManager.register();
 		clientThread.invokeLater(this::reinitBank);
 	}
 
@@ -195,7 +195,7 @@ public class BankTagsPlugin extends Plugin implements BankTagsService
 	public void shutDown()
 	{
 		eventBus.unregister(tabInterface);
-		eventBus.unregister(layoutManager);
+		layoutManager.unregister();
 		clientThread.invokeLater(() ->
 		{
 			// since the tab interface is unregistered from the eventbus, manually deinit it
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 527fe9f26..c7d452b24 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
@@ -64,6 +64,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.eventbus.EventBus;
 import net.runelite.client.eventbus.Subscribe;
 import net.runelite.client.game.ItemManager;
 import net.runelite.client.game.ItemVariationMapping;
@@ -94,11 +95,15 @@ public class LayoutManager
 	private final TabManager tabManager;
 	private final TabInterface tabInterface;
 	private final ChatMessageManager chatMessageManager;
+	private final PotionStorage potionStorage;
+	private final EventBus eventBus;
 
 	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)
+	LayoutManager(Client client, ItemManager itemManager, BankTagsPlugin plugin, ChatboxPanelManager chatboxPanelManager,
+		BankSearch bankSearch, TabManager tabManager, TabInterface tabInterface, ChatMessageManager chatMessageManager,
+		PotionStorage potionStorage, EventBus eventBus)
 	{
 		this.client = client;
 		this.itemManager = itemManager;
@@ -108,10 +113,24 @@ public class LayoutManager
 		this.tabManager = tabManager;
 		this.tabInterface = tabInterface;
 		this.chatMessageManager = chatMessageManager;
+		this.potionStorage = potionStorage;
+		this.eventBus = eventBus;
 
 		registerAutoLayout(plugin, "Default", new DefaultLayout());
 	}
 
+	public void register()
+	{
+		eventBus.register(this);
+		eventBus.register(potionStorage);
+	}
+
+	public void unregister()
+	{
+		eventBus.unregister(this);
+		eventBus.unregister(potionStorage);
+	}
+
 	private void layout(Layout l)
 	{
 		ItemContainer bank = client.getItemContainer(InventoryID.BANK);
@@ -144,7 +163,8 @@ public class LayoutManager
 		ItemMatcher[] matchers = {
 			this::matchExact,
 			this::matchPlaceholder,
-			this::matchesVariant
+			this::matchesVariant,
+			potionStorage::matches
 		};
 
 		Map<Integer, Integer> layoutToBank = new HashMap<>();
@@ -165,13 +185,19 @@ public class LayoutManager
 					layoutToBank.put(itemId, matchedId);
 					bankItems.remove(matchedId);
 
+					// Items with objvars can have both the item and the placeholder in the bank at the same time.
+					// Remove both from bankItems so that it won't get added later as an unlayouted tagged item.
+					ItemComposition matchedItemDef = client.getItemDefinition(matchedId);
+					boolean removedPlaceholder = bankItems.remove(matchedItemDef.getPlaceholderId());
+
 					if (log.isDebugEnabled())
 					{
-						var from = itemManager.getItemComposition(itemId);
-						var to = itemManager.getItemComposition(matchedId);
-						log.debug("Matched {}{} -> {}{}",
+						ItemComposition from = itemManager.getItemComposition(itemId);
+						ItemComposition to = matchedItemDef;
+						log.debug("Matched {}{} -> {}{} removed placeholder: {}",
 							from.getName(), from.getPlaceholderId() > -1 && from.getPlaceholderTemplateId() > -1 ? " (placeholder)" : "",
-							to.getName(), to.getPlaceholderId() > -1 && to.getPlaceholderTemplateId() > -1 ? " (placeholder)" : ""
+							to.getName(), to.getPlaceholderId() > -1 && to.getPlaceholderTemplateId() > -1 ? " (placeholder)" : "",
+							removedPlaceholder
 						);
 					}
 				}
@@ -201,7 +227,7 @@ public class LayoutManager
 			}
 
 			Widget c = itemContainer.getChild(pos);
-			drawItem(l, c, bankItemId, bank.count(bankItemId), pos);
+			drawItem(l, c, bankItemId, count(bank, bankItemId), pos);
 		}
 
 		int lastEmptySlot = -1;
@@ -221,13 +247,17 @@ public class LayoutManager
 				break;
 			}
 
-			drawItem(l, c, itemId, bank.count(itemId), lastEmptySlot);
+			drawItem(l, c, itemId, count(bank, itemId), lastEmptySlot);
 
-			int layoutItemId = itemManager.canonicalize(itemId);
 			if (log.isDebugEnabled())
 			{
-				log.debug("Adding {} to layout", itemManager.getItemComposition(layoutItemId).getName());
+				ItemComposition def = itemManager.getItemComposition(itemId);
+				log.debug("Bank contains {}{} but is not in the layout",
+					def.getName(),
+					def.getPlaceholderTemplateId() > -1 && def.getPlaceholderId() > -1 ? " (placeholder)" : "");
 			}
+
+			int layoutItemId = itemManager.canonicalize(itemId);
 			l.addItem(layoutItemId);
 			modified = true;
 		}
@@ -256,6 +286,16 @@ public class LayoutManager
 		}
 	}
 
+	private int count(ItemContainer bank, int itemId)
+	{
+		int count = bank.count(itemId);
+		if (count > 0)
+		{
+			return count;
+		}
+		return potionStorage.count(itemId);
+	}
+
 	// mostly from ~bankmain_drawitem
 	private void drawItem(Layout l, Widget c, int item, int qty, int idx)
 	{
@@ -271,9 +311,9 @@ public class LayoutManager
 			c.clearActions();
 
 			// Jagex Placeholder
-			if (qty == 1 && def.getPlaceholderTemplateId() >= 0 && def.getPlaceholderId() >= 0)
+			if (def.getPlaceholderTemplateId() >= 0 && def.getPlaceholderId() >= 0)
 			{
-				c.setItemQuantity(0);
+				c.setItemQuantity(qty);
 				c.setOpacity(120);
 				c.setAction(8 - 1, "Release");
 				c.setAction(10 - 1, "Examine");
@@ -507,6 +547,12 @@ public class LayoutManager
 			BankTag activeTag = plugin.getActiveTag();
 			if (activeTag != null)
 			{
+				// Since the script vm isn't reentrant, we can't call into POTIONSTORE_DOSES/POTIONSTORE_WITHDRAW_DOSES
+				// from bankmain_finishbuilding for the layout. Instead, we record all of the potions on client tick,
+				// which is after this is run, but before the var/inv transmit listeners run, so that we will have
+				// them by the time the inv transmit listener runs.
+				potionStorage.cachePotions = true;
+
 				Layout layout = activeTag.layout();
 				if (layout != null)
 				{
@@ -598,6 +644,15 @@ public class LayoutManager
 				if (idx > -1 && menu.getParam0() != idx)
 				{
 					menu.setParam0(idx);
+					return;
+				}
+
+				idx = potionStorage.find(w.getItemId());
+				if (idx > -1)
+				{
+					potionStorage.prepareWidgets();
+					menu.setParam1(ComponentID.BANK_POTIONSTORE_CONTENT);
+					menu.setParam0(idx * PotionStorage.COMPONENTS_PER_POTION);
 				}
 			}
 		}
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
new file mode 100644
index 000000000..9a2fd23f4
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/PotionStorage.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (c) 2024, Adam <Adam@sigterm.info>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package net.runelite.client.plugins.banktags.tabs;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.api.Client;
+import net.runelite.api.EnumComposition;
+import net.runelite.api.EnumID;
+import net.runelite.api.ScriptID;
+import net.runelite.api.events.ClientTick;
+import net.runelite.api.events.VarbitChanged;
+import net.runelite.api.events.WidgetClosed;
+import net.runelite.api.widgets.ComponentID;
+import net.runelite.api.widgets.InterfaceID;
+import net.runelite.api.widgets.Widget;
+import net.runelite.api.widgets.WidgetType;
+import net.runelite.client.eventbus.Subscribe;
+import net.runelite.client.game.ItemManager;
+import net.runelite.client.plugins.bank.BankSearch;
+import net.runelite.client.plugins.banktags.BankTag;
+import net.runelite.client.plugins.banktags.BankTagsPlugin;
+
+class Potion
+{
+	EnumComposition potionEnum;
+	int itemId;
+	int doses;
+	int withdrawDoses;
+}
+
+@Singleton
+@RequiredArgsConstructor(onConstructor = @__(@Inject))
+@Slf4j
+class PotionStorage
+{
+	static final int BANKTAB_POTIONSTORE = 15;
+	static final int COMPONENTS_PER_POTION = 5;
+
+	private final Client client;
+	private final BankTagsPlugin plugin;
+	private final ItemManager itemManager;
+	private final BankSearch bankSearch;
+
+	private Potion[] potions;
+	boolean cachePotions;
+	private boolean layout;
+	private Set<Integer> potionStoreVars;
+
+	@Subscribe
+	public void onClientTick(ClientTick event)
+	{
+		if (cachePotions)
+		{
+			log.debug("Rebuilding potions");
+			cachePotions = false;
+			rebuildPotions();
+
+			Widget w = client.getWidget(ComponentID.BANK_POTIONSTORE_CONTENT);
+			if (w != null && potionStoreVars == null)
+			{
+				// cache varps that the potion store rebuilds on
+				int[] trigger = w.getVarTransmitTrigger();
+				potionStoreVars = new HashSet<>();
+				Arrays.stream(trigger).forEach(potionStoreVars::add);
+			}
+
+			if (layout)
+			{
+				layout = false;
+				BankTag activeTag = plugin.getActiveTag();
+				if (activeTag != null)
+				{
+					bankSearch.layoutBank();
+				}
+			}
+		}
+	}
+
+	// Use varp change event instead of a widget change listener so that we can recache the potions prior to
+	// the cs2 vm running.
+	@Subscribe
+	public void onVarbitChanged(VarbitChanged varbitChanged)
+	{
+		if (potionStoreVars != null && potionStoreVars.contains(varbitChanged.getVarpId()))
+		{
+			cachePotions = true;
+			layout = true; // trigger a bank rebuild as the qty has changed
+		}
+	}
+
+
+	@Subscribe
+	public void onWidgetClosed(WidgetClosed event)
+	{
+		if (event.getGroupId() == InterfaceID.BANK && event.isUnload())
+		{
+			log.debug("Invalidating potions");
+			potions = null;
+		}
+	}
+
+	private void rebuildPotions()
+	{
+		var potionStorePotions = client.getEnum(EnumID.POTIONSTORE_POTIONS);
+		potions = new Potion[potionStorePotions.size()];
+		int potionsIdx = 0;
+		for (int potionEnumId : potionStorePotions.getIntVals())
+		{
+			var potionEnum = client.getEnum(potionEnumId);
+			client.runScript(ScriptID.POTIONSTORE_DOSES, potionEnumId);
+			int doses = client.getIntStack()[0];
+			client.runScript(ScriptID.POTIONSTORE_WITHDRAW_DOSES, potionEnumId);
+			int withdrawDoses = client.getIntStack()[0];
+
+			if (doses > 0 && withdrawDoses > 0)
+			{
+				Potion p = new Potion();
+				p.potionEnum = potionEnum;
+				p.itemId = potionEnum.getIntValue(withdrawDoses);
+				p.doses = doses;
+				p.withdrawDoses = withdrawDoses;
+				potions[potionsIdx] = p;
+
+				if (log.isDebugEnabled())
+				{
+					log.debug("Potion store has {} doses of {}", p.doses, itemManager.getItemComposition(p.itemId).getName());
+				}
+			}
+
+			++potionsIdx;
+		}
+	}
+
+	int matches(Set<Integer> bank, int itemId)
+	{
+		if (potions == null)
+		{
+			return -1;
+		}
+
+		for (Potion potion : potions)
+		{
+			if (potion == null)
+			{
+				continue;
+			}
+
+			var potionEnum = potion.potionEnum;
+			int potionItemId1 = potionEnum.getIntValue(1);
+			int potionItemId2 = potionEnum.getIntValue(2);
+			int potionItemId3 = potionEnum.getIntValue(3);
+			int potionItemId4 = potionEnum.getIntValue(4);
+
+			if (potionItemId1 == itemId || potionItemId2 == itemId || potionItemId3 == itemId || potionItemId4 == itemId)
+			{
+				int potionStoreItem = potionEnum.getIntValue(potion.withdrawDoses);
+
+				if (log.isDebugEnabled())
+				{
+					log.debug("Item {} matches a potion from potion store {}", itemId, itemManager.getItemComposition(potionStoreItem).getName());
+				}
+
+				return potionStoreItem;
+			}
+		}
+
+		return -1;
+	}
+
+	int count(int itemId)
+	{
+		if (potions == null)
+		{
+			return 0;
+		}
+
+		for (Potion potion : potions)
+		{
+			if (potion != null && potion.itemId == itemId)
+			{
+				return potion.doses / potion.withdrawDoses;
+			}
+		}
+		return 0;
+	}
+
+	int find(int itemId)
+	{
+		if (potions == null)
+		{
+			return -1;
+		}
+
+		int potionIdx = 0;
+		for (Potion potion : potions)
+		{
+			++potionIdx;
+			if (potion != null && potion.itemId == itemId)
+			{
+				return potionIdx - 1;
+			}
+		}
+		return -1;
+	}
+
+	void prepareWidgets()
+	{
+		// if the potion store hasn't been opened yet, the client components won't have been made yet.
+		// they need to exist for the click to work correctly.
+		Widget potStoreContent = client.getWidget(ComponentID.BANK_POTIONSTORE_CONTENT);
+		if (potStoreContent.getChildren() == null)
+		{
+			int childIdx = 0;
+			for (int i = 0; i < potions.length; ++i) // NOPMD: ForLoopCanBeForeach
+			{
+				for (int j = 0; j < COMPONENTS_PER_POTION; ++j)
+				{
+					potStoreContent.createChild(childIdx++, WidgetType.GRAPHIC);
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
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 1c5512cb7..d66e683a9 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
@@ -631,7 +631,15 @@ public class TabInterface
 		{
 			case TAB_OP_OPEN_TAG:
 			{
+				if (client.getVarbitValue(Varbits.CURRENT_BANK_TAB) == PotionStorage.BANKTAB_POTIONSTORE)
+				{
+					// Opening a tag tab with the potion store open would leave the store open in the bankground,
+					// making deposits not work. Force close the potion store.
+					log.debug("Closing potion store");
+					client.menuAction(-1, ComponentID.BANK_POTION_STORE, MenuAction.CC_OP, 1, -1, "Potion store", "");
+				}
 				client.setVarbit(Varbits.CURRENT_BANK_TAB, 0);
+
 				Widget clicked = event.getSource();
 
 				TagTab tab = tabManager.find(Text.removeTags(clicked.getName()));
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java
index 120f42c21..120b36ad7 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/Boss.java
@@ -75,6 +75,7 @@ enum Boss
 	// but we still want the respawn timer to show when it despawns.
 	ARAXXOR(NpcID.ARAXXOR_13669, 15, RSTimeUnit.GAME_TICKS, ItemID.NID, true),
 	AMOXLIATL(NpcID.AMOXLIATL, 28, RSTimeUnit.GAME_TICKS, ItemID.MOXI),
+	HUEYCOATL(NpcID.THE_HUEYCOATL_14012, 50, RSTimeUnit.GAME_TICKS, ItemID.HUBERTE),
 	;
 
 	private static final Map<Integer, Boss> bosses;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/BossTimersPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/BossTimersPlugin.java
index 27a6a0c9e..2d4e2414e 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/BossTimersPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/bosstimer/BossTimersPlugin.java
@@ -28,6 +28,7 @@ package net.runelite.client.plugins.bosstimer;
 import javax.inject.Inject;
 import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.NPC;
+import net.runelite.api.events.NpcChanged;
 import net.runelite.api.events.NpcDespawned;
 import net.runelite.client.eventbus.Subscribe;
 import net.runelite.client.game.ItemManager;
@@ -65,13 +66,26 @@ public class BossTimersPlugin extends Plugin
 		NPC npc = npcDespawned.getNpc();
 		Boss boss = Boss.find(npc.getId());
 
-		if (boss == null || (!boss.isIgnoreDead() && !npcUtil.isDying(npc)))
+		if (boss != null && (boss.isIgnoreDead() || npcUtil.isDying(npc)))
 		{
-			return;
+			createTimer(npc, boss);
 		}
+	}
 
-		// remove existing timer
-		infoBoxManager.removeIf(t -> t instanceof RespawnTimer && ((RespawnTimer) t).getBoss() == boss);
+	@Subscribe
+	public void onNpcChanged(NpcChanged npcChanged)
+	{
+		NPC npc = npcChanged.getNpc();
+		Boss boss = Boss.find(npc.getId());
+		if (boss == Boss.HUEYCOATL)
+		{
+			createTimer(npc, boss);
+		}
+	}
+
+	private void createTimer(NPC npc, Boss boss)
+	{
+		clearTimer(boss);
 
 		log.debug("Creating spawn timer for {} ({})", npc.getName(), boss.getSpawnTime());
 
@@ -79,4 +93,9 @@ public class BossTimersPlugin extends Plugin
 		timer.setTooltip(npc.getName());
 		infoBoxManager.addInfoBox(timer);
 	}
+
+	private void clearTimer(Boss boss)
+	{
+		infoBoxManager.removeIf(t -> t instanceof RespawnTimer && ((RespawnTimer) t).getBoss() == boss);
+	}
 }
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 254b96e09..666e74484 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
@@ -118,7 +118,7 @@ public class EmoteClue extends ClueScroll implements LocationClueScroll
 		new EmoteClue("Cheer in the Barbarian Agility Arena. Headbang before you talk to me. Equip a steel platebody, maple shortbow and a Wilderness cape.", "Barbarian Outpost", BARBARIAN_OUTPOST_OBSTACLE_COURSE, new WorldPoint(2552, 3556, 0), CHEER, HEADBANG, item(STEEL_PLATEBODY), item(MAPLE_SHORTBOW), ANY_TEAM_CAPE),
 		new EmoteClue("Bow upstairs in the Edgeville Monastery. Equip a completed prayer book.", "Edgeville Monastery", SOUTHEAST_CORNER_OF_THE_MONASTERY, new WorldPoint(3056, 3484, 1), BOW, any("Any god book", item(HOLY_BOOK), item(BOOK_OF_BALANCE), item(UNHOLY_BOOK), item(BOOK_OF_LAW), item(BOOK_OF_WAR), item(BOOK_OF_DARKNESS), item(HOLY_BOOK_OR), item(BOOK_OF_BALANCE_OR), item(UNHOLY_BOOK_OR), item(BOOK_OF_LAW_OR), item(BOOK_OF_WAR_OR), item(BOOK_OF_DARKNESS_OR))),
 		new EmoteClue("Cheer in the Shadow dungeon. Equip a rune crossbow, climbing boots and any mitre.", "Shadow dungeon", ENTRANCE_OF_THE_CAVE_OF_DAMIS, new WorldPoint(2629, 5071, 0), CHEER, any("Any mitre", item(GUTHIX_MITRE), item(SARADOMIN_MITRE), item(ZAMORAK_MITRE), item(ANCIENT_MITRE), item(BANDOS_MITRE), item(ARMADYL_MITRE)), any("Rune crossbow", item(RUNE_CROSSBOW), item(RUNE_CROSSBOW_OR)), any("Climbing boots", item(CLIMBING_BOOTS), item(CLIMBING_BOOTS_G)), any("Ring of visibility or ring of shadows", item(RING_OF_VISIBILITY), item(RING_OF_SHADOWS), item(RING_OF_SHADOWS_UNCHARGED))),
-		new EmoteClue("Cheer at the top of the agility pyramid. Beware of double agents! Equip a blue mystic robe top, and any rune heraldic shield.", "Agility Pyramid", AGILITY_PYRAMID, new WorldPoint(3043, 4697, 3), DOUBLE_AGENT_108, CHEER, item(MYSTIC_ROBE_TOP), any("Any rune heraldic shield", item(RUNE_SHIELD_H1), item(RUNE_SHIELD_H2), item(RUNE_SHIELD_H3), item(RUNE_SHIELD_H4), item(RUNE_SHIELD_H5))),
+		new EmoteClue("Cheer at the top of the agility pyramid. Beware of double agents! Equip a blue mystic robe top and any rune heraldic shield.", "Agility Pyramid", AGILITY_PYRAMID, new WorldPoint(3043, 4697, 3), DOUBLE_AGENT_108, CHEER, item(MYSTIC_ROBE_TOP), any("Any rune heraldic shield", item(RUNE_SHIELD_H1), item(RUNE_SHIELD_H2), item(RUNE_SHIELD_H3), item(RUNE_SHIELD_H4), item(RUNE_SHIELD_H5))),
 		new EmoteClue("Dance in Iban's temple. Beware of double agents! Equip Iban's staff, a black mystic top and a black mystic bottom.", "Iban's temple", WELL_OF_VOYAGE, new WorldPoint(2011, 4712, 0), DOUBLE_AGENT_141, DANCE, any("Any iban's staff", item(IBANS_STAFF), item(IBANS_STAFF_U)), item(MYSTIC_ROBE_TOP_DARK), item(MYSTIC_ROBE_BOTTOM_DARK)),
 		new EmoteClue("Dance on the Fishing Platform. Equip barrows gloves, an amulet of glory and a dragon med helm.", "Fishing Platform", SOUTHEAST_CORNER_OF_THE_FISHING_PLATFORM, new WorldPoint(2782, 3273, 0), DANCE, any("Any amulet of glory", item(AMULET_OF_GLORY), item(AMULET_OF_GLORY1), item(AMULET_OF_GLORY2), item(AMULET_OF_GLORY3), item(AMULET_OF_GLORY4), item(AMULET_OF_GLORY5), item(AMULET_OF_GLORY6)), item(BARROWS_GLOVES), item(DRAGON_MED_HELM)),
 		new EmoteClue("Flap at the death altar. Beware of double agents! Equip a death tiara, a legend's cape and any ring of wealth.", "Death altar", DEATH_ALTAR, new WorldPoint(2205, 4838, 0), DOUBLE_AGENT_141, FLAP, ANY_RING_OF_WEALTH, item(DEATH_TIARA), item(CAPE_OF_LEGENDS)),
@@ -181,26 +181,26 @@ public class EmoteClue extends ClueScroll implements LocationClueScroll
 		new EmoteClue("Jump for joy in the Ancient Cavern. Equip a granite shield, splitbark body and any rune heraldic helm.", "Ancient cavern", ENTRANCE_OF_THE_CAVERN_UNDER_THE_WHIRLPOOL, new WorldPoint(1768, 5366, 1), JUMP_FOR_JOY, item(GRANITE_SHIELD), item(SPLITBARK_BODY), range("Any rune heraldic helm", RUNE_HELM_H1, RUNE_HELM_H5)),
 		new EmoteClue("Jump for joy at the Neitiznot rune rock. Equip Rune boots, a proselyte hauberk and a dragonstone ring.", "Fremennik Isles", NEAR_A_RUNITE_ROCK_IN_THE_FREMENNIK_ISLES, new WorldPoint(2375, 3850, 0), JUMP_FOR_JOY, item(RUNE_BOOTS), item(PROSELYTE_HAUBERK), item(DRAGONSTONE_RING)),
 		new EmoteClue("Jump for joy in the centre of Zul-Andra. Beware of double agents! Equip a dragon 2h sword, bandos boots and an obsidian cape.", "Zul-Andra", NEAR_THE_PIER_IN_ZULANDRA, new WorldPoint(2199, 3056, 0), DOUBLE_AGENT_141, JUMP_FOR_JOY, item(DRAGON_2H_SWORD), any("Bandos boots", item(BANDOS_BOOTS), item(GUARDIAN_BOOTS), item(ECHO_BOOTS)), item(OBSIDIAN_CAPE)),
-		new EmoteClue("Jump for Joy in the mine near the Twilight Temple. Bow before you talk to me. Equip a maple longbow, a ruby amulet and some steel platelegs.", "Twilight Temple", null, new WorldPoint (1672, 3284, 0), JUMP_FOR_JOY, item(MAPLE_LONGBOW), item(RUBY_AMULET), item(STEEL_PLATELEGS)),
+		new EmoteClue("Jump for Joy in the mine near the Twilight Temple. Bow before you talk to me. Equip a maple longbow, a ruby amulet and some steel platelegs.", "Twilight Temple", TWILIGHT_TEMPLE_MINE, new WorldPoint (1672, 3284, 0), JUMP_FOR_JOY, item(MAPLE_LONGBOW), item(RUBY_AMULET), item(STEEL_PLATELEGS)),
 		new EmoteClue("Laugh by the fountain of heroes. Equip splitbark legs, dragon boots and a Rune longsword.", "Fountain of heroes", FOUNTAIN_OF_HEROES, new WorldPoint(2920, 9893, 0), LAUGH, item(SPLITBARK_LEGS), any("Dragon boots", item(DRAGON_BOOTS), item(DRAGON_BOOTS_G), item(PRIMORDIAL_BOOTS)), item(RUNE_LONGSWORD)),
 		new EmoteClue("Laugh in Jokul's tent in the Mountain Camp. Beware of double agents! Equip a rune full helmet, blue dragonhide chaps and a fire battlestaff.", "Mountain Camp", MOUNTAIN_CAMP_GOAT_ENCLOSURE, new WorldPoint(2812, 3681, 0), DOUBLE_AGENT_108, LAUGH, item(RUNE_FULL_HELM), item(BLUE_DHIDE_CHAPS), item(FIRE_BATTLESTAFF)),
 		new EmoteClue("Laugh at the crossroads south of the Sinclair Mansion. Equip a cowl, a blue wizard robe top and an iron scimitar.", "Sinclair Mansion", ROAD_JUNCTION_SOUTH_OF_SINCLAIR_MANSION, new WorldPoint(2741, 3536, 0), LAUGH, item(LEATHER_COWL), item(BLUE_WIZARD_ROBE), item(IRON_SCIMITAR)),
 		new EmoteClue("Laugh in front of the gem store in Ardougne market. Equip a Castlewars bracelet, a dragonstone amulet and a ring of forging.", "Ardougne", NEAR_THE_GEM_STALL_IN_ARDOUGNE_MARKET, new WorldPoint(2666, 3304, 0), LAUGH, any("Castle wars bracelet", range(CASTLE_WARS_BRACELET3, CASTLE_WARS_BRACELET1)), item(DRAGONSTONE_AMULET), item(RING_OF_FORGING)),
-		new EmoteClue("Nod your head where the River Ortus meets the Proudspire. Indicate 'no' before you talk to me. Equip a blue wizard hat, a blue wizard robe and wear nothing on your legs.", "Proudspire", null, new WorldPoint(1626, 3241, 0), YES, NO, item(BLUE_WIZARD_HAT), item(BLUE_WIZARD_ROBE), emptySlot("Nothing on legs", LEGS)),
+		new EmoteClue("Nod your head where the River Ortus meets the Proudspire. Indicate 'no' before you talk to me. Equip a blue wizard hat, a blue wizard robe and wear nothing on your legs.", "East of The Proudspire", ORTUS_MEETS_PROUDSPIRE, new WorldPoint(1626, 3241, 0), YES, NO, item(BLUE_WIZARD_HAT), item(BLUE_WIZARD_ROBE), emptySlot("Nothing on legs", LEGS)),
 		new EmoteClue("Panic in the Limestone Mine. Equip bronze platelegs, a steel pickaxe and a steel medium helmet.", "Limestone Mine", LIMESTONE_MINE, new WorldPoint(3372, 3498, 0), PANIC, item(BRONZE_PLATELEGS), item(STEEL_PICKAXE), item(STEEL_MED_HELM)),
 		new EmoteClue("Panic by the mausoleum in Morytania. Wave before you speak to me. Equip a mithril plate skirt, a maple longbow and no boots.", "Morytania mausoleum, access via the experiments cave", MAUSOLEUM_OFF_THE_MORYTANIA_COAST, new WorldPoint(3504, 3576, 0), PANIC, WAVE, item(MITHRIL_PLATESKIRT), item(MAPLE_LONGBOW), emptySlot("No boots", BOOTS)),
 		new EmoteClue("Panic on the Wilderness volcano bridge. Beware of double agents! Equip any headband and crozier.", "Wilderness volcano", VOLCANO_IN_THE_NORTHEASTERN_WILDERNESS, new WorldPoint(3368, 3935, 0), DOUBLE_AGENT_65, PANIC, any("Any headband", range(RED_HEADBAND, BROWN_HEADBAND), range(WHITE_HEADBAND, GREEN_HEADBAND)), any("Any crozier", item(ANCIENT_CROZIER), item(ARMADYL_CROZIER), item(BANDOS_CROZIER), range(SARADOMIN_CROZIER, ZAMORAK_CROZIER))),
 		new EmoteClue("Panic by the pilot on White Wolf Mountain. Beware of double agents! Equip mithril platelegs, a ring of life and a rune axe.", "White Wolf Mountain", GNOME_GLIDER_ON_WHITE_WOLF_MOUNTAIN, new WorldPoint(2847, 3499, 0), DOUBLE_AGENT_108, PANIC, item(MITHRIL_PLATELEGS), item(RING_OF_LIFE), item(RUNE_AXE)),
 		new EmoteClue("Panic by the big egg where no one dare goes and the ground is burnt. Beware of double agents! Equip a dragon med helm, a TokTz-Ket-Xil, a brine sabre, rune platebody and an uncharged amulet of glory.", "Lava dragon isle", SOUTHEAST_CORNER_OF_LAVA_DRAGON_ISLE, new WorldPoint(3227, 3831, 0), DOUBLE_AGENT_141, PANIC, item(DRAGON_MED_HELM), item(TOKTZKETXIL), item(BRINE_SABRE), item(RUNE_PLATEBODY), any("Uncharged Amulet of glory", item(AMULET_OF_GLORY))),
 		new EmoteClue("Panic at the area flowers meet snow. Equip Blue D'hide vambraces, a dragon spear and a rune plateskirt.", "Trollweiss mountain", HALFWAY_DOWN_TROLLWEISS_MOUNTAIN, new WorldPoint(2776, 3781, 0), PANIC, item(BLUE_DHIDE_VAMBRACES), item(DRAGON_SPEAR), item(RUNE_PLATESKIRT), item(SLED_4084)),
-		new EmoteClue("Panic outside the Twlight Temple. Beware of double agents! Equip a dragon longsword, granite body and a dragon plateskirt.", "Twilight Temple", null, new WorldPoint(1694, 3247, 0), DOUBLE_AGENT_108, PANIC, item(DRAGON_LONGSWORD), item(GRANITE_BODY), item(DRAGON_PLATESKIRT)),
+		new EmoteClue("Panic outside the Twilight Temple. Beware of double agents! Equip a rune longsword, rune platebody and a rune plateskirt.", "Twilight Temple", OUTSIDE_TWILIGHT_TEMPLE, new WorldPoint(1694, 3247, 0), DOUBLE_AGENT_108, PANIC, item(RUNE_LONGSWORD), item(RUNE_PLATEBODY), item(RUNE_PLATESKIRT)),
 		new EmoteClue("Blow a raspberry in the bank of the Warriors' Guild. Beware of double agents! Equip a dragon battleaxe, a slayer helm of any kind and a dragon defender or avernic defender.", "Warriors' guild", WARRIORS_GUILD_BANK_29047, new WorldPoint(2843, 3543, 0), DOUBLE_AGENT_141, RASPBERRY, item(DRAGON_BATTLEAXE), any("Dragon defender or Avernic defender", item(DRAGON_DEFENDER), item(DRAGON_DEFENDER_T), item(DRAGON_DEFENDER_L), item(AVERNIC_DEFENDER), item(AVERNIC_DEFENDER_L), item(GHOMMALS_AVERNIC_DEFENDER_5), item(GHOMMALS_AVERNIC_DEFENDER_5_L), item(GHOMMALS_AVERNIC_DEFENDER_6), item(GHOMMALS_AVERNIC_DEFENDER_6_L)), ANY_SLAYER_HELMET),
 		new EmoteClue("Blow a raspberry at the monkey cage in Ardougne Zoo. Equip a studded leather body, bronze platelegs and a normal staff with no orb.", "Ardougne Zoo", NEAR_THE_PARROTS_IN_ARDOUGNE_ZOO, new WorldPoint(2607, 3282, 0), RASPBERRY, item(STUDDED_BODY), item(BRONZE_PLATELEGS), item(STAFF)),
 		new EmoteClue("Blow raspberries outside the entrance to Keep Le Faye. Equip a coif, an iron platebody and leather gloves.", "Keep Le Faye", OUTSIDE_KEEP_LE_FAYE, new WorldPoint(2757, 3401, 0), RASPBERRY, item(COIF), item(IRON_PLATEBODY), item(LEATHER_GLOVES)),
 		new EmoteClue("Blow a raspberry in the Fishing Guild bank. Beware of double agents! Equip an elemental shield, blue dragonhide chaps and a rune warhammer.", "Fishing Guild", FISHING_GUILD_BANK, new WorldPoint(2588, 3419, 0), DOUBLE_AGENT_108, RASPBERRY, item(ELEMENTAL_SHIELD), item(BLUE_DHIDE_CHAPS), item(RUNE_WARHAMMER)),
 		new EmoteClue("Salute in the banana plantation. Beware of double agents! Equip a diamond ring, amulet of power, and nothing on your chest and legs.", "Karamja", WEST_SIDE_OF_THE_KARAMJA_BANANA_PLANTATION, new WorldPoint(2914, 3168, 0), DOUBLE_AGENT_108, SALUTE, item(DIAMOND_RING), item(AMULET_OF_POWER), emptySlot("Nothing on chest & legs", BODY, LEGS)),
 		new EmoteClue("Salute in the Warriors' Guild bank. Equip only a black salamander.", "Warriors' guild", WARRIORS_GUILD_BANK, new WorldPoint(2844, 3542, 0), SALUTE, item(BLACK_SALAMANDER), emptySlot("Nothing else", HEAD, CAPE, AMULET, BODY, SHIELD, LEGS, GLOVES, BOOTS, RING, AMMO)),
-		new EmoteClue("Salute in the centre of the mess hall. Beware of double agents! Equip a rune halberd rune platebody, and an amulet of strength.", "Hosidius mess hall", HOSIDIUS_MESS, new WorldPoint(1646, 3631, 0), DOUBLE_AGENT_108, SALUTE, item(RUNE_HALBERD), item(RUNE_PLATEBODY), item(AMULET_OF_STRENGTH)),
+		new EmoteClue("Salute in the centre of the mess hall. Beware of double agents! Equip a rune halberd rune platebody and an amulet of strength.", "Hosidius mess hall", HOSIDIUS_MESS, new WorldPoint(1646, 3631, 0), DOUBLE_AGENT_108, SALUTE, item(RUNE_HALBERD), item(RUNE_PLATEBODY), item(AMULET_OF_STRENGTH)),
 		new EmoteClue("Salute outside the gates of Cam Torum. Beware of double agents! Equip a full set of blue moon equipment.", "Cam Torum", CAM_TORUM_ENTRANCE, new WorldPoint(1436, 3115, 0), DOUBLE_AGENT_141, SALUTE, any("Blue moon helm", item(BLUE_MOON_HELM), item(BLUE_MOON_HELM_29041)), any("Blue moon chestplate", item(BLUE_MOON_CHESTPLATE), item(BLUE_MOON_CHESTPLATE_29037)), any("Blue moon tassets", item(BLUE_MOON_TASSETS), item(BLUE_MOON_TASSETS_29039)), item(BLUE_MOON_SPEAR)),
 		new EmoteClue("Shrug in the mine near Rimmington. Equip a gold necklace, a gold ring and a bronze spear.", "Rimmington mine", RIMMINGTON_MINE, new WorldPoint(2976, 3238, 0), SHRUG, item(GOLD_NECKLACE), item(GOLD_RING), item(BRONZE_SPEAR)),
 		new EmoteClue("Shrug in Catherby bank. Yawn before you talk to me. Equip a maple longbow, green d'hide chaps and an iron med helm.", "Catherby", OUTSIDE_CATHERBY_BANK, new WorldPoint(2808, 3440, 0), SHRUG, YAWN, item(MAPLE_LONGBOW), item(GREEN_DHIDE_CHAPS), item(IRON_MED_HELM)),
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/emote/STASHUnit.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/emote/STASHUnit.java
index 4ffb6022b..cdbe5fb8a 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/emote/STASHUnit.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/emote/STASHUnit.java
@@ -144,6 +144,9 @@ public enum STASHUnit
 	TEMPLE_SOUTHEAST_OF_THE_BAZAAR(NullObjectID.NULL_50739, new WorldPoint(1702, 3079, 0)),
 	CAM_TORUM_ENTRANCE(NullObjectID.NULL_50740, new WorldPoint(1428, 3118, 0)),
 	WESTERN_SALVAGER_OVERLOOK(NullObjectID.NULL_54275, new WorldPoint(1614, 3296, 0)),
+	TWILIGHT_TEMPLE_MINE(NullObjectID.NULL_55403, new WorldPoint(1668, 3287, 0)),
+	ORTUS_MEETS_PROUDSPIRE(NullObjectID.NULL_55404, new WorldPoint(1629, 3239, 0)),
+	OUTSIDE_TWILIGHT_TEMPLE(NullObjectID.NULL_55405, new WorldPoint(1693, 3243, 0)),
 	;
 
 	private final int objectId;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java
index 16611b439..b24e2361f 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java
@@ -126,21 +126,20 @@ class DevToolsOverlay extends Overlay
 			renderGraphicsObjects(graphics);
 		}
 
-		if (plugin.getRoofs().isActive())
+		if (plugin.getTileFlags().isActive())
 		{
-			renderRoofs(graphics);
+			renderTileFlags(graphics);
 		}
 
 		return null;
 	}
 
-	private void renderRoofs(Graphics2D graphics)
+	private void renderTileFlags(Graphics2D graphics)
 	{
 		Scene scene = client.getScene();
 		Tile[][][] tiles = scene.getTiles();
 		byte[][][] settings = client.getTileSettings();
 		int z = client.getPlane();
-		String text = "R";
 
 		for (int x = 0; x < Constants.SCENE_SIZE; ++x)
 		{
@@ -153,19 +152,36 @@ class DevToolsOverlay extends Overlay
 					continue;
 				}
 
+				boolean isbridge = (settings[1][x][y] & Constants.TILE_FLAG_BRIDGE) != 0;
 				int flag = settings[z][x][y];
-				if ((flag & Constants.TILE_FLAG_UNDER_ROOF) == 0)
+				boolean isvisbelow = (flag & Constants.TILE_FLAG_VIS_BELOW) != 0;
+				boolean hasroof = (flag & Constants.TILE_FLAG_UNDER_ROOF) != 0;
+				if (!isbridge && !isvisbelow && !hasroof)
 				{
 					continue;
 				}
 
-				Point loc = Perspective.getCanvasTextLocation(client, graphics, tile.getLocalLocation(), text, z);
+				String s = "";
+				if (isbridge)
+				{
+					s += "B";
+				}
+				if (isvisbelow)
+				{
+					s += "V";
+				}
+				if (hasroof)
+				{
+					s += "R";
+				}
+
+				Point loc = Perspective.getCanvasTextLocation(client, graphics, tile.getLocalLocation(), s, z);
 				if (loc == null)
 				{
 					continue;
 				}
 
-				OverlayUtil.renderTextLocation(graphics, loc, text, Color.RED);
+				OverlayUtil.renderTextLocation(graphics, loc, s, Color.RED);
 			}
 		}
 	}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java
index c751b25aa..05b09c468 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPanel.java
@@ -118,7 +118,7 @@ class DevToolsPanel extends PluginPanel
 		container.add(plugin.getTileLocation());
 		container.add(plugin.getCameraPosition());
 
-		container.add(plugin.getChunkBorders());
+		container.add(plugin.getZoneBorders());
 		container.add(plugin.getMapSquares());
 		container.add(plugin.getLoadingLines());
 
@@ -185,7 +185,7 @@ class DevToolsPanel extends PluginPanel
 		disconnectBtn.addActionListener(e -> clientThread.invoke(() -> client.setGameState(GameState.CONNECTION_LOST)));
 		container.add(disconnectBtn);
 
-		container.add(plugin.getRoofs());
+		container.add(plugin.getTileFlags());
 
 		try
 		{
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java
index a2e42b8f2..a6c935603 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsPlugin.java
@@ -148,7 +148,7 @@ public class DevToolsPlugin extends Plugin
 	private DevToolsButton decorations;
 	private DevToolsButton projectiles;
 	private DevToolsButton location;
-	private DevToolsButton chunkBorders;
+	private DevToolsButton zoneBorders;
 	private DevToolsButton mapSquares;
 	private DevToolsButton loadingLines;
 	private DevToolsButton validMovement;
@@ -165,7 +165,7 @@ public class DevToolsPlugin extends Plugin
 	private DevToolsButton soundEffects;
 	private DevToolsButton scriptInspector;
 	private DevToolsButton inventoryInspector;
-	private DevToolsButton roofs;
+	private DevToolsButton tileFlags;
 	private DevToolsButton shell;
 	private DevToolsButton menus;
 	private DevToolsButton uiDefaultsInspector;
@@ -250,7 +250,7 @@ public class DevToolsPlugin extends Plugin
 		tileLocation = new DevToolsButton("Tile Location");
 		cameraPosition = new DevToolsButton("Camera Position");
 
-		chunkBorders = new DevToolsButton("Chunk Borders");
+		zoneBorders = new DevToolsButton("Zone Borders");
 		mapSquares = new DevToolsButton("Map Squares");
 		loadingLines = new DevToolsButton("Loading Lines");
 
@@ -266,7 +266,7 @@ public class DevToolsPlugin extends Plugin
 		soundEffects = new DevToolsButton("Sound Effects");
 		scriptInspector = new DevToolsButton("Script Inspector");
 		inventoryInspector = new DevToolsButton("Inventory Inspector");
-		roofs = new DevToolsButton("Roofs");
+		tileFlags = new DevToolsButton("Tile flags");
 		shell = new DevToolsButton("Shell");
 		menus = new DevToolsButton("Menus");
 
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 1eae7a09a..2c089ba73 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
@@ -36,6 +36,7 @@ import java.util.stream.Stream;
 import javax.inject.Inject;
 import net.runelite.api.Actor;
 import net.runelite.api.Client;
+import net.runelite.api.Constants;
 import net.runelite.api.NPC;
 import net.runelite.api.Perspective;
 import net.runelite.api.Player;
@@ -58,8 +59,7 @@ public class SceneOverlay extends Overlay
 	private static final Color INTERACTING_COLOR = Color.CYAN;
 
 	private static final int LOCAL_TILE_SIZE = Perspective.LOCAL_TILE_SIZE;
-	private static final int CHUNK_SIZE = 8;
-	private static final int MAP_SQUARE_SIZE = CHUNK_SIZE * CHUNK_SIZE; // 64
+	private static final int MAP_SQUARE_SIZE = Constants.CHUNK_SIZE * Constants.CHUNK_SIZE; // 64
 	private static final int CULL_CHUNK_BORDERS_RANGE = 16;
 	private static final int STROKE_WIDTH = 4;
 	private static final int CULL_LINE_OF_SIGHT_RANGE = 10;
@@ -87,9 +87,9 @@ public class SceneOverlay extends Overlay
 	@Override
 	public Dimension render(Graphics2D graphics)
 	{
-		if (plugin.getChunkBorders().isActive())
+		if (plugin.getZoneBorders().isActive())
 		{
-			renderChunkBorders(graphics);
+			renderZoneBorders(graphics);
 		}
 
 		if (plugin.getMapSquares().isActive())
@@ -120,28 +120,24 @@ public class SceneOverlay extends Overlay
 		return null;
 	}
 
-	private void renderChunkBorders(Graphics2D graphics)
+	private void renderZoneBorders(Graphics2D graphics)
 	{
-		WorldPoint wp = client.getLocalPlayer().getWorldLocation();
-		int startX = (wp.getX() - CULL_CHUNK_BORDERS_RANGE + CHUNK_SIZE - 1) / CHUNK_SIZE * CHUNK_SIZE;
-		int startY = (wp.getY() - CULL_CHUNK_BORDERS_RANGE + CHUNK_SIZE - 1) / CHUNK_SIZE * CHUNK_SIZE;
-		int endX = (wp.getX() + CULL_CHUNK_BORDERS_RANGE) / CHUNK_SIZE * CHUNK_SIZE;
-		int endY = (wp.getY() + CULL_CHUNK_BORDERS_RANGE) / CHUNK_SIZE * CHUNK_SIZE;
+		int startX = Constants.CHUNK_SIZE;
+		int startZ = Constants.CHUNK_SIZE;
+		int endX = Constants.SCENE_SIZE - Constants.CHUNK_SIZE;
+		int endZ = Constants.SCENE_SIZE - Constants.CHUNK_SIZE;
 
 		graphics.setStroke(new BasicStroke(STROKE_WIDTH));
 		graphics.setColor(CHUNK_BORDER_COLOR);
 
 		GeneralPath path = new GeneralPath();
-		for (int x = startX; x <= endX; x += CHUNK_SIZE)
+		for (int x = startX; x <= endX; x += Constants.CHUNK_SIZE)
 		{
-			LocalPoint lp1 = LocalPoint.fromWorld(client, x, wp.getY() - CULL_CHUNK_BORDERS_RANGE);
-			LocalPoint lp2 = LocalPoint.fromWorld(client, x, wp.getY() + CULL_CHUNK_BORDERS_RANGE);
-
 			boolean first = true;
-			for (int y = lp1.getY(); y <= lp2.getY(); y += LOCAL_TILE_SIZE)
+			for (int z = startZ; z <= endZ; z += Constants.CHUNK_SIZE)
 			{
 				Point p = Perspective.localToCanvas(client,
-					new LocalPoint(lp1.getX() - LOCAL_TILE_SIZE / 2, y - LOCAL_TILE_SIZE / 2),
+					x << Perspective.LOCAL_COORD_BITS, z << Perspective.LOCAL_COORD_BITS,
 					client.getPlane());
 				if (p != null)
 				{
@@ -157,16 +153,13 @@ public class SceneOverlay extends Overlay
 				}
 			}
 		}
-		for (int y = startY; y <= endY; y += CHUNK_SIZE)
+		for (int z = startZ; z <= endZ; z += Constants.CHUNK_SIZE)
 		{
-			LocalPoint lp1 = LocalPoint.fromWorld(client, wp.getX() - CULL_CHUNK_BORDERS_RANGE, y);
-			LocalPoint lp2 = LocalPoint.fromWorld(client, wp.getX() + CULL_CHUNK_BORDERS_RANGE, y);
-
 			boolean first = true;
-			for (int x = lp1.getX(); x <= lp2.getX(); x += LOCAL_TILE_SIZE)
+			for (int x = startX; x <= endX; x += Constants.CHUNK_SIZE)
 			{
 				Point p = Perspective.localToCanvas(client,
-					new LocalPoint(x - LOCAL_TILE_SIZE / 2, lp1.getY() - LOCAL_TILE_SIZE / 2),
+					x << Perspective.LOCAL_COORD_BITS, z << Perspective.LOCAL_COORD_BITS,
 					client.getPlane());
 				if (p != null)
 				{
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java
index 24a137054..ad29096fe 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java
@@ -342,6 +342,10 @@ public class IdleNotifierPlugin extends Plugin
 			case HERBLORE_PESTLE_AND_MORTAR:
 			case HERBLORE_POTIONMAKING:
 			case HERBLORE_MAKE_TAR:
+			case HERBLORE_MIXOLOGY_CONCENTRATE:
+			case HERBLORE_MIXOLOGY_CRYSTALIZE:
+			case HERBLORE_MIXOLOGY_HOMOGENIZE:
+			case HERBLORE_MIXOLOGY_REFINER:
 			/* Magic */
 			case MAGIC_CHARGING_ORBS:
 			case MAGIC_LUNAR_PLANK_MAKE:
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightPlugin.java
index 6814fcf1e..0664e2b0a 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/interacthighlight/InteractHighlightPlugin.java
@@ -31,6 +31,7 @@ import lombok.AccessLevel;
 import lombok.Getter;
 import net.runelite.api.Actor;
 import net.runelite.api.Client;
+import net.runelite.api.Constants;
 import net.runelite.api.DecorativeObject;
 import net.runelite.api.GameObject;
 import net.runelite.api.GameState;
@@ -199,8 +200,10 @@ public class InteractHighlightPlugin extends Plugin
 
 	TileObject findTileObject(int x, int y, int id)
 	{
+		x += (Constants.EXTENDED_SCENE_SIZE - Constants.SCENE_SIZE) / 2;
+		y += (Constants.EXTENDED_SCENE_SIZE - Constants.SCENE_SIZE) / 2;
 		Scene scene = client.getScene();
-		Tile[][][] tiles = scene.getTiles();
+		Tile[][][] tiles = scene.getExtendedTiles();
 		Tile tile = tiles[client.getPlane()][x][y];
 		if (tile != null)
 		{
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/motherlode/MotherlodeConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/motherlode/MotherlodeConfig.java
index e59fd611e..32f02fac5 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/motherlode/MotherlodeConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/motherlode/MotherlodeConfig.java
@@ -28,7 +28,6 @@ package net.runelite.client.plugins.motherlode;
 import net.runelite.client.config.Config;
 import net.runelite.client.config.ConfigGroup;
 import net.runelite.client.config.ConfigItem;
-import net.runelite.client.config.Units;
 
 @ConfigGroup("motherlode")
 public interface MotherlodeConfig extends Config
@@ -53,37 +52,6 @@ public interface MotherlodeConfig extends Config
 		return true;
 	}
 
-	@ConfigItem(
-		keyName = "statTimeout",
-		name = "Reset stats",
-		description = "Configures the time until statistics are reset"
-	)
-	@Units(Units.MINUTES)
-	default int statTimeout()
-	{
-		return 5;
-	}
-
-	@ConfigItem(
-		keyName = "showMiningStats",
-		name = "Show mining session stats",
-		description = "Configures whether to display mining session stats"
-	)
-	default boolean showMiningStats()
-	{
-		return true;
-	}
-
-	@ConfigItem(
-		keyName = "showMiningState",
-		name = "Show current mining state",
-		description = "Shows current mining state. 'You are currently mining' / 'You are currently NOT mining'"
-	)
-	default boolean showMiningState()
-	{
-		return true;
-	}
-
 	@ConfigItem(
 		keyName = "showGemsFound",
 		name = "Track gems found",
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java
index 1e7d100ba..6e92bc229 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/npchighlight/NpcIndicatorsPlugin.java
@@ -87,6 +87,8 @@ import net.runelite.client.util.WildcardMatcher;
 public class NpcIndicatorsPlugin extends Plugin
 {
 	private static final int MAX_ACTOR_VIEW_RANGE = 15;
+	// Longest known respawn time is currently held by Suspicious Water.
+	private static final int MAX_RESPAWN_TIME_TICKS = 500;
 
 	// Option added to NPC menu
 	private static final String TAG = "Tag";
@@ -748,7 +750,9 @@ public class NpcIndicatorsPlugin extends Plugin
 						// By killing a monster and leaving the area before seeing it again, an erroneously lengthy
 						// respawn time can be recorded. Thus, if the respawn time is already set and is greater than
 						// the observed time, assume that the lower observed respawn time is correct.
-						if (mn.getRespawnTime() == -1 || respawnTime < mn.getRespawnTime())
+						// Similarly, if the observed respawn time exceeds the maximum known respawn time,
+						// we can ignore the calculated time as obviously incorrect.
+						if ((mn.getRespawnTime() == -1 || respawnTime < mn.getRespawnTime()) && respawnTime <= MAX_RESPAWN_TIME_TICKS)
 						{
 							mn.setRespawnTime(respawnTime);
 						}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/roofremoval/RoofRemovalPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/roofremoval/RoofRemovalPlugin.java
index e13f88541..28c338696 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/roofremoval/RoofRemovalPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/roofremoval/RoofRemovalPlugin.java
@@ -49,8 +49,9 @@ import static net.runelite.api.Constants.ROOF_FLAG_POSITION;
 import net.runelite.api.GameState;
 import net.runelite.api.Scene;
 import net.runelite.api.Tile;
+import net.runelite.api.WorldView;
 import net.runelite.api.coords.WorldPoint;
-import net.runelite.api.events.GameStateChanged;
+import net.runelite.api.events.PreMapLoad;
 import net.runelite.client.callback.ClientThread;
 import net.runelite.client.config.ConfigManager;
 import net.runelite.client.eventbus.Subscribe;
@@ -115,7 +116,7 @@ public class RoofRemovalPlugin extends Plugin
 
 			if (client.getGameState() == GameState.LOGGED_IN)
 			{
-				performRoofRemoval();
+				client.setGameState(GameState.LOADING);
 			}
 			return true;
 		});
@@ -137,12 +138,9 @@ public class RoofRemovalPlugin extends Plugin
 	}
 
 	@Subscribe
-	public void onGameStateChanged(GameStateChanged e)
+	public void onPreMapLoad(PreMapLoad preMapLoad)
 	{
-		if (e.getGameState() == GameState.LOGGED_IN)
-		{
-			performRoofRemoval();
-		}
+		performRoofRemoval(preMapLoad.getWorldView(), preMapLoad.getScene());
 	}
 
 	@Subscribe
@@ -204,14 +202,13 @@ public class RoofRemovalPlugin extends Plugin
 		}
 	}
 
-	private void performRoofRemoval()
+	private void performRoofRemoval(WorldView wv, Scene scene)
 	{
-		assert client.isClientThread();
-		applyRoofOverrides();
+		applyRoofOverrides(wv, scene);
 
 		Stopwatch sw = Stopwatch.createStarted();
-		client.getScene().generateHouses();
-		log.debug("House generation duration: {}", sw.stop());
+		scene.buildRoofs();
+		log.debug("Roof building duration: {}", sw.stop());
 	}
 
 	private void loadRoofOverrides() throws IOException
@@ -250,13 +247,13 @@ public class RoofRemovalPlugin extends Plugin
 		}
 	}
 
-	private void applyRoofOverrides()
+	private void applyRoofOverrides(WorldView wv, Scene scene)
 	{
 		Stopwatch sw = Stopwatch.createStarted();
 		boolean regionsHaveOverrides = false;
 
 		outer:
-		for (int regionID : client.getMapRegions())
+		for (int regionID : wv.getMapRegions())
 		{
 			if (configOverrideRegions.contains(regionID))
 			{
@@ -277,10 +274,10 @@ public class RoofRemovalPlugin extends Plugin
 			return;
 		}
 
-		Tile[][][] tiles = client.getScene().getTiles();
+		Tile[][][] tiles = scene.getTiles();
 		// the extended tile settings control what is actually drawn, the normal
 		// tile settings are just a copy
-		byte[][][] settings = client.getScene().getExtendedTileSettings();
+		byte[][][] settings = scene.getExtendedTileSettings();
 		final int SCENE_OFFSET = (Constants.EXTENDED_SCENE_SIZE - Constants.SCENE_SIZE) / 2;
 
 		for (int z = 0; z < Constants.MAX_Z; z++)
@@ -296,7 +293,7 @@ public class RoofRemovalPlugin extends Plugin
 					}
 
 					// Properly account for instances shifting worldpoints around
-					final WorldPoint wp = WorldPoint.fromLocalInstance(client, tile.getLocalLocation(), tile.getPlane());
+					final WorldPoint wp = WorldPoint.fromLocalInstance(scene, tile.getLocalLocation(), tile.getPlane());
 
 					int regionAndPlane = wp.getRegionID() << 2 | wp.getPlane();
 					if (configOverrideRegions.contains(wp.getRegionID()))
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/wiki/WikiPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/wiki/WikiPlugin.java
index 45a63c149..502bf6676 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/wiki/WikiPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/wiki/WikiPlugin.java
@@ -31,6 +31,7 @@ import javax.inject.Inject;
 import javax.inject.Provider;
 import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.Client;
+import net.runelite.api.Menu;
 import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.NPC;
@@ -386,18 +387,27 @@ public class WikiPlugin extends Plugin
 
 		if (wikiSelected && event.getType() == MenuAction.WIDGET_TARGET_ON_WIDGET.getId())
 		{
-			MenuEntry[] menuEntries = client.getMenuEntries();
+			Menu menu = client.getMenu();
+			MenuEntry[] menuEntries = menu.getMenuEntries();
 			Widget w = getWidget(widgetID, widgetIndex);
 			if (w.getType() == WidgetType.GRAPHIC && w.getItemId() != -1 && w.getItemId() != NullItemID.NULL_6512)
 			{
-				for (int ourEntry = menuEntries.length - 1;ourEntry >= 0; ourEntry--)
+				for (int ourEntry = menuEntries.length - 1; ourEntry >= 0; ourEntry--)
 				{
 					MenuEntry entry = menuEntries[ourEntry];
 					if (entry.getType() == MenuAction.WIDGET_TARGET_ON_WIDGET)
 					{
 						int id = itemManager.canonicalize(w.getItemId());
 						String name = itemManager.getItemComposition(id).getMembersName();
-						entry.setTarget(JagexColors.MENU_TARGET_TAG + name);
+						if ("null".equals(name))
+						{
+							// may be a dummy item for the UI without a name
+							menu.removeMenuEntry(entry);
+						}
+						else
+						{
+							entry.setTarget(JagexColors.MENU_TARGET_TAG + name);
+						}
 						break;
 					}
 				}
diff --git a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java
index 2690b5550..7ee5c856c 100644
--- a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java
+++ b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java
@@ -925,6 +925,7 @@ public class ClientUI
 		{
 			case MacOS:
 				OSXUtil.requestForeground();
+				frame.setState(Frame.NORMAL);
 				break;
 			case Windows:
 				WinUtil.requestForeground(frame);
diff --git a/runelite-client/src/main/resources/item_variations.json b/runelite-client/src/main/resources/item_variations.json
index 41ad4188b..2a43036c3 100644
--- a/runelite-client/src/main/resources/item_variations.json
+++ b/runelite-client/src/main/resources/item_variations.json
@@ -12270,7 +12270,7 @@
     29996,
     29998
   ],
-  "prepot device": [
+  "chugging barrel": [
     30000,
     30002
   ],


Test patch:
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/attackstyles/AttackStylesPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/attackstyles/AttackStylesPluginTest.java
index 26cc018af..f3d7a3ee2 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/attackstyles/AttackStylesPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/attackstyles/AttackStylesPluginTest.java
@@ -37,11 +37,14 @@ import net.runelite.api.Skill;
 import net.runelite.api.StructComposition;
 import net.runelite.api.VarPlayer;
 import net.runelite.api.Varbits;
+import net.runelite.api.events.GameTick;
 import net.runelite.api.events.VarbitChanged;
 import net.runelite.api.widgets.ComponentID;
 import net.runelite.api.widgets.Widget;
+import net.runelite.client.Notifier;
 import net.runelite.client.callback.ClientThread;
 import net.runelite.client.chat.ChatMessageManager;
+import net.runelite.client.config.Notification;
 import net.runelite.client.events.ConfigChanged;
 import net.runelite.client.ui.overlay.OverlayManager;
 import static org.junit.Assert.assertFalse;
@@ -50,10 +53,12 @@ import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import org.mockito.Mock;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import org.mockito.junit.MockitoJUnitRunner;
 
@@ -80,6 +85,10 @@ public class AttackStylesPluginTest
 	@Bind
 	ChatMessageManager chatMessageManager;
 
+	@Mock
+	@Bind
+	Notifier notifier;
+
 	@Inject
 	AttackStylesPlugin attackPlugin;
 
@@ -119,6 +128,8 @@ public class AttackStylesPluginTest
 
 			when(client.getStructComposition(styleStructId)).thenReturn(s);
 		}
+
+		when(attackConfig.warningNotification()).thenReturn(Notification.OFF);
 	}
 
 	/*
@@ -149,6 +160,9 @@ public class AttackStylesPluginTest
 
 		assertTrue(attackPlugin.isWarnedSkillSelected());
 
+		attackPlugin.onGameTick(new GameTick());
+		verify(notifier).notify(any(Notification.class), eq("Attack style changed to Accurate!"));
+
 		// Switch to attack style that doesn't give attack xp
 		when(client.getVarpValue(VarPlayer.ATTACK_STYLE)).thenReturn(AttackStyle.AGGRESSIVE.ordinal());