diff --git a/src/main/java/dev/tatsi/reloadmc/smp/command/DeathStatsCommand.java b/src/main/java/dev/tatsi/reloadmc/smp/command/DeathStatsCommand.java index 9ac170a..d50abc4 100644 --- a/src/main/java/dev/tatsi/reloadmc/smp/command/DeathStatsCommand.java +++ b/src/main/java/dev/tatsi/reloadmc/smp/command/DeathStatsCommand.java @@ -3,93 +3,219 @@ package dev.tatsi.reloadmc.smp.command; import dev.tatsi.reloadmc.smp.manager.DeathCounterManager; import dev.tatsi.reloadmc.smp.model.DeathRecord; 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.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; 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.Locale; import java.util.UUID; public class DeathStatsCommand implements CommandExecutor { private final DeathCounterManager deathCounterManager; + private static final int PAGE_SIZE = 3; public DeathStatsCommand(DeathCounterManager deathCounterManager) { this.deathCounterManager = deathCounterManager; } @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) { - // Show own stats if player, or usage if console - if (sender instanceof Player) { - showPlayerStats(sender, (Player) sender); - } else { - sender.sendMessage("§cUsage: /deathstats "); + if (self == null) { + sender.sendMessage(Component.text("Usage: /deathstats [page]", NamedTextColor.RED)); + return true; } + 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; } - // Show stats for specified player - 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); - } - + showPlayerStatsByUuid(sender, targetUuid, targetName, page, label); return true; } - private void showPlayerStats(CommandSender sender, Player player) { - showPlayerStatsByUuid(sender, player.getUniqueId(), player.getName()); - } - - private void showPlayerStatsByUuid(CommandSender sender, UUID playerUuid, String playerName) { + private void showPlayerStatsByUuid(CommandSender sender, UUID playerUuid, String playerName, int page, String label) { PlayerDeathData deathData = deathCounterManager.getPlayerDeathData(playerUuid); - - if (deathData == null || deathData.getDeathCount() == 0) { - sender.sendMessage("§a" + playerName + " has no recorded deaths. Lucky!"); + + int total = (deathData == null) ? 0 : deathData.getDeathCount(); + if (total == 0) { + sender.sendMessage( + Component.text(playerName, NamedTextColor.RED, TextDecoration.BOLD) + .append(Component.text(" has no recorded deaths.", NamedTextColor.WHITE)) + ); return; } - sender.sendMessage("§6=== Death Statistics for " + playerName + " ==="); - sender.sendMessage("§eTotalDeaths: §c" + deathData.getDeathCount()); - +// Header: === Death Statistics of name (total) === + 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 deaths = deathData.getDeaths(); - if (deaths.size() > 0) { - sender.sendMessage("§eRecent Deaths:"); - 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", " ") - )); + int totalPages = (int) Math.ceil(total / (double) PAGE_SIZE); + page = Math.max(1, Math.min(page, totalPages)); + + // We want latest first. Data assumed chronological -> iterate from end backwards + 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); } - - if (deaths.size() > 5) { - sender.sendMessage("§7... and " + (deaths.size() - 5) + " more deaths."); + 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); } } -} \ No newline at end of file + + 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"); + } +} diff --git a/src/main/java/dev/tatsi/reloadmc/smp/listener/PlayerDeathListener.java b/src/main/java/dev/tatsi/reloadmc/smp/listener/PlayerDeathListener.java index dc645e8..0f45c60 100644 --- a/src/main/java/dev/tatsi/reloadmc/smp/listener/PlayerDeathListener.java +++ b/src/main/java/dev/tatsi/reloadmc/smp/listener/PlayerDeathListener.java @@ -23,37 +23,39 @@ public class PlayerDeathListener implements Listener { Player player = event.getEntity(); Location deathLocation = player.getLocation(); - // Original-Death-Message (kann den Spielernamen enthalten) + // Vanilla death message, may include player name String raw = event.getDeathMessage(); if (raw == null || raw.trim().isEmpty()) { raw = "died"; } - // Heuristik: Spielernamen vorne entfernen, falls vorhanden String name = player.getName(); 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(); } + + // Ensure reason is not empty if (reason.isEmpty()) { reason = "died"; } - // Death record speichern + // Store clean reason (without player name) DeathRecord deathRecord = new DeathRecord( deathLocation.getX(), deathLocation.getY(), deathLocation.getZ(), deathLocation.getWorld().getName(), - raw // originaler Grund für Historie + reason ); 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) .append(Component.text(" " + reason, NamedTextColor.GRAY)); - // Setze die Death-Message (Paper/Adventure) event.deathMessage(message); } } diff --git a/src/main/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManager.java b/src/main/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManager.java index 87740dc..00a4211 100644 --- a/src/main/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManager.java +++ b/src/main/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManager.java @@ -21,75 +21,78 @@ public class DeathCounterManager { private final JavaPlugin plugin; private final File dataFile; private final Gson gson; - private final Map playerDeathData; public DeathCounterManager(JavaPlugin plugin) { this.plugin = plugin; this.dataFile = new File(plugin.getDataFolder(), "death_counter.json"); 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()) { 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) { - PlayerDeathData data = playerDeathData.computeIfAbsent(playerUuid, + Map all = loadAll(); + + PlayerDeathData data = all.computeIfAbsent(playerUuid, uuid -> new PlayerDeathData(uuid, playerName)); + // keep latest known name + if (playerName != null && !playerName.isBlank() && !playerName.equals(data.getPlayerName())) { + data.setPlayerName(playerName); + } data.addDeath(deathRecord); - saveData(); + saveAll(all); plugin.getLogger().info(String.format("Recorded death for %s: %s", playerName, deathRecord)); } public PlayerDeathData getPlayerDeathData(UUID playerUuid) { - return playerDeathData.get(playerUuid); + Map all = loadAll(); + return all.get(playerUuid); } public int getDeathCount(UUID playerUuid) { - PlayerDeathData data = playerDeathData.get(playerUuid); + PlayerDeathData data = getPlayerDeathData(playerUuid); return data != null ? data.getDeathCount() : 0; } public Map getAllPlayerData() { - return new HashMap<>(playerDeathData); + // return a copy + return new HashMap<>(loadAll()); } - private void loadData() { + private Map loadAll() { if (!dataFile.exists()) { - plugin.getLogger().info("Death counter data file not found, starting with empty data."); - return; + return new HashMap<>(); } - try (FileReader reader = new FileReader(dataFile)) { Type type = new TypeToken>() { }.getType(); - Map loadedData = gson.fromJson(reader, type); - - if (loadedData != null) { - playerDeathData.putAll(loadedData); - plugin.getLogger().info(String.format("Loaded death data for %d players.", loadedData.size())); - } + Map loaded = gson.fromJson(reader, type); + return (loaded != null) ? loaded : new HashMap<>(); } catch (IOException e) { plugin.getLogger().log(Level.SEVERE, "Failed to load death counter data", e); + return new HashMap<>(); } } - private void saveData() { + private void saveAll(Map data) { try (FileWriter writer = new FileWriter(dataFile)) { - gson.toJson(playerDeathData, writer); + gson.toJson(data, writer); } catch (IOException e) { plugin.getLogger().log(Level.SEVERE, "Failed to save death counter data", e); } } public void shutdown() { - saveData(); - plugin.getLogger().info("Death counter data saved on shutdown."); + // nothing to do here } -} \ No newline at end of file +} diff --git a/src/main/java/dev/tatsi/reloadmc/smp/model/PlayerDeathData.java b/src/main/java/dev/tatsi/reloadmc/smp/model/PlayerDeathData.java index b2b7e8d..cc8958c 100644 --- a/src/main/java/dev/tatsi/reloadmc/smp/model/PlayerDeathData.java +++ b/src/main/java/dev/tatsi/reloadmc/smp/model/PlayerDeathData.java @@ -6,7 +6,7 @@ import java.util.UUID; public class PlayerDeathData { private final UUID playerUuid; - private final String playerName; + private String playerName; // not final anymore private final List deaths; public PlayerDeathData(UUID playerUuid, String playerName) { @@ -43,6 +43,11 @@ public class PlayerDeathData { return new ArrayList<>(deaths); // Return copy to prevent external modification } + // Setter for playerName + public void setPlayerName(String playerName) { + this.playerName = playerName; + } + @Override public String toString() { return String.format("PlayerDeathData{uuid=%s, name='%s', deathCount=%d}", diff --git a/src/test/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManagerTest.java b/src/test/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManagerTest.java index ca9a7d2..8013509 100644 --- a/src/test/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManagerTest.java +++ b/src/test/java/dev/tatsi/reloadmc/smp/manager/DeathCounterManagerTest.java @@ -18,7 +18,8 @@ import java.util.UUID; import java.util.logging.Logger; 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) class DeathCounterManagerTest { @@ -39,41 +40,35 @@ class DeathCounterManagerTest { @BeforeEach void setUp() { - // Setup mock plugin + // Use a fresh temporary data folder per test dataFolder = tempDir.toFile(); 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(); testPlayerName = "TestPlayer"; - // Create DeathCounterManager instance deathCounterManager = new DeathCounterManager(mockPlugin); } @Test void testConstructor_CreatesDataFolderIfNotExists() { - // Given: A new temp directory that doesn't exist File newDataFolder = new File(tempDir.toFile(), "newFolder"); when(mockPlugin.getDataFolder()).thenReturn(newDataFolder); - // When: Creating a new DeathCounterManager new DeathCounterManager(mockPlugin); - // Then: The data folder should be created - assertTrue(newDataFolder.exists()); + assertTrue(newDataFolder.exists(), "Expected data folder to be created by constructor"); } @Test void testAddDeath_NewPlayer() { - // Given: A new death record 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); - // Then: Player data should be created and death should be recorded PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); assertNotNull(playerData); assertEquals(testPlayerUuid, playerData.getPlayerUuid()); @@ -85,15 +80,12 @@ class DeathCounterManagerTest { @Test 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 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, secondDeath); - // Then: Both deaths should be recorded PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); assertNotNull(playerData); assertEquals(2, playerData.getDeathCount()); @@ -102,44 +94,34 @@ class DeathCounterManagerTest { @Test void testGetPlayerDeathData_NonExistentPlayer() { - // Given: A UUID that doesn't exist in the data UUID nonExistentUuid = UUID.randomUUID(); - // When: Getting player data for non-existent player PlayerDeathData result = deathCounterManager.getPlayerDeathData(nonExistentUuid); - // Then: Should return null - assertNull(result); + assertNull(result, "Expected null for non-existent player UUID"); } @Test 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"); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); - // When: Getting death count int deathCount = deathCounterManager.getDeathCount(testPlayerUuid); - // Then: Should return correct count assertEquals(1, deathCount); } @Test void testGetDeathCount_NonExistentPlayer() { - // Given: A UUID that doesn't exist in the data UUID nonExistentUuid = UUID.randomUUID(); - // When: Getting death count for non-existent player int deathCount = deathCounterManager.getDeathCount(nonExistentUuid); - // Then: Should return 0 assertEquals(0, deathCount); } @Test void testGetAllPlayerData() { - // Given: Multiple players with death data UUID player1Uuid = UUID.randomUUID(); UUID player2Uuid = UUID.randomUUID(); 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(player2Uuid, "Player2", death2); - // When: Getting all player data Map allData = deathCounterManager.getAllPlayerData(); - // Then: Should return copy of all data assertEquals(2, allData.size()); assertTrue(allData.containsKey(player1Uuid)); assertTrue(allData.containsKey(player2Uuid)); - // Verify it's a copy (modifying returned map shouldn't affect internal data) + // verify defensive copy allData.clear(); assertEquals(2, deathCounterManager.getAllPlayerData().size()); } @Test 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"); 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); - // Then: Data should be loaded correctly PlayerDeathData loadedData = newManager.getPlayerDeathData(testPlayerUuid); assertNotNull(loadedData); assertEquals(testPlayerUuid, loadedData.getPlayerUuid()); @@ -180,35 +158,37 @@ class DeathCounterManagerTest { @Test void testLoadData_EmptyFile() throws IOException { - // Given: An empty data file - File dataFile = new File(dataFolder, "death_counter.json"); // Create an empty file - dataFile.createNewFile(); - assertTrue(dataFile.exists()); + File dataFile = new File(dataFolder, "death_counter.json"); + 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); - // Then: Should handle empty data gracefully assertEquals(0, newManager.getAllPlayerData().size()); } @Test - void testShutdown_SavesData() { - // Given: Some death data + void testShutdown_Noop() { + // Add some data DeathRecord deathRecord = new DeathRecord(100.0, 64.0, 200.0, "world", "Fell from a high place"); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); - // When: Calling shutdown - deathCounterManager.shutdown(); + File dataFile = new File(dataFolder, "death_counter.json"); + long beforeLen = dataFile.length(); + long beforeMod = dataFile.lastModified(); - // Then: Should log shutdown message - verify(mockLogger).info("Death counter data saved on shutdown."); + // Should not throw and should not alter the file + 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 void testDeathRecordDetails() { - // Given: A death record with specific details double x = 123.45; double y = 67.89; double z = 234.56; @@ -218,11 +198,10 @@ class DeathCounterManagerTest { DeathRecord deathRecord = new DeathRecord(x, y, z, world, reason); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, deathRecord); - // When: Retrieving the death record PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); + assertNotNull(playerData); DeathRecord retrievedRecord = playerData.getDeaths().get(0); - // Then: All details should be preserved assertEquals(x, retrievedRecord.getX(), 0.001); assertEquals(y, retrievedRecord.getY(), 0.001); assertEquals(z, retrievedRecord.getZ(), 0.001); @@ -233,18 +212,16 @@ class DeathCounterManagerTest { @Test void testMultipleDeathsPreserveOrder() { - // Given: Multiple deaths added in sequence 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 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, death2); deathCounterManager.addDeath(testPlayerUuid, testPlayerName, death3); - // Then: Deaths should be preserved in order PlayerDeathData playerData = deathCounterManager.getPlayerDeathData(testPlayerUuid); + assertNotNull(playerData); assertEquals(3, playerData.getDeathCount()); assertEquals("First death", playerData.getDeaths().get(0).getDeathReason()); assertEquals("Second death", playerData.getDeaths().get(1).getDeathReason());