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(); +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/SkillChallengeClue.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/SkillChallengeClue.java index be50cbd31c..596d2ae821 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/SkillChallengeClue.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/SkillChallengeClue.java @@ -32,7 +32,12 @@ import net.runelite.api.EquipmentInventorySlot; import net.runelite.api.Item; import net.runelite.api.ItemID; import net.runelite.api.NPC; +import net.runelite.api.Point; +import net.runelite.api.TileObject; import net.runelite.client.plugins.cluescrolls.ClueScrollPlugin; +import static net.runelite.client.plugins.cluescrolls.ClueScrollWorldOverlay.CLICKBOX_BORDER_COLOR; +import static net.runelite.client.plugins.cluescrolls.ClueScrollWorldOverlay.CLICKBOX_FILL_COLOR; +import static net.runelite.client.plugins.cluescrolls.ClueScrollWorldOverlay.CLICKBOX_HOVER_BORDER_COLOR; import net.runelite.client.plugins.cluescrolls.clues.item.AnyRequirementCollection; import static net.runelite.client.plugins.cluescrolls.clues.item.ItemRequirements.*; import net.runelite.client.plugins.cluescrolls.clues.item.ItemRequirement; @@ -50,7 +55,7 @@ import static net.runelite.client.plugins.cluescrolls.ClueScrollOverlay.TITLED_C import static net.runelite.client.plugins.cluescrolls.ClueScrollWorldOverlay.IMAGE_Z_OFFSET; @Getter -public class SkillChallengeClue extends ClueScroll implements NpcClueScroll +public class SkillChallengeClue extends ClueScroll implements NpcClueScroll, NamedObjectClueScroll { @AllArgsConstructor @Getter @@ -138,7 +143,7 @@ public class SkillChallengeClue extends ClueScroll implements NpcClueScroll new SkillChallengeClue("Smith a mithril 2h sword.", item(ItemID.HAMMER), xOfItem(ItemID.MITHRIL_BAR, 3)), new SkillChallengeClue("Catch a raw shark.", ANY_HARPOON), new SkillChallengeClue("Cut a yew log.", ANY_AXE), - new SkillChallengeClue("Fix a magical lamp in Dorgesh-Kaan.", item(ItemID.LIGHT_ORB)), + new SkillChallengeClue("Fix a magical lamp in Dorgesh-Kaan.", new String[] { "Broken lamp" }, new int[] { 10834, 10835 }, item(ItemID.LIGHT_ORB)), new SkillChallengeClue("Burn a yew log.", item(ItemID.YEW_LOGS), item(ItemID.TINDERBOX)), new SkillChallengeClue("Cook a swordfish", "cook a swordfish", item(ItemID.RAW_SWORDFISH)), new SkillChallengeClue("Craft multiple cosmic runes from a single essence.", item(ItemID.PURE_ESSENCE)), @@ -187,6 +192,8 @@ public class SkillChallengeClue extends ClueScroll implements NpcClueScroll private final ItemRequirement[] itemRequirements; private final SingleItemRequirement returnItem; private final boolean requireEquip; + private final String[] objectNames; + private final int[] objectRegions; @Setter private boolean challengeCompleted; @@ -201,6 +208,8 @@ public class SkillChallengeClue extends ClueScroll implements NpcClueScroll this.returnItem = returnItem; this.challengeCompleted = false; this.requireEquip = false; + this.objectNames = new String[0]; + this.objectRegions = null; } // Non-cryptic Sherlock Tasks @@ -209,20 +218,32 @@ public class SkillChallengeClue extends ClueScroll implements NpcClueScroll this(challenge, challenge.toLowerCase(), itemRequirements); } + // Non-cryptic Sherlock Tasks + private SkillChallengeClue(String challenge, String[] objectNames, int[] objectRegions, ItemRequirement ... itemRequirements) + { + this(challenge, challenge.toLowerCase(), false, objectNames, objectRegions, itemRequirements); + } + // Non-cryptic Sherlock Tasks private SkillChallengeClue(String challenge, boolean requireEquip, ItemRequirement ... itemRequirements) { - this(challenge, challenge.toLowerCase(), requireEquip, itemRequirements); + this(challenge, challenge.toLowerCase(), requireEquip, new String[0], null, itemRequirements); } // Sherlock Tasks private SkillChallengeClue(String challenge, String rawChallenge, ItemRequirement ... itemRequirements) { - this(challenge, rawChallenge, false, itemRequirements); + this(challenge, rawChallenge, false, new String[0], null, itemRequirements); } // Sherlock Tasks private SkillChallengeClue(String challenge, String rawChallenge, boolean requireEquip, ItemRequirement ... itemRequirements) + { + this(challenge, rawChallenge, requireEquip, new String[0], null, itemRequirements); + } + + // Sherlock Tasks + private SkillChallengeClue(String challenge, String rawChallenge, boolean requireEquip, String[] objectNames, int[] objectRegions, ItemRequirement ... itemRequirements) { this.type = ChallengeType.SHERLOCK; this.challenge = challenge; @@ -230,6 +251,8 @@ public class SkillChallengeClue extends ClueScroll implements NpcClueScroll this.itemRequirements = itemRequirements; this.challengeCompleted = false; this.requireEquip = requireEquip; + this.objectNames = objectNames; + this.objectRegions = objectRegions; this.returnText = "" + rawChallenge + ""; this.returnItem = null; @@ -294,6 +317,25 @@ public class SkillChallengeClue extends ClueScroll implements NpcClueScroll OverlayUtil.renderActorOverlayImage(graphics, npc, plugin.getClueScrollImage(), Color.ORANGE, IMAGE_Z_OFFSET); } } + + // Mark objects + if (!challengeCompleted && objectNames.length > 0 && plugin.getNamedObjectsToMark() != null) + { + final Point mousePosition = plugin.getClient().getMouseCanvasPosition(); + + for (final TileObject object : plugin.getNamedObjectsToMark()) + { + if (plugin.getClient().getPlane() != object.getPlane()) + { + continue; + } + + OverlayUtil.renderHoverableArea(graphics, object.getClickbox(), mousePosition, + CLICKBOX_FILL_COLOR, CLICKBOX_BORDER_COLOR, CLICKBOX_HOVER_BORDER_COLOR); + + OverlayUtil.renderImageLocation(plugin.getClient(), graphics, object.getLocalLocation(), plugin.getClueScrollImage(), IMAGE_Z_OFFSET); + } + } } private static List getRequirements(ClueScrollPlugin plugin, boolean requireEquipped, ItemRequirement ... requirements)