Improve messages & logs

This commit is contained in:
2025-09-02 00:05:53 +02:00
parent ccfb2decd3
commit 5a6a74cd8a
5 changed files with 251 additions and 138 deletions

View File

@@ -3,93 +3,219 @@ package dev.tatsi.reloadmc.smp.command;
import dev.tatsi.reloadmc.smp.manager.DeathCounterManager; import dev.tatsi.reloadmc.smp.manager.DeathCounterManager;
import dev.tatsi.reloadmc.smp.model.DeathRecord; import dev.tatsi.reloadmc.smp.model.DeathRecord;
import dev.tatsi.reloadmc.smp.model.PlayerDeathData; import dev.tatsi.reloadmc.smp.model.PlayerDeathData;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.UUID; import java.util.UUID;
public class DeathStatsCommand implements CommandExecutor { public class DeathStatsCommand implements CommandExecutor {
private final DeathCounterManager deathCounterManager; private final DeathCounterManager deathCounterManager;
private static final int PAGE_SIZE = 3;
public DeathStatsCommand(DeathCounterManager deathCounterManager) { public DeathStatsCommand(DeathCounterManager deathCounterManager) {
this.deathCounterManager = deathCounterManager; this.deathCounterManager = deathCounterManager;
} }
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(@NotNull CommandSender sender, Command command, String label, String[] args) {
// Parse args: self vs target; optional page
Player self = (sender instanceof Player p) ? p : null;
String targetName;
UUID targetUuid = null;
int page = 1;
if (args.length == 0) { if (args.length == 0) {
// Show own stats if player, or usage if console if (self == null) {
if (sender instanceof Player) { sender.sendMessage(Component.text("Usage: /deathstats <player> [page]", NamedTextColor.RED));
showPlayerStats(sender, (Player) sender); return true;
} else {
sender.sendMessage("§cUsage: /deathstats <player>");
} }
targetName = self.getName();
targetUuid = self.getUniqueId();
} else if (args.length == 1) {
// One arg: if numeric and sender is player -> page for self; else treat as player name
if (isPositiveInt(args[0]) && self != null) {
page = Integer.parseInt(args[0]);
targetName = self.getName();
targetUuid = self.getUniqueId();
} else {
targetName = args[0];
Player online = Bukkit.getPlayerExact(targetName);
if (online != null) {
targetUuid = online.getUniqueId();
targetName = online.getName(); // normalize case
} else {
// search offline data
targetUuid = findUuidByNameInsensitive(targetName);
}
}
} else {
targetName = args[0];
if (isPositiveInt(args[1])) page = Integer.parseInt(args[1]);
Player online = Bukkit.getPlayerExact(targetName);
if (online != null) {
targetUuid = online.getUniqueId();
targetName = online.getName();
} else {
targetUuid = findUuidByNameInsensitive(targetName);
}
}
if (targetUuid == null) {
sender.sendMessage(
Component.text("Player '", NamedTextColor.RED)
.append(Component.text(targetName, NamedTextColor.WHITE))
.append(Component.text("' not found or has no death records.", NamedTextColor.RED))
);
return true; return true;
} }
// Show stats for specified player showPlayerStatsByUuid(sender, targetUuid, targetName, page, label);
String targetPlayerName = args[0];
Player targetPlayer = Bukkit.getPlayer(targetPlayerName);
if (targetPlayer == null) {
// Try to find offline player data
UUID targetUuid = null;
for (PlayerDeathData data : deathCounterManager.getAllPlayerData().values()) {
if (data.getPlayerName().equalsIgnoreCase(targetPlayerName)) {
targetUuid = data.getPlayerUuid();
break;
}
}
if (targetUuid == null) {
sender.sendMessage("§cPlayer '" + targetPlayerName + "' not found or has no death records.");
return true;
}
showPlayerStatsByUuid(sender, targetUuid, targetPlayerName);
} else {
showPlayerStats(sender, targetPlayer);
}
return true; return true;
} }
private void showPlayerStats(CommandSender sender, Player player) { private void showPlayerStatsByUuid(CommandSender sender, UUID playerUuid, String playerName, int page, String label) {
showPlayerStatsByUuid(sender, player.getUniqueId(), player.getName());
}
private void showPlayerStatsByUuid(CommandSender sender, UUID playerUuid, String playerName) {
PlayerDeathData deathData = deathCounterManager.getPlayerDeathData(playerUuid); PlayerDeathData deathData = deathCounterManager.getPlayerDeathData(playerUuid);
if (deathData == null || deathData.getDeathCount() == 0) { int total = (deathData == null) ? 0 : deathData.getDeathCount();
sender.sendMessage("§a" + playerName + " has no recorded deaths. Lucky!"); if (total == 0) {
sender.sendMessage(
Component.text(playerName, NamedTextColor.RED, TextDecoration.BOLD)
.append(Component.text(" has no recorded deaths.", NamedTextColor.WHITE))
);
return; return;
} }
sender.sendMessage("§6=== Death Statistics for " + playerName + " ==="); // Header: === Death Statistics of <red bold>name</red bold> (total) ===
sender.sendMessage("§eTotalDeaths: §c" + deathData.getDeathCount()); Component header = Component.text("===", NamedTextColor.DARK_GRAY)
.append(Component.text(" Death Statistics of ", NamedTextColor.WHITE))
.append(Component.text(playerName, NamedTextColor.RED, TextDecoration.BOLD))
.append(Component.text(" (", NamedTextColor.DARK_GRAY))
.append(Component.text(String.valueOf(total), NamedTextColor.RED))
.append(Component.text(") ", NamedTextColor.DARK_GRAY))
.append(Component.text("===", NamedTextColor.DARK_GRAY));
sender.sendMessage(header);
List<DeathRecord> deaths = deathData.getDeaths(); List<DeathRecord> deaths = deathData.getDeaths();
if (deaths.size() > 0) { int totalPages = (int) Math.ceil(total / (double) PAGE_SIZE);
sender.sendMessage("§eRecent Deaths:"); page = Math.max(1, Math.min(page, totalPages));
int showCount = Math.min(5, deaths.size()); // Show last 5 deaths
for (int i = deaths.size() - showCount; i < deaths.size(); i++) {
DeathRecord death = deaths.get(i);
sender.sendMessage(String.format("§7- §f%s §7at §e%.1f, %.1f, %.1f §7in §b%s §7(%s)",
death.getDeathReason(),
death.getX(), death.getY(), death.getZ(),
death.getWorld(),
death.getTimestamp().replace("T", " ")
));
}
if (deaths.size() > 5) { // We want latest first. Data assumed chronological -> iterate from end backwards
sender.sendMessage("§7... and " + (deaths.size() - 5) + " more deaths."); int endIdxExclusive = deaths.size(); // last element is newest
int startNewestIndex = endIdxExclusive - 1; // newest index
int startIndex = startNewestIndex - (PAGE_SIZE * (page - 1));
int endIndex = Math.max(startNewestIndex - (PAGE_SIZE * page) + 1, 0);
// Display items from startIndex down to endIndex inclusive
DateTimeFormatter outFmt = DateTimeFormatter.ofPattern("dd.MM.yy HH:mm", Locale.ROOT);
for (int i = startIndex; i >= endIndex && i >= 0 && i < deaths.size(); i--) {
DeathRecord d = deaths.get(i);
String ts = formatTimestampShort(d.getTimestamp(), outFmt);
String world = d.getWorld();
String coords = (int) d.getX() + ", " + (int) d.getY() + ", " + (int) d.getZ();
String reason = safeReason(d.getDeathReason());
// » [dd-MM-yy HH:mm] (world) X,Y,Z reason
Component line = Component.text("» ", NamedTextColor.GRAY) // prefix
.append(Component.text("[", NamedTextColor.GRAY))
.append(Component.text(ts, NamedTextColor.WHITE))
.append(Component.text("] ", NamedTextColor.GRAY))
.append(Component.text("(" + world + ") ", NamedTextColor.GRAY))
.append(Component.text(coords, NamedTextColor.WHITE))
.append(Component.text(" ", NamedTextColor.GRAY))
.append(Component.text(reason, NamedTextColor.WHITE));
sender.sendMessage(line);
}
// Footer with paging hint
if (totalPages > 1) {
String nextHint;
if (sender instanceof Player p && p.getName().equalsIgnoreCase(playerName)) {
// self
nextHint = "/" + "deathstats" + " " + (page + 1);
} else {
nextHint = "/" + "deathstats" + " " + playerName + " " + (page + 1);
} }
Component footer = Component.text("(Page " + page + "/" + totalPages + ")", NamedTextColor.GRAY);
if (page < totalPages) {
footer = footer.append(Component.text(" • next: ", NamedTextColor.GRAY))
.append(Component.text(nextHint, NamedTextColor.WHITE));
}
sender.sendMessage(footer);
} }
} }
private boolean isPositiveInt(String s) {
try {
int v = Integer.parseInt(s);
return v > 0;
} catch (NumberFormatException e) {
return false;
}
}
private UUID findUuidByNameInsensitive(String name) {
return deathCounterManager.getAllPlayerData().values().stream()
.filter(d -> d.getPlayerName() != null && d.getPlayerName().equalsIgnoreCase(name))
.map(PlayerDeathData::getPlayerUuid)
.findFirst()
.orElse(null);
}
private String safeReason(String reason) {
if (reason == null) return "unknown";
String r = reason.trim();
return r.isEmpty() ? "unknown" : r;
}
/**
* Attempts to parse various ISO-like strings and format as "yy-MM-dd HH:mm" (no seconds).
* Falls back to the raw string if parsing fails.
*/
private String formatTimestampShort(String raw, DateTimeFormatter outFmt) {
if (raw == null || raw.isBlank()) return "??-??-?? ??:??";
try {
// Try OffsetDateTime (e.g., 2025-09-01T12:34:56Z)
return outFmt.format(OffsetDateTime.parse(raw).atZoneSameInstant(ZoneId.systemDefault()));
} catch (Exception ignored) {
}
try {
// Try ZonedDateTime
return outFmt.format(ZonedDateTime.parse(raw).withZoneSameInstant(ZoneId.systemDefault()));
} catch (Exception ignored) {
}
try {
// Try LocalDateTime (assume system zone)
return outFmt.format(LocalDateTime.parse(raw).atZone(ZoneId.systemDefault()));
} catch (Exception ignored) {
}
// Fallback: best-effort trim seconds if present like "2025-09-01 12:34:56"
int tIdx = raw.indexOf(':');
if (tIdx > 0) {
int lastColon = raw.lastIndexOf(':');
if (lastColon > tIdx) {
// remove last :ss
return raw.substring(0, lastColon);
}
}
return raw;
}
private void showPlayerStats(CommandSender sender, Player player) {
showPlayerStatsByUuid(sender, player.getUniqueId(), player.getName(), 1, "deathstats");
}
} }

View File

@@ -23,37 +23,39 @@ public class PlayerDeathListener implements Listener {
Player player = event.getEntity(); Player player = event.getEntity();
Location deathLocation = player.getLocation(); Location deathLocation = player.getLocation();
// Original-Death-Message (kann den Spielernamen enthalten) // Vanilla death message, may include player name
String raw = event.getDeathMessage(); String raw = event.getDeathMessage();
if (raw == null || raw.trim().isEmpty()) { if (raw == null || raw.trim().isEmpty()) {
raw = "died"; raw = "died";
} }
// Heuristik: Spielernamen vorne entfernen, falls vorhanden
String name = player.getName(); String name = player.getName();
String reason = raw.trim(); String reason = raw.trim();
if (reason.startsWith(name)) {
// Strip player name if it's at the beginning
if (reason.toLowerCase().startsWith(name.toLowerCase())) {
reason = reason.substring(name.length()).trim(); reason = reason.substring(name.length()).trim();
} }
// Ensure reason is not empty
if (reason.isEmpty()) { if (reason.isEmpty()) {
reason = "died"; reason = "died";
} }
// Death record speichern // Store clean reason (without player name)
DeathRecord deathRecord = new DeathRecord( DeathRecord deathRecord = new DeathRecord(
deathLocation.getX(), deathLocation.getX(),
deathLocation.getY(), deathLocation.getY(),
deathLocation.getZ(), deathLocation.getZ(),
deathLocation.getWorld().getName(), deathLocation.getWorld().getName(),
raw // originaler Grund für Historie reason
); );
deathCounterManager.addDeath(player.getUniqueId(), name, deathRecord); deathCounterManager.addDeath(player.getUniqueId(), name, deathRecord);
// Schlanke Chat-Nachricht: Roter, fetter Spielername + Grund in Grau // Broadcast custom death message: red bold name + reason
Component message = Component.text(name, NamedTextColor.RED, TextDecoration.BOLD) Component message = Component.text(name, NamedTextColor.RED, TextDecoration.BOLD)
.append(Component.text(" " + reason, NamedTextColor.GRAY)); .append(Component.text(" " + reason, NamedTextColor.GRAY));
// Setze die Death-Message (Paper/Adventure)
event.deathMessage(message); event.deathMessage(message);
} }
} }

View File

@@ -21,75 +21,78 @@ public class DeathCounterManager {
private final JavaPlugin plugin; private final JavaPlugin plugin;
private final File dataFile; private final File dataFile;
private final Gson gson; private final Gson gson;
private final Map<UUID, PlayerDeathData> playerDeathData;
public DeathCounterManager(JavaPlugin plugin) { public DeathCounterManager(JavaPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.dataFile = new File(plugin.getDataFolder(), "death_counter.json"); this.dataFile = new File(plugin.getDataFolder(), "death_counter.json");
this.gson = new GsonBuilder().setPrettyPrinting().create(); this.gson = new GsonBuilder().setPrettyPrinting().create();
this.playerDeathData = new HashMap<>();
// Create plugin data folder if it doesn't exist // Ensure data folder exists
if (!plugin.getDataFolder().exists()) { if (!plugin.getDataFolder().exists()) {
plugin.getDataFolder().mkdirs(); plugin.getDataFolder().mkdirs();
} }
loadData(); // Ensure file exists (empty JSON) to simplify reads
if (!dataFile.exists()) {
saveAll(new HashMap<>());
}
} }
public void addDeath(UUID playerUuid, String playerName, DeathRecord deathRecord) { public void addDeath(UUID playerUuid, String playerName, DeathRecord deathRecord) {
PlayerDeathData data = playerDeathData.computeIfAbsent(playerUuid, Map<UUID, PlayerDeathData> all = loadAll();
PlayerDeathData data = all.computeIfAbsent(playerUuid,
uuid -> new PlayerDeathData(uuid, playerName)); uuid -> new PlayerDeathData(uuid, playerName));
// keep latest known name
if (playerName != null && !playerName.isBlank() && !playerName.equals(data.getPlayerName())) {
data.setPlayerName(playerName);
}
data.addDeath(deathRecord); data.addDeath(deathRecord);
saveData(); saveAll(all);
plugin.getLogger().info(String.format("Recorded death for %s: %s", playerName, deathRecord)); plugin.getLogger().info(String.format("Recorded death for %s: %s", playerName, deathRecord));
} }
public PlayerDeathData getPlayerDeathData(UUID playerUuid) { public PlayerDeathData getPlayerDeathData(UUID playerUuid) {
return playerDeathData.get(playerUuid); Map<UUID, PlayerDeathData> all = loadAll();
return all.get(playerUuid);
} }
public int getDeathCount(UUID playerUuid) { public int getDeathCount(UUID playerUuid) {
PlayerDeathData data = playerDeathData.get(playerUuid); PlayerDeathData data = getPlayerDeathData(playerUuid);
return data != null ? data.getDeathCount() : 0; return data != null ? data.getDeathCount() : 0;
} }
public Map<UUID, PlayerDeathData> getAllPlayerData() { public Map<UUID, PlayerDeathData> getAllPlayerData() {
return new HashMap<>(playerDeathData); // return a copy
return new HashMap<>(loadAll());
} }
private void loadData() { private Map<UUID, PlayerDeathData> loadAll() {
if (!dataFile.exists()) { if (!dataFile.exists()) {
plugin.getLogger().info("Death counter data file not found, starting with empty data."); return new HashMap<>();
return;
} }
try (FileReader reader = new FileReader(dataFile)) { try (FileReader reader = new FileReader(dataFile)) {
Type type = new TypeToken<Map<UUID, PlayerDeathData>>() { Type type = new TypeToken<Map<UUID, PlayerDeathData>>() {
}.getType(); }.getType();
Map<UUID, PlayerDeathData> loadedData = gson.fromJson(reader, type); Map<UUID, PlayerDeathData> loaded = gson.fromJson(reader, type);
return (loaded != null) ? loaded : new HashMap<>();
if (loadedData != null) {
playerDeathData.putAll(loadedData);
plugin.getLogger().info(String.format("Loaded death data for %d players.", loadedData.size()));
}
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to load death counter data", e); plugin.getLogger().log(Level.SEVERE, "Failed to load death counter data", e);
return new HashMap<>();
} }
} }
private void saveData() { private void saveAll(Map<UUID, PlayerDeathData> data) {
try (FileWriter writer = new FileWriter(dataFile)) { try (FileWriter writer = new FileWriter(dataFile)) {
gson.toJson(playerDeathData, writer); gson.toJson(data, writer);
} catch (IOException e) { } catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to save death counter data", e); plugin.getLogger().log(Level.SEVERE, "Failed to save death counter data", e);
} }
} }
public void shutdown() { public void shutdown() {
saveData(); // nothing to do here
plugin.getLogger().info("Death counter data saved on shutdown.");
} }
} }

View File

@@ -6,7 +6,7 @@ import java.util.UUID;
public class PlayerDeathData { public class PlayerDeathData {
private final UUID playerUuid; private final UUID playerUuid;
private final String playerName; private String playerName; // not final anymore
private final List<DeathRecord> deaths; private final List<DeathRecord> deaths;
public PlayerDeathData(UUID playerUuid, String playerName) { public PlayerDeathData(UUID playerUuid, String playerName) {
@@ -43,6 +43,11 @@ public class PlayerDeathData {
return new ArrayList<>(deaths); // Return copy to prevent external modification return new ArrayList<>(deaths); // Return copy to prevent external modification
} }
// Setter for playerName
public void setPlayerName(String playerName) {
this.playerName = playerName;
}
@Override @Override
public String toString() { public String toString() {
return String.format("PlayerDeathData{uuid=%s, name='%s', deathCount=%d}", return String.format("PlayerDeathData{uuid=%s, name='%s', deathCount=%d}",

View File

@@ -18,7 +18,8 @@ import java.util.UUID;
import java.util.logging.Logger; import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class DeathCounterManagerTest { class DeathCounterManagerTest {
@@ -39,41 +40,35 @@ class DeathCounterManagerTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
// Setup mock plugin // Use a fresh temporary data folder per test
dataFolder = tempDir.toFile(); dataFolder = tempDir.toFile();
when(mockPlugin.getDataFolder()).thenReturn(dataFolder); when(mockPlugin.getDataFolder()).thenReturn(dataFolder);
when(mockPlugin.getLogger()).thenReturn(mockLogger);
// Test data // Lenient to avoid UnnecessaryStubbingException in tests that don't hit the logger
lenient().when(mockPlugin.getLogger()).thenReturn(mockLogger);
testPlayerUuid = UUID.randomUUID(); testPlayerUuid = UUID.randomUUID();
testPlayerName = "TestPlayer"; testPlayerName = "TestPlayer";
// Create DeathCounterManager instance
deathCounterManager = new DeathCounterManager(mockPlugin); deathCounterManager = new DeathCounterManager(mockPlugin);
} }
@Test @Test
void testConstructor_CreatesDataFolderIfNotExists() { void testConstructor_CreatesDataFolderIfNotExists() {
// Given: A new temp directory that doesn't exist
File newDataFolder = new File(tempDir.toFile(), "newFolder"); File newDataFolder = new File(tempDir.toFile(), "newFolder");
when(mockPlugin.getDataFolder()).thenReturn(newDataFolder); when(mockPlugin.getDataFolder()).thenReturn(newDataFolder);
// When: Creating a new DeathCounterManager
new DeathCounterManager(mockPlugin); new DeathCounterManager(mockPlugin);
// Then: The data folder should be created assertTrue(newDataFolder.exists(), "Expected data folder to be created by constructor");
assertTrue(newDataFolder.exists());
} }
@Test @Test
void testAddDeath_NewPlayer() { void testAddDeath_NewPlayer() {
// Given: A new death record
DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place");
// When: Adding a death for a new player
deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord);
// Then: Player data should be created and death should be recorded
PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid);
assertNotNull(playerData); assertNotNull(playerData);
assertEquals(testPlayerUuid, playerData.getPlayerUuid()); assertEquals(testPlayerUuid, playerData.getPlayerUuid());
@@ -85,15 +80,12 @@ class DeathCounterManagerTest {
@Test @Test
void testAddDeath_ExistingPlayer() { void testAddDeath_ExistingPlayer() {
// Given: A player with existing death data
DeathRecord firstDeath = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); DeathRecord firstDeath = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place");
DeathRecord secondDeath = new DeathRecord(150.0, 70.0, 250.0, "nether", "Burned to death"); DeathRecord secondDeath = new DeathRecord(150.0, 70.0, 250.0, "nether", "Burned to death");
// When: Adding multiple deaths for the same player
deathCounterManager.addDeath(testPlayerUuid, testPlayerName, firstDeath); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, firstDeath);
deathCounterManager.addDeath(testPlayerUuid, testPlayerName, secondDeath); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, secondDeath);
// Then: Both deaths should be recorded
PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid);
assertNotNull(playerData); assertNotNull(playerData);
assertEquals(2, playerData.getDeathCount()); assertEquals(2, playerData.getDeathCount());
@@ -102,44 +94,34 @@ class DeathCounterManagerTest {
@Test @Test
void testGetPlayerDeathData_NonExistentPlayer() { void testGetPlayerDeathData_NonExistentPlayer() {
// Given: A UUID that doesn't exist in the data
UUID nonExistentUuid = UUID.randomUUID(); UUID nonExistentUuid = UUID.randomUUID();
// When: Getting player data for non-existent player
PlayerDeathData result = deathCounterManager.getPlayerDeathData(nonExistentUuid); PlayerDeathData result = deathCounterManager.getPlayerDeathData(nonExistentUuid);
// Then: Should return null assertNull(result, "Expected null for non-existent player UUID");
assertNull(result);
} }
@Test @Test
void testGetDeathCount_ExistingPlayer() { void testGetDeathCount_ExistingPlayer() {
// Given: A player with death data
DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place");
deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord);
// When: Getting death count
int deathCount = deathCounterManager.getDeathCount(testPlayerUuid); int deathCount = deathCounterManager.getDeathCount(testPlayerUuid);
// Then: Should return correct count
assertEquals(1, deathCount); assertEquals(1, deathCount);
} }
@Test @Test
void testGetDeathCount_NonExistentPlayer() { void testGetDeathCount_NonExistentPlayer() {
// Given: A UUID that doesn't exist in the data
UUID nonExistentUuid = UUID.randomUUID(); UUID nonExistentUuid = UUID.randomUUID();
// When: Getting death count for non-existent player
int deathCount = deathCounterManager.getDeathCount(nonExistentUuid); int deathCount = deathCounterManager.getDeathCount(nonExistentUuid);
// Then: Should return 0
assertEquals(0, deathCount); assertEquals(0, deathCount);
} }
@Test @Test
void testGetAllPlayerData() { void testGetAllPlayerData() {
// Given: Multiple players with death data
UUID player1Uuid = UUID.randomUUID(); UUID player1Uuid = UUID.randomUUID();
UUID player2Uuid = UUID.randomUUID(); UUID player2Uuid = UUID.randomUUID();
DeathRecord death1 = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); DeathRecord death1 = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place");
@@ -148,29 +130,25 @@ class DeathCounterManagerTest {
deathCounterManager.addDeath(player1Uuid, "Player1", death1); deathCounterManager.addDeath(player1Uuid, "Player1", death1);
deathCounterManager.addDeath(player2Uuid, "Player2", death2); deathCounterManager.addDeath(player2Uuid, "Player2", death2);
// When: Getting all player data
Map<UUID, PlayerDeathData> allData = deathCounterManager.getAllPlayerData(); Map<UUID, PlayerDeathData> allData = deathCounterManager.getAllPlayerData();
// Then: Should return copy of all data
assertEquals(2, allData.size()); assertEquals(2, allData.size());
assertTrue(allData.containsKey(player1Uuid)); assertTrue(allData.containsKey(player1Uuid));
assertTrue(allData.containsKey(player2Uuid)); assertTrue(allData.containsKey(player2Uuid));
// Verify it's a copy (modifying returned map shouldn't affect internal data) // verify defensive copy
allData.clear(); allData.clear();
assertEquals(2, deathCounterManager.getAllPlayerData().size()); assertEquals(2, deathCounterManager.getAllPlayerData().size());
} }
@Test @Test
void testDataPersistence_SaveAndLoad() throws IOException { void testDataPersistence_SaveAndLoad() throws IOException {
// Given: Some death data
DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place");
deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord);
// When: Creating a new DeathCounterManager (which should load existing data) // Recreate manager -> should load existing data from disk
DeathCounterManager newManager = new DeathCounterManager(mockPlugin); DeathCounterManager newManager = new DeathCounterManager(mockPlugin);
// Then: Data should be loaded correctly
PlayerDeathData loadedData = newManager.getPlayerDeathData(testPlayerUuid); PlayerDeathData loadedData = newManager.getPlayerDeathData(testPlayerUuid);
assertNotNull(loadedData); assertNotNull(loadedData);
assertEquals(testPlayerUuid, loadedData.getPlayerUuid()); assertEquals(testPlayerUuid, loadedData.getPlayerUuid());
@@ -180,35 +158,37 @@ class DeathCounterManagerTest {
@Test @Test
void testLoadData_EmptyFile() throws IOException { void testLoadData_EmptyFile() throws IOException {
// Given: An empty data file
File dataFile = new File(dataFolder, "death_counter.json");
// Create an empty file // Create an empty file
dataFile.createNewFile(); File dataFile = new File(dataFolder, "death_counter.json");
assertTrue(dataFile.exists()); assertTrue(dataFile.delete() || !dataFile.exists());
assertTrue(dataFolder.exists() || dataFolder.mkdirs());
assertTrue(dataFile.createNewFile(), "Expected to create empty data file");
// When: Creating a new DeathCounterManager
DeathCounterManager newManager = new DeathCounterManager(mockPlugin); DeathCounterManager newManager = new DeathCounterManager(mockPlugin);
// Then: Should handle empty data gracefully
assertEquals(0, newManager.getAllPlayerData().size()); assertEquals(0, newManager.getAllPlayerData().size());
} }
@Test @Test
void testShutdown_SavesData() { void testShutdown_Noop() {
// Given: Some death data // Add some data
DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place");
deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord);
// When: Calling shutdown File dataFile = new File(dataFolder, "death_counter.json");
deathCounterManager.shutdown(); long beforeLen = dataFile.length();
long beforeMod = dataFile.lastModified();
// Then: Should log shutdown message // Should not throw and should not alter the file
verify(mockLogger).info("Death counter data saved on shutdown."); assertDoesNotThrow(() -> deathCounterManager.shutdown());
// Manager does not touch the file on shutdown -> length & timestamp should remain unchanged
assertEquals(beforeLen, dataFile.length(), "shutdown() should not change data file size");
assertEquals(beforeMod, dataFile.lastModified(), "shutdown() should not modify data file timestamp");
} }
@Test @Test
void testDeathRecordDetails() { void testDeathRecordDetails() {
// Given: A death record with specific details
double x = 123.45; double x = 123.45;
double y = 67.89; double y = 67.89;
double z = 234.56; double z = 234.56;
@@ -218,11 +198,10 @@ class DeathCounterManagerTest {
DeathRecord deathRecord = new DeathRecord(x, y, z, world, reason); DeathRecord deathRecord = new DeathRecord(x, y, z, world, reason);
deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord);
// When: Retrieving the death record
PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid);
assertNotNull(playerData);
DeathRecord retrievedRecord = playerData.getDeaths().get(0); DeathRecord retrievedRecord = playerData.getDeaths().get(0);
// Then: All details should be preserved
assertEquals(x, retrievedRecord.getX(), 0.001); assertEquals(x, retrievedRecord.getX(), 0.001);
assertEquals(y, retrievedRecord.getY(), 0.001); assertEquals(y, retrievedRecord.getY(), 0.001);
assertEquals(z, retrievedRecord.getZ(), 0.001); assertEquals(z, retrievedRecord.getZ(), 0.001);
@@ -233,18 +212,16 @@ class DeathCounterManagerTest {
@Test @Test
void testMultipleDeathsPreserveOrder() { void testMultipleDeathsPreserveOrder() {
// Given: Multiple deaths added in sequence
DeathRecord death1 = new DeathRecord(100.0, 64.0, 200.0, "world", "First death"); DeathRecord death1 = new DeathRecord(100.0, 64.0, 200.0, "world", "First death");
DeathRecord death2 = new DeathRecord(150.0, 70.0, 250.0, "nether", "Second death"); DeathRecord death2 = new DeathRecord(150.0, 70.0, 250.0, "nether", "Second death");
DeathRecord death3 = new DeathRecord(200.0, 80.0, 300.0, "end", "Third death"); DeathRecord death3 = new DeathRecord(200.0, 80.0, 300.0, "end", "Third death");
// When: Adding deaths in sequence
deathCounterManager.addDeath(testPlayerUuid, testPlayerName, death1); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, death1);
deathCounterManager.addDeath(testPlayerUuid, testPlayerName, death2); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, death2);
deathCounterManager.addDeath(testPlayerUuid, testPlayerName, death3); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, death3);
// Then: Deaths should be preserved in order
PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid);
assertNotNull(playerData);
assertEquals(3, playerData.getDeathCount()); assertEquals(3, playerData.getDeathCount());
assertEquals("First death", playerData.getDeaths().get(0).getDeathReason()); assertEquals("First death", playerData.getDeaths().get(0).getDeathReason());
assertEquals("Second death", playerData.getDeaths().get(1).getDeathReason()); assertEquals("Second death", playerData.getDeaths().get(1).getDeathReason());