Repository: pajlads/DinkPlugin (java)

Issue:
 Feature Request: Option to ignore deaths in specific regionIDs
### Checklist

- [X] I've searched the [issues and pull requests](https://github.com/pajlads/Dinkplugin/issues?q=) for similar looking suggestions.
- [X] I've checked the `Unreleased` section of the [changelog](https://github.com/pajlads/DinkPlugin/blob/master/CHANGELOG.md) for newly added features that sound like my suggestion.

### Describe your Suggestion

UIMs love to deathpile in specific spots, mostly in the middle of priff. It'd be cool if they could disable sending death screenshots when they're just deathpiling.

### Reasoning

It's quite spammy when you get multiple of these per day from each UIM:

![afbeelding](https://github.com/pajlads/DinkPlugin/assets/7929021/cd75370a-9109-450f-9619-76c374b2255f)

They're not really fun to look at, since it's a very intentional death; not someone fucking up.

Pull request:
feat(death): allow customization of ignored regions
Closes #414


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 659f002..5720dff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
 ## Unreleased
 
+- Minor: Allow customization of region IDs where deaths should be ignored. (#415)
+
 ## 1.8.3
 
 - Minor: Include Scurrius pet name in pet notifications. (#410)
diff --git a/README.md b/README.md
index 0ebe7b1..5603fdc 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,15 @@ Note: There is no undo button for this command, so consider making a backup of y
 Warning: If you import override URLs for a notifier (that previously did not have any overrides), this will result in the plugin no longer sending messages from that notifier to your old primary URLs.
 As such, you can manually add your primary URLs to the newly populated override URL boxes so that notifications are still sent to the old primary URLs.
 
+### Get Current Region ID via `::dinkregion`
+
+The death notifier allows you to customize any region that should be ignored.
+This is particularly relevant for ultimate ironmen (UIM) who frequently use particular locations to deathbank/deathpile.
+
+To facilitate this process, the `::dinkregion` chat command outputs the player's current region ID.
+
+For example, Prifddinas spans the following region IDs: 12894, 12895, 13150, and 13151.
+
 ---
 
 ## Notifier Configuration
diff --git a/src/main/java/dinkplugin/DinkPlugin.java b/src/main/java/dinkplugin/DinkPlugin.java
index f6fe67c..e0461e4 100644
--- a/src/main/java/dinkplugin/DinkPlugin.java
+++ b/src/main/java/dinkplugin/DinkPlugin.java
@@ -95,6 +95,7 @@ public class DinkPlugin extends Plugin {
         log.debug("Started up Dink");
         settingsManager.init();
         lootNotifier.init();
+        deathNotifier.init();
     }
 
     @Override
@@ -110,6 +111,7 @@ public class DinkPlugin extends Plugin {
         clueNotifier.reset();
         diaryNotifier.reset();
         levelNotifier.reset();
+        deathNotifier.reset();
         slayerNotifier.reset();
         killCountNotifier.reset();
         groupStorageNotifier.reset();
@@ -140,6 +142,7 @@ public class DinkPlugin extends Plugin {
 
         settingsManager.onConfigChanged(event);
         lootNotifier.onConfigChanged(event.getKey(), event.getNewValue());
+        deathNotifier.onConfigChanged(event.getKey(), event.getNewValue());
     }
 
     @Subscribe
diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java
index d551db8..a297890 100644
--- a/src/main/java/dinkplugin/DinkPluginConfig.java
+++ b/src/main/java/dinkplugin/DinkPluginConfig.java
@@ -943,12 +943,25 @@ public interface DinkPluginConfig extends Config {
         return EnumSet.of(ExceptionalDeath.FIGHT_CAVE, ExceptionalDeath.INFERNO);
     }
 
+    @ConfigItem(
+        keyName = "deathIgnoredRegions",
+        name = "Ignored Region IDs",
+        description = "User-specified, comma-separated Region IDs where deaths should be ignored.<br/>" +
+            "Use the '::dinkregion' chat command or an online map to obtain the region IDs.<br/>" +
+            "For example, Prifddinas spans 12894, 12895, 13150, 13151",
+        position = 45,
+        section = deathSection
+    )
+    default String deathIgnoredRegions() {
+        return "123, 456, 789";
+    }
+
     @ConfigItem(
         keyName = "deathMinValue",
         name = "Min Lost Value",
         description = "The minimum value of the lost items for a notification to be sent.<br/>" +
             "This setting does not apply for safe deaths",
-        position = 45,
+        position = 46,
         section = deathSection
     )
     default int deathMinValue() {
@@ -961,7 +974,7 @@ public interface DinkPluginConfig extends Config {
         description = "The message to be sent through the webhook.<br/>" +
             "Use %USERNAME% to insert your username<br/>" +
             "Use %VALUELOST% to insert the GE value of the stuff you lost",
-        position = 46,
+        position = 47,
         section = deathSection
     )
     default String deathNotifyMessage() {
@@ -972,7 +985,7 @@ public interface DinkPluginConfig extends Config {
         keyName = "deathNotifPvpEnabled",
         name = "Distinguish PvP deaths",
         description = "Should the plugin use a different message for dying in PvP?",
-        position = 47,
+        position = 48,
         section = deathSection
     )
     default boolean deathNotifPvpEnabled() {
@@ -986,7 +999,7 @@ public interface DinkPluginConfig extends Config {
             "Use %PKER% to insert the killer<br/>" +
             "Use %USERNAME% to insert your username<br/>" +
             "Use %VALUELOST% to insert the GE value of the stuff you lost",
-        position = 48,
+        position = 49,
         section = deathSection
     )
     default String deathNotifPvpMessage() {
diff --git a/src/main/java/dinkplugin/SettingsManager.java b/src/main/java/dinkplugin/SettingsManager.java
index fb748f4..8a985be 100644
--- a/src/main/java/dinkplugin/SettingsManager.java
+++ b/src/main/java/dinkplugin/SettingsManager.java
@@ -9,6 +9,7 @@ import dinkplugin.notifiers.CombatTaskNotifier;
 import dinkplugin.notifiers.KillCountNotifier;
 import dinkplugin.notifiers.PetNotifier;
 import dinkplugin.util.Utils;
+import dinkplugin.util.WorldUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
@@ -158,6 +159,9 @@ public class SettingsManager {
             }
 
             exportConfig(includeKey);
+        } else if ("DinkRegion".equalsIgnoreCase(cmd)) {
+            int regionId = WorldUtils.getLocation(client).getRegionID();
+            plugin.addChatSuccess(String.format("Your current region ID is: %d", regionId));
         }
     }
 
diff --git a/src/main/java/dinkplugin/notifiers/DeathNotifier.java b/src/main/java/dinkplugin/notifiers/DeathNotifier.java
index 8267a1b..cea2101 100644
--- a/src/main/java/dinkplugin/notifiers/DeathNotifier.java
+++ b/src/main/java/dinkplugin/notifiers/DeathNotifier.java
@@ -6,6 +6,7 @@ import dinkplugin.domain.ExceptionalDeath;
 import dinkplugin.message.Embed;
 import dinkplugin.message.templating.Replacements;
 import dinkplugin.message.templating.Template;
+import dinkplugin.util.ConfigUtil;
 import dinkplugin.util.ItemUtils;
 import dinkplugin.util.Utils;
 import dinkplugin.message.NotificationBody;
@@ -13,6 +14,7 @@ import dinkplugin.message.NotificationType;
 import dinkplugin.notifiers.data.DeathNotificationData;
 import dinkplugin.notifiers.data.SerializedItemStack;
 import dinkplugin.util.WorldUtils;
+import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.Actor;
 import net.runelite.api.Item;
@@ -43,6 +45,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.BiFunction;
@@ -86,6 +89,11 @@ public class DeathNotifier extends BaseNotifier {
      */
     private static final Function<Player, Comparator<Player>> PK_COMPARATOR;
 
+    /**
+     * User-specified Region IDs where death notifications should not be triggered.
+     */
+    private final Collection<Integer> ignoredRegions = new HashSet<>();
+
     @Inject
     private ItemManager itemManager;
 
@@ -114,6 +122,20 @@ public class DeathNotifier extends BaseNotifier {
         return config.deathWebhook();
     }
 
+    public void init() {
+        setIgnoredRegions(config.deathIgnoredRegions());
+    }
+
+    public void reset() {
+        setIgnoredRegions(null);
+    }
+
+    public void onConfigChanged(String key, String value) {
+        if ("deathIgnoredRegions".equals(key)) {
+            setIgnoredRegions(value);
+        }
+    }
+
     public void onActorDeath(ActorDeath actor) {
         boolean self = client.getLocalPlayer() == actor.getActor();
 
@@ -153,7 +175,11 @@ public class DeathNotifier extends BaseNotifier {
     }
 
     private void handleNotify(Danger dangerOverride) {
-        Danger danger = dangerOverride != null ? dangerOverride : WorldUtils.getDangerLevel(client, config.deathSafeExceptions());
+        int regionId = WorldUtils.getLocation(client).getRegionID();
+        if (ignoredRegions.contains(regionId))
+            return;
+
+        Danger danger = dangerOverride != null ? dangerOverride : WorldUtils.getDangerLevel(client, regionId, config.deathSafeExceptions());
         if (danger == Danger.SAFE && config.deathIgnoreSafe())
             return;
 
@@ -260,6 +286,20 @@ public class DeathNotifier extends BaseNotifier {
         return isEnabled();
     }
 
+    @Synchronized
+    private void setIgnoredRegions(@Nullable String configValue) {
+        ignoredRegions.clear();
+        ConfigUtil.readDelimited(configValue).forEach(str -> {
+            try {
+                int regionId = Integer.parseInt(str);
+                ignoredRegions.add(regionId);
+            } catch (NumberFormatException e) {
+                log.warn("Failed to parse death ignored region as integer: {}", str);
+            }
+        });
+        log.debug("Updated ignored regions to: {}", ignoredRegions);
+    }
+
     /**
      * @return the number of items the player would keep on an unsafe death
      */
diff --git a/src/main/java/dinkplugin/util/WorldUtils.java b/src/main/java/dinkplugin/util/WorldUtils.java
index ef1af2d..c06b028 100644
--- a/src/main/java/dinkplugin/util/WorldUtils.java
+++ b/src/main/java/dinkplugin/util/WorldUtils.java
@@ -35,7 +35,6 @@ public class WorldUtils {
     private final Set<Integer> SOUL_REGIONS = ImmutableSet.of(8493, 8748, 8749, 9005);
     private final Set<Integer> TOA_REGIONS = ImmutableSet.of(14160, 14162, 14164, 14674, 14676, 15184, 15186, 15188, 15696, 15698, 15700);
     private final Set<Integer> TOB_REGIONS = ImmutableSet.of(12611, 12612, 12613, 12867, 12869, 13122, 13123, 13125, 13379);
-    private final int BURTHORPE_REGION = 8781;
     private final int INFERNO_REGION = 9043;
     private final int NMZ_REGION = 9033;
     private final int TZHAAR_CAVE = 9551;
@@ -84,10 +83,6 @@ public class WorldUtils {
         return BA_REGIONS.contains(regionId);
     }
 
-    public boolean isBurthorpeGameRoom(int regionId) {
-        return regionId == BURTHORPE_REGION;
-    }
-
     public boolean isCastleWars(int regionId) {
         return CASTLE_WARS_REGIONS.contains(regionId);
     }
@@ -142,7 +137,7 @@ public class WorldUtils {
     }
 
     public boolean isSafeArea(Client client) {
-        return getDangerLevel(client, Collections.emptySet()) == Danger.SAFE;
+        return getDangerLevel(client, getLocation(client).getRegionID(), Collections.emptySet()) == Danger.SAFE;
     }
 
     public boolean isSoulWars(int regionId) {
@@ -165,9 +160,7 @@ public class WorldUtils {
         return regionId == TZHAAR_PIT;
     }
 
-    public Danger getDangerLevel(Client client, Set<ExceptionalDeath> exceptions) {
-        int regionId = getLocation(client).getRegionID();
-
+    public Danger getDangerLevel(Client client, int regionId, Set<ExceptionalDeath> exceptions) {
         if (isGauntlet(regionId)) {
             // Players can't take items in or out of (Corrupted) Gauntlet, so these deaths are effectively safe
             // However: any Gauntlet death is still dangerous for hardcore (group) ironmen


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java b/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java
index 9f7efb5..9e6a7a7 100644
--- a/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/DeathNotifierTest.java
@@ -663,6 +663,22 @@ class DeathNotifierTest extends MockedNotifierTest {
         verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
     }
 
+    @Test
+    void testIgnoreCustom() {
+        // update config mocks
+        when(config.deathIgnoredRegions()).thenReturn("12336");
+        notifier.init();
+
+        // mock tutorial island
+        when(localPlayer.getWorldLocation()).thenReturn(new WorldPoint(3100, 3100, 0));
+
+        // fire event
+        plugin.onActorDeath(new ActorDeath(localPlayer));
+
+        // ensure no notification occurred
+        verify(messageHandler, never()).createMessage(any(), anyBoolean(), any());
+    }
+
     @Test
     void testDisabled() {
         // disable notifier