diff --git a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPlugin.java index 2780851b2f..276d08f405 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPlugin.java @@ -30,7 +30,7 @@ ) @Slf4j public class FarmTreeRunPlugin extends Plugin { - public static final String version = "1.2.3"; + public static final String version = "1.2.5"; @Inject private FarmTreeRunConfig config; @Provides diff --git a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunScript.java b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunScript.java index f349d18695..119f417a7e 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunScript.java @@ -868,7 +868,11 @@ private boolean handlePatch(FarmTreeRunConfig config, Patch patch) { // Gagex named actions differently, sometimes it's Pick-fruit and sometimes Pick-banana. // Also seen "Chop down" and "Chop-down". - List exactTreeActions = Arrays.stream(Rs2GameObject.findObjectComposition(treePatch.getId()).getActions()).collect(Collectors.toList()); + ObjectComposition treeComposition = Rs2GameObject.findObjectComposition(treePatch.getId()); + if (treeComposition == null || treeComposition.getActions() == null) { + return false; + } + List exactTreeActions = Arrays.stream(treeComposition.getActions()).collect(Collectors.toList()); for (String action : exactTreeActions) { if (action == null) continue; @@ -954,13 +958,13 @@ private void handleNotingFruit(Patch patch) { * @return {@code true} if payment was successful, else {@code false} */ private boolean handlePayment(FarmTreeRunConfig config, Patch patch, PaymentKind action) { - if (isTreePatch(patch) && !isPatchEmpty(patch) && !shouldProtectTree(config) && action != PaymentKind.CLEAR) + if (isTreePatch(patch) && !shouldProtectTree(config) && action != PaymentKind.CLEAR) return true; - if (isFruitTreePatch(patch) && !isPatchEmpty(patch) && !shouldProtectFruitTree(config) && action != PaymentKind.CLEAR) + if (isFruitTreePatch(patch) && !shouldProtectFruitTree(config) && action != PaymentKind.CLEAR) return true; - if (isHardTreePatch(patch) && !isPatchEmpty(patch) && !shouldProtectHardTree(config) && action != PaymentKind.CLEAR) + if (isHardTreePatch(patch) && !shouldProtectHardTree(config) && action != PaymentKind.CLEAR) return true; int paymentId = isHardTreePatch(patch) ? config.selectedHardTree().getPaymentId() @@ -1207,8 +1211,12 @@ private boolean isCompostEnabled(FarmTreeRunConfig config) { } private boolean isPatchEmpty(Patch patch) { - String name = Rs2GameObject.getObjectComposition(patch.getId()).getName().toLowerCase(); - return name.endsWith("patch"); + return isEmptyPatchComposition(Rs2GameObject.getObjectComposition(patch.getId())); + } + + static boolean isEmptyPatchComposition(ObjectComposition composition) { + if (composition == null || composition.getName() == null) return true; + return composition.getName().toLowerCase().endsWith("patch"); } private Integer getLinkedId(int id) { diff --git a/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunHandlePatchTest.java b/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunHandlePatchTest.java new file mode 100644 index 0000000000..59f54c92d9 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunHandlePatchTest.java @@ -0,0 +1,99 @@ +package net.runelite.client.plugins.microbot.farmtreerun; + +import net.runelite.api.GameObject; +import net.runelite.api.ObjectComposition; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; +import net.runelite.client.plugins.microbot.util.npc.Rs2Npc; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.concurrent.ScheduledFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +class FarmTreeRunHandlePatchTest { + + private static FarmTreeRunScript runningScript(FarmTreeRunConfig config) throws Exception { + FarmTreeRunScript script = new FarmTreeRunScript(mock(FarmTreeRunPlugin.class), config); + ScheduledFuture future = mock(ScheduledFuture.class); + when(future.isDone()).thenReturn(false); + Field field = Script.class.getDeclaredField("mainScheduledFuture"); + field.setAccessible(true); + field.set(script, future); + return script; + } + + @Test + void nullTreeCompositionSkipsPatchInsteadOfThrowing() throws Exception { + FarmTreeRunConfig config = mock(FarmTreeRunConfig.class); + FarmTreeRunScript script = runningScript(config); + GameObject treePatch = mock(GameObject.class); + when(treePatch.getId()).thenReturn(8392); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic gameObject = mockStatic(Rs2GameObject.class)) { + microbot.when(Microbot::isLoggedIn).thenReturn(true); + gameObject.when(() -> Rs2GameObject.findObjectByImposter(anyInt(), anyString(), eq(false))).thenReturn(treePatch); + gameObject.when(() -> Rs2GameObject.findObjectComposition(anyInt())).thenReturn(null); + + Method handlePatch = FarmTreeRunScript.class.getDeclaredMethod("handlePatch", FarmTreeRunConfig.class, FarmTreeRunScript.Patch.class); + handlePatch.setAccessible(true); + Object result = handlePatch.invoke(script, config, FarmTreeRunScript.Patch.GNOME_STRONGHOLD_TREE_PATCH); + + assertEquals(Boolean.FALSE, result); + } + } + + @Test + void nullTreeActionsSkipsPatchInsteadOfThrowing() throws Exception { + FarmTreeRunConfig config = mock(FarmTreeRunConfig.class); + FarmTreeRunScript script = runningScript(config); + GameObject treePatch = mock(GameObject.class); + when(treePatch.getId()).thenReturn(8392); + ObjectComposition composition = mock(ObjectComposition.class); + when(composition.getActions()).thenReturn(null); + try (MockedStatic microbot = mockStatic(Microbot.class); + MockedStatic gameObject = mockStatic(Rs2GameObject.class)) { + microbot.when(Microbot::isLoggedIn).thenReturn(true); + gameObject.when(() -> Rs2GameObject.findObjectByImposter(anyInt(), anyString(), eq(false))).thenReturn(treePatch); + gameObject.when(() -> Rs2GameObject.findObjectComposition(anyInt())).thenReturn(composition); + + Method handlePatch = FarmTreeRunScript.class.getDeclaredMethod("handlePatch", FarmTreeRunConfig.class, FarmTreeRunScript.Patch.class); + handlePatch.setAccessible(true); + Object result = handlePatch.invoke(script, config, FarmTreeRunScript.Patch.GNOME_STRONGHOLD_TREE_PATCH); + + assertEquals(Boolean.FALSE, result); + } + } + + @Test + void disabledProtectionSkipsPaymentWithoutReadingComposition() throws Exception { + FarmTreeRunConfig config = mock(FarmTreeRunConfig.class); + when(config.protectTrees()).thenReturn(false); + FarmTreeRunScript script = runningScript(config); + try (MockedStatic gameObject = mockStatic(Rs2GameObject.class); + MockedStatic npc = mockStatic(Rs2Npc.class)) { + gameObject.when(() -> Rs2GameObject.getObjectComposition(anyInt())).thenReturn(null); + + Class paymentKind = Class.forName(FarmTreeRunScript.class.getName() + "$PaymentKind"); + Object protect = java.util.Arrays.stream(paymentKind.getEnumConstants()).filter(k -> k.toString().equals("PROTECT")).findFirst().orElseThrow(); + Method handlePayment = FarmTreeRunScript.class.getDeclaredMethod("handlePayment", FarmTreeRunConfig.class, FarmTreeRunScript.Patch.class, paymentKind); + handlePayment.setAccessible(true); + Object result = handlePayment.invoke(script, config, FarmTreeRunScript.Patch.GNOME_STRONGHOLD_TREE_PATCH, protect); + + assertEquals(Boolean.TRUE, result); + gameObject.verify(() -> Rs2GameObject.getObjectComposition(anyInt()), never()); + npc.verify(() -> Rs2Npc.getNearestNpcWithAction(anyString()), never()); + } + } +} diff --git a/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPatchEmptyTest.java b/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPatchEmptyTest.java new file mode 100644 index 0000000000..71e73d2a36 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/farmtreerun/FarmTreeRunPatchEmptyTest.java @@ -0,0 +1,40 @@ +package net.runelite.client.plugins.microbot.farmtreerun; + +import net.runelite.api.ObjectComposition; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class FarmTreeRunPatchEmptyTest { + + private static ObjectComposition named(String name) { + ObjectComposition composition = mock(ObjectComposition.class); + when(composition.getName()).thenReturn(name); + return composition; + } + + @Test + void nullCompositionIsTreatedAsEmptyInsteadOfThrowing() { + assertTrue(FarmTreeRunScript.isEmptyPatchComposition(null)); + } + + @Test + void nullNameIsTreatedAsEmpty() { + assertTrue(FarmTreeRunScript.isEmptyPatchComposition(named(null))); + } + + @Test + void patchNameIsEmpty() { + assertTrue(FarmTreeRunScript.isEmptyPatchComposition(named("Tree patch"))); + assertTrue(FarmTreeRunScript.isEmptyPatchComposition(named("Fruit Tree Patch"))); + } + + @Test + void grownTreeIsNotEmpty() { + assertFalse(FarmTreeRunScript.isEmptyPatchComposition(named("Magic tree"))); + assertFalse(FarmTreeRunScript.isEmptyPatchComposition(named("Papaya tree"))); + } +}