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
80 changes: 80 additions & 0 deletions src/main/java/world/bentobox/level/LevelsManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -592,4 +592,84 @@ public Map<String, Integer> getDonatedBlocks(@NonNull Island island) {
return getLevelsData(island).getDonatedBlocks();
}

// ---- Per-island death tracking ----

/**
* Ensure this island's death data has been seeded from the legacy per-world death
* counts. The seed reproduces what the old calculation would produce right now:
* the sum of all members' world death counts if {@code sumteamdeaths} is set,
* otherwise the owner's count. It is stored as anonymous deaths so the island's
* level does not change when the tracking model changes. Runs at most once per
* island; new islands are created already migrated with zero deaths.
*
* @param island the island to check
* @return the island's levels data, migrated
*/
@SuppressWarnings("deprecation") // sumteamdeaths is intentionally read for the legacy seed
@NonNull
public IslandLevels checkDeathsMigration(@NonNull Island island) {
IslandLevels data = getLevelsData(island);
if (data.isDeathsMigrated()) {
return data;
}
long seed = 0;
if (island.getWorld() != null) {
if (addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : island.getMemberSet()) {
seed += addon.getPlayers().getDeaths(island.getWorld(), uuid);
}
} else if (island.getOwner() != null) {
seed = addon.getPlayers().getDeaths(island.getWorld(), island.getOwner());
}
}
data.setAnonymousDeaths(seed);
data.setDeathsMigrated(true);
handler.saveObjectAsync(data);
return data;
}

/**
* Record a death for a player on this island. The per-player count is capped at
* the game mode's {@code deaths.max} setting.
*
* @param island the island in whose space the player died
* @param playerUUID the player who died
*/
public void addDeath(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
int max = addon.getPlugin().getIWM().getDeathsMax(island.getWorld());
if (max <= 0) {
return;
}
data.getMemberDeaths().merge(playerUUID.toString(), 1, (old, one) -> Math.min(max, old + one));
handler.saveObjectAsync(data);
}

/**
* Fold a departing member's death balance into the island's anonymous death count
* so that the island's level does not change when they leave.
*
* @param island the island the player is leaving
* @param playerUUID the departing player
*/
public void rollDeathsToAnonymous(@NonNull Island island, @NonNull UUID playerUUID) {
IslandLevels data = checkDeathsMigration(island);
Integer balance = data.getMemberDeaths().remove(playerUUID.toString());
if (balance != null && balance > 0) {
data.setAnonymousDeaths(data.getAnonymousDeaths() + balance);
}
handler.saveObjectAsync(data);
}

/**
* Get the death handicap for an island: anonymous deaths plus all current member
* deaths in this island's space.
*
* @param island the island
* @return total deaths counting against the island
*/
public int getDeathHandicap(@NonNull Island island) {
return (int) Math.min(Integer.MAX_VALUE, checkDeathsMigration(island).getTotalDeaths());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import world.bentobox.level.Level;
import world.bentobox.level.calculators.Results.Result;
import world.bentobox.level.config.BlockConfig;
import world.bentobox.level.objects.IslandLevels;
import world.bentobox.level.util.Utils;

public class IslandLevelCalculator {
Expand Down Expand Up @@ -264,6 +265,15 @@ private List<String> getReport() {
reportLines.add("Level cost = " + addon.getSettings().getLevelCost());
reportLines.add("Island members = " + island.getMemberSet().size());
reportLines.add("Deaths handicap = " + results.deathHandicap.get());
IslandLevels levelsData = addon.getManager().checkDeathsMigration(island);
levelsData.getMemberDeaths().forEach((uuid, deaths) -> {
String name = addon.getPlayers().getName(UUID.fromString(uuid));
reportLines.add(" Deaths by " + (name.isEmpty() ? uuid : name) + " = " + deaths);
});
if (levelsData.getAnonymousDeaths() > 0) {
reportLines.add(" Deaths by former members or migrated from pre-island tracking = "
+ levelsData.getAnonymousDeaths());
}
/*
if (addon.getSettings().isZeroNewIslandLevels()) {
reportLines.add("Initial island level = " + (0L - addon.getManager().getInitialLevel(island)));
Expand Down Expand Up @@ -746,16 +756,9 @@ public void tidyUp() {
results.rawBlockCount.addAndGet(donatedPoints);
results.donatedPoints.set(donatedPoints);

// Set the death penalty
if (this.addon.getSettings().isSumTeamDeaths()) {
for (UUID uuid : this.island.getMemberSet()) {
this.results.deathHandicap.addAndGet(this.addon.getPlayers().getDeaths(island.getWorld(), uuid));
}
} else {
// At this point, it may be that the island has become unowned.
this.results.deathHandicap.set(this.island.getOwner() == null ? 0
: this.addon.getPlayers().getDeaths(island.getWorld(), this.island.getOwner()));
}
// Set the death penalty. Deaths are tracked per island: only deaths in this
// island's space count, and deaths of former members are retained anonymously.
this.results.deathHandicap.set(this.addon.getManager().getDeathHandicap(island));

long blockAndDeathPoints = this.results.rawBlockCount.get();
this.results.totalPoints.set(blockAndDeathPoints);
Expand Down
17 changes: 12 additions & 5 deletions src/main/java/world/bentobox/level/config/ConfigSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,19 @@

@ConfigComment("")
@ConfigComment("Death penalty")
@ConfigComment("How many block values a player will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)")
@ConfigComment("How many block values the island will lose per death.")
@ConfigComment("Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)")
@ConfigComment("Deaths are tracked per island: only deaths that happen in the island's space count against it,")
@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("Set to zero to not use this feature")
@ConfigEntry(path = "deathpenalty")
private int deathPenalty = 100;

@ConfigComment("Sum team deaths - if true, all the teams deaths are summed")
@ConfigComment("If false, only the leader's deaths counts")
@ConfigComment("For other death related settings, see the GameModeAddon's config.yml settings.")
@ConfigComment("Deprecated - no longer used for level calculation, which now always counts all deaths")
@ConfigComment("that occurred on the island. Only read once, when migrating legacy per-world death counts:")
@ConfigComment("if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.")
@ConfigEntry(path = "sumteamdeaths")
private boolean sumTeamDeaths = false;

Expand Down Expand Up @@ -318,8 +322,11 @@

/**
* @return the sumTeamDeaths
* @deprecated deaths are now tracked per island; this setting is only read when
* migrating legacy per-world death counts
*/
@Deprecated(since = "2.29.0")
public boolean isSumTeamDeaths() {

Check warning on line 329 in src/main/java/world/bentobox/level/config/ConfigSettings.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Level&issues=AZ--dEHbqFO0EDsItfWg&open=AZ--dEHbqFO0EDsItfWg&pullRequest=456
return sumTeamDeaths;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package world.bentobox.level.listeners;

import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;

import world.bentobox.bentobox.api.events.island.IslandCreatedEvent;
import world.bentobox.bentobox.api.events.island.IslandDeleteEvent;
Expand Down Expand Up @@ -108,16 +110,32 @@ public void onIsland(IslandRegisteredEvent e) {

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamLeaveEvent e) {
// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the leaver's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIsland(TeamKickEvent e) {
//// TODO: anything to do here?
// Remove player from the top ten and level
// remove(e.getIsland().getWorld(), e.getPlayerUUID());
// Deaths stay with the island: fold the kicked player's balance into the anonymous count
addon.getManager().rollDeathsToAnonymous(e.getIsland(), e.getPlayerUUID());
}

/**
* Record deaths per island. A death only counts if it happens in the island space
* of an island the player is a member of; deaths anywhere else are ignored.
* @param e death event
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerDeath(PlayerDeathEvent e) {
Location location = e.getEntity().getLocation();
World world = location.getWorld();
if (world == null || !addon.isRegisteredGameModeWorld(world)
|| !addon.getPlugin().getIWM().getWorldSettings(world).isDeathsCounted()) {
return;
}
addon.getIslands().getIslandAt(location)
.filter(island -> island.getMemberSet().contains(e.getEntity().getUniqueId()))
.ifPresent(island -> addon.getManager().addDeath(island, e.getEntity().getUniqueId()));
}

}
85 changes: 85 additions & 0 deletions src/main/java/world/bentobox/level/objects/IslandLevels.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,30 @@ public class IslandLevels implements DataObject {
@Expose
private List<DonationRecord> donationLog;

/**
* Deaths on this island by current members. Key is the player's UUID as a string,
* value is the number of times they died in this island's space.
* Null-safe for backwards compatibility with legacy data.
*/
@Expose
private Map<String, Integer> memberDeaths;

/**
* Deaths that count against this island but are no longer attributable to a current
* member: the one-time migration seed from the legacy per-world death counts, plus
* the balances of members who have since left the team.
*/
@Expose
private long anonymousDeaths;

/**
* Whether the legacy per-world death counts have been folded into this island's
* death data. Legacy records load as false and are seeded on first touch; new
* islands start migrated with zero deaths.
*/
@Expose
private boolean deathsMigrated;

/**
* Constructor for new island
* @param islandUUID - island UUID
Expand All @@ -106,6 +130,8 @@ public IslandLevels(String islandUUID) {
uniqueId = islandUUID;
uwCount = new HashMap<>();
mdCount = new HashMap<>();
// A brand-new record has no legacy death history to import
deathsMigrated = true;
}

/**
Expand Down Expand Up @@ -352,6 +378,65 @@ public void addDonation(String donorUUID, String material, int count, long point
getDonationLog().add(new DonationRecord(System.currentTimeMillis(), donorUUID, material, count, points));
}

// ---- Death tracking fields (null-safe for backwards compatibility) ----

/**
* Get the deaths of current members in this island's space.
* @return map of player UUID string to death count, never null
*/
public Map<String, Integer> getMemberDeaths() {
if (memberDeaths == null) {
memberDeaths = new HashMap<>();
}
return memberDeaths;
}

/**
* @param memberDeaths the memberDeaths to set
*/
public void setMemberDeaths(Map<String, Integer> memberDeaths) {
this.memberDeaths = memberDeaths;
}

/**
* Get the deaths not attributable to a current member (migration seed plus
* balances of former members).
* @return the anonymousDeaths
*/
public long getAnonymousDeaths() {
return anonymousDeaths;
}

/**
* @param anonymousDeaths the anonymousDeaths to set
*/
public void setAnonymousDeaths(long anonymousDeaths) {
this.anonymousDeaths = anonymousDeaths;
}

/**
* @return true if the legacy per-world death counts have been folded into this island
*/
public boolean isDeathsMigrated() {
return deathsMigrated;
}

/**
* @param deathsMigrated the deathsMigrated to set
*/
public void setDeathsMigrated(boolean deathsMigrated) {
this.deathsMigrated = deathsMigrated;
}

/**
* Get the total number of deaths counting against this island: anonymous deaths
* plus all current member deaths.
* @return total deaths for this island
*/
public long getTotalDeaths() {
return anonymousDeaths + getMemberDeaths().values().stream().mapToLong(Integer::longValue).sum();
}

/**
* @return the initialLevel
* @deprecated only used for backwards compatibility. Use {@link #getInitialCount()} instead
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,17 @@ level-calc: blocks / level_cost
levelwait: 60
#
# Death penalty
# How many block values a player will lose per death.
# Default value of 100 means that for every death, the player will lose 1 level (if levelcost is 100)
# How many block values the island will lose per death.
# Default value of 100 means that for every death, the island will lose 1 level (if levelcost is 100)
# Deaths are tracked per island: only deaths that happen in the island's space count against it,
# 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.
# Set to zero to not use this feature
deathpenalty: 100
# Sum team deaths - if true, all the teams deaths are summed
# If false, only the leader's deaths counts
# For other death related settings, see the GameModeAddon's config.yml settings.
# Deprecated - no longer used for level calculation, which now always counts all deaths
# that occurred on the island. Only read once, when migrating legacy per-world death counts:
# if true the migrated count is the sum of all team members' deaths, otherwise the owner's deaths.
sumteamdeaths: false
# Shorthand island level
# Shows large level values rounded down, e.g., 10,345 -> 10k
Expand Down
Loading
Loading