From d10a9b02c5ebe20b4afe2fa7d18ef101294e1569 Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 24 May 2018 11:47:34 -0500 Subject: [PATCH] Add notifications to Grand Exchange plugin Refs #3168 --- .../grandexchange/GrandExchangeConfig.java | 23 ++++ .../GrandExchangeNotification.java | 63 ++++++++++ .../GrandExchangeNotificationHandler.java | 113 ++++++++++++++++++ .../grandexchange/GrandExchangePlugin.java | 35 ++++++ 4 files changed, 234 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeNotification.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeNotificationHandler.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeConfig.java index 94d4e981c6..3294f9d6b4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeConfig.java @@ -36,6 +36,7 @@ import net.runelite.client.config.ConfigItem; public interface GrandExchangeConfig extends Config { @ConfigItem( + position = 1, keyName = "quickLookup", name = "Hotkey lookup (Alt + Left click)", description = "Configures whether to enable the hotkey lookup for ge searches" @@ -44,4 +45,26 @@ public interface GrandExchangeConfig extends Config { return true; } + + @ConfigItem( + position = 2, + keyName = "enableNotifications", + name = "Enable Notifications", + description = "Configures whether to enable notifications when an offer updates" + ) + default boolean enableNotifications() + { + return true; + } + + @ConfigItem( + position = 3, + keyName = "notificationDelay", + name = "Notification Delay", + description = "Number of seconds between notifications on offer updates" + ) + default int notificationDelay() + { + return 5; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeNotification.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeNotification.java new file mode 100644 index 0000000000..48e281959d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeNotification.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018, Ethan + * 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.grandexchange; + +import java.time.Instant; +import lombok.Value; +import net.runelite.api.GrandExchangeOfferState; + +@Value +class GrandExchangeNotification +{ + private final int slot; + private final int quantitySold; + private final int totalQuantity; + private final String itemName; + private final GrandExchangeOfferState state; + private final Instant insertedOn = Instant.now(); + + String getNotificationMessage() + { + // Send complete or X/Y notification + switch (this.state) + { + case BUYING: + return String.format("Grand Exchange: Bought %d / %d x %s", quantitySold, totalQuantity, itemName); + + case SELLING: + return String.format("Grand Exchange: Sold %d / %d x %s", quantitySold, totalQuantity, itemName); + + case BOUGHT: + return String.format("Grand Exchange: Finished buying %d x %s", totalQuantity, itemName); + + case SOLD: + return String.format("Grand Exchange: Finished selling %d x %s", totalQuantity, itemName); + + default: + // Not possible + return null; + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeNotificationHandler.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeNotificationHandler.java new file mode 100644 index 0000000000..e9714ddea4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeNotificationHandler.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2018, Ethan + * 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.grandexchange; + +import java.time.Duration; +import java.time.Instant; +import java.util.LinkedList; +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.api.GrandExchangeOffer; +import net.runelite.api.GrandExchangeOfferState; +import net.runelite.api.ItemComposition; + +@Singleton +class GrandExchangeNotificationHandler +{ + @Inject + private GrandExchangeConfig config; + + private final LinkedList notificationQueue = new LinkedList<>(); + + private Instant lastNotificationSent = Instant.now(); + + void queueNotification(int slot, ItemComposition offerItem, GrandExchangeOffer newOffer) + { + // Get offer data + int itemId = offerItem.getId(); + int quantitySold = newOffer.getQuantitySold(); + int totalQuantity = newOffer.getTotalQuantity(); + String itemName = offerItem.getName(); + + // Ignore empty/cancelled offers, or those with invalid data + if (newOffer.getState() == GrandExchangeOfferState.EMPTY || + newOffer.getState() == GrandExchangeOfferState.CANCELLED_BUY || + newOffer.getState() == GrandExchangeOfferState.CANCELLED_SELL || + quantitySold == 0 || itemId == 0 || itemId == -1) + { + return; + } + + // Inspect the queue for an existing notification from the same offer slot + for (GrandExchangeNotification existingNotification : notificationQueue) + { + if (existingNotification.getSlot() == slot) + { + // Don't replace a "finished" notification with a "in-progress" one + if (existingNotification.getState() == GrandExchangeOfferState.BOUGHT || + existingNotification.getState() == GrandExchangeOfferState.SOLD) + { + return; + } + + // Remove the "in-progress" notification so we can replace it with a "finished" one + notificationQueue.remove(existingNotification); + break; + } + } + + // If not, just add a new notification to the queue + notificationQueue.add(new GrandExchangeNotification(slot, quantitySold, totalQuantity, itemName, newOffer.getState())); + } + + boolean canSendNotification() + { + // Can't send a notification if none are in the queue + if (notificationQueue.isEmpty()) + { + return false; + } + + // Make sure we aren't sending notifications too quickly + long timeSinceLastNotification = Duration.between(this.lastNotificationSent, Instant.now()).toMillis(); + boolean canSendNotification = timeSinceLastNotification > (this.config.notificationDelay() * 1000); + + // Make sure the next notification has been given time to update to a complete status if it instantly bought/sold + boolean notificationAvailable = Duration.between(notificationQueue.peek().getInsertedOn(), Instant.now()).toMillis() > 600; + return canSendNotification && notificationAvailable; + } + + String getNextNotification() + { + // Reset the notification timer + this.lastNotificationSent = Instant.now(); + + // Get the next notification, remove it from the queue + GrandExchangeNotification nextNotification = notificationQueue.pop(); + + // Return the notification + return nextNotification.getNotificationMessage(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java index 62228d4577..7f1062447e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java @@ -45,10 +45,12 @@ import net.runelite.api.MenuEntry; import net.runelite.api.events.ConfigChanged; import net.runelite.api.events.FocusChanged; import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; import net.runelite.api.events.GrandExchangeOfferChanged; import net.runelite.api.events.MenuEntryAdded; import net.runelite.api.widgets.WidgetID; import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.Notifier; import net.runelite.client.config.ConfigManager; import net.runelite.client.game.ItemManager; import net.runelite.client.input.KeyManager; @@ -94,6 +96,12 @@ public class GrandExchangePlugin extends Plugin @Inject private GrandExchangeConfig config; + @Inject + private Notifier notifier; + + @Inject + private GrandExchangeNotificationHandler notificationHandler; + @Provides GrandExchangeConfig provideConfig(ConfigManager configManager) { @@ -164,6 +172,33 @@ public class GrandExchangePlugin extends Plugin boolean shouldStack = offerItem.isStackable() || offer.getTotalQuantity() > 1; BufferedImage itemImage = itemManager.getImage(offer.getItemId(), offer.getTotalQuantity(), shouldStack); SwingUtilities.invokeLater(() -> panel.getOffersPanel().updateOffer(offerItem, itemImage, offerEvent.getOffer(), offerEvent.getSlot())); + this.queueNotification(offerItem, offerEvent.getOffer(), offerEvent.getSlot()); + } + + private void queueNotification(ItemComposition offerItem, GrandExchangeOffer newOffer, int slot) + { + if (!this.config.enableNotifications()) + { + return; + } + + // Queue a notification + this.notificationHandler.queueNotification(slot, offerItem, newOffer); + } + + @Subscribe + public void onTick(GameTick tick) + { + // Send a notification is the handler and a notification are available + if (this.notificationHandler.canSendNotification()) + { + // Get the next notification and send it + String notification = this.notificationHandler.getNextNotification(); + if (notification != null) + { + this.notifier.notify(notification); + } + } } @Subscribe