From c48d8db3a18ce14265eb6cba50304e666531580e Mon Sep 17 00:00:00 2001 From: Jordan Atwood Date: Tue, 11 Feb 2020 13:39:01 -0800 Subject: [PATCH 1/2] clues: Create NamedObjectClueScroll interface This commit adds a new clue type, NamedObjectClueScroll, which allows highlighting of objects based on object name. (and optionally region ID(s) where that object should reside) This commit introduces two new fidlds to the ClueScrollPlugin class to track player plane changes to address some unusual client behavior. Namely, when a player travels up or down planes, multiloc objects only have their varbit values updated on the tick after the plane change occurs. Since these objects must be in their desired state before checking their name, these fields are used to track both the player's plane, and whether the plugin should wait an extra tick to process the new plane's objects based on a plane change occurring. --- .../plugins/cluescrolls/ClueScrollPlugin.java | 219 +++++++++++++++++- .../clues/NamedObjectClueScroll.java | 41 ++++ 2 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/NamedObjectClueScroll.java 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(); +} From c2785628348effda93d675cb89e6d059fc163de2 Mon Sep 17 00:00:00 2001 From: Jordan Atwood Date: Tue, 11 Feb 2020 13:40:12 -0800 Subject: [PATCH 2/2] SkillChallengeClue: Highlight broken Dorgesh-kaan lamps For the skill challenge clue "Fix a magical lamp in Dorgesh-Kaan.", highlight lamps in the area which are broken. Fixes runelite/runelite#10069 Closes runelite/runelite#10117 --- .../cluescrolls/clues/SkillChallengeClue.java | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) 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)