Repository: runelite/runelite
Pull request #17795 (2024-04-26 11:57:01)

loot tracker: Add muddy chest
![image](https://github.com/runelite/runelite/assets/53493631/d090c9e7-2c0f-46d7-9ccc-c1dfcbf2f314)
Should hopefully also work for the rare blighted items.


Patch:
diff --git a/runelite-api/src/main/java/net/runelite/api/TileItem.java b/runelite-api/src/main/java/net/runelite/api/TileItem.java
index 0a92f982a..3fe30f00c 100644
--- a/runelite-api/src/main/java/net/runelite/api/TileItem.java
+++ b/runelite-api/src/main/java/net/runelite/api/TileItem.java
@@ -24,11 +24,18 @@
  */
 package net.runelite.api;
 
+import org.intellij.lang.annotations.MagicConstant;
+
 /**
  * Represents an item inside an {@link ItemLayer}.
  */
 public interface TileItem extends Renderable
 {
+	int OWNERSHIP_NONE = 0;
+	int OWNERSHIP_SELF = 1;
+	int OWNERSHIP_OTHER = 2;
+	int OWNERSHIP_GROUP = 3;
+
 	/**
 	 * @return the ID of the item
 	 * @see ItemID
@@ -38,15 +45,29 @@ public interface TileItem extends Renderable
 	int getQuantity();
 
 	/**
-	 * Get the time, in server ticks, when the item despawns, relative to the spawn time.
+	 * Get the time, in server ticks, when the item becomes visible to other players
+	 * @see Client#getTickCount()
+	 * @return
+	 */
+	int getVisibleTime();
+
+	/**
+	 * Get the time, in server ticks, when the item despawns
+	 * @see Client#getTickCount()
 	 * @return
 	 */
 	int getDespawnTime();
 
 	/**
-	 * Get the time, in server ticks, when the item becomes visible to other players, relative
-	 * to the spawn time.
+	 * Get the item ownership
 	 * @return
 	 */
-	int getVisibleTime();
+	@MagicConstant(intValues = {OWNERSHIP_NONE, OWNERSHIP_SELF, OWNERSHIP_OTHER, OWNERSHIP_GROUP})
+	int getOwnership();
+
+	/**
+	 * Test whether the item is private
+	 * @return
+	 */
+	boolean isPrivate();
 }
diff --git a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java
index 65be8088b..cc14fa46d 100644
--- a/runelite-api/src/main/java/net/runelite/api/VarPlayer.java
+++ b/runelite-api/src/main/java/net/runelite/api/VarPlayer.java
@@ -34,7 +34,9 @@ package net.runelite.api;
  */
 public final class VarPlayer
 {
+	public static final int CANNON_STATE = 2;
 	public static final int CANNON_AMMO = 3;
+	public static final int CANNON_COORD = 4;
 	public static final int ATTACK_STYLE = 43;
 	public static final int QUEST_POINTS = 101;
 	/**
diff --git a/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java b/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java
index 707604f2c..077e004b1 100644
--- a/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java
+++ b/runelite-api/src/main/java/net/runelite/api/coords/WorldPoint.java
@@ -543,4 +543,12 @@ public class WorldPoint
 	{
 		return new WorldArea(this, 1, 1);
 	}
+
+	/**
+	 * Create a WorldPoint from a packed Jagex coordinate
+	 */
+	public static WorldPoint fromCoord(int c)
+	{
+		return new WorldPoint((c >>> 14) & 0x3FFF, c & 0x3FFF, (c >>> 28) & 0x3);
+	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/config/Keybind.java b/runelite-client/src/main/java/net/runelite/client/config/Keybind.java
index eba500b85..4ba1ada45 100644
--- a/runelite-client/src/main/java/net/runelite/client/config/Keybind.java
+++ b/runelite-client/src/main/java/net/runelite/client/config/Keybind.java
@@ -125,9 +125,17 @@ public class Keybind
 			keyCode = KeyEvent.VK_UNDEFINED;
 		}
 
-		if (e.getID() == KeyEvent.KEY_RELEASED && keyCode != KeyEvent.VK_UNDEFINED)
+		if (e.getID() == KeyEvent.KEY_RELEASED)
 		{
-			return this.keyCode == keyCode;
+			if (keyCode != KeyEvent.VK_UNDEFINED)
+			{
+				return this.keyCode == keyCode;
+			}
+			else
+			{
+				assert mf != null;
+				return this.keyCode == keyCode && (this.modifiers & modifiers) == this.modifiers && ((mf & this.modifiers) == mf);
+			}
 		}
 
 		if (ignoreModifiers && keyCode != KeyEvent.VK_UNDEFINED)
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java
index bb602c735..88362c9d5 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/cannon/CannonPlugin.java
@@ -31,29 +31,19 @@ import java.util.List;
 import javax.inject.Inject;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
-import net.runelite.api.AnimationID;
 import net.runelite.api.ChatMessageType;
 import net.runelite.api.Client;
-import net.runelite.api.GameObject;
 import net.runelite.api.GameState;
 import net.runelite.api.InventoryID;
 import net.runelite.api.Item;
 import net.runelite.api.ItemID;
-import net.runelite.api.MenuAction;
-import net.runelite.api.ObjectID;
-import net.runelite.api.Player;
 import net.runelite.api.VarPlayer;
-import net.runelite.api.coords.LocalPoint;
 import net.runelite.api.coords.WorldArea;
 import net.runelite.api.coords.WorldPoint;
 import net.runelite.api.events.ChatMessage;
-import net.runelite.api.events.GameObjectSpawned;
 import net.runelite.api.events.GameStateChanged;
 import net.runelite.api.events.ItemContainerChanged;
-import net.runelite.api.events.MenuOptionClicked;
 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.config.ConfigManager;
@@ -77,8 +67,6 @@ public class CannonPlugin extends Plugin
 
 	private CannonCounter counter;
 	private boolean cannonBallNotificationSent;
-	private WorldPoint clickedCannonLocation;
-	private boolean firstCannonLoad;
 
 	@Getter
 	private int cballsLeft;
@@ -92,9 +80,6 @@ public class CannonPlugin extends Plugin
 	@Getter
 	private int cannonWorld = -1;
 
-	@Getter
-	private GameObject cannon;
-
 	@Getter
 	private List<WorldPoint> spotPoints = new ArrayList<>();
 
@@ -247,65 +232,40 @@ public class CannonPlugin extends Plugin
 	}
 
 	@Subscribe
-	public void onGameObjectSpawned(GameObjectSpawned event)
+	public void onVarbitChanged(VarbitChanged varbitChanged)
 	{
-		GameObject gameObject = event.getGameObject();
-
-		Player localPlayer = client.getLocalPlayer();
-		if ((gameObject.getId() == ObjectID.CANNON_BASE || gameObject.getId() == ObjectID.CANNON_BASE_43029) && !cannonPlaced)
+		if (varbitChanged.getVarpId() == VarPlayer.CANNON_AMMO)
 		{
-			if (localPlayer.getWorldLocation().distanceTo(gameObject.getWorldLocation()) <= 2
-				&& localPlayer.getAnimation() == AnimationID.BURYING_BONES)
+			int old = cballsLeft;
+			cballsLeft = varbitChanged.getValue();
+
+			if (cballsLeft > old)
 			{
-				cannonPosition = buildCannonWorldArea(gameObject.getWorldLocation());
-				cannonWorld = client.getWorld();
-				cannon = gameObject;
+				cannonBallNotificationSent = false;
 			}
-		}
-	}
-
-	@Subscribe
-	public void onMenuOptionClicked(MenuOptionClicked event)
-	{
-		if (cannonPosition != null || (event.getId() != ObjectID.DWARF_MULTICANNON && event.getId() != ObjectID.DWARF_MULTICANNON_43027))
-		{
-			return;
-		}
 
-		// Check if cannonballs are being used on the cannon
-		if (event.getMenuAction() == MenuAction.WIDGET_TARGET_ON_GAME_OBJECT
-			&& client.getSelectedWidget() != null
-			&& client.getSelectedWidget().getId() == ComponentID.INVENTORY_CONTAINER)
-		{
-			final Widget selected = client.getSelectedWidget();
-			final int itemId = selected.getItemId();
-			if (itemId != ItemID.CANNONBALL && itemId != ItemID.GRANITE_CANNONBALL)
+			if (!cannonBallNotificationSent && cballsLeft > 0 && config.lowWarningThreshold() >= cballsLeft)
 			{
-				return;
+				notifier.notify(config.showCannonNotifications(), String.format("Your cannon has %d cannon balls remaining!", cballsLeft));
+				cannonBallNotificationSent = true;
 			}
 		}
-		// Check for the Fire option being selected on the cannon.
-		else if (event.getMenuAction() != MenuAction.GAME_OBJECT_FIRST_OPTION)
+		else if (varbitChanged.getVarpId() == VarPlayer.CANNON_COORD)
 		{
-			return;
+			WorldPoint c = WorldPoint.fromCoord(varbitChanged.getValue());
+			cannonPosition = buildCannonWorldArea(c);
 		}
-
-		// Store the click location as a WorldPoint to avoid issues with scene loads
-		clickedCannonLocation = WorldPoint.fromScene(client, event.getParam0(), event.getParam1(), client.getPlane());
-		log.debug("Updated cannon location: {}", clickedCannonLocation);
-	}
-
-	@Subscribe
-	public void onVarbitChanged(VarbitChanged varbitChanged)
-	{
-		if (varbitChanged.getVarpId() == VarPlayer.CANNON_AMMO)
+		else if (varbitChanged.getVarpId() == VarPlayer.CANNON_STATE)
 		{
-			cballsLeft = varbitChanged.getValue();
+			cannonPlaced = varbitChanged.getValue() == 4;
 
-			if (!cannonBallNotificationSent && cballsLeft > 0 && config.lowWarningThreshold() >= cballsLeft)
+			if (cannonPlaced)
 			{
-				notifier.notify(config.showCannonNotifications(), String.format("Your cannon has %d cannon balls remaining!", cballsLeft));
-				cannonBallNotificationSent = true;
+				addCounter();
+			}
+			else
+			{
+				removeCounter();
 			}
 		}
 	}
@@ -320,58 +280,12 @@ public class CannonPlugin extends Plugin
 
 		if (event.getMessage().equals("You add the furnace."))
 		{
-			cannonPlaced = true;
-			addCounter();
-			firstCannonLoad = true;
-		}
-		else if (event.getMessage().contains("You pick up the cannon")
-			|| event.getMessage().contains("Your cannon has decayed. Speak to Nulodion to get a new one!")
-			|| event.getMessage().contains("Your cannon has been destroyed!"))
-		{
-			cannonPlaced = false;
-			removeCounter();
-			cannonPosition = null;
-		}
-		else if (event.getMessage().startsWith("You load the cannon with"))
-		{
-			// Set the cannon's position and object if the player's animation was interrupted during setup
-			if (cannonPosition == null && clickedCannonLocation != null)
-			{
-				// There is a window of 1 tick where the player can add the furnace, click on another cannon, and then
-				// the initial cannon load message arrives. This can cause the client to confuse the other cannon with
-				// the player's, so ignore that first message when deciding the cannon's location.
-				if (firstCannonLoad)
-				{
-					firstCannonLoad = false;
-				}
-				else
-				{
-					LocalPoint lp = LocalPoint.fromWorld(client, clickedCannonLocation);
-					if (lp != null)
-					{
-						GameObject[] objects = client.getScene().getTiles()[client.getPlane()][lp.getSceneX()][lp.getSceneY()].getGameObjects();
-						if (objects.length > 0 && client.getLocalPlayer().getWorldLocation().distanceTo(objects[0].getWorldLocation()) <= 2)
-						{
-							cannonPlaced = true;
-							cannonWorld = client.getWorld();
-							cannon = objects[0];
-							cannonPosition = buildCannonWorldArea(cannon.getWorldLocation());
-						}
-					}
-				}
-				clickedCannonLocation = null;
-			}
-
-			cannonBallNotificationSent = false;
+			cannonWorld = client.getWorld();
 		}
 		else if (event.getMessage().contains("Your cannon is out of ammo!"))
 		{
 			notifier.notify(config.showCannonNotifications(), "Your cannon is out of ammo!");
 		}
-		else if (event.getMessage().equals("This isn't your cannon!") || event.getMessage().equals("This is not your cannon."))
-		{
-			clickedCannonLocation = null;
-		}
 	}
 
 	Color getStateColor()
@@ -414,6 +328,6 @@ public class CannonPlugin extends Plugin
 
 	private static WorldArea buildCannonWorldArea(WorldPoint worldPoint)
 	{
-		return new WorldArea(worldPoint.getX() - 1, worldPoint.getY() - 1, 3, 3, worldPoint.getPlane());
+		return new WorldArea(worldPoint.getX(), worldPoint.getY(), 3, 3, worldPoint.getPlane());
 	}
 }
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java
index 9cbfaf667..785711b46 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java
@@ -80,8 +80,6 @@ class GrandExchangeSearchPanel extends JPanel
 	/*  The error panel, this displays an error message */
 	private final PluginErrorPanel errorPanel = new PluginErrorPanel();
 
-	private final List<GrandExchangeItems> itemsList = new ArrayList<>();
-
 	@Inject
 	private GrandExchangeSearchPanel(ClientThread clientThread, ItemManager itemManager,
 		ScheduledExecutorService executor, RuneLiteConfig runeLiteConfig, GrandExchangePlugin grandExchangePlugin)
@@ -195,7 +193,7 @@ class GrandExchangeSearchPanel extends JPanel
 
 	private void processResult(List<ItemPrice> result, String lookup, boolean exactMatch)
 	{
-		itemsList.clear();
+		final List<GrandExchangeItems> itemsList = new ArrayList<>();
 
 		cardLayout.show(centerPanel, RESULTS_PANEL);
 
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 44bfd0336..677fa6706 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
@@ -27,11 +27,15 @@ package net.runelite.client.plugins.grounditems;
 import java.awt.Color;
 import java.time.Duration;
 import java.time.Instant;
-import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import lombok.Builder;
 import lombok.Data;
+import static net.runelite.api.TileItem.OWNERSHIP_GROUP;
+import static net.runelite.api.TileItem.OWNERSHIP_NONE;
+import static net.runelite.api.TileItem.OWNERSHIP_OTHER;
+import static net.runelite.api.TileItem.OWNERSHIP_SELF;
 import net.runelite.api.coords.WorldPoint;
+import org.intellij.lang.annotations.MagicConstant;
 
 @Data
 @Builder
@@ -47,13 +51,13 @@ class GroundItem
 	private int gePrice;
 	private int offset;
 	private boolean tradeable;
-	@Nonnull
-	private LootType lootType;
+	@MagicConstant(intValues = {OWNERSHIP_NONE, OWNERSHIP_SELF, OWNERSHIP_OTHER, OWNERSHIP_GROUP})
+	private int ownership;
+	private boolean isPrivate;
 	@Nullable
 	private Instant spawnTime;
 	private boolean stackable;
 	private Duration despawnTime;
-	@Nullable
 	private Duration visibleTime;
 
 	// cached values derived from config
@@ -73,7 +77,7 @@ class GroundItem
 
 	boolean isMine()
 	{
-		return lootType != LootType.UNKNOWN;
+		return ownership != OWNERSHIP_OTHER;
 	}
 
 	void reset()
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 6a9c0216b..169350546 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
@@ -363,11 +363,11 @@ public interface GroundItemsConfig extends Config
 
 	@ConfigItem(
 		keyName = "onlyShowLoot",
-		name = "Only show loot",
-		description = "Only shows drops from NPCs and players",
+		name = "Only show own items",
+		description = "Only shows items that are yours or you can pick up",
 		position = 25
 	)
-	default boolean onlyShowLoot()
+	default boolean onlyShowOwnItems()
 	{
 		return false;
 	}
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 5e1d1f871..38366542f 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
@@ -168,7 +168,7 @@ public class GroundItemsOverlay extends Overlay
 		plugin.setHiddenBoxBounds(null);
 		plugin.setHighlightBoxBounds(null);
 
-		final boolean onlyShowLoot = config.onlyShowLoot();
+		final boolean onlyShowLoot = config.onlyShowOwnItems();
 		final DespawnTimerMode groundItemTimers = config.groundItemTimers();
 		final boolean outline = config.textOutline();
 
@@ -406,9 +406,8 @@ public class GroundItemsOverlay extends Overlay
 		final Instant now = Instant.now();
 		final Instant despawnTime = spawnTime.plus(groundItem.getDespawnTime());
 
-		if (groundItem.getVisibleTime() == null)
+		if (groundItem.isPrivate())
 		{
-			// if there is no visible time, it is always private
 			if (despawnTime.isAfter(now))
 			{
 				return PRIVATE_TIMER_COLOR;
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 a6a1b1c31..253058dbf 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
@@ -28,7 +28,6 @@ package net.runelite.client.plugins.grounditems;
 import com.google.common.base.MoreObjects;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.LoadingCache;
-import com.google.common.collect.EvictingQueue;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Table;
@@ -47,7 +46,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Queue;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -76,8 +74,6 @@ import net.runelite.api.events.ItemDespawned;
 import net.runelite.api.events.ItemQuantityChanged;
 import net.runelite.api.events.ItemSpawned;
 import net.runelite.api.events.MenuEntryAdded;
-import net.runelite.api.events.MenuOptionClicked;
-import net.runelite.api.widgets.ComponentID;
 import net.runelite.client.Notifier;
 import net.runelite.client.callback.ClientThread;
 import net.runelite.client.config.ConfigManager;
@@ -194,8 +190,6 @@ public class GroundItemsPlugin extends Plugin
 	private List<PriceHighlight> priceChecks = ImmutableList.of();
 	private LoadingCache<NamedQuantity, Boolean> highlightedItems;
 	private LoadingCache<NamedQuantity, Boolean> hiddenItems;
-	private final Queue<Integer> droppedItemQueue = EvictingQueue.create(16); // recently dropped items
-	private int lastUsedItem;
 	private final Map<WorldPoint, Lootbeam> lootbeams = new HashMap<>();
 
 	@Provides
@@ -211,7 +205,6 @@ public class GroundItemsPlugin extends Plugin
 		mouseManager.registerMouseListener(mouseAdapter);
 		keyManager.registerKeyListener(hotkeyListener);
 		executor.execute(this::reset);
-		lastUsedItem = -1;
 	}
 
 	@Override
@@ -268,7 +261,7 @@ public class GroundItemsPlugin extends Plugin
 			collectedGroundItems.put(tile.getWorldLocation(), item.getId(), groundItem);
 		}
 
-		if (!config.onlyShowLoot())
+		if (!config.onlyShowOwnItems())
 		{
 			notifyHighlightedItem(groundItem);
 		}
@@ -343,14 +336,14 @@ public class GroundItemsPlugin extends Plugin
 	public void onNpcLootReceived(NpcLootReceived npcLootReceived)
 	{
 		Collection<ItemStack> items = npcLootReceived.getItems();
-		lootReceived(items, LootType.PVM);
+		lootReceived(items);
 	}
 
 	@Subscribe
 	public void onPlayerLootReceived(PlayerLootReceived playerLootReceived)
 	{
 		Collection<ItemStack> items = playerLootReceived.getItems();
-		lootReceived(items, LootType.PVP);
+		lootReceived(items);
 	}
 
 	@Subscribe
@@ -402,7 +395,7 @@ public class GroundItemsPlugin extends Plugin
 		}).toArray(MenuEntry[]::new));
 	}
 
-	private void lootReceived(Collection<ItemStack> items, LootType lootType)
+	private void lootReceived(Collection<ItemStack> items)
 	{
 		for (ItemStack itemStack : items)
 		{
@@ -410,9 +403,7 @@ public class GroundItemsPlugin extends Plugin
 			GroundItem groundItem = collectedGroundItems.get(location, itemStack.getId());
 			if (groundItem != null)
 			{
-				groundItem.setLootType(lootType);
-
-				if (config.onlyShowLoot())
+				if (config.onlyShowOwnItems())
 				{
 					notifyHighlightedItem(groundItem);
 				}
@@ -434,8 +425,8 @@ public class GroundItemsPlugin extends Plugin
 		final ItemComposition itemComposition = itemManager.getItemComposition(itemId);
 		final int realItemId = itemComposition.getNote() != -1 ? itemComposition.getLinkedNoteId() : itemId;
 		final int alchPrice = itemComposition.getHaPrice();
-		final boolean dropped = tile.getWorldLocation().equals(client.getLocalPlayer().getWorldLocation()) && droppedItemQueue.remove(itemId);
-		final boolean table = itemId == lastUsedItem && tile.getItemLayer().getHeight() > 0;
+		final int despawnTime = item.getDespawnTime() - client.getTickCount();
+		final int visibleTime = item.getVisibleTime() - client.getTickCount();
 
 		final GroundItem groundItem = GroundItem.builder()
 			.id(itemId)
@@ -446,11 +437,12 @@ public class GroundItemsPlugin extends Plugin
 			.haPrice(alchPrice)
 			.height(tile.getItemLayer().getHeight())
 			.tradeable(itemComposition.isTradeable())
-			.lootType(dropped ? LootType.DROPPED : (table ? LootType.TABLE : LootType.UNKNOWN))
+			.ownership(item.getOwnership())
+			.isPrivate(item.isPrivate())
 			.spawnTime(Instant.now())
 			.stackable(itemComposition.isStackable())
-			.despawnTime(Duration.of(item.getDespawnTime(), RSTimeUnit.GAME_TICKS))
-			.visibleTime(item.getVisibleTime() > 0 ? Duration.of(item.getVisibleTime(), RSTimeUnit.GAME_TICKS) : null)
+			.despawnTime(Duration.of(despawnTime, RSTimeUnit.GAME_TICKS))
+			.visibleTime(Duration.of(visibleTime, RSTimeUnit.GAME_TICKS))
 			.build();
 
 		// Update item price in case it is coins
@@ -770,24 +762,6 @@ public class GroundItemsPlugin extends Plugin
 		}
 	}
 
-	@Subscribe
-	public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked)
-	{
-		if (menuOptionClicked.isItemOp() && menuOptionClicked.getMenuOption().equals("Drop"))
-		{
-			int itemId = menuOptionClicked.getItemId();
-			// Keep a queue of recently dropped items to better detect
-			// item spawns that are drops
-			droppedItemQueue.add(itemId);
-		}
-		else if (menuOptionClicked.getMenuAction() == MenuAction.WIDGET_TARGET_ON_GAME_OBJECT
-			&& client.getSelectedWidget() != null
-			&& client.getSelectedWidget().getId() == ComponentID.INVENTORY_CONTAINER)
-		{
-			lastUsedItem = client.getSelectedWidget().getItemId();
-		}
-	}
-
 	private void handleLootbeam(WorldPoint worldPoint)
 	{
 		/*
@@ -805,7 +779,7 @@ public class GroundItemsPlugin extends Plugin
 		Collection<GroundItem> groundItems = collectedGroundItems.row(worldPoint).values();
 		for (GroundItem groundItem : groundItems)
 		{
-			if ((config.onlyShowLoot() && !groundItem.isMine()))
+			if ((config.onlyShowOwnItems() && !groundItem.isMine()))
 			{
 				continue;
 			}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java
deleted file mode 100644
index 604815732..000000000
--- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/LootType.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (c) 2019, 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.grounditems;
-
-enum LootType
-{
-	UNKNOWN,
-	TABLE,
-	DROPPED,
-	PVP,
-	PVM;
-}
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 5ff0061cc..3bf802401 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
@@ -181,6 +181,7 @@ public class LootTrackerPlugin extends Plugin
 		put(7827, "Dark Chest").
 		put(13117, "Rogues' Chest").
 		put(13156, "Chest (Ancient Vault)").
+		put(12348, "Muddy Chest").
 		build();
 
 	// Chests opened with keys from slayer tasks
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningPlugin.java
index f9ad5c4c9..225037203 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/mining/MiningPlugin.java
@@ -398,7 +398,7 @@ public class MiningPlugin extends Plugin
 				case DEPLETED_VEIN_26667: // Depleted motherlode vein
 				case DEPLETED_VEIN_26668: // Depleted motherlode vein
 				{
-					WorldPoint worldPoint = new WorldPoint((locCoord >>> 14) & 0x3FFF, locCoord & 0x3FFF, (locCoord >>> 28) & 0x3);
+					WorldPoint worldPoint = WorldPoint.fromCoord(locCoord);
 					Rock rock = Rock.ORE_VEIN;
 					RockRespawn rockRespawn = new RockRespawn(rock, worldPoint, Instant.now(), ticks * Constants.GAME_TICK_LENGTH, rock.getZOffset());
 					respawns.add(rockRespawn);
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java
index 1ec3069d4..c1371b50e 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java
@@ -542,7 +542,7 @@ public class WoodcuttingPlugin extends Plugin
 				case NullObjectID.NULL_34637:
 				case NullObjectID.NULL_34639:
 				{
-					WorldPoint worldPoint = new WorldPoint((locCoord >>> 14) & 0x3FFF, locCoord & 0x3FFF, (locCoord >>> 28) & 0x3);
+					WorldPoint worldPoint = WorldPoint.fromCoord(locCoord);
 					GameObject gameObject = findObject(worldPoint);
 					if (gameObject == null)
 					{
diff --git a/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java b/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java
index 8451ca134..d7ba5d675 100644
--- a/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java
+++ b/runelite-client/src/main/java/net/runelite/client/util/HotkeyListener.java
@@ -81,7 +81,7 @@ public abstract class HotkeyListener implements KeyListener
 	@Override
 	public void keyReleased(KeyEvent e)
 	{
-		if (keybind.get().matches(e))
+		if (keybind.get().matches(e) && isPressed)
 		{
 			isPressed = false;
 			isConsumingTyped = false;


Test patch:
diff --git a/runelite-client/src/test/java/net/runelite/client/config/KeybindTest.java b/runelite-client/src/test/java/net/runelite/client/config/KeybindTest.java
new file mode 100644
index 000000000..14c1a34f2
--- /dev/null
+++ b/runelite-client/src/test/java/net/runelite/client/config/KeybindTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.config;
+
+import static java.awt.event.InputEvent.CTRL_DOWN_MASK;
+import static java.awt.event.InputEvent.SHIFT_DOWN_MASK;
+import java.awt.event.KeyEvent;
+import static java.awt.event.KeyEvent.KEY_PRESSED;
+import static java.awt.event.KeyEvent.KEY_RELEASED;
+import static java.awt.event.KeyEvent.VK_CONTROL;
+import static java.awt.event.KeyEvent.VK_SHIFT;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class KeybindTest
+{
+	/**
+	 * Regression test for a bug where a modifier key keybind will not match key release if other modifiers are held down.
+	 */
+	@Test
+	public void testModifierOnlyKeybindReleaseWithOtherModifierKeyHeldDown()
+	{
+		Keybind keybind = new Keybind(VK_CONTROL, CTRL_DOWN_MASK);
+
+		// when a modifier key is pressed, the keyevent will have the modifier in the modifiers field and in the keyCode field.
+		// when it is released, it will only be in the keyCode field.
+		Assert.assertTrue(keybind.matches(createKeyEvent(KEY_PRESSED, CTRL_DOWN_MASK, VK_CONTROL)));
+		Assert.assertFalse(keybind.matches(createKeyEvent(KEY_PRESSED, SHIFT_DOWN_MASK | CTRL_DOWN_MASK, VK_SHIFT)));
+		Assert.assertTrue(keybind.matches(createKeyEvent(KEY_RELEASED, SHIFT_DOWN_MASK, VK_CONTROL)));
+		Assert.assertFalse(keybind.matches(createKeyEvent(KEY_RELEASED, 0, VK_SHIFT)));
+	}
+
+	private KeyEvent createKeyEvent(int id, int modifiersEx, int extendedKeyCode)
+	{
+		KeyEvent mock = Mockito.mock(KeyEvent.class);
+		Mockito.when(mock.getExtendedKeyCode()).thenReturn(extendedKeyCode);
+		Mockito.when(mock.getModifiersEx()).thenReturn(modifiersEx);
+		Mockito.when(mock.getID()).thenReturn(id);
+		return mock;
+	}
+}
diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java
index a13e29e04..47efe5541 100644
--- a/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java
+++ b/runelite-client/src/test/java/net/runelite/client/plugins/cannon/CannonPluginTest.java
@@ -104,11 +104,11 @@ public class CannonPluginTest
 	@Test
 	public void testAmmoCountOnPlace()
 	{
-		ChatMessage chatMessage = new ChatMessage();
-		chatMessage.setType(ChatMessageType.SPAM);
-		chatMessage.setMessage("You add the furnace.");
+		VarbitChanged varbitChanged = new VarbitChanged();
+		varbitChanged.setVarpId(VarPlayer.CANNON_STATE);
+		varbitChanged.setValue(4);
 
-		plugin.onChatMessage(chatMessage);
+		plugin.onVarbitChanged(varbitChanged);
 		assertTrue(plugin.isCannonPlaced());
 
 		plugin.onVarbitChanged(cannonAmmoChanged);
@@ -126,11 +126,11 @@ public class CannonPluginTest
 	{
 		when(config.showInfobox()).thenReturn(true);
 
-		ChatMessage chatMessage = new ChatMessage();
-		chatMessage.setType(ChatMessageType.SPAM);
-		chatMessage.setMessage("You add the furnace.");
+		VarbitChanged varbitChanged = new VarbitChanged();
+		varbitChanged.setVarpId(VarPlayer.CANNON_STATE);
+		varbitChanged.setValue(4);
 
-		plugin.onChatMessage(chatMessage);
+		plugin.onVarbitChanged(varbitChanged);
 		assertTrue(plugin.isCannonPlaced());
 
 		assertEquals(0, plugin.getCballsLeft());
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 6ed28b867..1c3d2e238 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
@@ -33,7 +33,6 @@ import net.runelite.api.Client;
 import net.runelite.api.ItemComposition;
 import net.runelite.api.ItemID;
 import net.runelite.api.ItemLayer;
-import net.runelite.api.Player;
 import net.runelite.api.Tile;
 import net.runelite.api.TileItem;
 import net.runelite.api.coords.WorldPoint;
@@ -114,7 +113,6 @@ public class GroundItemsPluginTest
 			return null;
 		}).when(executor).execute(any(Runnable.class));
 
-		when(client.getLocalPlayer()).thenReturn(mock(Player.class));
 		when(config.getHiddenItems()).thenReturn("");
 		when(config.showLootbeamForHighlighted()).thenReturn(false);
 		when(config.showLootbeamTier()).thenReturn(HighlightTier.OFF);