Repository: runelite/runelite
Pull request #18363 (2024-09-30 18:46:00)

itemmapping: add varlamore graceful.
using cache dump values.


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/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/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/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/cluescrolls/clues/EmoteClue.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/EmoteClue.java
index 254b96e09..13d30810f 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
@@ -181,19 +181,19 @@ 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)),
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/cluescrolls/clues/hotcold/HotColdLocation.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/hotcold/HotColdLocation.java
index 0af7b2f86..22632b4df 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/hotcold/HotColdLocation.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/hotcold/HotColdLocation.java
@@ -182,7 +182,7 @@ public enum HotColdLocation
 	ZEAH_COMBAT_RING(MASTER, new WorldPoint(1557, 3624, 0), ZEAH, "Shayzien Encampment, south-east of the Combat Ring.", BRASSICAN_MAGE),
 	ZEAH_SHAYZIEN_BANK_2(MASTER, new WorldPoint(1490, 3602, 0), ZEAH, "North of the bank in Shayzien.", BRASSICAN_MAGE),
 	ZEAH_LIBRARY(MASTER, new WorldPoint(1603, 3843, 0), ZEAH, "North-west of the Arceuus Library.", BRASSICAN_MAGE),
-	ZEAH_HOUSECHURCH(MASTER, new WorldPoint(1682, 3792, 0), ZEAH, "By the entrance to the Arceuus church.", ANCIENT_WIZARDS),
+	ZEAH_HOUSECHURCH(MASTER, new WorldPoint(1681, 3794, 0), ZEAH, "By the entrance to the Arceuus church.", ANCIENT_WIZARDS),
 	ZEAH_DARK_ALTAR(MASTER, new WorldPoint(1698, 3880, 0), ZEAH, "West of the Dark Altar.", BRASSICAN_MAGE),
 	ZEAH_ARCEUUS_HOUSE(MASTER, new WorldPoint(1710, 3700, 0), ZEAH, "By the south-eastern entrance to Arceuus.", BRASSICAN_MAGE),
 	ZEAH_ESSENCE_MINE(MASTER, new WorldPoint(1762, 3852, 0), ZEAH, "By the Arceuus essence mine.", BRASSICAN_MAGE),
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/grounditems/GroundItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java
index c372eae99..d736f48c8 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItem.java
@@ -75,11 +75,6 @@ class GroundItem
 		return gePrice * quantity;
 	}
 
-	boolean isMine()
-	{
-		return ownership == OWNERSHIP_SELF || ownership == OWNERSHIP_GROUP;
-	}
-
 	void reset()
 	{
 		highlighted = hidden = false;
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java
index 169350546..08f25dfa7 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsConfig.java
@@ -39,11 +39,13 @@ import net.runelite.client.plugins.grounditems.config.ItemHighlightMode;
 import net.runelite.client.plugins.grounditems.config.MenuHighlightMode;
 import net.runelite.client.plugins.grounditems.config.PriceDisplayMode;
 import net.runelite.client.plugins.grounditems.config.ValueCalculationMode;
+import net.runelite.client.plugins.grounditems.config.OwnershipFilterMode;
 
 @ConfigGroup(GroundItemsConfig.GROUP)
 public interface GroundItemsConfig extends Config
 {
 	String GROUP = "grounditems";
+	String OWNERSHIP_FILTER_MODE = "ownershipFilterMode";
 
 	@ConfigSection(
 		name = "Item Lists",
@@ -362,14 +364,14 @@ public interface GroundItemsConfig extends Config
 	}
 
 	@ConfigItem(
-		keyName = "onlyShowLoot",
-		name = "Only show own items",
-		description = "Only shows items that are yours or you can pick up",
+		keyName = OWNERSHIP_FILTER_MODE,
+		name = "Ownership filter",
+		description = "Show all items, takeable items, or only your or your group's drops.",
 		position = 25
 	)
-	default boolean onlyShowOwnItems()
+	default OwnershipFilterMode ownershipFilterMode()
 	{
-		return false;
+		return OwnershipFilterMode.ALL;
 	}
 
 	@ConfigItem(
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java
index 0684df67d..c03aa0b7b 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java
@@ -43,11 +43,13 @@ import net.runelite.api.ItemID;
 import net.runelite.api.Perspective;
 import net.runelite.api.Player;
 import net.runelite.api.Point;
+import net.runelite.api.Varbits;
 import net.runelite.api.coords.LocalPoint;
 import net.runelite.api.coords.WorldPoint;
 import net.runelite.client.plugins.grounditems.config.DespawnTimerMode;
 import static net.runelite.client.plugins.grounditems.config.ItemHighlightMode.MENU;
 import static net.runelite.client.plugins.grounditems.config.ItemHighlightMode.NONE;
+import net.runelite.client.plugins.grounditems.config.OwnershipFilterMode;
 import net.runelite.client.plugins.grounditems.config.PriceDisplayMode;
 import net.runelite.client.ui.overlay.Overlay;
 import net.runelite.client.ui.overlay.OverlayLayer;
@@ -169,16 +171,17 @@ public class GroundItemsOverlay extends Overlay
 		plugin.setHiddenBoxBounds(null);
 		plugin.setHighlightBoxBounds(null);
 
-		final boolean onlyShowLoot = config.onlyShowOwnItems();
 		final DespawnTimerMode groundItemTimers = config.groundItemTimers();
 		final boolean outline = config.textOutline();
+		final OwnershipFilterMode ownershipFilterMode = config.ownershipFilterMode();
+		final int accountType = client.getVarbitValue(Varbits.ACCOUNT_TYPE);
 
 		for (GroundItem item : groundItemList)
 		{
 			final LocalPoint groundPoint = LocalPoint.fromWorld(client, item.getLocation());
 
 			if (groundPoint == null || localLocation.distanceTo(groundPoint) > MAX_DISTANCE
-				|| (onlyShowLoot && !item.isMine()))
+				|| !plugin.shouldDisplayItem(ownershipFilterMode, item.getOwnership(), accountType))
 			{
 				continue;
 			}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java
index ee62ad4b7..7a7c7518a 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsPlugin.java
@@ -67,6 +67,9 @@ import net.runelite.api.MenuAction;
 import net.runelite.api.MenuEntry;
 import net.runelite.api.Tile;
 import net.runelite.api.TileItem;
+import static net.runelite.api.TileItem.OWNERSHIP_GROUP;
+import static net.runelite.api.TileItem.OWNERSHIP_OTHER;
+import static net.runelite.api.TileItem.OWNERSHIP_SELF;
 import net.runelite.api.Varbits;
 import net.runelite.api.coords.WorldPoint;
 import net.runelite.api.events.ClientTick;
@@ -81,6 +84,7 @@ 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.events.ProfileChanged;
 import net.runelite.client.game.ItemManager;
 import net.runelite.client.input.KeyManager;
 import net.runelite.client.input.MouseManager;
@@ -92,6 +96,7 @@ import net.runelite.client.plugins.grounditems.config.MenuHighlightMode;
 import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.BOTH;
 import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.NAME;
 import static net.runelite.client.plugins.grounditems.config.MenuHighlightMode.OPTION;
+import net.runelite.client.plugins.grounditems.config.OwnershipFilterMode;
 import net.runelite.client.ui.components.colorpicker.ColorPickerManager;
 import net.runelite.client.ui.components.colorpicker.RuneliteColorPicker;
 import net.runelite.client.ui.overlay.OverlayManager;
@@ -203,6 +208,7 @@ public class GroundItemsPlugin extends Plugin
 		overlayManager.add(overlay);
 		mouseManager.registerMouseListener(mouseAdapter);
 		keyManager.registerKeyListener(hotkeyListener);
+		migrate();
 		executor.execute(this::reset);
 	}
 
@@ -231,6 +237,27 @@ public class GroundItemsPlugin extends Plugin
 		}
 	}
 
+	@Subscribe
+	public void onProfileChanged(ProfileChanged profileChanged)
+	{
+		migrate();
+	}
+
+	private void migrate()
+	{
+		//This was the old "Only show own items" config, which is now obsolete and replaced by ownership filter
+		Boolean onlyShowOwnItems = configManager.getConfiguration(GroundItemsConfig.GROUP, "onlyShowLoot", Boolean.class);
+		if (onlyShowOwnItems != null)
+		{
+			if (onlyShowOwnItems)
+			{
+				//The old behavior maps to 'Drops' in the new dropdown
+				configManager.setConfiguration(GroundItemsConfig.GROUP, GroundItemsConfig.OWNERSHIP_FILTER_MODE, OwnershipFilterMode.DROPS);
+			}
+			configManager.unsetConfiguration(GroundItemsConfig.GROUP, "onlyShowLoot");
+		}
+	}
+
 	@Subscribe
 	public void onGameStateChanged(final GameStateChanged event)
 	{
@@ -260,7 +287,7 @@ public class GroundItemsPlugin extends Plugin
 			collectedGroundItems.put(tile.getWorldLocation(), item.getId(), groundItem);
 		}
 
-		if (groundItem.isMine() || !config.onlyShowOwnItems())
+		if (shouldDisplayItem(config.ownershipFilterMode(), groundItem.getOwnership(), client.getVarbitValue(Varbits.ACCOUNT_TYPE)))
 		{
 			notifyHighlightedItem(groundItem);
 		}
@@ -389,16 +416,6 @@ public class GroundItemsPlugin extends Plugin
 		final int alchPrice = itemComposition.getHaPrice();
 		final int despawnTime = item.getDespawnTime() - client.getTickCount();
 		final int visibleTime = item.getVisibleTime() - client.getTickCount();
-		int ownership = item.getOwnership();
-		final int accountType = client.getVarbitValue(Varbits.ACCOUNT_TYPE);
-		boolean isGim = accountType >= 4 && accountType <= 6; // ~is_group_iron
-
-		// from ~script7240
-		if (ownership == TileItem.OWNERSHIP_GROUP && !isGim)
-		{
-			// non-gims see loot from other people as "group" loop since they are both group -1.
-			ownership = TileItem.OWNERSHIP_OTHER;
-		}
 
 		final GroundItem groundItem = GroundItem.builder()
 			.id(itemId)
@@ -409,7 +426,7 @@ public class GroundItemsPlugin extends Plugin
 			.haPrice(alchPrice)
 			.height(tile.getItemLayer().getHeight())
 			.tradeable(itemComposition.isTradeable())
-			.ownership(ownership)
+			.ownership(item.getOwnership())
 			.isPrivate(item.isPrivate())
 			.spawnTime(Instant.now())
 			.stackable(itemComposition.isStackable())
@@ -747,9 +764,11 @@ public class GroundItemsPlugin extends Plugin
 		int highestPrice = -1;
 		GroundItem highestItem = null;
 		Collection<GroundItem> groundItems = collectedGroundItems.row(worldPoint).values();
+		final OwnershipFilterMode ownershipFilterMode = config.ownershipFilterMode();
+		final int accountType = client.getVarbitValue(Varbits.ACCOUNT_TYPE);
 		for (GroundItem groundItem : groundItems)
 		{
-			if ((config.onlyShowOwnItems() && !groundItem.isMine()))
+			if (!shouldDisplayItem(ownershipFilterMode, groundItem.getOwnership(), accountType))
 			{
 				continue;
 			}
@@ -854,4 +873,22 @@ public class GroundItemsPlugin extends Plugin
 	{
 		configManager.unsetConfiguration(GroundItemsConfig.GROUP, HIGHLIGHT_COLOR_PREFIX + itemId);
 	}
+
+	/*
+	 * All      -> none | self | other | group
+	 * Drops    -> self | group
+	 * Takeable -> none | self | group | (if a main then other)
+	 */
+	boolean shouldDisplayItem(OwnershipFilterMode filterMode, int ownership, int accountType)
+	{
+		switch (filterMode)
+		{
+			case DROPS:
+				return ownership == OWNERSHIP_SELF || ownership == OWNERSHIP_GROUP;
+			case TAKEABLE:
+				return ownership != OWNERSHIP_OTHER || accountType == 0; // Mains can always take items
+			default:
+				return true;
+		}
+	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/config/OwnershipFilterMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/config/OwnershipFilterMode.java
new file mode 100644
index 000000000..ccdef21c1
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/config/OwnershipFilterMode.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2024, Moshe Ben-Zacharia <https://github.com/MosheBenZacharia>
+ * 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.grounditems.config;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum OwnershipFilterMode
+{
+	ALL("All"),
+	TAKEABLE("Takeable"),
+	DROPS("Drops");
+
+	private final String name;
+
+	@Override
+	public String toString()
+	{
+		return name;
+	}
+}
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/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());
 
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginTest.java
index 1c3d2e238..7741203fc 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/grounditems/GroundItemsPluginTest.java
@@ -44,6 +44,7 @@ import net.runelite.client.game.ItemManager;
 import net.runelite.client.input.KeyManager;
 import net.runelite.client.input.MouseManager;
 import net.runelite.client.plugins.grounditems.config.HighlightTier;
+import net.runelite.client.plugins.grounditems.config.OwnershipFilterMode;
 import net.runelite.client.ui.overlay.OverlayManager;
 import org.junit.Before;
 import org.junit.Test;
@@ -124,6 +125,7 @@ public class GroundItemsPluginTest
 		when(config.getHighlightItems()).thenReturn("abyssal whip");
 		when(config.notifyTier()).thenReturn(HighlightTier.OFF);
 		when(config.notifyHighlightedDrops()).thenReturn(true);
+		when(config.ownershipFilterMode()).thenReturn(OwnershipFilterMode.ALL);
 
 		when(itemManager.getItemComposition(ItemID.ABYSSAL_WHIP)).thenAnswer(a ->
 		{