From c74b3b9ab5ccbdba1216ddc0e54dfbf5cc8f7994 Mon Sep 17 00:00:00 2001 From: WooxSolo Date: Tue, 19 Mar 2019 15:38:31 -0400 Subject: [PATCH] Add NPC unaggression timer --- .../net/runelite/api/coords/WorldArea.java | 26 +- .../net/runelite/api/geometry/Geometry.java | 454 +++++++++++++++++ .../npcunaggroarea/AggressionTimer.java | 68 +++ .../npcunaggroarea/NpcAggroAreaConfig.java | 95 ++++ .../NpcAggroAreaNotWorkingOverlay.java | 66 +++ .../npcunaggroarea/NpcAggroAreaOverlay.java | 119 +++++ .../npcunaggroarea/NpcAggroAreaPlugin.java | 474 ++++++++++++++++++ 7 files changed, 1300 insertions(+), 2 deletions(-) create mode 100644 runelite-api/src/main/java/net/runelite/api/geometry/Geometry.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/AggressionTimer.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaConfig.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaNotWorkingOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaOverlay.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaPlugin.java diff --git a/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java b/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java index ea96884f72..9abb3aeac8 100644 --- a/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java +++ b/runelite-api/src/main/java/net/runelite/api/coords/WorldArea.java @@ -114,8 +114,7 @@ public class WorldArea return Integer.MAX_VALUE; } - Point distances = getAxisDistances(other); - return Math.max(distances.getX(), distances.getY()); + return distanceTo2D(other); } /** @@ -129,6 +128,29 @@ public class WorldArea return distanceTo(new WorldArea(other, 1, 1)); } + /** + * Computes the shortest distance to another area while ignoring the plane. + * + * @param other the passed area + * @return the distance + */ + public int distanceTo2D(WorldArea other) + { + Point distances = getAxisDistances(other); + return Math.max(distances.getX(), distances.getY()); + } + + /** + * Computes the shortest distance to a world coordinate. + * + * @param other the passed coordinate + * @return the distance + */ + public int distanceTo2D(WorldPoint other) + { + return distanceTo2D(new WorldArea(other, 1, 1)); + } + /** * Checks whether this area is within melee distance of another. *

diff --git a/runelite-api/src/main/java/net/runelite/api/geometry/Geometry.java b/runelite-api/src/main/java/net/runelite/api/geometry/Geometry.java new file mode 100644 index 0000000000..39efa74afa --- /dev/null +++ b/runelite-api/src/main/java/net/runelite/api/geometry/Geometry.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2018, Woox + * 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.api.geometry; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.util.LinkedList; +import java.util.List; +import java.util.function.BiPredicate; +import java.util.function.Consumer; + +public class Geometry +{ + /** + * Find the point where two lines intersect. + * + * @param x1 X coordinate of the first endpoint of the first line. + * @param y1 Y coordinate of the first endpoint of the first line. + * @param x2 X coordinate of the second endpoint of the first line. + * @param y2 Y coordinate of the second endpoint of the first line. + * @param x3 X coordinate of the first endpoint of the second line. + * @param y3 Y coordinate of the first endpoint of the second line. + * @param x4 X coordinate of the second endpoint of the second line. + * @param y4 Y coordinate of the second endpoint of the second line. + * @return The intersection point of the lines, or null if the lines don't intersect. + */ + public static Point2D.Float lineIntersectionPoint( + float x1, float y1, float x2, float y2, + float x3, float y3, float x4, float y4) + { + // https://stackoverflow.com/a/1968345 + + float p1x = x2 - x1; + float p1y = y2 - y1; + float p2x = x4 - x3; + float p2y = y4 - y3; + + float s = (-p1y * (x1 - x3) + p1x * (y1 - y3)) / (-p2x * p1y + p1x * p2y); + float t = ( p2x * (y1 - y3) - p2y * (x1 - x3)) / (-p2x * p1y + p1x * p2y); + + if (s >= 0 && s <= 1 && t >= 0 && t <= 1) + { + float x = x1 + (t * p1x); + float y = y1 + (t * p1y); + return new Point2D.Float(x, y); + } + + // No intersection + return null; + } + + /** + * Find the intersection points between a Shape and a line. + * + * @param shape The shape. + * @param x1 X coordinate of the first endpoint of the line. + * @param y1 Y coordinate of the first endpoint of the line. + * @param x2 X coordinate of the second endpoint of the line. + * @param y2 Y coordinate of the second endpoint of the line. + * @return A list with the intersection points. + */ + public static List intersectionPoints(Shape shape, float x1, float y1, float x2, float y2) + { + List intersections = new LinkedList<>(); + + PathIterator it = shape.getPathIterator(new AffineTransform()); + float[] coords = new float[2]; + float[] prevCoords = new float[2]; + float[] start = new float[2]; + while (!it.isDone()) + { + int type = it.currentSegment(coords); + if (type == PathIterator.SEG_MOVETO) + { + start[0] = coords[0]; + start[1] = coords[1]; + prevCoords[0] = coords[0]; + prevCoords[1] = coords[1]; + } + else if (type == PathIterator.SEG_LINETO) + { + Point2D.Float intersection = lineIntersectionPoint( + prevCoords[0], prevCoords[1], coords[0], coords[1], x1, y1, x2, y2); + if (intersection != null) + { + intersections.add(intersection); + } + prevCoords[0] = coords[0]; + prevCoords[1] = coords[1]; + } + else if (type == PathIterator.SEG_CLOSE) + { + Point2D.Float intersection = lineIntersectionPoint( + coords[0], coords[1], start[0], start[1], x1, y1, x2, y2); + if (intersection != null) + { + intersections.add(intersection); + } + } + it.next(); + } + + return intersections; + } + + /** + * Transforms the points in a path according to a method. + * + * @param it The iterator of the path to change the points on. + * @param method The method to use to transform the points. Takes a float[2] array with x and y coordinates as parameter. + * @return The transformed path. + */ + public static GeneralPath transformPath(PathIterator it, Consumer method) + { + GeneralPath path = new GeneralPath(); + float[] coords = new float[2]; + while (!it.isDone()) + { + int type = it.currentSegment(coords); + if (type == PathIterator.SEG_MOVETO) + { + method.accept(coords); + path.moveTo(coords[0], coords[1]); + } + else if (type == PathIterator.SEG_LINETO) + { + method.accept(coords); + path.lineTo(coords[0], coords[1]); + } + else if (type == PathIterator.SEG_CLOSE) + { + path.closePath(); + } + it.next(); + } + + return path; + } + + /** + * Transforms the points in a path according to a method. + * + * @param path The path to change the points on. + * @param method The method to use to transform the points. Takes a float[2] array with x and y coordinates as parameter. + * @return The transformed path. + */ + public static GeneralPath transformPath(GeneralPath path, Consumer method) + { + return transformPath(path.getPathIterator(new AffineTransform()), method); + } + + /** + * Splits a line into smaller segments and appends the segments to a path. + * + * @param path The path to append lines to. + * @param segmentLength The desired length to use for the segmented lines. + * @param x1 X coordinate of the first endpoint of the line. + * @param y1 Y coordinate of the first endpoint of the line. + * @param x2 X coordinate of the second endpoint of the line. + * @param y2 Y coordinate of the second endpoint of the line. + */ + private static void appendSegmentLines(GeneralPath path, float segmentLength, + float x1, float y1, float x2, float y2) + { + float x = x1; + float y = y1; + float angle = (float)Math.atan2(y2 - y1, x2 - x1); + float dx = (float)Math.cos(angle) * segmentLength; + float dy = (float)Math.sin(angle) * segmentLength; + float length = (float)Math.hypot(x2 - x1, y2 - y1); + int steps = (int)((length - 1e-4) / segmentLength); + for (int i = 0; i < steps; i++) + { + x += dx; + y += dy; + path.lineTo(x, y); + } + } + + /** + * Splits a path into smaller segments. + * For example, calling this on a path with a line of length 6, with desired + * segment length of 2, would split the path into 3 consecutive lines of length 2. + * + * @param it The iterator of the path to modify. + * @param segmentLength The desired length to use for the segments. + * @return The modified path. + */ + public static GeneralPath splitIntoSegments(PathIterator it, float segmentLength) + { + GeneralPath newPath = new GeneralPath(); + float[] prevCoords = new float[2]; + float[] coords = new float[2]; + float[] startCoords = new float[2]; + while (!it.isDone()) + { + int type = it.currentSegment(coords); + if (type == PathIterator.SEG_MOVETO) + { + startCoords[0] = coords[0]; + startCoords[1] = coords[1]; + newPath.moveTo(coords[0], coords[1]); + prevCoords[0] = coords[0]; + prevCoords[1] = coords[1]; + } + else if (type == PathIterator.SEG_LINETO) + { + appendSegmentLines(newPath, segmentLength, prevCoords[0], prevCoords[1], coords[0], coords[1]); + newPath.lineTo(coords[0], coords[1]); + prevCoords[0] = coords[0]; + prevCoords[1] = coords[1]; + } + else if (type == PathIterator.SEG_CLOSE) + { + appendSegmentLines(newPath, segmentLength, coords[0], coords[1], startCoords[0], startCoords[1]); + newPath.closePath(); + } + it.next(); + } + + return newPath; + } + + /** + * Splits a path into smaller segments. + * For example, calling this on a path with a line of length 6, with desired + * segment length of 2, would split the path into 3 consecutive lines of length 2. + * + * @param path The path to modify. + * @param segmentLength The desired length to use for the segments. + * @return The modified path. + */ + public static GeneralPath splitIntoSegments(GeneralPath path, float segmentLength) + { + return splitIntoSegments(path.getPathIterator(new AffineTransform()), segmentLength); + } + + /** + * Removes lines from a path according to a method. + * + * @param it The iterator of the path to filter. + * @param method The method to use to decide which lines to remove. Takes two float[2] arrays with x and y coordinates of the endpoints of the line. Lines for which the predicate returns false are removed. + * @return The filtered path. + */ + public static GeneralPath filterPath(PathIterator it, BiPredicate method) + { + GeneralPath newPath = new GeneralPath(); + float[] prevCoords = new float[2]; + float[] coords = new float[2]; + float[] start = new float[2]; + boolean shouldMoveNext = false; + while (!it.isDone()) + { + int type = it.currentSegment(coords); + if (type == PathIterator.SEG_MOVETO) + { + start[0] = coords[0]; + start[1] = coords[1]; + prevCoords[0] = coords[0]; + prevCoords[1] = coords[1]; + shouldMoveNext = true; + } + else if (type == PathIterator.SEG_LINETO) + { + if (method.test(prevCoords, coords)) + { + if (shouldMoveNext) + { + newPath.moveTo(prevCoords[0], prevCoords[1]); + shouldMoveNext = false; + } + newPath.lineTo(coords[0], coords[1]); + } + else + { + shouldMoveNext = true; + } + prevCoords[0] = coords[0]; + prevCoords[1] = coords[1]; + } + else if (type == PathIterator.SEG_CLOSE) + { + if (shouldMoveNext) + { + newPath.moveTo(prevCoords[0], prevCoords[1]); + } + if (method.test(prevCoords, start)) + { + newPath.lineTo(start[0], start[1]); + } + shouldMoveNext = false; + } + it.next(); + } + + return newPath; + } + + /** + * Removes lines from a path according to a method. + * + * @param path The path to filter. + * @param method The method to use to decide which lines to remove. Takes two float[2] arrays with x and y coordinates of the endpoints of the line. Lines for which the predicate returns false are removed. + * @return The filtered path. + */ + public static GeneralPath filterPath(GeneralPath path, BiPredicate method) + { + return filterPath(path.getPathIterator(new AffineTransform()), method); + } + + /** + * Removes lines from a path that lie outside the clipping area and cuts + * lines intersecting with the clipping area so the resulting lines + * lie within the clipping area. + * + * @param it The iterator of the path to clip. + * @param shape The clipping area to clip with. + * @return The clipped path. + */ + public static GeneralPath clipPath(PathIterator it, Shape shape) + { + GeneralPath newPath = new GeneralPath(); + float[] prevCoords = new float[2]; + float[] coords = new float[2]; + float[] start = new float[2]; + float[] nextMove = new float[2]; + boolean shouldMove = false; + boolean wasInside = false; + while (!it.isDone()) + { + int type = it.currentSegment(coords); + if (type == PathIterator.SEG_MOVETO) + { + start[0] = coords[0]; + start[1] = coords[1]; + wasInside = shape.contains(coords[0], coords[1]); + if (wasInside) + { + nextMove[0] = coords[0]; + nextMove[1] = coords[1]; + shouldMove = true; + } + prevCoords[0] = coords[0]; + prevCoords[1] = coords[1]; + } + else if (type == PathIterator.SEG_LINETO || type == PathIterator.SEG_CLOSE) + { + if (type == PathIterator.SEG_CLOSE) + { + coords[0] = start[0]; + coords[1] = start[1]; + } + + List intersections = intersectionPoints(shape, prevCoords[0], prevCoords[1], coords[0], coords[1]); + intersections.sort((a, b) -> + { + double diff = a.distance(prevCoords[0], prevCoords[1]) - b.distance(prevCoords[0], prevCoords[1]); + if (diff < 0) + { + return -1; + } + if (diff > 0) + { + return 1; + } + return 0; + }); + + for (Point2D.Float intersection : intersections) + { + if (wasInside) + { + if (shouldMove) + { + newPath.moveTo(nextMove[0], nextMove[1]); + shouldMove = false; + } + newPath.lineTo(intersection.getX(), intersection.getY()); + } + else + { + nextMove[0] = intersection.x; + nextMove[1] = intersection.y; + shouldMove = true; + } + wasInside = !wasInside; + prevCoords[0] = intersection.x; + prevCoords[1] = intersection.y; + } + + wasInside = shape.contains(coords[0], coords[1]); + if (wasInside) + { + if (shouldMove) + { + newPath.moveTo(nextMove[0], nextMove[1]); + shouldMove = false; + } + newPath.lineTo(coords[0], coords[1]); + } + else + { + nextMove[0] = coords[0]; + nextMove[1] = coords[1]; + shouldMove = true; + } + + prevCoords[0] = coords[0]; + prevCoords[1] = coords[1]; + } + it.next(); + } + return newPath; + } + + /** + * Removes lines from a path that lie outside the clipping area and cuts + * lines intersecting with the clipping area so the resulting lines + * lie within the clipping area. + * + * @param path The path to clip. + * @param shape The clipping area to clip with. + * @return The clipped path. + */ + public static GeneralPath clipPath(GeneralPath path, Shape shape) + { + return clipPath(path.getPathIterator(new AffineTransform()), shape); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/AggressionTimer.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/AggressionTimer.java new file mode 100644 index 0000000000..7194d2ddd6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/AggressionTimer.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2018, Woox + * 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.npcunaggroarea; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import lombok.Getter; +import lombok.Setter; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.overlay.infobox.Timer; + +class AggressionTimer extends Timer +{ + @Getter + @Setter + private boolean visible; + + AggressionTimer(Duration duration, BufferedImage image, Plugin plugin, boolean visible) + { + super(duration.toMillis(), ChronoUnit.MILLIS, image, plugin); + setTooltip("Time until NPCs become unaggressive"); + this.visible = visible; + } + + @Override + public Color getTextColor() + { + Duration timeLeft = Duration.between(Instant.now(), getEndTime()); + + if (timeLeft.getSeconds() < 60) + { + return Color.RED.brighter(); + } + + return Color.WHITE; + } + + @Override + public boolean render() + { + return visible && super.render(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaConfig.java new file mode 100644 index 0000000000..a0e4992e31 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaConfig.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2018, Woox + * 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.npcunaggroarea; + +import java.awt.Color; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup("npcUnaggroArea") +public interface NpcAggroAreaConfig extends Config +{ + String CONFIG_GROUP = "npcUnaggroArea"; + String CONFIG_CENTER1 = "center1"; + String CONFIG_CENTER2 = "center2"; + String CONFIG_LOCATION = "location"; + String CONFIG_DURATION = "duration"; + + @ConfigItem( + keyName = "npcUnaggroAlwaysActive", + name = "Always active", + description = "Always show this plugins overlays
Otherwise, they will only be shown when any NPC name matches the list", + position = 1 + ) + default boolean alwaysActive() + { + return false; + } + + @ConfigItem( + keyName = "npcUnaggroNames", + name = "NPC names", + description = "Enter names of NPCs where you wish to use this plugin", + position = 2 + ) + default String npcNamePatterns() + { + return ""; + } + + @ConfigItem( + keyName = "npcUnaggroShowTimer", + name = "Show timer", + description = "Display a timer until NPCs become unaggressive", + position = 3 + ) + default boolean showTimer() + { + return true; + } + + @ConfigItem( + keyName = "npcUnaggroShowAreaLines", + name = "Show area lines", + description = "Display lines, when walked past, the unaggressive timer resets", + position = 4 + ) + default boolean showAreaLines() + { + return false; + } + + @ConfigItem( + keyName = "npcUnaggroAreaColor", + name = "Area lines colour", + description = "Choose colour to use for marking NPC unaggressive area", + position = 5 + ) + default Color aggroAreaColor() + { + return Color.YELLOW; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaNotWorkingOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaNotWorkingOverlay.java new file mode 100644 index 0000000000..0a9dedb357 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaNotWorkingOverlay.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018, Woox + * 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.npcunaggroarea; + +import com.google.inject.Inject; +import java.awt.Dimension; +import java.awt.Graphics2D; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayPriority; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.PanelComponent; + +class NpcAggroAreaNotWorkingOverlay extends Overlay +{ + private final NpcAggroAreaPlugin plugin; + private final PanelComponent panelComponent; + + @Inject + private NpcAggroAreaNotWorkingOverlay(NpcAggroAreaPlugin plugin) + { + this.plugin = plugin; + + panelComponent = new PanelComponent(); + panelComponent.setPreferredSize(new Dimension(150, 0)); + panelComponent.getChildren().add(LineComponent.builder() + .left("Unaggressive NPC timers will start working when you teleport far away or enter a dungeon.") + .build()); + + setPriority(OverlayPriority.LOW); + setPosition(OverlayPosition.TOP_LEFT); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (!plugin.isActive() || plugin.getSafeCenters()[1] != null) + { + return null; + } + + return panelComponent.render(graphics); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaOverlay.java new file mode 100644 index 0000000000..811952de57 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaOverlay.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2018, Woox + * 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.npcunaggroarea; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.GeneralPath; +import java.time.Instant; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Perspective; +import net.runelite.api.Point; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.geometry.Geometry; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.OverlayPriority; + +class NpcAggroAreaOverlay extends Overlay +{ + private static final int MAX_LOCAL_DRAW_LENGTH = 20 * Perspective.LOCAL_TILE_SIZE; + + private final Client client; + private final NpcAggroAreaConfig config; + private final NpcAggroAreaPlugin plugin; + + @Inject + private NpcAggroAreaOverlay(Client client, NpcAggroAreaConfig config, NpcAggroAreaPlugin plugin) + { + this.client = client; + this.config = config; + this.plugin = plugin; + + setLayer(OverlayLayer.ABOVE_SCENE); + setPriority(OverlayPriority.LOW); + setPosition(OverlayPosition.DYNAMIC); + } + + @Override + public Dimension render(Graphics2D graphics) + { + if (!plugin.isActive() || plugin.getSafeCenters()[1] == null) + { + return null; + } + + GeneralPath lines = plugin.getLinesToDisplay()[client.getPlane()]; + if (lines == null) + { + return null; + } + + Color outlineColor = config.aggroAreaColor(); + AggressionTimer timer = plugin.getCurrentTimer(); + if (timer == null || Instant.now().compareTo(timer.getEndTime()) < 0) + { + outlineColor = new Color( + outlineColor.getRed(), + outlineColor.getGreen(), + outlineColor.getBlue(), + 100); + } + + renderPath(graphics, lines, outlineColor); + return null; + } + + private void renderPath(Graphics2D graphics, GeneralPath path, Color color) + { + LocalPoint playerLp = client.getLocalPlayer().getLocalLocation(); + Rectangle viewArea = new Rectangle( + playerLp.getX() - MAX_LOCAL_DRAW_LENGTH, + playerLp.getY() - MAX_LOCAL_DRAW_LENGTH, + MAX_LOCAL_DRAW_LENGTH * 2, + MAX_LOCAL_DRAW_LENGTH * 2); + + graphics.setColor(color); + graphics.setStroke(new BasicStroke(1)); + + path = Geometry.clipPath(path, viewArea); + path = Geometry.filterPath(path, (p1, p2) -> + Perspective.localToCanvas(client, new LocalPoint((int)p1[0], (int)p1[1]), client.getPlane()) != null && + Perspective.localToCanvas(client, new LocalPoint((int)p2[0], (int)p2[1]), client.getPlane()) != null); + path = Geometry.transformPath(path, coords -> + { + Point point = Perspective.localToCanvas(client, new LocalPoint((int)coords[0], (int)coords[1]), client.getPlane()); + coords[0] = point.getX(); + coords[1] = point.getY(); + }); + + graphics.draw(path); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaPlugin.java new file mode 100644 index 0000000000..0020d54d25 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/npcunaggroarea/NpcAggroAreaPlugin.java @@ -0,0 +1,474 @@ +/* + * Copyright (c) 2018, Woox + * 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.npcunaggroarea; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.inject.Provides; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.geom.Area; +import java.awt.geom.GeneralPath; +import java.awt.image.BufferedImage; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import javax.inject.Inject; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.Constants; +import net.runelite.api.ItemID; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.Perspective; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ConfigChanged; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.NpcSpawned; +import net.runelite.api.geometry.Geometry; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import net.runelite.client.util.WildcardMatcher; + +@Slf4j +@PluginDescriptor( + name = "Unaggressive NPC timer", + description = "Highlights the unaggressive area of NPCs nearby and timer until it becomes active", + tags = {"highlight", "lines", "unaggro", "aggro", "aggressive", "npcs", "area", "timer", "slayer"}, + enabledByDefault = false +) +public class NpcAggroAreaPlugin extends Plugin +{ + /* + How it works: The game remembers 2 tiles. When the player goes >10 steps + away from both tiles, the oldest one is moved to under the player and the + NPC aggression timer resets. + So to first figure out where the 2 tiles are, we wait until the player teleports + a long enough distance. At that point it's very likely that the player + moved out of the radius of both tiles, which resets one of them. The other + should reset shortly after as the player starts moving around. + */ + + private static final int SAFE_AREA_RADIUS = 10; + private static final int UNKNOWN_AREA_RADIUS = SAFE_AREA_RADIUS * 2; + private static final int AGGRESSIVE_TIME_SECONDS = 600; + private static final Splitter NAME_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults(); + private static final WorldArea WILDERNESS_ABOVE_GROUND = new WorldArea(2944, 3523, 448, 448, 0); + private static final WorldArea WILDERNESS_UNDERGROUND = new WorldArea(2944, 9918, 320, 442, 0); + + @Inject + private Client client; + + @Inject + private NpcAggroAreaConfig config; + + @Inject + private NpcAggroAreaOverlay overlay; + + @Inject + private NpcAggroAreaNotWorkingOverlay notWorkingOverlay; + + @Inject + private OverlayManager overlayManager; + + @Inject + private ItemManager itemManager; + + @Inject + private InfoBoxManager infoBoxManager; + + @Inject + private ConfigManager configManager; + + @Getter + private final WorldPoint[] safeCenters = new WorldPoint[2]; + + @Getter + private final GeneralPath[] linesToDisplay = new GeneralPath[Constants.MAX_Z]; + + @Getter + private boolean active; + + @Getter + private AggressionTimer currentTimer; + + private WorldPoint lastPlayerLocation; + private WorldPoint previousUnknownCenter; + private boolean loggingIn; + private List npcNamePatterns; + + @Provides + NpcAggroAreaConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(NpcAggroAreaConfig.class); + } + + @Override + protected void startUp() throws Exception + { + overlayManager.add(overlay); + overlayManager.add(notWorkingOverlay); + npcNamePatterns = NAME_SPLITTER.splitToList(config.npcNamePatterns()); + } + + @Override + protected void shutDown() throws Exception + { + removeTimer(); + overlayManager.remove(overlay); + overlayManager.remove(notWorkingOverlay); + Arrays.fill(safeCenters, null); + lastPlayerLocation = null; + currentTimer = null; + loggingIn = false; + npcNamePatterns = null; + active = false; + + Arrays.fill(linesToDisplay, null); + } + + private Area generateSafeArea() + { + final Area area = new Area(); + + for (WorldPoint wp : safeCenters) + { + if (wp == null) + { + continue; + } + + Polygon poly = new Polygon(); + poly.addPoint(wp.getX() - SAFE_AREA_RADIUS, wp.getY() - SAFE_AREA_RADIUS); + poly.addPoint(wp.getX() - SAFE_AREA_RADIUS, wp.getY() + SAFE_AREA_RADIUS + 1); + poly.addPoint(wp.getX() + SAFE_AREA_RADIUS + 1, wp.getY() + SAFE_AREA_RADIUS + 1); + poly.addPoint(wp.getX() + SAFE_AREA_RADIUS + 1, wp.getY() - SAFE_AREA_RADIUS); + area.add(new Area(poly)); + } + + return area; + } + + private void transformWorldToLocal(float[] coords) + { + final LocalPoint lp = LocalPoint.fromWorld(client, (int)coords[0], (int)coords[1]); + coords[0] = lp.getX() - Perspective.LOCAL_TILE_SIZE / 2f; + coords[1] = lp.getY() - Perspective.LOCAL_TILE_SIZE / 2f; + } + + private void reevaluateActive() + { + if (currentTimer != null) + { + currentTimer.setVisible(active && config.showTimer()); + } + + calculateLinesToDisplay(); + } + + private void calculateLinesToDisplay() + { + if (!active || !config.showAreaLines()) + { + Arrays.fill(linesToDisplay, null); + return; + } + + Rectangle sceneRect = new Rectangle( + client.getBaseX() + 1, client.getBaseY() + 1, + Constants.SCENE_SIZE - 2, Constants.SCENE_SIZE - 2); + + for (int i = 0; i < linesToDisplay.length; i++) + { + GeneralPath lines = new GeneralPath(generateSafeArea()); + lines = Geometry.clipPath(lines, sceneRect); + lines = Geometry.splitIntoSegments(lines, 1); + lines = Geometry.transformPath(lines, this::transformWorldToLocal); + linesToDisplay[i] = lines; + } + } + + private void removeTimer() + { + infoBoxManager.removeInfoBox(currentTimer); + currentTimer = null; + } + + private void createTimer(Duration duration) + { + removeTimer(); + BufferedImage image = itemManager.getImage(ItemID.ENSOULED_DEMON_HEAD); + currentTimer = new AggressionTimer(duration, image, this, active && config.showTimer()); + infoBoxManager.addInfoBox(currentTimer); + } + + private void resetTimer() + { + createTimer(Duration.ofSeconds(AGGRESSIVE_TIME_SECONDS)); + } + + private static boolean isInWilderness(WorldPoint location) + { + return WILDERNESS_ABOVE_GROUND.distanceTo2D(location) == 0 || WILDERNESS_UNDERGROUND.distanceTo2D(location) == 0; + } + + private boolean isNpcMatch(NPC npc) + { + NPCComposition composition = npc.getTransformedComposition(); + if (composition == null) + { + return false; + } + + if (Strings.isNullOrEmpty(composition.getName())) + { + return false; + } + + // Most NPCs stop aggroing when the player has more than double + // its combat level. + int playerLvl = client.getLocalPlayer().getCombatLevel(); + int npcLvl = composition.getCombatLevel(); + String npcName = composition.getName().toLowerCase(); + if (npcLvl > 0 && playerLvl > npcLvl * 2 && !isInWilderness(npc.getWorldLocation())) + { + return false; + } + + for (String pattern : npcNamePatterns) + { + if (WildcardMatcher.matches(pattern, npcName)) + { + return true; + } + } + + return false; + } + + private void checkAreaNpcs(final NPC... npcs) + { + for (NPC npc : npcs) + { + if (npc == null) + { + continue; + } + + if (isNpcMatch(npc)) + { + active = true; + break; + } + } + + reevaluateActive(); + } + + private void recheckActive() + { + active = config.alwaysActive(); + checkAreaNpcs(client.getCachedNPCs()); + } + + @Subscribe + public void onNpcSpawned(NpcSpawned event) + { + if (config.alwaysActive()) + { + return; + } + + checkAreaNpcs(event.getNpc()); + } + + @Subscribe + public void onGameTick(GameTick event) + { + WorldPoint newLocation = client.getLocalPlayer().getWorldLocation(); + if (lastPlayerLocation != null) + { + if (safeCenters[1] == null && newLocation.distanceTo2D(lastPlayerLocation) > SAFE_AREA_RADIUS * 4) + { + safeCenters[0] = null; + safeCenters[1] = newLocation; + resetTimer(); + calculateLinesToDisplay(); + + // We don't know where the previous area was, so if the player e.g. + // entered a dungeon and then goes back out, he/she may enter the previous + // area which is unknown and would make the plugin inaccurate + previousUnknownCenter = lastPlayerLocation; + } + } + + if (safeCenters[0] == null && previousUnknownCenter != null && + previousUnknownCenter.distanceTo2D(newLocation) <= UNKNOWN_AREA_RADIUS) + { + // Player went back to their previous unknown area before the 2nd + // center point was found, which means we don't know where it is again. + safeCenters[1] = null; + removeTimer(); + calculateLinesToDisplay(); + } + + if (safeCenters[1] != null) + { + if (Arrays.stream(safeCenters).noneMatch( + x -> x != null && x.distanceTo2D(newLocation) <= SAFE_AREA_RADIUS)) + { + safeCenters[0] = safeCenters[1]; + safeCenters[1] = newLocation; + resetTimer(); + calculateLinesToDisplay(); + previousUnknownCenter = null; + } + } + + lastPlayerLocation = newLocation; + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + String key = event.getKey(); + switch (key) + { + case "npcUnaggroAlwaysActive": + recheckActive(); + break; + case "npcUnaggroShowTimer": + if (currentTimer != null) + { + currentTimer.setVisible(active && config.showTimer()); + } + break; + case "npcUnaggroCollisionDetection": + case "npcUnaggroShowAreaLines": + calculateLinesToDisplay(); + break; + case "npcUnaggroNames": + npcNamePatterns = NAME_SPLITTER.splitToList(config.npcNamePatterns()); + recheckActive(); + break; + } + } + + private void loadConfig() + { + safeCenters[0] = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER1, WorldPoint.class); + safeCenters[1] = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER2, WorldPoint.class); + lastPlayerLocation = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_LOCATION, WorldPoint.class); + + Duration timeLeft = configManager.getConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_DURATION, Duration.class); + if (timeLeft != null) + { + createTimer(timeLeft); + } + } + + private void resetConfig() + { + configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER1); + configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER2); + configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_LOCATION); + configManager.unsetConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_DURATION); + } + + private void saveConfig() + { + if (safeCenters[0] == null || safeCenters[1] == null || lastPlayerLocation == null || currentTimer == null) + { + resetConfig(); + } + else + { + configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER1, safeCenters[0]); + configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_CENTER2, safeCenters[1]); + configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_LOCATION, lastPlayerLocation); + configManager.setConfiguration(NpcAggroAreaConfig.CONFIG_GROUP, NpcAggroAreaConfig.CONFIG_DURATION, Duration.between(Instant.now(), currentTimer.getEndTime())); + } + } + + private void onLogin() + { + loadConfig(); + resetConfig(); + + WorldPoint newLocation = client.getLocalPlayer().getWorldLocation(); + assert newLocation != null; + + // If the player isn't at the location he/she logged out at, + // the safe unaggro area probably changed, and should be disposed. + if (lastPlayerLocation == null || newLocation.distanceTo(lastPlayerLocation) != 0) + { + safeCenters[0] = null; + safeCenters[1] = null; + lastPlayerLocation = newLocation; + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + switch (event.getGameState()) + { + case LOGGED_IN: + if (loggingIn) + { + loggingIn = false; + onLogin(); + } + + recheckActive(); + break; + + case LOGGING_IN: + loggingIn = true; + break; + + case LOGIN_SCREEN: + if (lastPlayerLocation != null) + { + saveConfig(); + } + + safeCenters[0] = null; + safeCenters[1] = null; + lastPlayerLocation = null; + break; + } + } +}