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;
+ }
+ }
+}