diff --git a/pom.xml b/pom.xml index 6964411..cf83be3 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ 1.17.5 1.21.11-R0.1-SNAPSHOT - 2.7.1-SNAPSHOT + 3.17.0-SNAPSHOT ${build.version}-SNAPSHOT @@ -114,14 +114,6 @@ - - - jitpack.io - https://jitpack.io - - true - - spigot-repo https://hub.spigotmc.org/nexus/content/repositories/snapshots @@ -146,9 +138,9 @@ - com.github.MockBukkit - MockBukkit - v1.21-SNAPSHOT + org.mockbukkit.mockbukkit + mockbukkit-v1.21 + 4.110.0 test diff --git a/src/main/java/com/wasteofplastic/invswitcher/Store.java b/src/main/java/com/wasteofplastic/invswitcher/Store.java index 9454697..89c0c37 100644 --- a/src/main/java/com/wasteofplastic/invswitcher/Store.java +++ b/src/main/java/com/wasteofplastic/invswitcher/Store.java @@ -676,4 +676,134 @@ private static void setTotalExperience(final Player player, final int exp) public void saveOnShutdown() { Bukkit.getOnlinePlayers().forEach(p -> this.storeAndSave(p, p.getWorld(), true)); } + + /** + * Compute the storage key for a player and island event world, without using + * the player's current location. Used when the player is not in the target world. + * @param player - player + * @param world - the BentoBox event world + * @param island - the island involved in the event (may be null) + * @return storage key for this world/island combination + */ + String getStorageKeyForEvent(Player player, World world, Island island) { + String overworldName = getOverworldName(world); + if (!addon.getSettings().isIslandsActive()) { + return overworldName; + } + World overworld = Util.getWorld(world); + if (overworld == null) { + return overworldName; + } + int count = addon.getIslands().getNumberOfConcurrentIslands(player.getUniqueId(), overworld); + if (count <= 1) { + return overworldName; + } + // Only use island-specific key if the player owns the island + if (island != null && island.getOwner() != null && island.getOwner().equals(player.getUniqueId())) { + return overworldName + "/" + island.getUniqueId(); + } + return overworldName; + } + + /** + * Clears the stored inventory for a BentoBox world when the player is not currently in + * that world. Called when BentoBox fires a {@code PlayerResetInventoryEvent} while the + * player is in a non-BentoBox world so the player's current inventory is not affected. + * @param player - online player + * @param world - the BentoBox world whose stored inventory should be cleared + * @param island - the island involved in the reset (may be null) + */ + public void clearStoredInventoryForWorld(Player player, World world, Island island) { + InventoryStorage store = getInv(player); + String key = getStorageKeyForEvent(player, world, island); + String worldKey = getOverworldName(world); + Settings settings = addon.getSettings(); + if (settings.isInventory()) { + String k = settings.isIslandsInventory() ? key : worldKey; + store.setInventory(k, Collections.emptyList()); + } + database.saveObjectAsync(store); + } + + /** + * Clears the stored ender chest for a BentoBox world when the player is not currently in + * that world. Called when BentoBox fires a {@code PlayerResetEnderChestEvent} while the + * player is in a non-BentoBox world. + * @param player - online player + * @param world - the BentoBox world whose stored ender chest should be cleared + * @param island - the island involved in the reset (may be null) + */ + public void clearStoredEnderChestForWorld(Player player, World world, Island island) { + InventoryStorage store = getInv(player); + String key = getStorageKeyForEvent(player, world, island); + String worldKey = getOverworldName(world); + Settings settings = addon.getSettings(); + if (settings.isEnderChest()) { + String k = settings.isIslandsEnderChest() ? key : worldKey; + store.setEnderChest(k, Collections.emptyList()); + } + database.saveObjectAsync(store); + } + + /** + * Zeroes the stored experience for a BentoBox world when the player is not currently in + * that world. Called when BentoBox fires a {@code PlayerResetExpEvent} while the + * player is in a non-BentoBox world. + * @param player - online player + * @param world - the BentoBox world whose stored experience should be zeroed + * @param island - the island involved in the reset (may be null) + */ + public void clearStoredExpForWorld(Player player, World world, Island island) { + InventoryStorage store = getInv(player); + String key = getStorageKeyForEvent(player, world, island); + String worldKey = getOverworldName(world); + Settings settings = addon.getSettings(); + if (settings.isExperience()) { + String k = settings.isIslandsExperience() ? key : worldKey; + store.setExp(k, 0); + } + database.saveObjectAsync(store); + } + + /** + * Removes the stored health for a BentoBox world when the player is not currently in + * that world. Called when BentoBox fires a {@code PlayerResetHealthEvent} while the + * player is in a non-BentoBox world. Removing the entry means the player will receive + * maximum health the next time they enter the world. + * @param player - online player + * @param world - the BentoBox world whose stored health should be removed + * @param island - the island involved in the reset (may be null) + */ + public void clearStoredHealthForWorld(Player player, World world, Island island) { + InventoryStorage store = getInv(player); + String key = getStorageKeyForEvent(player, world, island); + String worldKey = getOverworldName(world); + Settings settings = addon.getSettings(); + if (settings.isHealth()) { + String k = settings.isIslandsHealth() ? key : worldKey; + store.getHealth().remove(k); + } + database.saveObjectAsync(store); + } + + /** + * Resets the stored food level to full (20) for a BentoBox world when the player is not + * currently in that world. Called when BentoBox fires a {@code PlayerResetHungerEvent} + * while the player is in a non-BentoBox world. + * @param player - online player + * @param world - the BentoBox world whose stored food level should be reset + * @param island - the island involved in the reset (may be null) + */ + public void clearStoredFoodForWorld(Player player, World world, Island island) { + InventoryStorage store = getInv(player); + String key = getStorageKeyForEvent(player, world, island); + String worldKey = getOverworldName(world); + Settings settings = addon.getSettings(); + if (settings.isFood()) { + String k = settings.isIslandsFood() ? key : worldKey; + store.setFood(k, 20); + } + database.saveObjectAsync(store); + } + } diff --git a/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java b/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java index 97110c5..b404d40 100644 --- a/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java +++ b/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java @@ -17,6 +17,12 @@ import com.wasteofplastic.invswitcher.InvSwitcher; import world.bentobox.bentobox.api.events.island.IslandEnterEvent; +import world.bentobox.bentobox.api.events.player.PlayerBaseEvent; +import world.bentobox.bentobox.api.events.player.PlayerResetEnderChestEvent; +import world.bentobox.bentobox.api.events.player.PlayerResetExpEvent; +import world.bentobox.bentobox.api.events.player.PlayerResetHealthEvent; +import world.bentobox.bentobox.api.events.player.PlayerResetHungerEvent; +import world.bentobox.bentobox.api.events.player.PlayerResetInventoryEvent; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Util; @@ -183,5 +189,100 @@ public void onPlayerQuit(final PlayerQuitEvent event) { addon.getStore().removeFromCache(event.getPlayer()); } + /** + * Intercepts BentoBox's inventory reset when the player is not in the BentoBox world. + * Cancels the direct clear and instead wipes the stored inventory data for that world. + * @param event - event + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPlayerResetInventory(PlayerResetInventoryEvent event) { + if (!shouldInterceptPlayerReset(event)) return; + event.setCancelled(true); + Player player = Bukkit.getPlayer(event.getPlayerUUID()); + if (player != null) { + addon.getStore().clearStoredInventoryForWorld(player, event.getWorld(), event.getIsland()); + } + } + + /** + * Intercepts BentoBox's ender chest reset when the player is not in the BentoBox world. + * Cancels the direct clear and instead wipes the stored ender chest data for that world. + * @param event - event + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPlayerResetEnderChest(PlayerResetEnderChestEvent event) { + if (!shouldInterceptPlayerReset(event)) return; + event.setCancelled(true); + Player player = Bukkit.getPlayer(event.getPlayerUUID()); + if (player != null) { + addon.getStore().clearStoredEnderChestForWorld(player, event.getWorld(), event.getIsland()); + } + } + + /** + * Intercepts BentoBox's experience reset when the player is not in the BentoBox world. + * Cancels the direct clear and instead zeroes the stored experience for that world. + * @param event - event + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPlayerResetExp(PlayerResetExpEvent event) { + if (!shouldInterceptPlayerReset(event)) return; + event.setCancelled(true); + Player player = Bukkit.getPlayer(event.getPlayerUUID()); + if (player != null) { + addon.getStore().clearStoredExpForWorld(player, event.getWorld(), event.getIsland()); + } + } + + /** + * Intercepts BentoBox's health reset when the player is not in the BentoBox world. + * Cancels the direct reset and instead removes the stored health for that world + * (so the player receives max health when they next enter the world). + * @param event - event + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPlayerResetHealth(PlayerResetHealthEvent event) { + if (!shouldInterceptPlayerReset(event)) return; + event.setCancelled(true); + Player player = Bukkit.getPlayer(event.getPlayerUUID()); + if (player != null) { + addon.getStore().clearStoredHealthForWorld(player, event.getWorld(), event.getIsland()); + } + } + + /** + * Intercepts BentoBox's hunger reset when the player is not in the BentoBox world. + * Cancels the direct reset and instead sets stored food to full (20) for that world. + * @param event - event + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPlayerResetHunger(PlayerResetHungerEvent event) { + if (!shouldInterceptPlayerReset(event)) return; + event.setCancelled(true); + Player player = Bukkit.getPlayer(event.getPlayerUUID()); + if (player != null) { + addon.getStore().clearStoredFoodForWorld(player, event.getWorld(), event.getIsland()); + } + } + + /** + * Determines whether InvSwitcher should intercept a BentoBox player reset event. + * Returns true if the event's world is managed by InvSwitcher, the player is online, + * and the player is currently in a different world (not the event world). + * @param event - the reset event + * @return true if InvSwitcher should cancel the event and handle it itself + */ + private boolean shouldInterceptPlayerReset(PlayerBaseEvent event) { + World eventWorld = event.getWorld(); + if (!addon.getWorlds().contains(eventWorld)) { + return false; + } + Player player = Bukkit.getPlayer(event.getPlayerUUID()); + if (player == null) { + return false; + } + // Only intercept if the player is not currently in the event world + return !Util.sameWorld(player.getWorld(), eventWorld); + } } diff --git a/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java b/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java index 9cdb000..fbddcf0 100644 --- a/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java +++ b/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java @@ -645,4 +645,180 @@ public void testMigrationFromOldWorldKey() { verify(player.getInventory(), atLeastOnce()).setContents(any(ItemStack[].class)); } + // --- clearStoredXForWorld Tests --- + + /** + * When inventory is enabled and the event fires for a world the player is not in, + * the stored inventory for that world should be cleared. + * After clearing, loading inventory for that world should give empty contents. + */ + @Test + public void testClearStoredInventoryForWorld() { + sets.setStatistics(false); + sets.setAdvancements(false); + Island island = mock(Island.class); + // First save something in the store + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) { + s.storeInventory(player, world); + assertTrue(s.isWorldStored(player, world), "Inventory should be stored before clearing"); + + // Clear stored inventory for the world + s.clearStoredInventoryForWorld(player, world, island); + + // isWorldStored returns true even after clearing (entry exists but is empty list) + assertTrue(s.isWorldStored(player, world)); + + // Load inventory back - should set empty contents to player + s.getInventory(player, world); + // setContents should have been called with empty array + verify(player.getInventory(), atLeastOnce()).setContents(any(ItemStack[].class)); + } + } + + /** + * When ender chest is enabled, clearStoredEnderChestForWorld should work without error. + */ + @Test + public void testClearStoredEnderChestForWorld() { + sets.setStatistics(false); + sets.setAdvancements(false); + Island island = mock(Island.class); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) { + s.storeInventory(player, world); + // Should not throw + s.clearStoredEnderChestForWorld(player, world, island); + } + } + + /** + * When experience is enabled, clearStoredExpForWorld should zero out the stored exp. + */ + @Test + public void testClearStoredExpForWorld() { + sets.setStatistics(false); + sets.setAdvancements(false); + Island island = mock(Island.class); + when(player.getTotalExperience()).thenReturn(500); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) { + s.storeInventory(player, world); + // Should not throw + s.clearStoredExpForWorld(player, world, island); + } + } + + /** + * When health is enabled, clearStoredHealthForWorld should work without error. + */ + @Test + public void testClearStoredHealthForWorld() { + sets.setStatistics(false); + sets.setAdvancements(false); + Island island = mock(Island.class); + when(player.getHealth()).thenReturn(10.0); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) { + s.storeInventory(player, world); + // Should not throw + s.clearStoredHealthForWorld(player, world, island); + } + } + + /** + * When food is enabled, clearStoredFoodForWorld should work without error. + */ + @Test + public void testClearStoredFoodForWorld() { + sets.setStatistics(false); + sets.setAdvancements(false); + Island island = mock(Island.class); + when(player.getFoodLevel()).thenReturn(8); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) { + s.storeInventory(player, world); + // Should not throw + s.clearStoredFoodForWorld(player, world, island); + } + } + + /** + * clearStoredInventoryForWorld should be a no-op when inventory is disabled in settings. + * No exceptions should be thrown. + */ + @Test + public void testClearStoredInventoryForWorldInventoryDisabled() { + sets.setInventory(false); + sets.setStatistics(false); + sets.setAdvancements(false); + Island island = mock(Island.class); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) { + s.storeInventory(player, world); + // With inventory disabled, nothing is stored in the inventory map + assertFalse(s.isWorldStored(player, world)); + + // Calling clear should be a no-op (no exception, no effect) + s.clearStoredInventoryForWorld(player, world, island); + assertFalse(s.isWorldStored(player, world)); + } + } + + /** + * getStorageKeyForEvent should return the world name when islands mode is inactive. + */ + @Test + public void testGetStorageKeyForEventIslandsDisabled() { + sets.setIslandsActive(false); + Island island = mock(Island.class); + String key = s.getStorageKeyForEvent(player, world, island); + assertEquals("world", key); + } + + /** + * getStorageKeyForEvent should return world name when player has only 1 island. + */ + @Test + public void testGetStorageKeyForEventSingleIsland() { + sets.setIslandsActive(true); + Island island = mock(Island.class); + try (MockedStatic mockedUtil = mockStatic(Util.class)) { + mockedUtil.when(() -> Util.getWorld(world)).thenReturn(world); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(1); + String key = s.getStorageKeyForEvent(player, world, island); + assertEquals("world", key); + } + } + + /** + * getStorageKeyForEvent should return island-specific key when player owns the island + * and has multiple islands. + */ + @Test + public void testGetStorageKeyForEventMultipleIslandsOwner() { + sets.setIslandsActive(true); + Island island = mock(Island.class); + when(island.getOwner()).thenReturn(playerUUID); + when(island.getUniqueId()).thenReturn("island-abc"); + try (MockedStatic mockedUtil = mockStatic(Util.class)) { + mockedUtil.when(() -> Util.getWorld(world)).thenReturn(world); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(2); + String key = s.getStorageKeyForEvent(player, world, island); + assertEquals("world/island-abc", key); + } + } + + /** + * getStorageKeyForEvent should return just the world name when player does not own the island + * (e.g., kicked from a team). Uses the world-level key since the player is a member, not owner. + */ + @Test + public void testGetStorageKeyForEventMultipleIslandsNotOwner() { + sets.setIslandsActive(true); + Island island = mock(Island.class); + UUID otherOwner = UUID.randomUUID(); + when(island.getOwner()).thenReturn(otherOwner); // player is NOT the owner + try (MockedStatic mockedUtil = mockStatic(Util.class)) { + mockedUtil.when(() -> Util.getWorld(world)).thenReturn(world); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(2); + String key = s.getStorageKeyForEvent(player, world, island); + assertEquals("world", key); // Falls back to world name + } + } + } diff --git a/src/test/java/com/wasteofplastic/invswitcher/listeners/PlayerListenerTest.java b/src/test/java/com/wasteofplastic/invswitcher/listeners/PlayerListenerTest.java index cd3f6b1..0c4c61f 100644 --- a/src/test/java/com/wasteofplastic/invswitcher/listeners/PlayerListenerTest.java +++ b/src/test/java/com/wasteofplastic/invswitcher/listeners/PlayerListenerTest.java @@ -1,6 +1,8 @@ package com.wasteofplastic.invswitcher.listeners; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -37,6 +39,11 @@ import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.api.events.island.IslandEnterEvent; +import world.bentobox.bentobox.api.events.player.PlayerResetEnderChestEvent; +import world.bentobox.bentobox.api.events.player.PlayerResetExpEvent; +import world.bentobox.bentobox.api.events.player.PlayerResetHealthEvent; +import world.bentobox.bentobox.api.events.player.PlayerResetHungerEvent; +import world.bentobox.bentobox.api.events.player.PlayerResetInventoryEvent; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.util.Util; @@ -64,6 +71,8 @@ public class PlayerListenerTest { private Settings settings; @Mock private IslandsManager islandsManager; + @Mock + private Island island; private UUID playerUUID; private MockedStatic mockedBentoBox; @@ -353,4 +362,159 @@ public void testOnPlayerRespawnNoIsland() { verify(store, never()).storeAndSave(any(), any(), any(boolean.class)); } + // --- Player Reset Event Tests --- + + /** + * When the event world is not managed by InvSwitcher, the event should not be intercepted + * and the store clear methods should not be called. + */ + @Test + public void testOnPlayerResetInventoryWorldNotManaged() { + // notWorld is not in the addon's worlds set + when(addon.getWorlds()).thenReturn(Set.of(world)); // only 'world' is managed + PlayerResetInventoryEvent event = new PlayerResetInventoryEvent(notWorld, island, playerUUID); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + pl.onPlayerResetInventory(event); + } + assertFalse(event.isCancelled(), "Event should not be cancelled when world is not managed"); + verify(store, never()).clearStoredInventoryForWorld(any(), any(), any()); + } + + /** + * When the player is offline, the event should not be intercepted. + */ + @Test + public void testOnPlayerResetInventoryPlayerOffline() { + PlayerResetInventoryEvent event = new PlayerResetInventoryEvent(world, island, playerUUID); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(null); // offline + pl.onPlayerResetInventory(event); + } + assertFalse(event.isCancelled(), "Event should not be cancelled when player is offline"); + verify(store, never()).clearStoredInventoryForWorld(any(), any(), any()); + } + + /** + * When the player is currently in the event world, BentoBox should handle the reset directly. + */ + @Test + public void testOnPlayerResetInventoryPlayerInEventWorld() { + // player.getWorld() returns 'world', event world is also 'world' + when(player.getWorld()).thenReturn(world); + PlayerResetInventoryEvent event = new PlayerResetInventoryEvent(world, island, playerUUID); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class); + MockedStatic mockedUtil = mockStatic(Util.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + mockedUtil.when(() -> Util.sameWorld(world, world)).thenReturn(true); + pl.onPlayerResetInventory(event); + } + assertFalse(event.isCancelled(), "Event should not be cancelled when player is in event world"); + verify(store, never()).clearStoredInventoryForWorld(any(), any(), any()); + } + + /** + * When the player is in a non-BentoBox world, the inventory reset event should be cancelled + * and the stored inventory for the BentoBox world should be cleared. + */ + @Test + public void testOnPlayerResetInventoryPlayerInDifferentWorld() { + // player is in notWorld, event fires for world + when(player.getWorld()).thenReturn(notWorld); + PlayerResetInventoryEvent event = new PlayerResetInventoryEvent(world, island, playerUUID); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class); + MockedStatic mockedUtil = mockStatic(Util.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + mockedUtil.when(() -> Util.sameWorld(notWorld, world)).thenReturn(false); + pl.onPlayerResetInventory(event); + } + assertTrue(event.isCancelled(), "Event should be cancelled when player is in a different world"); + verify(store).clearStoredInventoryForWorld(player, world, island); + } + + /** + * Ender chest reset should be intercepted when the player is in a different world. + */ + @Test + public void testOnPlayerResetEnderChestPlayerInDifferentWorld() { + when(player.getWorld()).thenReturn(notWorld); + PlayerResetEnderChestEvent event = new PlayerResetEnderChestEvent(world, island, playerUUID); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class); + MockedStatic mockedUtil = mockStatic(Util.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + mockedUtil.when(() -> Util.sameWorld(notWorld, world)).thenReturn(false); + pl.onPlayerResetEnderChest(event); + } + assertTrue(event.isCancelled(), "Event should be cancelled when player is in a different world"); + verify(store).clearStoredEnderChestForWorld(player, world, island); + } + + /** + * Ender chest reset should not be intercepted when the player is in the event world. + */ + @Test + public void testOnPlayerResetEnderChestPlayerInEventWorld() { + when(player.getWorld()).thenReturn(world); + PlayerResetEnderChestEvent event = new PlayerResetEnderChestEvent(world, island, playerUUID); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class); + MockedStatic mockedUtil = mockStatic(Util.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + mockedUtil.when(() -> Util.sameWorld(world, world)).thenReturn(true); + pl.onPlayerResetEnderChest(event); + } + assertFalse(event.isCancelled()); + verify(store, never()).clearStoredEnderChestForWorld(any(), any(), any()); + } + + /** + * Experience reset should be intercepted when the player is in a different world. + */ + @Test + public void testOnPlayerResetExpPlayerInDifferentWorld() { + when(player.getWorld()).thenReturn(notWorld); + PlayerResetExpEvent event = new PlayerResetExpEvent(world, island, playerUUID); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class); + MockedStatic mockedUtil = mockStatic(Util.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + mockedUtil.when(() -> Util.sameWorld(notWorld, world)).thenReturn(false); + pl.onPlayerResetExp(event); + } + assertTrue(event.isCancelled(), "Event should be cancelled when player is in a different world"); + verify(store).clearStoredExpForWorld(player, world, island); + } + + /** + * Health reset should be intercepted when the player is in a different world. + */ + @Test + public void testOnPlayerResetHealthPlayerInDifferentWorld() { + when(player.getWorld()).thenReturn(notWorld); + PlayerResetHealthEvent event = new PlayerResetHealthEvent(world, island, playerUUID); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class); + MockedStatic mockedUtil = mockStatic(Util.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + mockedUtil.when(() -> Util.sameWorld(notWorld, world)).thenReturn(false); + pl.onPlayerResetHealth(event); + } + assertTrue(event.isCancelled(), "Event should be cancelled when player is in a different world"); + verify(store).clearStoredHealthForWorld(player, world, island); + } + + /** + * Hunger reset should be intercepted when the player is in a different world. + */ + @Test + public void testOnPlayerResetHungerPlayerInDifferentWorld() { + when(player.getWorld()).thenReturn(notWorld); + PlayerResetHungerEvent event = new PlayerResetHungerEvent(world, island, playerUUID); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class); + MockedStatic mockedUtil = mockStatic(Util.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + mockedUtil.when(() -> Util.sameWorld(notWorld, world)).thenReturn(false); + pl.onPlayerResetHunger(event); + } + assertTrue(event.isCancelled(), "Event should be cancelled when player is in a different world"); + verify(store).clearStoredFoodForWorld(player, world, island); + } + }