Repository: runelite/runelite
Pull request #18567 (2024-11-27 14:29:57)

bank tags: support vials in potion storage
This PR is intended to make it so that empty vials can also be taken out from potion storage when in a bank tag. As empty vials seem to be unique in how they work in the potion storage, I've added unique methods for handling it.

I've made an assumption that the position for the vials widget in the potion storage children will always be the sum of all potions * 5 + 4, to try and make this more future-proofed. If there's a better way to determine where it'll be placed that'd be better, otherwise the main time this assumption will be challenged is when a new potion gets added to the game.

To allow for vials to be removed without having already opened the potion storage interface, I've added 5 new widgets to prepareWidgets. I've assumed I need to make the non-relevant widgets (line 292-295) be the correct WidgetType, but if they don't need to be then that can be simplified to a loop.

Zoinkwiz: Thanks! Rebased it.

Patch:
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 6bbd4ead8..8abe0bc33 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
@@ -697,13 +697,13 @@ public class LayoutManager
 					return;
 				}
 
-				idx = potionStorage.find(w.getItemId());
+				idx = potionStorage.getIdx(w.getItemId());
 				if (idx > -1)
 				{
 					potionStorage.prepareWidgets();
 					menu.setIdentifier(mungeBankToPotionStore(menu.getIdentifier()));
 					menu.setParam1(InterfaceID.Bankmain.POTIONSTORE_ITEMS);
-					menu.setParam0(idx * PotionStorage.COMPONENTS_PER_POTION);
+					menu.setParam0(idx);
 				}
 			}
 		}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/PotionStorage.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/PotionStorage.java
index 388868fe6..a8c410d15 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/PotionStorage.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/PotionStorage.java
@@ -39,6 +39,8 @@ import net.runelite.api.events.ClientTick;
 import net.runelite.api.events.VarbitChanged;
 import net.runelite.api.events.WidgetClosed;
 import net.runelite.api.gameval.InterfaceID;
+import net.runelite.api.gameval.ItemID;
+import net.runelite.api.gameval.VarPlayerID;
 import net.runelite.api.widgets.Widget;
 import net.runelite.api.widgets.WidgetType;
 import net.runelite.client.eventbus.Subscribe;
@@ -164,6 +166,16 @@ class PotionStorage
 
 	int matches(Set<Integer> bank, int itemId)
 	{
+		if (itemId == ItemID.VIAL_EMPTY)
+		{
+			if (hasVialsInPotionStorage())
+			{
+				return ItemID.VIAL_EMPTY;
+			}
+
+			return -1;
+		}
+
 		if (potions == null)
 		{
 			return -1;
@@ -200,6 +212,11 @@ class PotionStorage
 
 	int count(int itemId)
 	{
+		if (itemId == ItemID.VIAL_EMPTY)
+		{
+			return getVialsInPotionStorage();
+		}
+
 		if (potions == null)
 		{
 			return 0;
@@ -215,25 +232,45 @@ class PotionStorage
 		return 0;
 	}
 
-	int find(int itemId)
+	int getIdx(int itemId)
 	{
 		if (potions == null)
 		{
 			return -1;
 		}
 
+		if (itemId == ItemID.VIAL_EMPTY)
+		{
+			if (hasVialsInPotionStorage())
+			{
+				return potions.length * COMPONENTS_PER_POTION + 4;
+			}
+
+			return -1;
+		}
+
 		int potionIdx = 0;
 		for (Potion potion : potions)
 		{
 			++potionIdx;
 			if (potion != null && potion.itemId == itemId)
 			{
-				return potionIdx - 1;
+				return (potionIdx - 1) * COMPONENTS_PER_POTION;
 			}
 		}
 		return -1;
 	}
 
+	boolean hasVialsInPotionStorage()
+	{
+		return getVialsInPotionStorage() > 0;
+	}
+
+	int getVialsInPotionStorage()
+	{
+		return client.getVarpValue(VarPlayerID.POTIONSTORE_VIALS);
+	}
+
 	void prepareWidgets()
 	{
 		// if the potion store hasn't been opened yet, the client components won't have been made yet.
@@ -249,6 +286,13 @@ class PotionStorage
 					potStoreContent.createChild(childIdx++, WidgetType.GRAPHIC);
 				}
 			}
+
+			// Add widgets so that we can also take out vials
+			potStoreContent.createChild(childIdx++, WidgetType.GRAPHIC);
+			potStoreContent.createChild(childIdx++, WidgetType.RECTANGLE);
+			potStoreContent.createChild(childIdx++, WidgetType.TEXT);
+			potStoreContent.createChild(childIdx++, WidgetType.RECTANGLE);
+			potStoreContent.createChild(childIdx++, WidgetType.TEXT);
 		}
 	}
 }
\ No newline at end of file
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollOverlay.java
index 8242c4fcc..0634810ca 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollOverlay.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollOverlay.java
@@ -69,6 +69,7 @@ public class ClueScrollOverlay extends OverlayPanel
 		item(ItemID.SEERS_HEADBAND_HARD),
 		item(ItemID.SEERS_HEADBAND_ELITE),
 		item(ItemID.WINT_TORCH),
+		item(ItemID.WINT_TORCH_OFFHAND),
 		item(ItemID.SKILLCAPE_MAX),
 		item(ItemID.SKILLCAPE_MAX_WORN),
 		item(ItemID.ABYSSAL_LANTERN_NORMAL),
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/SkillChallengeClue.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/SkillChallengeClue.java
index 101e1fb8f..8ba1ffcc4 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/SkillChallengeClue.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/SkillChallengeClue.java
@@ -153,7 +153,8 @@ public class SkillChallengeClue extends ClueScroll implements NpcClueScroll, Nam
 
 	private static final AnyRequirementCollection ANY_HAMMER = any("Hammer",
 		item(ItemID.HAMMER),
-		item(ItemID.IMCANDO_HAMMER)
+		item(ItemID.IMCANDO_HAMMER),
+		item(ItemID.IMCANDO_HAMMER_OFFHAND)
 	);
 
 	private static final AllRequirementsCollection A_FULL_GRACEFUL_SET = all("A full Graceful set",
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java
index 246f76ed9..13f4a4987 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java
@@ -52,7 +52,6 @@ import java.util.concurrent.ScheduledExecutorService;
 import java.util.function.Function;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -494,19 +493,19 @@ class PluginHubPanel extends PluginPanel
 			@Override
 			public void insertUpdate(DocumentEvent e)
 			{
-				filter();
+				executor.execute(PluginHubPanel.this::filter);
 			}
 
 			@Override
 			public void removeUpdate(DocumentEvent e)
 			{
-				filter();
+				executor.execute(PluginHubPanel.this::filter);
 			}
 
 			@Override
 			public void changedUpdate(DocumentEvent e)
 			{
-				filter();
+				executor.execute(PluginHubPanel.this::filter);
 			}
 		});
 
@@ -653,6 +652,12 @@ class PluginHubPanel extends PluginPanel
 
 		Set<String> installed = new HashSet<>(externalPluginManager.getInstalledExternalPlugins());
 
+		plugins = Sets.union(display.keySet(), loadedPlugins.keySet())
+			.stream()
+			.map(id -> new PluginItem(display.get(id), jars.get(id), loadedPlugins.get(id),
+				pluginCounts.getOrDefault(id, -1), installed.contains(id)))
+			.collect(Collectors.toList());
+
 		SwingUtilities.invokeLater(() ->
 		{
 			if (!refreshing.isVisible())
@@ -660,14 +665,8 @@ class PluginHubPanel extends PluginPanel
 				return;
 			}
 
-			plugins = Sets.union(display.keySet(), loadedPlugins.keySet())
-				.stream()
-				.map(id -> new PluginItem(display.get(id), jars.get(id), loadedPlugins.get(id),
-					pluginCounts.getOrDefault(id, -1), installed.contains(id)))
-				.collect(Collectors.toList());
-
 			refreshing.setVisible(false);
-			filter();
+			executor.execute(PluginHubPanel.this::filter);
 		});
 	}
 
@@ -678,29 +677,31 @@ class PluginHubPanel extends PluginPanel
 			return;
 		}
 
-		mainPanel.removeAll();
-
-		Stream<PluginItem> stream = plugins.stream();
-
 		String query = searchBar.getText();
 		boolean isSearching = query != null && !query.trim().isEmpty();
+		List<PluginItem> pluginItems;
 		if (isSearching)
 		{
-			PluginSearch.search(plugins, query).forEach(mainPanel::add);
+			pluginItems = PluginSearch.search(plugins, query);
 		}
 		else
 		{
-			stream.filter(p -> p.isInstalled() || p.getJarData() != null)
+			pluginItems = plugins.stream().filter(p -> p.isInstalled() || p.getJarData() != null)
 				.sorted(Comparator.comparing((PluginItem p) -> p.getJarData() == null)
 					.thenComparing(PluginItem::isInstalled)
 					.thenComparingInt(PluginItem::getUserCount)
 					.reversed()
 					.thenComparing(p -> p.manifest.getDisplayName())
 				)
-				.forEach(mainPanel::add);
+				.collect(Collectors.toList());
 		}
 
-		mainPanel.revalidate();
+		SwingUtilities.invokeLater(() ->
+		{
+			mainPanel.removeAll();
+			pluginItems.forEach(mainPanel::add);
+			mainPanel.revalidate();
+		});
 	}
 
 	@Override
@@ -732,17 +733,13 @@ class PluginHubPanel extends PluginPanel
 	@Subscribe
 	private void onExternalPluginsChanged(ExternalPluginsChanged ev)
 	{
-		Map<String, Integer> pluginCounts = Collections.emptyMap();
-		if (plugins != null)
-		{
-			pluginCounts = plugins.stream()
-				.collect(Collectors.toMap(pi -> pi.manifest.getInternalName(), PluginItem::getUserCount));
-		}
-
 		if (!refreshing.isVisible() && lastManifest != null)
 		{
 			refreshing.setVisible(true);
-			reloadPluginList(lastManifest, pluginCounts);
+
+			Map<String, Integer> pluginCounts = plugins == null ? Collections.emptyMap()
+				: plugins.stream().collect(Collectors.toMap(pi -> pi.manifest.getInternalName(), PluginItem::getUserCount));
+			executor.submit(() -> reloadPluginList(lastManifest, pluginCounts));
 		}
 	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java
index 95734fdcb..91e058d29 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java
@@ -171,7 +171,7 @@ public class LootTrackerPlugin extends Plugin
 	// Used by Stone Chest, Isle of Souls chest, Dark Chest
 	private static final String OTHER_CHEST_LOOTED_MESSAGE = "You steal some loot from the chest.";
 	private static final String DORGESH_KAAN_CHEST_LOOTED_MESSAGE = "You find treasure inside!";
-	private static final String GRUBBY_CHEST_LOOTED_MESSAGE = "You have opened the Grubby Chest";
+	private static final Pattern GRUBBY_CHEST_LOOTED_MESSAGE = Pattern.compile("You find treasure(?: and supplies|, supplies, and a weirdly coloured egg sac) within the chest.");
 	private static final String ANCIENT_CHEST_LOOTED_MESSAGE = "You open the chest and find";
 	private static final Pattern HAM_CHEST_LOOTED_PATTERN = Pattern.compile("Your (?<key>[a-z]+) key breaks in the lock.*");
 	private static final int HAM_STOREROOM_REGION = 10321;
@@ -1013,7 +1013,7 @@ public class LootTrackerPlugin extends Plugin
 		final String message = event.getMessage();
 
 		if (message.equals(CHEST_LOOTED_MESSAGE) || message.equals(OTHER_CHEST_LOOTED_MESSAGE)
-			|| message.equals(DORGESH_KAAN_CHEST_LOOTED_MESSAGE) || message.startsWith(GRUBBY_CHEST_LOOTED_MESSAGE)
+			|| message.equals(DORGESH_KAAN_CHEST_LOOTED_MESSAGE) || GRUBBY_CHEST_LOOTED_MESSAGE.matcher(message).matches()
 			|| message.startsWith(ANCIENT_CHEST_LOOTED_MESSAGE)
 			|| LARRAN_LOOTED_PATTERN.matcher(message).matches() || ROGUES_CHEST_PATTERN.matcher(message).matches())
 		{


Test patch:
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java
index c01e7bdaf..7f5e846db 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java
@@ -355,6 +355,11 @@ public class LootTrackerPluginTest
 	@Test
 	public void testGrubbyChest()
 	{
+		// The order of events is as follows:
+		// 1. The key is removed from the players inventory
+		// 2. The MESBOX chat message prompt is delivered
+		// 3. The loot is added to the inventory/ground
+
 		Player player = mock(Player.class);
 		when(player.getWorldLocation()).thenReturn(new WorldPoint(7323 >> 2, (7323 & 0xff) << 6, 0));
 		when(client.getLocalPlayer()).thenReturn(player);
@@ -365,29 +370,44 @@ public class LootTrackerPluginTest
 		ItemContainer itemContainer = mock(ItemContainer.class);
 		when(itemContainer.getItems()).thenReturn(new Item[]{
 			new Item(ItemID.TWISTED_BOW, 1),
-			new Item(ItemID.HOSDUN_GRUBBY_KEY, 1)
+			new Item(ItemID.HOSDUN_GRUBBY_KEY, 2)
 		});
 		when(client.getItemContainer(InventoryID.INV)).thenReturn(itemContainer);
 
-		ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You unlock the chest with your key.", "", 0);
+		lootTrackerPluginSpy.onItemContainerChanged(new ItemContainerChanged(InventoryID.INV, itemContainer));
+
+		// Default loot message
+		ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.MESBOX, "", "You find treasure and supplies within the chest.", "", 0);
 		lootTrackerPluginSpy.onChatMessage(chatMessage);
 
 		when(itemContainer.getItems()).thenReturn(new Item[]{
-			new Item(ItemID.TWISTED_BOW, 1)
+			new Item(ItemID.TWISTED_BOW, 1),
+			new Item(ItemID.HOSDUN_GRUBBY_KEY, 1),
+			new Item(ItemID.SHARK, 42)
 		});
 		lootTrackerPluginSpy.onItemContainerChanged(new ItemContainerChanged(InventoryID.INV, itemContainer));
 
-		chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "You have opened the Grubby Chest 2 times.", "", 0);
+		verify(lootTrackerPluginSpy).addLoot("Grubby Chest", -1, LootRecordType.EVENT, null, Arrays.asList(
+			new ItemStack(ItemID.SHARK, 42)
+		));
+
+		// Unique loot message
+		chatMessage = new ChatMessage(null, ChatMessageType.MESBOX, "", "You find treasure, supplies, and a weirdly coloured egg sac within the chest.", "", 0);
 		lootTrackerPluginSpy.onChatMessage(chatMessage);
 
 		when(itemContainer.getItems()).thenReturn(new Item[]{
 			new Item(ItemID.TWISTED_BOW, 1),
-			new Item(ItemID.SHARK, 42)
+			new Item(ItemID.SHARK, 42),
+			new Item(ItemID._2DOSEPOTIONOFSARADOMIN, 3),
+			new Item(ItemID.BR_2DOSE2RESTORE, 1),
+			new Item(ItemID.HOSDUN_ORANGE_EGG_SAC, 1),
 		});
 		lootTrackerPluginSpy.onItemContainerChanged(new ItemContainerChanged(InventoryID.INV, itemContainer));
 
 		verify(lootTrackerPluginSpy).addLoot("Grubby Chest", -1, LootRecordType.EVENT, null, Arrays.asList(
-			new ItemStack(ItemID.SHARK, 42)
+			new ItemStack(ItemID._2DOSEPOTIONOFSARADOMIN, 3),
+			new ItemStack(ItemID.BR_2DOSE2RESTORE, 1),
+			new ItemStack(ItemID.HOSDUN_ORANGE_EGG_SAC, 1)
 		));
 	}