diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollPlugin.java index 08b5bb46c4..6087d683ee 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/ClueScrollPlugin.java @@ -37,9 +37,13 @@ import java.awt.geom.Area; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.stream.Stream; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Named; import joptsimple.internal.Strings; @@ -65,12 +69,24 @@ import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ChatMessage; import net.runelite.api.events.CommandExecuted; +import net.runelite.api.events.DecorativeObjectChanged; +import net.runelite.api.events.DecorativeObjectDespawned; +import net.runelite.api.events.DecorativeObjectSpawned; +import net.runelite.api.events.GameObjectChanged; +import net.runelite.api.events.GameObjectDespawned; +import net.runelite.api.events.GameObjectSpawned; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; +import net.runelite.api.events.GroundObjectChanged; +import net.runelite.api.events.GroundObjectDespawned; +import net.runelite.api.events.GroundObjectSpawned; import net.runelite.api.events.ItemContainerChanged; import net.runelite.api.events.MenuOptionClicked; import net.runelite.api.events.NpcDespawned; import net.runelite.api.events.NpcSpawned; +import net.runelite.api.events.WallObjectChanged; +import net.runelite.api.events.WallObjectDespawned; +import net.runelite.api.events.WallObjectSpawned; import net.runelite.api.events.WidgetLoaded; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetID; @@ -96,6 +112,7 @@ import net.runelite.client.plugins.cluescrolls.clues.LocationClueScroll; import net.runelite.client.plugins.cluescrolls.clues.LocationsClueScroll; import net.runelite.client.plugins.cluescrolls.clues.MapClue; import net.runelite.client.plugins.cluescrolls.clues.MusicClue; +import net.runelite.client.plugins.cluescrolls.clues.NamedObjectClueScroll; import net.runelite.client.plugins.cluescrolls.clues.NpcClueScroll; import net.runelite.client.plugins.cluescrolls.clues.ObjectClueScroll; import net.runelite.client.plugins.cluescrolls.clues.SkillChallengeClue; @@ -108,6 +125,7 @@ import net.runelite.client.ui.overlay.components.TextComponent; import net.runelite.client.ui.overlay.worldmap.WorldMapPointManager; import net.runelite.client.util.ImageUtil; import net.runelite.client.util.Text; +import org.apache.commons.lang3.ArrayUtils; @PluginDescriptor( name = "Clue Scroll", @@ -137,6 +155,9 @@ public class ClueScrollPlugin extends Plugin @Getter private final List objectsToMark = new ArrayList<>(); + @Getter + private final Set namedObjectsToMark = new HashSet<>(); + @Getter private Item[] equippedItems; @@ -180,6 +201,12 @@ public class ClueScrollPlugin extends Plugin private Integer clueItemId; private boolean worldMapPointsSet = false; + // Some objects will only update to their "active" state when changing to their plane after varbit changes, + // which take one extra tick to fire after the plane change. These fields are used to track those changes and delay + // scans of the current plane's tiles accordingly. + private int currentPlane = -1; + private boolean namedObjectCheckThisTick; + private final TextComponent textComponent = new TextComponent(); @Provides @@ -211,8 +238,11 @@ public class ClueScrollPlugin extends Plugin overlayManager.remove(clueScrollWorldOverlay); overlayManager.remove(clueScrollMusicOverlay); npcsToMark.clear(); + namedObjectsToMark.clear(); inventoryItems = null; equippedItems = null; + currentPlane = -1; + namedObjectCheckThisTick = false; resetClue(true); } @@ -344,6 +374,94 @@ public class ClueScrollPlugin extends Plugin } } + @Subscribe + public void onDecorativeObjectChanged(final DecorativeObjectChanged event) + { + tileObjectChangedHandler(event.getPrevious(), event.getDecorativeObject()); + } + + @Subscribe + public void onDecorativeObjectDespawned(final DecorativeObjectDespawned event) + { + tileObjectDespawnedHandler(event.getDecorativeObject()); + } + + @Subscribe + public void onDecorativeObjectSpawned(final DecorativeObjectSpawned event) + { + tileObjectSpawnedHandler(event.getDecorativeObject()); + } + + @Subscribe + public void onGameObjectChanged(final GameObjectChanged event) + { + tileObjectChangedHandler(event.getPrevious(), event.getGameObject()); + } + + @Subscribe + public void onGameObjectDespawned(final GameObjectDespawned event) + { + tileObjectDespawnedHandler(event.getGameObject()); + } + + @Subscribe + public void onGameObjectSpawned(final GameObjectSpawned event) + { + tileObjectSpawnedHandler(event.getGameObject()); + } + + @Subscribe + public void onGroundObjectChanged(final GroundObjectChanged event) + { + tileObjectChangedHandler(event.getPrevious(), event.getGroundObject()); + } + + @Subscribe + public void onGroundObjectDespawned(final GroundObjectDespawned event) + { + tileObjectDespawnedHandler(event.getGroundObject()); + } + + @Subscribe + public void onGroundObjectSpawned(final GroundObjectSpawned event) + { + tileObjectSpawnedHandler(event.getGroundObject()); + } + + @Subscribe + public void onWallObjectChanged(final WallObjectChanged event) + { + tileObjectChangedHandler(event.getPrevious(), event.getWallObject()); + } + + @Subscribe + public void onWallObjectDespawned(final WallObjectDespawned event) + { + tileObjectDespawnedHandler(event.getWallObject()); + } + + @Subscribe + public void onWallObjectSpawned(final WallObjectSpawned event) + { + tileObjectSpawnedHandler(event.getWallObject()); + } + + private void tileObjectChangedHandler(final TileObject prev, final TileObject changedTo) + { + tileObjectDespawnedHandler(prev); + tileObjectSpawnedHandler(changedTo); + } + + private void tileObjectDespawnedHandler(final TileObject despawned) + { + namedObjectsToMark.remove(despawned); + } + + private void tileObjectSpawnedHandler(final TileObject spawned) + { + checkClueNamedObject(clue, spawned); + } + @Subscribe public void onConfigChanged(ConfigChanged event) { @@ -356,7 +474,14 @@ public class ClueScrollPlugin extends Plugin @Subscribe public void onGameStateChanged(final GameStateChanged event) { - if (event.getGameState() == GameState.LOGIN_SCREEN) + final GameState state = event.getGameState(); + + if (state != GameState.LOGGED_IN) + { + namedObjectsToMark.clear(); + } + + if (state == GameState.LOGIN_SCREEN) { resetClue(true); } @@ -425,6 +550,20 @@ public class ClueScrollPlugin extends Plugin } } + // Load the current plane's tiles if a tick has elapsed since the player has changed planes + if (namedObjectCheckThisTick) + { + namedObjectCheckThisTick = false; + checkClueNamedObjects(clue); + } + + // Delay one tick when changing planes before scanning for new named objects on the new plane + if (currentPlane != client.getPlane()) + { + currentPlane = client.getPlane(); + namedObjectCheckThisTick = true; + } + // Reset clue when receiving a new beginner or master clue // These clues use a single item ID, so we cannot detect step changes based on the item ID changing final Widget chatDialogClueItem = client.getWidget(WidgetInfo.DIALOG_SPRITE_SPRITE); @@ -525,6 +664,7 @@ public class ClueScrollPlugin extends Plugin worldMapPointManager.removeIf(ClueScrollWorldMapPoint.class::isInstance); worldMapPointsSet = false; npcsToMark.clear(); + namedObjectsToMark.clear(); if (config.displayHintArrows()) { @@ -713,7 +853,6 @@ public class ClueScrollPlugin extends Plugin final Scene scene = client.getScene(); final Tile[][][] tiles = scene.getTiles(); final Tile tile = tiles[client.getPlane()][localLocation.getSceneX()][localLocation.getSceneY()]; - objectsToMark.clear(); for (GameObject object : tile.getGameObjects()) { @@ -781,6 +920,81 @@ public class ClueScrollPlugin extends Plugin } } + /** + * Scans all of the current plane's loaded tiles for {@link TileObject}s and passes any found objects to + * {@link ClueScrollPlugin#checkClueNamedObject(ClueScroll, TileObject)} for storing in the cache of discovered + * named objects. + * + * @param clue The active clue scroll + */ + private void checkClueNamedObjects(@Nullable ClueScroll clue) + { + if (!(clue instanceof NamedObjectClueScroll)) + { + return; + } + + // Search loaded tiles for objects + for (final Tile[] tiles : client.getScene().getTiles()[client.getPlane()]) + { + for (final Tile tile : tiles) + { + if (tile == null) + { + continue; + } + + for (final GameObject object : tile.getGameObjects()) + { + if (object == null) + { + continue; + } + + checkClueNamedObject(clue, object); + } + } + } + } + + /** + * Checks passed objects against the active clue's object names and regions. If the clue is a + * {@link NamedObjectClueScroll} and the object matches its allowable object names and is within its regions, the + * object will be stored in the cache of discovered named objects. + * + * @param clue The active clue scroll + * @param object The spawned or scanned object + */ + private void checkClueNamedObject(@Nullable final ClueScroll clue, @Nonnull final TileObject object) + { + if (!(clue instanceof NamedObjectClueScroll)) + { + return; + } + + final NamedObjectClueScroll namedObjectClue = (NamedObjectClueScroll) clue; + + final String[] objectNames = namedObjectClue.getObjectNames(); + final int[] regionIds = namedObjectClue.getObjectRegions(); + + if (objectNames == null || objectNames.length == 0 + || regionIds != null && !ArrayUtils.contains(regionIds, object.getWorldLocation().getRegionID())) + { + return; + } + + final ObjectComposition comp = client.getObjectDefinition(object.getId()); + final ObjectComposition impostor = comp.getImpostorIds() != null ? comp.getImpostor() : comp; + + for (final String name : objectNames) + { + if (comp.getName().equals(name) || impostor.getName().equals(name)) + { + namedObjectsToMark.add(object); + } + } + } + private void updateClue(final ClueScroll clue) { if (clue == null || clue == this.clue) @@ -790,6 +1004,7 @@ public class ClueScrollPlugin extends Plugin resetClue(false); checkClueNPCs(clue, client.getCachedNPCs()); + checkClueNamedObjects(clue); // If we have a clue, save that knowledge // so the clue window doesn't have to be open. this.clue = clue; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/NamedObjectClueScroll.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/NamedObjectClueScroll.java new file mode 100644 index 0000000000..1b52b1c213 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/NamedObjectClueScroll.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2020, Jordan Atwood + * 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.cluescrolls.clues; + +import javax.annotation.Nullable; + +/** + * Represents a clue which should highlight objects of a given name rather than a specific ID and location, as some + * clues will call for a general action which can be completed at any number of locations. The area in which this + * highlighting should occur can be restricted by giving a non-null array of region IDs where only objects within those + * regions will be highlighted. + */ +public interface NamedObjectClueScroll +{ + String[] getObjectNames(); + + @Nullable + int[] getObjectRegions(); +}