+ * Only registered when the running BentoBox has {@link PlayerDeathsChangedEvent}. + * + * @author tastybento + */ +public class AdminDeathsListener implements Listener { + + private final Level addon; + + /** + * @param addon - addon + */ + public AdminDeathsListener(Level addon) { + this.addon = addon; + } + + /** + * Apply an admin deaths change to the player's islands in that world. + * @param e event + */ + @EventHandler(priority = EventPriority.MONITOR) + public void onDeathsChanged(PlayerDeathsChangedEvent e) { + World world = e.getWorld(); + if (world == null || !addon.isRegisteredGameModeWorld(world)) { + return; + } + UUID uuid = e.getPlayerUUID(); + for (Island island : addon.getIslands().getIslands(world, uuid)) { + if (!island.getMemberSet().contains(uuid)) { + continue; + } + switch (e.getAction()) { + case SET -> addon.getManager().setDeaths(island, uuid, e.getAmount()); + case RESET -> addon.getManager().setDeaths(island, uuid, 0); + case ADD -> addon.getManager().addDeaths(island, uuid, e.getAmount()); + case REMOVE -> addon.getManager().removeDeaths(island, uuid, e.getAmount()); + } + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 57e5dd2..a9207ce 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -80,6 +80,8 @@ levelwait: 60 # and they stay with the island even if the player later leaves the team. # The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml, # and deaths are only recorded if deaths counted is enabled there. +# The game mode's admin deaths commands (set/add/remove/reset) also change the island's deaths. +# Set and reset also clear deaths left by former members and deaths carried over from older versions. # Set to zero to not use this feature deathpenalty: 100 # Deprecated - no longer used for level calculation, which now always counts all deaths diff --git a/src/test/java/world/bentobox/level/LevelsManagerTest.java b/src/test/java/world/bentobox/level/LevelsManagerTest.java index e8718e5..543a5ba 100644 --- a/src/test/java/world/bentobox/level/LevelsManagerTest.java +++ b/src/test/java/world/bentobox/level/LevelsManagerTest.java @@ -554,4 +554,81 @@ void testGetDeathHandicapSumsAnonymousAndMembers() throws Exception { assertEquals(6, lm.getDeathHandicap(island)); } + /** + * Test method for + * {@link world.bentobox.level.LevelsManager#setDeaths(Island, UUID, int)}. + * Reproduces the Discord report: migrated deaths sit in the anonymous count and + * an admin reset must clear them. + */ + @Test + void testSetDeathsZeroClearsAnonymousAndPlayer() throws Exception { + IslandLevels data = deathData(true); + data.setAnonymousDeaths(3); + data.getMemberDeaths().put(uuid.toString(), 2); + UUID mate = UUID.randomUUID(); + data.getMemberDeaths().put(mate.toString(), 1); + + lm.setDeaths(island, uuid, 0); + + assertEquals(0L, data.getAnonymousDeaths()); + assertFalse(data.getMemberDeaths().containsKey(uuid.toString())); + // Other members keep their deaths + assertEquals(1, lm.getDeathHandicap(island)); + } + + /** + * Test method for + * {@link world.bentobox.level.LevelsManager#setDeaths(Island, UUID, int)} + */ + @Test + void testSetDeathsCapsAtDeathsMax() throws Exception { + IslandLevels data = deathData(true); + when(iwm.getDeathsMax(world)).thenReturn(10); + + lm.setDeaths(island, uuid, 5); + assertEquals(5, data.getMemberDeaths().get(uuid.toString()).intValue()); + lm.setDeaths(island, uuid, 50); + assertEquals(10, data.getMemberDeaths().get(uuid.toString()).intValue()); + } + + /** + * Test method for + * {@link world.bentobox.level.LevelsManager#addDeaths(Island, UUID, int)} + */ + @Test + void testAddDeaths() throws Exception { + IslandLevels data = deathData(true); + when(iwm.getDeathsMax(world)).thenReturn(10); + data.setAnonymousDeaths(1); + + lm.addDeaths(island, uuid, 4); + lm.addDeaths(island, uuid, 4); + assertEquals(8, data.getMemberDeaths().get(uuid.toString()).intValue()); + lm.addDeaths(island, uuid, 4); + assertEquals(10, data.getMemberDeaths().get(uuid.toString()).intValue()); + assertEquals(11, lm.getDeathHandicap(island)); + } + + /** + * Test method for + * {@link world.bentobox.level.LevelsManager#removeDeaths(Island, UUID, int)} + */ + @Test + void testRemoveDeathsSpillsIntoAnonymous() throws Exception { + IslandLevels data = deathData(true); + data.setAnonymousDeaths(3); + data.getMemberDeaths().put(uuid.toString(), 2); + + lm.removeDeaths(island, uuid, 1); + assertEquals(1, data.getMemberDeaths().get(uuid.toString()).intValue()); + assertEquals(3L, data.getAnonymousDeaths()); + + lm.removeDeaths(island, uuid, 3); + assertFalse(data.getMemberDeaths().containsKey(uuid.toString())); + assertEquals(1L, data.getAnonymousDeaths()); + + lm.removeDeaths(island, uuid, 100); + assertEquals(0, lm.getDeathHandicap(island)); + } + } diff --git a/src/test/java/world/bentobox/level/listeners/AdminDeathsListenerTest.java b/src/test/java/world/bentobox/level/listeners/AdminDeathsListenerTest.java new file mode 100644 index 0000000..e6bbe31 --- /dev/null +++ b/src/test/java/world/bentobox/level/listeners/AdminDeathsListenerTest.java @@ -0,0 +1,107 @@ +package world.bentobox.level.listeners; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import com.google.common.collect.ImmutableSet; + +import world.bentobox.bentobox.api.events.player.PlayerDeathsChangedEvent; +import world.bentobox.bentobox.api.events.player.PlayerDeathsChangedEvent.Action; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.level.CommonTestSetup; +import world.bentobox.level.LevelsManager; + +/** + * Tests for {@link AdminDeathsListener} + */ +class AdminDeathsListenerTest extends CommonTestSetup { + + @Mock + private LevelsManager manager; + @Mock + private Island otherIsland; + + private AdminDeathsListener listener; + + @Override + @BeforeEach + protected void setUp() throws Exception { + super.setUp(); + when(addon.getManager()).thenReturn(manager); + when(addon.isRegisteredGameModeWorld(world)).thenReturn(true); + when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid)); + when(im.getIslands(world, uuid)).thenReturn(List.of(island)); + listener = new AdminDeathsListener(addon); + } + + @Override + @AfterEach + protected void tearDown() throws Exception { + super.tearDown(); + } + + private PlayerDeathsChangedEvent event(Action action, int amount) { + return new PlayerDeathsChangedEvent(world, uuid, action, amount, 3, 0); + } + + @Test + void testSet() { + listener.onDeathsChanged(event(Action.SET, 4)); + verify(manager).setDeaths(island, uuid, 4); + } + + @Test + void testReset() { + listener.onDeathsChanged(event(Action.RESET, 0)); + verify(manager).setDeaths(island, uuid, 0); + } + + @Test + void testAdd() { + listener.onDeathsChanged(event(Action.ADD, 2)); + verify(manager).addDeaths(island, uuid, 2); + } + + @Test + void testRemove() { + listener.onDeathsChanged(event(Action.REMOVE, 3)); + verify(manager).removeDeaths(island, uuid, 3); + } + + @Test + void testAppliesToEveryIslandPlayerIsMemberOf() { + when(otherIsland.getMemberSet()).thenReturn(ImmutableSet.of(uuid, UUID.randomUUID())); + when(im.getIslands(world, uuid)).thenReturn(List.of(island, otherIsland)); + listener.onDeathsChanged(event(Action.REMOVE, 3)); + verify(manager).removeDeaths(island, uuid, 3); + verify(manager).removeDeaths(otherIsland, uuid, 3); + } + + @Test + void testSkipsIslandsWherePlayerIsNotMember() { + // e.g. trusted/coop islands returned by the lookup + when(otherIsland.getMemberSet()).thenReturn(ImmutableSet.of(UUID.randomUUID())); + when(im.getIslands(world, uuid)).thenReturn(List.of(otherIsland)); + listener.onDeathsChanged(event(Action.RESET, 0)); + verify(manager, never()).setDeaths(any(), any(), anyInt()); + } + + @Test + void testIgnoresUnregisteredWorld() { + when(addon.isRegisteredGameModeWorld(world)).thenReturn(false); + listener.onDeathsChanged(event(Action.RESET, 0)); + verifyNoInteractions(manager); + } +}