Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<mock-bukkit.version>4.110.0</mock-bukkit.version>
<!-- More visible way how to change dependency versions -->
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
<bentobox.version>3.15.1-SNAPSHOT</bentobox.version>
<bentobox.version>3.23.1-SNAPSHOT</bentobox.version>
<!-- Warps addon version -->
<warps.version>1.12.0</warps.version>
<!-- Visit addon version -->
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/world/bentobox/level/Level.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import world.bentobox.level.commands.IslandValueCommand;
import world.bentobox.level.config.BlockConfig;
import world.bentobox.level.config.ConfigSettings;
import world.bentobox.level.listeners.AdminDeathsListener;
import world.bentobox.level.listeners.IslandActivitiesListeners;
import world.bentobox.level.listeners.JoinLeaveListener;
import world.bentobox.level.listeners.MigrationListener;
Expand Down Expand Up @@ -154,6 +155,13 @@ private void registerAllListeners() {
registerListener(new IslandActivitiesListeners(this));
registerListener(new JoinLeaveListener(this));
registerListener(new MigrationListener(this));
// The admin deaths event only exists in newer BentoBox versions
try {
Class.forName("world.bentobox.bentobox.api.events.player.PlayerDeathsChangedEvent");
registerListener(new AdminDeathsListener(this));
} catch (ClassNotFoundException e) {
log("This BentoBox version does not support syncing admin deaths commands to island levels. Update BentoBox to enable it.");
}
}

private void registerGameModeCommands() {
Expand Down
70 changes: 70 additions & 0 deletions src/main/java/world/bentobox/level/LevelsManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,76 @@ public void rollDeathsToAnonymous(@NonNull Island island, @NonNull UUID playerUU
handler.saveObjectAsync(data);
}

/**
* Admin set of a player's deaths on this island. The player's count becomes
* {@code deaths}, capped at the game mode's {@code deaths.max} setting. Anonymous
* deaths are cleared too: they are unattributed (migrated legacy deaths or deaths
* of former members), so an admin setting the deaths expects them to go.
*
* @param island the island
* @param playerUUID the player whose deaths are being set
* @param deaths the new death count, zero or more
*/
public void setDeaths(@NonNull Island island, @NonNull UUID playerUUID, int deaths) {
IslandLevels data = checkDeathsMigration(island);
data.setAnonymousDeaths(0);
int count = capDeaths(island, Math.max(0, deaths));
if (count > 0) {
data.getMemberDeaths().put(playerUUID.toString(), count);
} else {
data.getMemberDeaths().remove(playerUUID.toString());
}
handler.saveObjectAsync(data);
}

/**
* Admin addition of deaths to a player on this island, capped at the game mode's
* {@code deaths.max} setting.
*
* @param island the island
* @param playerUUID the player
* @param amount the number of deaths to add
*/
public void addDeaths(@NonNull Island island, @NonNull UUID playerUUID, int amount) {
if (amount <= 0) {
return;
}
IslandLevels data = checkDeathsMigration(island);
int current = data.getMemberDeaths().getOrDefault(playerUUID.toString(), 0);
data.getMemberDeaths().put(playerUUID.toString(), capDeaths(island, (int) Math.min(Integer.MAX_VALUE, (long) current + amount)));
handler.saveObjectAsync(data);
}

/**
* Admin removal of deaths from a player on this island. Deaths come off the
* player's own count first and any remainder comes off the island's anonymous
* deaths. Neither goes below zero.
*
* @param island the island
* @param playerUUID the player
* @param amount the number of deaths to remove
*/
public void removeDeaths(@NonNull Island island, @NonNull UUID playerUUID, int amount) {
if (amount <= 0) {
return;
}
IslandLevels data = checkDeathsMigration(island);
int current = data.getMemberDeaths().getOrDefault(playerUUID.toString(), 0);
int fromPlayer = Math.min(current, amount);
if (current - fromPlayer > 0) {
data.getMemberDeaths().put(playerUUID.toString(), current - fromPlayer);
} else {
data.getMemberDeaths().remove(playerUUID.toString());
}
data.setAnonymousDeaths(Math.max(0, data.getAnonymousDeaths() - (amount - fromPlayer)));
handler.saveObjectAsync(data);
}

private int capDeaths(Island island, int deaths) {
int max = addon.getPlugin().getIWM().getDeathsMax(island.getWorld());
return max > 0 ? Math.min(max, deaths) : deaths;
}

/**
* Get the death handicap for an island: anonymous deaths plus all current member
* deaths in this island's space.
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/world/bentobox/level/config/ConfigSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ public class ConfigSettings implements ConfigObject {
@ConfigComment("and they stay with the island even if the player later leaves the team.")
@ConfigComment("The per-player count is capped by the deaths max setting in the GameModeAddon's config.yml,")
@ConfigComment("and deaths are only recorded if deaths counted is enabled there.")
@ConfigComment("The game mode's admin deaths commands (set/add/remove/reset) also change the island's deaths.")
@ConfigComment("Set and reset also clear deaths left by former members and deaths carried over from older versions.")
@ConfigComment("Set to zero to not use this feature")
@ConfigEntry(path = "deathpenalty")
private int deathPenalty = 100;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package world.bentobox.level.listeners;

import java.util.UUID;

import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;

import world.bentobox.bentobox.api.events.player.PlayerDeathsChangedEvent;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.level.Level;

/**
* Applies the BentoBox admin deaths commands ({@code /<admin> deaths set|add|remove|reset})
* to Level's per-island death counts, so admins do not need a separate Level command.
* The change is applied to every island the player is a member of in that game mode.
* <p>
* 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());
}
}
}
}
2 changes: 2 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions src/test/java/world/bentobox/level/LevelsManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading