diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigInvocationHandler.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigInvocationHandler.java index b82addb1c3..70bf800b34 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigInvocationHandler.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigInvocationHandler.java @@ -112,7 +112,15 @@ class ConfigInvocationHandler implements InvocationHandler } } - manager.setConfiguration(group.keyName(), item.keyName(), args[0].toString()); + if (newValue == null) + { + manager.unsetConfiguration(group.keyName(), item.keyName()); + } + else + { + String newValueStr = ConfigManager.objectToString(newValue); + manager.setConfiguration(group.keyName(), item.keyName(), newValueStr); + } return null; } } diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index b1c3bceef7..ce731320a4 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -39,6 +39,7 @@ import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; +import java.time.Instant; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -461,6 +462,10 @@ public class ConfigManager { return Enum.valueOf((Class) type, str); } + if (type == Instant.class) + { + return Instant.parse(str); + } return str; } @@ -489,6 +494,10 @@ public class ConfigManager Rectangle r = (Rectangle)object; return r.x + ":" + r.y + ":" + r.width + ":" + r.height; } + if (object instanceof Instant) + { + return ((Instant) object).toString(); + } return object.toString(); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorConfig.java new file mode 100644 index 0000000000..eae1cea01d --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorConfig.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2018, Danny + * 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.deathindicator; + +import java.time.Instant; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup( + keyName = "deathIndicator", + name = "Death Indicator", + description = "Configuration for the death indicator plugin" +) +public interface DeathIndicatorConfig extends Config +{ + @ConfigItem( + position = 1, + keyName = "deathHintArrow", + name = "Death Hint Arrow", + description = "Configures whether or not to show a hint arrow to death location" + ) + default boolean showDeathHintArrow() + { + return true; + } + + @ConfigItem( + position = 2, + keyName = "deathInfoBox", + name = "Death InfoBox", + description = "Configures whether or not to show item reclaim timer and death world infobox" + ) + default boolean showDeathInfoBox() + { + return true; + } + + @ConfigItem( + position = 3, + keyName = "deathOnWorldMap", + name = "Mark on World Map", + description = "Configures whether or not to show death location on the world map" + ) + default boolean showDeathOnWorldMap() + { + return true; + } + + // Stored Data + @ConfigItem( + keyName = "deathWorld", + name = "", + description = "", + hidden = true + ) + default int deathWorld() + { + return -1; + } + + @ConfigItem( + keyName = "deathWorld", + name = "", + description = "" + ) + void deathWorld(int deathWorld); + + @ConfigItem( + keyName = "deathLocationX", + name = "", + description = "", + hidden = true + ) + default int deathLocationX() + { + return -1; + } + + @ConfigItem( + keyName = "deathLocationX", + name = "", + description = "" + ) + void deathLocationX(int deathLocationX); + + @ConfigItem( + keyName = "deathLocationY", + name = "", + description = "", + hidden = true + ) + default int deathLocationY() + { + return -1; + } + + @ConfigItem( + keyName = "deathLocationY", + name = "", + description = "" + ) + void deathLocationY(int deathLocationY); + + @ConfigItem( + keyName = "deathLocationPlane", + name = "", + description = "", + hidden = true + ) + default int deathLocationPlane() + { + return -1; + } + + @ConfigItem( + keyName = "deathLocationPlane", + name = "", + description = "" + ) + void deathLocationPlane(int deathLocationPlane); + + @ConfigItem( + keyName = "timeOfDeath", + name = "", + description = "", + hidden = true + ) + Instant timeOfDeath(); + + @ConfigItem( + keyName = "timeOfDeath", + name = "", + description = "" + ) + void timeOfDeath(Instant timeOfDeath); +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorPlugin.java new file mode 100644 index 0000000000..9c031fa819 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorPlugin.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2018, Danny + * 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.deathindicator; + +import com.google.common.eventbus.Subscribe; +import com.google.inject.Provides; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import javax.imageio.ImageIO; +import javax.inject.Inject; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.ConfigChanged; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.LocalPlayerDeath; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.infobox.InfoBoxManager; +import net.runelite.client.ui.overlay.infobox.Timer; +import net.runelite.client.ui.overlay.worldmap.WorldMapPointManager; + +@PluginDescriptor( + name = "Death Indicator" +) +public class DeathIndicatorPlugin extends Plugin +{ + static BufferedImage BONES; + + @Inject + private Client client; + + @Inject + private DeathIndicatorConfig config; + + @Inject + private WorldMapPointManager worldMapPointManager; + + @Inject + private InfoBoxManager infoBoxManager; + + private Timer deathTimer; + + private boolean hasRespawned = true; + + static + { + try + { + synchronized (ImageIO.class) + { + BONES = ImageIO.read(DeathIndicatorPlugin.class.getResourceAsStream("bones.png")); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + @Provides + DeathIndicatorConfig deathIndicatorConfig(ConfigManager configManager) + { + return configManager.getConfig(DeathIndicatorConfig.class); + } + + @Override + protected void startUp() + { + if (!hasDied()) + { + return; + } + + resetInfobox(); + + if (client.getWorld() != config.deathWorld()) + { + return; + } + + if (config.showDeathHintArrow()) + { + if (!client.hasHintArrow()) + { + client.setHintArrow(new WorldPoint(config.deathLocationX(), config.deathLocationY(), config.deathLocationPlane())); + } + } + + if (config.showDeathOnWorldMap()) + { + worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + worldMapPointManager.add(new DeathWorldMapPoint(new WorldPoint(config.deathLocationX(), config.deathLocationY(), config.deathLocationPlane()))); + } + } + + @Override + protected void shutDown() + { + if (client.hasHintArrow()) + { + client.clearHintArrow(); + } + + if (deathTimer != null) + { + infoBoxManager.removeInfoBox(deathTimer); + deathTimer = null; + } + + worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + } + + @Subscribe + public void onLocalPlayerDeath(LocalPlayerDeath event) + { + if (client.isInInstancedRegion()) + { + return; + } + + hasRespawned = false; + + config.deathLocationX(client.getLocalPlayer().getWorldLocation().getX()); + config.deathLocationY(client.getLocalPlayer().getWorldLocation().getY()); + config.deathLocationPlane(client.getLocalPlayer().getWorldLocation().getPlane()); + config.deathWorld(client.getWorld()); + config.timeOfDeath(Instant.now()); + + if (config.showDeathHintArrow()) + { + client.setHintArrow(new WorldPoint(config.deathLocationX(), config.deathLocationY(), config.deathLocationPlane())); + } + + if (config.showDeathOnWorldMap()) + { + worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + worldMapPointManager.add(new DeathWorldMapPoint(new WorldPoint(config.deathLocationX(), config.deathLocationY(), config.deathLocationPlane()))); + } + + resetInfobox(); + } + + @Subscribe + public void onChatMessage(ChatMessage event) + { + if (event.getType() == ChatMessageType.SERVER) + { + if (event.getMessage().equals("Oh dear, you are dead!")) + { + hasRespawned = true; + } + } + } + + @Subscribe + public void onGameTick(GameTick event) + { + if (!hasDied() || !hasRespawned || (client.getWorld() != config.deathWorld())) + { + return; + } + + // Check if the player is at their death location, or timer has passed + WorldPoint deathPoint = new WorldPoint(config.deathLocationX(), config.deathLocationY(), config.deathLocationPlane()); + if (deathPoint.equals(client.getLocalPlayer().getWorldLocation()) || (deathTimer != null && deathTimer.cull())) + { + client.clearHintArrow(); + + if (deathTimer != null) + { + infoBoxManager.removeInfoBox(deathTimer); + deathTimer = null; + } + + worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + + resetDeath(); + } + } + + @Subscribe + public void onConfigChanged(ConfigChanged event) + { + if (event.getGroup().equals("deathIndicator")) + { + if (!config.showDeathHintArrow() && hasDied()) + { + client.clearHintArrow(); + } + + if (!config.showDeathInfoBox() && deathTimer != null) + { + infoBoxManager.removeInfoBox(deathTimer); + deathTimer = null; + } + + if (!config.showDeathOnWorldMap()) + { + worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + } + + if (!hasDied()) + { + client.clearHintArrow(); + + resetInfobox(); + + worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + } + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged event) + { + if (!hasDied()) + { + return; + } + + if (event.getGameState() == GameState.LOGGED_IN) + { + if (client.getWorld() == config.deathWorld()) + { + WorldPoint deathPoint = new WorldPoint(config.deathLocationX(), config.deathLocationY(), config.deathLocationPlane()); + + if (config.showDeathHintArrow()) + { + client.setHintArrow(deathPoint); + } + + if (config.showDeathOnWorldMap()) + { + worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + worldMapPointManager.add(new DeathWorldMapPoint(deathPoint)); + } + } + else + { + worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + } + } + } + + private boolean hasDied() + { + return config.timeOfDeath() != null; + } + + private void resetDeath() + { + config.deathLocationX(0); + config.deathLocationY(0); + config.deathLocationPlane(0); + config.deathWorld(0); + config.timeOfDeath(null); + + hasRespawned = false; + } + + private void resetInfobox() + { + if (deathTimer != null) + { + infoBoxManager.removeInfoBox(deathTimer); + deathTimer = null; + } + + if (hasDied() && config.showDeathInfoBox()) + { + Instant now = Instant.now(); + Duration timeLeft = Duration.ofHours(1).minus(Duration.between(config.timeOfDeath(), now)); + if (!timeLeft.isNegative() && !timeLeft.isZero()) + { + deathTimer = new Timer(timeLeft.getSeconds(), ChronoUnit.SECONDS, BONES, this); + deathTimer.setTooltip("Died on world: " + config.deathWorld()); + infoBoxManager.addInfoBox(deathTimer); + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathWorldMapPoint.java b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathWorldMapPoint.java new file mode 100644 index 0000000000..629d05b27e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathWorldMapPoint.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2018, Danny + * 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.deathindicator; + +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.io.IOException; +import javax.imageio.ImageIO; +import net.runelite.api.Point; +import net.runelite.api.coords.WorldPoint; +import static net.runelite.client.plugins.deathindicator.DeathIndicatorPlugin.BONES; +import net.runelite.client.ui.overlay.worldmap.WorldMapPoint; + +class DeathWorldMapPoint extends WorldMapPoint +{ + private static final BufferedImage WORLDMAP_HINT_ARROW; + private static final Point WORLDMAP_HINT_ARROW_POINT; + + static + { + BufferedImage MAP_ARROW; + try + { + synchronized (ImageIO.class) + { + MAP_ARROW = ImageIO.read(DeathWorldMapPoint.class.getResourceAsStream("clue_arrow.png")); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + + WORLDMAP_HINT_ARROW = new BufferedImage(MAP_ARROW.getWidth(), + MAP_ARROW.getHeight(), BufferedImage.TYPE_INT_ARGB); + + Graphics graphics = WORLDMAP_HINT_ARROW.getGraphics(); + graphics.drawImage(MAP_ARROW, 0, 0, null); + graphics.drawImage(BONES, 0, 1, null); + WORLDMAP_HINT_ARROW_POINT = new Point(WORLDMAP_HINT_ARROW.getWidth() / 2, WORLDMAP_HINT_ARROW.getHeight()); + } + + DeathWorldMapPoint(final WorldPoint worldPoint) + { + super(worldPoint, null); + this.setSnapToEdge(true); + this.setJumpOnClick(true); + this.setImage(WORLDMAP_HINT_ARROW); + this.setImagePoint(WORLDMAP_HINT_ARROW_POINT); + this.setTooltip("Death Location"); + } + + @Override + public void onEdgeSnap() + { + this.setImage(BONES); + this.setImagePoint(null); + this.setTooltip(null); + } + + @Override + public void onEdgeUnsnap() + { + this.setImage(WORLDMAP_HINT_ARROW); + this.setImagePoint(WORLDMAP_HINT_ARROW_POINT); + this.setTooltip("Death Location"); + } +} diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/deathindicator/bones.png b/runelite-client/src/main/resources/net/runelite/client/plugins/deathindicator/bones.png new file mode 100644 index 0000000000..90ff52f208 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/deathindicator/bones.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/deathindicator/clue_arrow.png b/runelite-client/src/main/resources/net/runelite/client/plugins/deathindicator/clue_arrow.png new file mode 100644 index 0000000000..1d0e780564 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/deathindicator/clue_arrow.png differ