From 2aba47d1c362dce04a221a233999b134bafab7c8 Mon Sep 17 00:00:00 2001 From: Seth Date: Sun, 9 Jul 2017 10:03:12 -0500 Subject: [PATCH] Add fishing plugin (#108) --- .../src/main/java/net/runelite/api/Actor.java | 6 + .../java/net/runelite/api/Perspective.java | 29 ++++ .../client/plugins/PluginManager.java | 2 + .../client/plugins/fishing/FishingConfig.java | 147 ++++++++++++++++ .../plugins/fishing/FishingOverlay.java | 134 ++++++++++++++ .../client/plugins/fishing/FishingPlugin.java | 121 +++++++++++++ .../plugins/fishing/FishingSession.java | 93 ++++++++++ .../client/plugins/fishing/FishingSpot.java | 135 ++++++++++++++ .../plugins/fishing/FishingSpotOverlay.java | 164 ++++++++++++++++++ .../client/ui/overlay/OverlayUtil.java | 31 ++++ .../client/plugins/fishing/anglerfish.png | Bin 0 -> 1272 bytes .../runelite/client/plugins/fishing/barb.png | Bin 0 -> 568 bytes .../client/plugins/fishing/lobster.png | Bin 0 -> 729 bytes .../client/plugins/fishing/minnow.png | Bin 0 -> 3528 bytes .../client/plugins/fishing/monkfish.png | Bin 0 -> 1118 bytes .../client/plugins/fishing/salmon.png | Bin 0 -> 685 bytes .../runelite/client/plugins/fishing/shark.png | Bin 0 -> 1288 bytes .../client/plugins/fishing/shrimp.png | Bin 0 -> 4083 bytes 18 files changed, 862 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingPlugin.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSession.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSpot.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSpotOverlay.java create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/fishing/anglerfish.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/fishing/barb.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/fishing/lobster.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/fishing/minnow.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/fishing/monkfish.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/fishing/salmon.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/fishing/shark.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/fishing/shrimp.png diff --git a/runelite-api/src/main/java/net/runelite/api/Actor.java b/runelite-api/src/main/java/net/runelite/api/Actor.java index 39f275cf6a..88dff72d94 100644 --- a/runelite-api/src/main/java/net/runelite/api/Actor.java +++ b/runelite-api/src/main/java/net/runelite/api/Actor.java @@ -26,6 +26,7 @@ package net.runelite.api; import java.awt.Graphics2D; import java.awt.Polygon; +import java.awt.image.BufferedImage; import java.util.Objects; import net.runelite.rs.api.CombatInfo1; import net.runelite.rs.api.CombatInfo2; @@ -185,6 +186,11 @@ public abstract class Actor extends Renderable return Perspective.getCanvasTextLocation(client, graphics, getLocalLocation(), text, zOffset); } + public Point getCanvasImageLocation(Graphics2D graphics, BufferedImage image, int zOffset) + { + return Perspective.getCanvasImageLocation(client, graphics, getLocalLocation(), image, zOffset); + } + public Point getMinimapLocation() { return Perspective.worldToMiniMap(client, getX(), getY()); diff --git a/runelite-api/src/main/java/net/runelite/api/Perspective.java b/runelite-api/src/main/java/net/runelite/api/Perspective.java index f58f70a8f9..d6fa1f05ef 100644 --- a/runelite-api/src/main/java/net/runelite/api/Perspective.java +++ b/runelite-api/src/main/java/net/runelite/api/Perspective.java @@ -28,6 +28,7 @@ import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Polygon; import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; public class Perspective { @@ -281,4 +282,32 @@ public class Perspective return new Point(xOffset, p.getY()); } + /** + * Calculates image position and centers depending on image size. + * + * @param client + * @param graphics + * @param localLocation local location of the tile + * @param image image for size measurement + * @param zOffset offset from ground plane + * @return a {@link Point} on screen corresponding to the given + * localLocation. + */ + public static Point getCanvasImageLocation(Client client, Graphics2D graphics, Point localLocation, BufferedImage image, int zOffset) + { + int plane = client.getPlane(); + + Point p = Perspective.worldToCanvas(client, localLocation.getX(), localLocation.getY(), plane, zOffset); + + if (p == null) + { + return null; + } + + int xOffset = p.getX() - image.getWidth() / 2; + int yOffset = p.getY() - image.getHeight() / 2; + + return new Point(xOffset, yOffset); + } + } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java index 94e5503508..de920620e8 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java @@ -40,6 +40,7 @@ import net.runelite.client.plugins.combatnotifier.CombatNotifier; import net.runelite.client.plugins.config.ConfigPlugin; import net.runelite.client.plugins.devtools.DevTools; import net.runelite.client.plugins.examine.ExaminePlugin; +import net.runelite.client.plugins.fishing.FishingPlugin; import net.runelite.client.plugins.fpsinfo.FPS; import net.runelite.client.plugins.grounditems.GroundItems; import net.runelite.client.plugins.hiscore.Hiscore; @@ -93,6 +94,7 @@ public class PluginManager plugins.add(new JewelryCount()); plugins.add(new XPTracker()); plugins.add(new ExaminePlugin()); + plugins.add(new FishingPlugin()); if (RuneLite.getOptions().has("developer-mode")) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingConfig.java new file mode 100644 index 0000000000..744353c290 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingConfig.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2017, Seth + * 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.fishing; + +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup( + keyName = "fishing", + name = "Fishing", + description = "Configuration for the fishing plugin" +) +public interface FishingConfig +{ + @ConfigItem( + keyName = "enabled", + name = "Enable", + description = "Configures whether or not the fishing plugin is displayed" + ) + default boolean enabled() + { + return true; + } + + @ConfigItem( + keyName = "showIcons", + name = "Display Fish icons", + description = "Configures whether icons or text is displayed" + ) + default boolean showIcons() + { + return true; + } + + @ConfigItem( + keyName = "statTimeout", + name = "Reset stats (minutes)", + description = "Configures the time until statistic is reset" + ) + default int statTimeout() + { + return 5; + } + + @ConfigItem( + keyName = "showShrimp", + name = "Show Shrimp/Anchovies", + description = "Configures whether shrimp/anchovies is displayed" + ) + default boolean showShrimp() + { + return true; + } + + @ConfigItem( + keyName = "showLobster", + name = "Show Lobster/Swordfish/Tuna", + description = "Configures whether lobster/swordfish/tuna is displayed" + ) + default boolean showLobster() + { + return true; + } + + @ConfigItem( + keyName = "showShark", + name = "Show Shark", + description = "Configures whether shark is displayed" + ) + default boolean showShark() + { + return true; + } + + @ConfigItem( + keyName = "showMonkfish", + name = "Show Monkfish", + description = "Configures whether monkfish displayed" + ) + default boolean showMonkfish() + { + return true; + } + + @ConfigItem( + keyName = "showSalmon", + name = "Show Salmon/Trout", + description = "Configures whether salmon/trout is displayed" + ) + default boolean showSalmon() + { + return true; + } + + @ConfigItem( + keyName = "showBarb", + name = "Show Barbarian fish", + description = "Configures whether barbarian fish is displayed" + ) + default boolean showBarb() + { + return true; + } + + @ConfigItem( + keyName = "showAngler", + name = "Show Anglerfish", + description = "Configures whether anglerfish is displayed" + ) + default boolean showAngler() + { + return true; + } + + @ConfigItem( + keyName = "showMinnow", + name = "Show Minnow fish", + description = "Configures whether minnow fish is displayed" + ) + default boolean showMinnow() + { + return true; + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingOverlay.java new file mode 100644 index 0000000000..be38c45243 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingOverlay.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2017, Seth + * 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.fishing; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.time.Duration; +import java.time.Instant; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.client.RuneLite; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayPriority; + +class FishingOverlay extends Overlay +{ + private static final int WIDTH = 140; + + private static final int TOP_BORDER = 2; + private static final int LEFT_BORDER = 2; + private static final int RIGHT_BORDER = 2; + private static final int BOTTOM_BORDER = 2; + private static final int SEPARATOR = 2; + + private static final Color BACKGROUND = new Color(Color.gray.getRed(), Color.gray.getGreen(), Color.gray.getBlue(), 127); + private static final String FISHING_SPOT = "Fishing spot"; + + private final Client client = RuneLite.getClient(); + + private final FishingPlugin plugin; + private final FishingConfig config; + + public FishingOverlay(FishingPlugin plugin) + { + super(OverlayPosition.TOP_LEFT, OverlayPriority.LOW); + this.plugin = plugin; + this.config = plugin.getConfig(); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (client.getGameState() != GameState.LOGGED_IN || !config.enabled()) + { + return null; + } + + FishingSession session = plugin.getSession(); + + if (session.getLastFishCaught() == null) + { + return null; + } + + Duration statTimeout = Duration.ofMinutes(config.statTimeout()); + Duration sinceCaught = Duration.between(session.getLastFishCaught(), Instant.now()); + + if (sinceCaught.compareTo(statTimeout) >= 0) + { + return null; + } + + FontMetrics metrics = graphics.getFontMetrics(); + + int height = TOP_BORDER + (metrics.getHeight() * 3) + SEPARATOR * 3 + BOTTOM_BORDER; + int y = TOP_BORDER + metrics.getHeight(); + + graphics.setColor(BACKGROUND); + graphics.fillRect(0, 0, WIDTH, height); + + if (client.getLocalPlayer().getInteracting() != null && client.getLocalPlayer().getInteracting().getName().equals(FISHING_SPOT)) + { + graphics.setColor(Color.green); + String str = "You are fishing"; + graphics.drawString(str, (WIDTH - metrics.stringWidth(str)) / 2, y); + } + else + { + graphics.setColor(Color.red); + String str = "You are NOT fishing"; + graphics.drawString(str, (WIDTH - metrics.stringWidth(str)) / 2, y); + } + + y += metrics.getHeight() + SEPARATOR; + + graphics.setColor(Color.white); + graphics.drawString("Caught Fish:", LEFT_BORDER, y); + + String totalFishedStr = Integer.toString(session.getTotalFished()); + graphics.drawString(totalFishedStr, WIDTH - RIGHT_BORDER - metrics.stringWidth(totalFishedStr), y); + + y += metrics.getHeight() + SEPARATOR; + + graphics.drawString("Fish/hr:", LEFT_BORDER, y); + + String perHourStr; + if (session.getRecentFished() > 2) + { + perHourStr = Integer.toString(session.getPerHour()); + } + else + { + perHourStr = "~"; + } + graphics.drawString(perHourStr, WIDTH - RIGHT_BORDER - metrics.stringWidth(perHourStr), y); + + return new Dimension(WIDTH, height); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingPlugin.java new file mode 100644 index 0000000000..c217671aa0 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingPlugin.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2017, Seth + * 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.fishing; + +import com.google.common.eventbus.Subscribe; +import net.runelite.api.ChatMessageType; +import net.runelite.client.RuneLite; +import net.runelite.client.events.ChatMessage; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.overlay.Overlay; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class FishingPlugin extends Plugin +{ + private static final int CHECK_INTERVAL = 1; + + private final RuneLite runelite = RuneLite.getRunelite(); + private final FishingConfig config = runelite.getConfigManager().getConfig(FishingConfig.class); + private final FishingOverlay overlay = new FishingOverlay(this); + private final FishingSpotOverlay spotOverlay = new FishingSpotOverlay(this); + + private FishingSession session = new FishingSession(); + private ScheduledFuture future; + + @Override + public Collection getOverlays() + { + return Arrays.asList(overlay, spotOverlay); + } + + @Override + protected void startUp() throws Exception + { + ScheduledExecutorService executor = runelite.getExecutor(); + future = executor.scheduleAtFixedRate(this::checkFishing, CHECK_INTERVAL, CHECK_INTERVAL, TimeUnit.SECONDS); + } + + @Override + protected void shutDown() throws Exception + { + future.cancel(true); + } + + public FishingConfig getConfig() + { + return config; + } + + public FishingSession getSession() + { + return session; + } + + @Subscribe + public void onChatMessage(ChatMessage event) + { + if (event.getType() != ChatMessageType.FILTERED) + { + return; + } + + if (event.getMessage().contains("You catch a") || event.getMessage().contains("You catch some")) + { + session.incrementFishCaught(); + } + } + + @Subscribe + public void updateConfig(ConfigChanged event) + { + spotOverlay.updateConfig(); + } + + private void checkFishing() + { + Instant lastFishCaught = session.getLastFishCaught(); + if (lastFishCaught == null) + { + return; + } + + // reset recentcaught if you haven't caught anything recently + Duration statTimeout = Duration.ofMinutes(config.statTimeout()); + Duration sinceCaught = Duration.between(lastFishCaught, Instant.now()); + + if (sinceCaught.compareTo(statTimeout) >= 0) + { + session.resetRecent(); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSession.java b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSession.java new file mode 100644 index 0000000000..a995cb82dd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSession.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2017, Adam + * 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.fishing; + +import java.time.Duration; +import java.time.Instant; + +public class FishingSession +{ + private static final Duration HOUR = Duration.ofHours(1); + + private int perHour; + + private Instant lastFishCaught; + private int totalFished; + + private Instant recentFishCaught; + private int recentFished; + + public void incrementFishCaught() + { + Instant now = Instant.now(); + + lastFishCaught = now; + ++totalFished; + + if (recentFished == 0) + { + recentFishCaught = now; + } + ++recentFished; + + Duration timeSinceStart = Duration.between(recentFishCaught, now); + if (!timeSinceStart.isZero()) + { + perHour = (int) ((double) recentFished * (double) HOUR.toMillis() / (double) timeSinceStart.toMillis()); + } + } + + public void resetRecent() + { + recentFishCaught = null; + recentFished = 0; + } + + public int getPerHour() + { + return perHour; + } + + public Instant getLastFishCaught() + { + return lastFishCaught; + } + + public int getTotalFished() + { + return totalFished; + } + + public Instant getRecentFishCaught() + { + return recentFishCaught; + } + + public int getRecentFished() + { + return recentFished; + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSpot.java b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSpot.java new file mode 100644 index 0000000000..692a53c134 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSpot.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2017, Seth + * 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.fishing; + +import java.util.HashMap; +import java.util.Map; +import static net.runelite.api.NpcID.FISHING_SPOT_1506; +import static net.runelite.api.NpcID.FISHING_SPOT_1508; +import static net.runelite.api.NpcID.FISHING_SPOT_1510; +import static net.runelite.api.NpcID.FISHING_SPOT_1511; +import static net.runelite.api.NpcID.FISHING_SPOT_1515; +import static net.runelite.api.NpcID.FISHING_SPOT_1518; +import static net.runelite.api.NpcID.FISHING_SPOT_1519; +import static net.runelite.api.NpcID.FISHING_SPOT_1520; +import static net.runelite.api.NpcID.FISHING_SPOT_1521; +import static net.runelite.api.NpcID.FISHING_SPOT_1522; +import static net.runelite.api.NpcID.FISHING_SPOT_1525; +import static net.runelite.api.NpcID.FISHING_SPOT_1526; +import static net.runelite.api.NpcID.FISHING_SPOT_1527; +import static net.runelite.api.NpcID.FISHING_SPOT_1528; +import static net.runelite.api.NpcID.FISHING_SPOT_1530; +import static net.runelite.api.NpcID.FISHING_SPOT_1542; +import static net.runelite.api.NpcID.FISHING_SPOT_1544; +import static net.runelite.api.NpcID.FISHING_SPOT_4316; +import static net.runelite.api.NpcID.FISHING_SPOT_6825; +import static net.runelite.api.NpcID.FISHING_SPOT_7155; +import static net.runelite.api.NpcID.FISHING_SPOT_7199; +import static net.runelite.api.NpcID.FISHING_SPOT_7200; +import static net.runelite.api.NpcID.FISHING_SPOT_7469; +import static net.runelite.api.NpcID.FISHING_SPOT_7470; +import static net.runelite.api.NpcID.FISHING_SPOT_7730; +import static net.runelite.api.NpcID.FISHING_SPOT_7731; +import static net.runelite.api.NpcID.FISHING_SPOT_7732; +import static net.runelite.api.NpcID.FISHING_SPOT_7733; +import static net.runelite.api.NpcID.FISHING_SPOT_7734; + +public enum FishingSpot +{ + SHRIMP("Shrimp, Anchovies", "shrimp", + FISHING_SPOT_1518, FISHING_SPOT_1525, FISHING_SPOT_1528, + FISHING_SPOT_1530, FISHING_SPOT_1544, FISHING_SPOT_7155, + FISHING_SPOT_7469 + ), + LOBSTER("Lobster, Swordfish, Tuna", "lobster", + FISHING_SPOT_1510, FISHING_SPOT_1519, FISHING_SPOT_1521, + FISHING_SPOT_1522, FISHING_SPOT_7199, FISHING_SPOT_7470 + ), + SHARK("Shark, Bass", "shark", + FISHING_SPOT_1511, FISHING_SPOT_1520, FISHING_SPOT_7200 + ), + MONKFISH("Monkfish", "monkfish", + FISHING_SPOT_4316 + ), + SALMON("Salmon, Trout", "salmon", + FISHING_SPOT_1506, FISHING_SPOT_1508, FISHING_SPOT_1515, + FISHING_SPOT_1526, FISHING_SPOT_1527 + ), + BARB_FISH("Sturgeon, Salmon, Trout", "barb", + FISHING_SPOT_1542 + ), + ANGLERFISH("Anglerfish", "anglerfish", + FISHING_SPOT_6825 + ), + MINNOW("Minnow", "minnow", + FISHING_SPOT_7730, FISHING_SPOT_7731, FISHING_SPOT_7732, FISHING_SPOT_7733, FISHING_SPOT_7734 + ); + + private static final Map fishingSpots = new HashMap<>(); + + private final String name; + private final String image; + private final int[] spots; + + static + { + FishingSpot[] spots = values(); + + for (FishingSpot spot : spots) + { + for (int spotId : spot.getIds()) + { + fishingSpots.put(spotId, spot); + } + } + } + + FishingSpot(String spot, String image, int... spots) + { + this.name = spot; + this.image = image; + this.spots = spots; + } + + public String getName() + { + return name; + } + + public String getImage() + { + return image; + } + + public int[] getIds() + { + return spots; + } + + public static FishingSpot getSpot(int npcId) + { + return fishingSpots.get(npcId); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSpotOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSpotOverlay.java new file mode 100644 index 0000000000..23256b6df5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/fishing/FishingSpotOverlay.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2017, Seth + * 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.fishing; + +import com.google.common.primitives.Ints; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import javax.imageio.ImageIO; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.NPC; +import net.runelite.api.queries.NPCQuery; +import net.runelite.client.RuneLite; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class FishingSpotOverlay extends Overlay +{ + private static final Logger logger = LoggerFactory.getLogger(FishingSpotOverlay.class); + + private final BufferedImage[] imgCache = new BufferedImage[FishingSpot.values().length]; + + private final List ids = new ArrayList<>(); + + private final FishingConfig config; + private final static Client client = RuneLite.getClient(); + + public FishingSpotOverlay(FishingPlugin plugin) + { + super(OverlayPosition.DYNAMIC); + this.config = plugin.getConfig(); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (client.getGameState() != GameState.LOGGED_IN || !config.enabled()) + { + return null; + } + + NPCQuery query = new NPCQuery() + .idEquals(Ints.toArray(ids)); + NPC[] npcs = client.runQuery(query); + + for (NPC npc : npcs) + { + FishingSpot spot = FishingSpot.getSpot(npc.getId()); + + if (spot == null) + { + continue; + } + + if (config.showIcons()) + { + BufferedImage fishImage = getFishImage(spot); + if (fishImage != null) + { + OverlayUtil.renderActorOverlayImage(graphics, npc, fishImage, Color.cyan.darker()); + } + } + else + { + String text = spot.getName(); + OverlayUtil.renderActorOverlay(graphics, npc, text, Color.cyan.darker()); + } + } + + return null; + } + + private BufferedImage getFishImage(FishingSpot spot) + { + int fishIdx = spot.ordinal(); + BufferedImage fishImage = null; + + if (imgCache[fishIdx] != null) + { + return imgCache[fishIdx]; + } + + try + { + InputStream in = FishingSpotOverlay.class.getResourceAsStream(spot.getImage() + ".png"); + fishImage = ImageIO.read(in); + imgCache[fishIdx] = fishImage; + } + catch (IOException e) + { + logger.warn("Error Loading fish icon", e); + } + + return fishImage; + } + + public void updateConfig() + { + ids.clear(); + if (config.showShrimp()) + { + ids.addAll(Ints.asList(FishingSpot.SHRIMP.getIds())); + } + if (config.showLobster()) + { + ids.addAll(Ints.asList(FishingSpot.LOBSTER.getIds())); + } + if (config.showShark()) + { + ids.addAll(Ints.asList(FishingSpot.SHARK.getIds())); + } + if (config.showMonkfish()) + { + ids.addAll(Ints.asList(FishingSpot.MONKFISH.getIds())); + } + if (config.showSalmon()) + { + ids.addAll(Ints.asList(FishingSpot.SALMON.getIds())); + } + if (config.showBarb()) + { + ids.addAll(Ints.asList(FishingSpot.BARB_FISH.getIds())); + } + if (config.showAngler()) + { + ids.addAll(Ints.asList(FishingSpot.ANGLERFISH.getIds())); + } + if (config.showMinnow()) + { + ids.addAll(Ints.asList(FishingSpot.MINNOW.getIds())); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java index 51854aa445..f9ca92fa6e 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java @@ -28,6 +28,8 @@ import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Polygon; +import java.awt.image.BufferedImage; + import net.runelite.api.Actor; import net.runelite.api.Point; import net.runelite.api.TileObject; @@ -68,6 +70,14 @@ public class OverlayUtil graphics.drawString(text, x, y); } + public static void renderImageLocation(Graphics2D graphics, Point imgLoc, BufferedImage image) + { + int x = imgLoc.getX(); + int y = imgLoc.getY(); + + graphics.drawImage(image, x, y, null); + } + public static void renderActorOverlay(Graphics2D graphics, Actor actor, String text, Color color) { Polygon poly = actor.getCanvasTilePoly(); @@ -89,6 +99,27 @@ public class OverlayUtil } } + public static void renderActorOverlayImage(Graphics2D graphics, Actor actor, BufferedImage image, Color color) + { + Polygon poly = actor.getCanvasTilePoly(); + if (poly != null) + { + renderPolygon(graphics, poly, color); + } + + Point minimapLocation = actor.getMinimapLocation(); + if (minimapLocation != null) + { + renderMinimapLocation(graphics, minimapLocation, color); + } + + Point imageLocation = actor.getCanvasImageLocation(graphics, image, actor.getModelHeight()); + if (imageLocation != null) + { + renderImageLocation(graphics, imageLocation, image); + } + } + public static void renderTileOverlay(Graphics2D graphics, TileObject tileObject, String text, Color color) { Polygon poly = tileObject.getCanvasTilePoly(); diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/fishing/anglerfish.png b/runelite-client/src/main/resources/net/runelite/client/plugins/fishing/anglerfish.png new file mode 100644 index 0000000000000000000000000000000000000000..dfa4666ebf819c49c28014f2ea8574581cfceefc GIT binary patch literal 1272 zcmVPx(vPnciR7l6Ym)~m~RTRfRcYf~dZZ?~3VpA;}MNnJm4<9U2fAy(0)L-?-Ls3#t zMDZU`go0EN(N{t2gQzG7MO!gg4bqod6;p%;QfO_kO+wRl>2{Od$?VR|o$JFSyV)k2 zO{4X|g_%2Z&-tG7J?GqeVM!5+|4FpPhJ_l~)m?wS*o7hA@mceC!e zza%?)kuu`9KTnZKLE3TX%io3LSh!V_S|IC&Wf%kNOh(jvMIa--92^CdXqz^6GD{#M z3e$?JA1ew1D7%w!$ZhaLD3(2nqo>HP%VVe4F47aq*m%_sDOaXfo`hCcAG!QJoH#iK z)_P`KiRU-QflrQ+w!t#u0ibIdx~A2=4`oCs6{T4ySEdPrh+Pl#0z$O~P;qBDa%hl^ zTQ^blC0ibRipcEcS|!BwLOyz9H!=c8wdkQ+4V>yY)oB41O?V)bxbQ=rzMnw5jU>;_F1+}SCPkBFrx5bDrZ z@7HVh@Yf$;=o*QHNza-LPQoOR5vlBYys9J!8pf}eLb1g8$pVF93AYyF)1=vvIH)=N;iMAAvu0R(e6E?+4!e5N|5rONg# z9ax5rVdyN|2YxyF9o1T+o;xx<3qmTIa@Wu`+*z0oDp(081{p+*T`cj}c(tkN{Ceu5 z0_cC}_Ew!xsMq)0%1ot*X+SWCA(PH63a!g}vY7;Bx5ng!vvJ~uAqhLh>9Hxs3u2j# zsJRg}Mc{I=2!Q7|=DcE&l#^~!(X-B>?D}|Kt!chmbK4ka`Bpqzg?WlXxp2OMZ8`P# znR6qgb9YkpC2MZ!W9s4?}oi!l!Wt7cjpz^ zG*oL#FG{5{jao0o0ItvaRRCzt)O?S^Y0yr>1cigWfV5f&BV$#TKF+QBL?qP9`<`Uu z*6rZjf#-qe!OkZJD9w00^4K%f!iC0L9p_e&g!7{78it{9=FE9Oknd4ANju)2Z3{10 zeaPG1jP}3z5&(loPt?5|F9@83&3HlZ-iM#@!oa}Ix|)H|G!A}xWRZ7{W7oNU``v^8 iiTB*t#q{Rf5&r?XU?_42eo2r30000}M{P)h8w!HNdJJAuAMW-Kssj1 zAJBx%U8RI)C;9cGqo)6#e^L=?=SV~D&f5FN!V1=SexuRi1 z1BE?*p;>zMskIbXq4>ZmS01@wfm+(?gpq|DWH2VHTcAS8dh>qANn<8{pH?S4GNqwK zDAKDXnhn!AF>Wh7(QY9N8NFI^_TRnzWQAsh^c6v~@tgV=gKy0r6pAY8bPQR@D3J|v zkJq`nymWW?ACL{PLB}Uu09KVA_tx%^&8Pspe)VxoA4^)9em?Na2zy|8Rd6?*BEBg& zy=dRSENS%eo#8(ifN7YR1{|Ms$*Te-Cf(Q<sv^RJNK+)SgNzCjq^gyN6YBF!4lc3#r{`JKJpnl(wKG!CR!OB@_ETqk)|kjcpewMg;#&<@7%OFf^ZPH4nchtPJ8 zYZ!pd4;TULt22P82e$ZfW*AK*1BVTyGrNpm)wusiu=N+udD^(+RM5r%00005P)00004XF*Lt006O% z3;baP00009a7bBm000id000id0mpBsWB>pF8gxZibW?9;ba!ELWdKlNX>N2bPDNB8 zb~7$DE-^4L^m3s900LD>L_t(YOTCs)YZFlvN7FCRMHYd;5C|b4Sx7;;XckJKf}%*U zmXbmPZd|xZA`(iu7*WO>a-SX@1-Z zAz$mX4-mWBs9q=KFMz}15h&bLb;4Aun!M~L0aPSzCr zc9t;c**fyYJ4BMQd#SjQn9fS9Xa61{@2{Wr@`hq3`hniTxPO?% zlC!Bev%K>;J?meV2_>98>f~z=A<4Og1Z_ZZq)c<|iY4T_laOmqn})NG8 z$Yx{rs|cthmXz;G?!`PaD{2EAJHHvV_O`EQRe_{su#DM`JWFb0et&@0-M00VABzUQ zkrzZ#WK=1sgbbSF<#SrXV1!Si!B`MVYG*Ja3|4`p>;WW_);dm6unHvA5d>8k9l?k& zSQU~Zv(t;T%C#8=t766c)&E(y^U?i?saDe~d|OW#tV12y9jWyXMI0U|-MV)f00000 LNkvXXu0mjfiiAs1 literal 0 HcmV?d00001 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/fishing/minnow.png b/runelite-client/src/main/resources/net/runelite/client/plugins/fishing/minnow.png new file mode 100644 index 0000000000000000000000000000000000000000..7829433de1a2a9c5b389e9d60624d80bc288bc52 GIT binary patch literal 3528 zcmbVOduY{F9RK`&yZ7#Tx2yA&BQT0A97L8wAxmX3-DYkM(aKHD&B26B8%lqqe?=nM zBuo}+EA8PzH+-S_AkA&mpqL0QDkD>pEpwFabzi@Ge|W!bt##5bFoCh5JAj7Kg%HCJ`Qb9g!{`Q^y zYNtR(y;-0Fy)U}(%wJAqlVmfc0tMrvgDZ0e2n0{5eHp0@o@11K7uvtKEvpNma4*84UfjAJKwWLExZ&UUgCmgBzoRK8A_Vx>QQ`25- z-MUjnR!Pu#(b{^(Zf-uQ@`8EN*tl1%nTz@(e1FwM2S?`>=OLy!`pbfvw7fM5@q`-+ zDTm~~bkrczQqhLrk*V9X0|)A{ZQDnVX-XqOqCXS|<|YTz?|z5Lkdpx!agcZ<229!N zY29~>x+}uN+8wNky9k(xG>0T(zB!LHvjDNN*X_15&58@phd7u7n(*k_2^j59ny2Mz z=W@UB+8k_}HYGJmWYEyC8@kGfNQbhx$BnZv${&OkkHIuOh+9!S|3bMs7c@iV0DH;8 zm9N%}sOj*O)tZ0S+Jj0-?}=A|#xH70&3`=Qa%~OvkbGhwgeL z(h=dBHRkoIZh?fPaeq%Ae!JL#tCxRQ^_DylL`DQi?I*|AO+bc^(K$z6YN+Upk;;Aq zSa5YI=1-eqJ4`f^dtUfqw-cR=Kn8-NW2z?_hKb%DFG2;2arlqvL-WMV#J$$$(tnMO zpJM6K$~)SNV;Lg%U>O(&uajgg@$5nBiFEF~OS+5Dk%yxUmEag$P9u|!d_pDa%CiuO zNwlIC!WQuE@ji!OB*h$M9Lzxr?G=J`jeqfZo#b@_3Aw*kuU_v^lvb6)7sJoU^=Zmk z2=gWL^+iBFq?Z~rq%SRk;W6P8*ZMP)LQK=d$*jqMB>@-9I6rseigS6o71_0`7U}8f z^7)h!T8R6-K;?A@8t$C@lrmg&bevVaj_;V56-l~Qo=|adGt?YceK0Ck$E~<}kGZWU z4-U5=9cFiklUT$5r-X^br%yGZv#Sdg6>lgC&72!;8P^)_wqA#q=S9nxh#_&N(oDjm6Vjp(H7^}T}|GUYD!2>$g5p) zkrmZll6QaAa!7BRuG=71c_T&;3->mIS`r T(e~#|;up;;$v^bss(1baq(mOV literal 0 HcmV?d00001 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/fishing/monkfish.png b/runelite-client/src/main/resources/net/runelite/client/plugins/fishing/monkfish.png new file mode 100644 index 0000000000000000000000000000000000000000..0b0f0faf284424e508623733e109711b1932ec13 GIT binary patch literal 1118 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 literal 0 HcmV?d00001