From d94abb884d7aa607746aa81f361ac894dbfa5b73 Mon Sep 17 00:00:00 2001 From: Jordan Atwood Date: Thu, 24 Feb 2022 21:02:24 -0800 Subject: [PATCH] slayer: Fix name matching The Slayer plugin highlights target monsters based on their name rather than NPC ID, as many common monsters (skeletons, zombies, etc.) have nearly endless variations for different models and combat levels. Previously, this name matching was done via a simple `String#contains()`, which led to some incorrect matches such as pirates being highlighted while on rat tasks and Jonny the beard being highlighted while on bear tasks. This commit changes matching to use regex to match string boundaries or whitespace at either end of the task string, ensuring these substring matches can only happen when word breaks occur. The only known existing case where this would apply is for baby dragons and brutal dragons, which are valid alternatives for their respective chromatic dragon tasks. --- .../client/plugins/slayer/SlayerPlugin.java | 32 +++++++++++-------- .../plugins/slayer/SlayerPluginTest.java | 28 ++++++++++++++++ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java index b535ec643a..6c7b95ebd9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/slayer/SlayerPlugin.java @@ -203,7 +203,7 @@ public class SlayerPlugin extends Plugin private int cachedXp = -1; private Instant infoTimer; private boolean loginFlag; - private final List targetNames = new ArrayList<>(); + private final List targetNames = new ArrayList<>(); public final Function isTarget = (n) -> { @@ -664,7 +664,8 @@ public class SlayerPlugin extends Plugin SlayerUnlock.GROTESQUE_GUARDIAN_DOUBLE_COUNT.isEnabled(client); } - private boolean isTarget(NPC npc) + @VisibleForTesting + boolean isTarget(NPC npc) { if (targetNames.isEmpty()) { @@ -681,16 +682,15 @@ public class SlayerPlugin extends Plugin .replace('\u00A0', ' ') .toLowerCase(); - for (String target : targetNames) + for (Pattern target : targetNames) { - if (name.contains(target)) - { - if (ArrayUtils.contains(composition.getActions(), "Attack") + final Matcher targetMatcher = target.matcher(name); + if (targetMatcher.find() + && (ArrayUtils.contains(composition.getActions(), "Attack") // Pick action is for zygomite-fungi - || ArrayUtils.contains(composition.getActions(), "Pick")) - { - return true; - } + || ArrayUtils.contains(composition.getActions(), "Pick"))) + { + return true; } } return false; @@ -703,13 +703,18 @@ public class SlayerPlugin extends Plugin if (task != null) { Arrays.stream(task.getTargetNames()) - .map(String::toLowerCase) + .map(SlayerPlugin::targetNamePattern) .forEach(targetNames::add); - targetNames.add(taskName.toLowerCase().replaceAll("s$", "")); + targetNames.add(targetNamePattern(taskName.replaceAll("s$", ""))); } } + private static Pattern targetNamePattern(final String targetName) + { + return Pattern.compile("(?:\\s|^)" + targetName + "(?:\\s|$)", Pattern.CASE_INSENSITIVE); + } + private void rebuildTargetList() { targets.clear(); @@ -723,7 +728,8 @@ public class SlayerPlugin extends Plugin } } - private void setTask(String name, int amt, int initAmt) + @VisibleForTesting + void setTask(String name, int amt, int initAmt) { setTask(name, amt, initAmt, null); } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java index 3fc570c233..6ce2d86783 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/slayer/SlayerPluginTest.java @@ -60,6 +60,8 @@ import net.runelite.client.game.npcoverlay.NpcOverlayService; import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -926,4 +928,30 @@ public class SlayerPluginTest assertEquals("Suqahs", slayerPlugin.getTaskName()); assertEquals(229, slayerPlugin.getAmount()); // 2 kills } + + @Test + public void npcMatching() + { + assertTrue(matches("Abyssal demon", Task.ABYSSAL_DEMONS)); + assertTrue(matches("Baby blue dragon", Task.BLUE_DRAGONS)); + assertTrue(matches("Duck", Task.BIRDS)); + assertTrue(matches("Donny the Lad", Task.BANDITS)); + + assertFalse(matches("Rat", Task.PIRATES)); + assertFalse(matches("Wolf", Task.WEREWOLVES)); + assertFalse(matches("Scorpia's offspring", Task.SCORPIA)); + assertFalse(matches("Jonny the beard", Task.BEARS)); + } + + private boolean matches(final String npcName, final Task task) + { + final NPC npc = mock(NPC.class); + final NPCComposition comp = mock(NPCComposition.class); + when(npc.getTransformedComposition()).thenReturn(comp); + when(comp.getName()).thenReturn(npcName); + when(comp.getActions()).thenReturn(new String[] { "Attack" }); + + slayerPlugin.setTask(task.getName(), 0, 0); + return slayerPlugin.isTarget(npc); + } }