diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aef5026 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +/.idea/ +/.vscode/ +*.iml +*.ipr +*.iws + +/target/ +/logs/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties + +*.tmp +*.bak +*.swp +*~.nib +*.orig + +.DS_Store +Thumbs.db + +/settings/ +*.prefs + +!target/*.jar +target/*.jar.* + +/.mvn/ +mvnw +mvnw.cmd + +/server/ +/run/ +/plugins/ +/world*/ +/logs/ +*.log + +.build_number \ No newline at end of file diff --git a/compile.sh b/compile.sh new file mode 100755 index 0000000..2ab5d8f --- /dev/null +++ b/compile.sh @@ -0,0 +1,20 @@ +#! /bin/bash + +#CURRENT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) +#NEW_VERSION=$(echo $CURRENT_VERSION | perl -pe 's/(parar-test)(\d+)/$1.($2+1)/ge') +#mvn versions:set -DnewVersion=$NEW_VERSION + +NUMBER_FILE=".build_number" + +if [ -f "$NUMBER_FILE" ]; then + test_number=$(cat $NUMBER_FILE) +else + test_number=1 +fi + +mvn package -Dbuild_name="-test$test_number" + + +if [ $? -eq 0 ]; then + echo $((test_number + 1)) > $NUMBER_FILE +fi \ No newline at end of file diff --git a/pom.xml b/pom.xml index 656e508..39b0f0b 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ com.campfirecheckpoints CampfireCheckpoints - 1.1.0 + 1.2.0 jar CampfireCheckpoints @@ -17,6 +17,7 @@ 21 UTF-8 + @@ -49,6 +50,13 @@ + + maven-jar-plugin + 3.3.0 + + ${project.artifactId}-${project.version}${build_name} + + org.apache.maven.plugins maven-compiler-plugin @@ -61,18 +69,6 @@ - - org.apache.maven.plugins - maven-jar-plugin - 3.3.0 - - - - true - - - - diff --git a/src/main/java/com/campfirecheckpoints/command/CheckpointCommand.java b/src/main/java/com/campfirecheckpoints/command/CheckpointCommand.java index 37eb78d..e3a4623 100644 --- a/src/main/java/com/campfirecheckpoints/command/CheckpointCommand.java +++ b/src/main/java/com/campfirecheckpoints/command/CheckpointCommand.java @@ -36,14 +36,16 @@ public CheckpointCommand(@NotNull CampfireCheckpoints plugin) { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - + if (!(sender instanceof Player player)) { sender.sendMessage("This command can only be used by players."); return true; } + ConfigManager configManager = plugin.getConfigManager(); + if (args.length == 0) { - showHelp(player); + showHelp(player, configManager); return true; } @@ -51,19 +53,27 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command switch (subCommand) { case "list" -> handleList(player); - case "delete" -> handleDelete(player, args); + case "delete" -> { + if (configManager.isDeleteCommandAllowed()) { + handleDelete(player, args); + } else { + MessageUtil.send(player, "&cThe delete command is disabled by the server configuration."); + } + } case "reload" -> handleReload(player); case "info" -> handleInfo(player); - default -> showHelp(player); + default -> showHelp(player, configManager); } return true; } - private void showHelp(@NotNull Player player) { + private void showHelp(@NotNull Player player, ConfigManager configManager) { MessageUtil.send(player, "&6&l=== Campfire Checkpoints ==="); MessageUtil.send(player, "&e/cc list &7- List your checkpoints"); - MessageUtil.send(player, "&e/cc delete &7- Delete a checkpoint"); + if (configManager.isDeleteCommandAllowed()) { + MessageUtil.send(player, "&e/cc delete &7- Delete a checkpoint"); + } MessageUtil.send(player, "&e/cc info &7- Show plugin info"); if (player.hasPermission("campfirecheckpoints.reload")) { MessageUtil.send(player, "&e/cc reload &7- Reload configuration"); @@ -81,6 +91,8 @@ private void handleList(@NotNull Player player) { CheckpointManager manager = plugin.getCheckpointManager(); List checkpoints = manager.getPlayerCheckpoints(player.getUniqueId()); + manager.validateAllCheckpoints(player.getUniqueId()); + if (checkpoints.isEmpty()) { MessageUtil.send(player, "&eYou have no checkpoints set."); MessageUtil.send(player, "&7Right-click a lit campfire to set one!"); @@ -92,24 +104,34 @@ private void handleList(@NotNull Player player) { String limitDisplay = max > 0 ? " &7(" + checkpoints.size() + "/" + max + ")" : ""; MessageUtil.send(player, "&6&l=== Your Checkpoints ===" + limitDisplay); - + for (int i = 0; i < checkpoints.size(); i++) { Checkpoint cp = checkpoints.get(i); Location loc = cp.getBlockLocation(); - - String status = cp.isLit() ? "&a✓ Lit" : "&c✗ Extinguished"; + + String status = cp.isLit() ? "&a✓ Lit" : "&c✗ Put out"; String coords = loc != null ? String.format("&f%d, %d, %d", loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()) : "&cUnknown"; - String world = cp.getWorldName(); + String world = cp.getWorldName().replace("world_", ""); String date = dateFormat.format(new Date(cp.getCreatedAt())); - MessageUtil.send(player, "&e[" + i + "] " + status + " &7| " + coords + + String type = ""; + if (cp.isSoul()) { + type += " &9(soul)"; + } else if (cp.isAnchor()) { + type += " &d(anchor)"; + status = cp.isLit() ? "&a✓ Charged" : "&c✗ Disabled"; + } + + MessageUtil.send(player, "&e[" + i + "] " + status + type + " &7| " + coords + " &7(" + world + ")"); MessageUtil.send(player, " &7Created: " + date); } - MessageUtil.send(player, "&7Use &e/cc delete &7to remove a checkpoint."); + if (plugin.getConfigManager().isDeleteCommandAllowed()) { + MessageUtil.send(player, "&7Use &e/cc delete &7to remove a checkpoint."); + } } private void handleDelete(@NotNull Player player, @NotNull String[] args) { @@ -171,7 +193,7 @@ private void handleReload(@NotNull Player player) { private void handleInfo(@NotNull Player player) { CheckpointManager manager = plugin.getCheckpointManager(); ConfigManager configManager = plugin.getConfigManager(); - + int playerCount = manager.getPlayerCheckpoints(player.getUniqueId()).size(); int totalCount = manager.getTotalCheckpointCount(); int max = configManager.getMaxCheckpointsPerPlayer(); @@ -182,7 +204,25 @@ private void handleInfo(@NotNull Player player) { MessageUtil.send(player, "&eYour Checkpoints: &f" + playerCount + (max > 0 ? " / " + max : "")); MessageUtil.send(player, "&eTotal Checkpoints: &f" + totalCount); - MessageUtil.send(player, "&eRadius: &f" + configManager.getRadius() + " blocks"); + + MessageUtil.send(player, "&eRegular campfires enabled (overworld): &f" + configManager.isDimentionEnabledOverworld()); + MessageUtil.send(player, "&eRegular campfires enabled (nether): &f" + configManager.isDimentionEnabledNether()); + MessageUtil.send(player, "&eRegular campfires enabled (the end): &f" + configManager.isDimentionEnabledEnd()); + + MessageUtil.send(player, "&eSoul campfires enabled (overworld): &f" + configManager.isDimentionEnabledOverworldSoul()); + MessageUtil.send(player, "&eSoul campfires enabled (nether): &f" + configManager.isDimentionEnabledNetherSoul()); + MessageUtil.send(player, "&eSoul campfires enabled (the end): &f" + configManager.isDimentionEnabledEndSoul()); + + MessageUtil.send(player, "&eRespawn anchors enabled (overworld): &f" + configManager.isDimentionEnabledOverworldAnchor()); + MessageUtil.send(player, "&eRespawn anchors enabled (nether): &f" + configManager.isDimentionEnabledNetherAnchor()); + MessageUtil.send(player, "&eRespawn anchors enabled (the end): &f" + configManager.isDimentionEnabledEndAnchor()); + + MessageUtil.send(player, "&eRadius (regular campfires): &f" + configManager.getRadius() + " blocks"); + MessageUtil.send(player, "&eRadius (soul campfires): &f" + configManager.getSoulRadius() + " blocks"); + + MessageUtil.send(player, "&eMin. distance between checkpoints: &f" + configManager.getMinDistance() + " blocks"); + MessageUtil.send(player, "&eMin. distance between checkpoints (soul): &f" + configManager.getSoulMinDistance() + " blocks"); + MessageUtil.send(player, "&eExtinguish on Respawn: &f" + configManager.isExtinguishOnRespawn()); MessageUtil.send(player, "&eConfirmation Timeout: &f" + configManager.getOverrideConfirmationTimeout() + "s"); @@ -199,8 +239,13 @@ private void handleInfo(@NotNull Player player) { return null; } + ConfigManager configManager = plugin.getConfigManager(); + if (args.length == 1) { - List completions = new ArrayList<>(Arrays.asList("list", "delete", "info")); + List completions = new ArrayList<>(List.of("list", "info")); + if (configManager.isDeleteCommandAllowed()) { + completions.add("delete"); + } if (player.hasPermission("campfirecheckpoints.reload")) { completions.add("reload"); } @@ -209,10 +254,10 @@ private void handleInfo(@NotNull Player player) { .collect(Collectors.toList()); } - if (args.length == 2 && args[0].equalsIgnoreCase("delete")) { + if (args.length == 2 && args[0].equalsIgnoreCase("delete") && configManager.isDeleteCommandAllowed()) { int checkpointCount = plugin.getCheckpointManager() .getPlayerCheckpoints(player.getUniqueId()).size(); - + return IntStream.range(0, checkpointCount) .mapToObj(String::valueOf) .filter(s -> s.startsWith(args[1])) diff --git a/src/main/java/com/campfirecheckpoints/listener/CheckpointListener.java b/src/main/java/com/campfirecheckpoints/listener/CheckpointListener.java index ae3b31f..66c9a2d 100644 --- a/src/main/java/com/campfirecheckpoints/listener/CheckpointListener.java +++ b/src/main/java/com/campfirecheckpoints/listener/CheckpointListener.java @@ -11,6 +11,7 @@ import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.Lightable; +import org.bukkit.block.data.type.RespawnAnchor; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -53,19 +54,152 @@ public void onPlayerInteract(@NotNull PlayerInteractEvent event) { return; } - Material type = clickedBlock.getType(); - if (type != Material.CAMPFIRE && type != Material.SOUL_CAMPFIRE) { + // If player interacts with a bed, clear their respawn-anchor checkpoint(s), + // but do not cancel the bed interaction. + if (clickedBlock.getBlockData() instanceof org.bukkit.block.data.type.Bed) { + plugin.getCheckpointManager().removeAnchorCheckpoints(event.getPlayer().getUniqueId(), true); return; } + Material type = clickedBlock.getType(); + boolean isRegularCampfire = (type == Material.CAMPFIRE); + boolean isSoulCampfire = (type == Material.SOUL_CAMPFIRE); + boolean isRespawnAnchor = (type == Material.RESPAWN_ANCHOR); + + ConfigManager configManager = plugin.getConfigManager(); + World.Environment env = event.getPlayer().getWorld().getEnvironment(); Player player = event.getPlayer(); - - if (!player.hasPermission("campfirecheckpoints.use")) { + + // Deny respawn anchor spawnpoint in Nether unless enabled, but allow charging with glowstone + if (isRespawnAnchor && configManager.RespawnAnchorsEnabled()) { + + // If vanilla respawn anchors are enabled in the Nether, don't intercept anchor interaction there + if (env == World.Environment.NETHER + && !configManager.isDimentionEnabledNetherAnchor() + && configManager.vanillaAnchorsEnabledInNether()) { + return; + } + + // Prevent any respawn anchor logic if player is sneaking + if (player.isSneaking()) { + return; + } + + Material itemInHand = player.getInventory().getItemInMainHand().getType(); + + if (itemInHand == Material.GLOWSTONE) { + // Allow charging with glowstone, but prevent setting spawn if fully charged in Nether + if (env == World.Environment.NETHER) { + BlockData blockData = clickedBlock.getBlockData(); + if (blockData instanceof RespawnAnchor anchorData) { + if (anchorData.getCharges() >= ((RespawnAnchor) blockData).getMaximumCharges()) { + // Anchor is fully charged, prevent further charging and setting spawn + event.setCancelled(true); + return; + } + } + } + return; + } + + if (env == World.Environment.NORMAL && !configManager.isDimentionEnabledOverworldAnchor()) { + MessageUtil.send(player, "&cSetting respawn anchors as checkpoints is not allowed in the Overworld."); + //event.setCancelled(true); + return; + } + + if (env == World.Environment.NETHER && !configManager.isDimentionEnabledNetherAnchor()) { + MessageUtil.send(player, "&cSetting respawn anchors as checkpoints is not allowed in the Nether."); + event.setCancelled(true); + return; + } + + if (env == World.Environment.THE_END && !configManager.isDimentionEnabledEndAnchor()) { + MessageUtil.send(player, "&cSetting respawn anchors as checkpoints is not allowed in the End."); + //event.setCancelled(true); + return; + } + + // Create checkpoint on respawn anchor + UUID playerUUID = player.getUniqueId(); + Location blockLocation = clickedBlock.getLocation(); + CheckpointManager checkpointManager = plugin.getCheckpointManager(); + + checkpointManager.validateAllCheckpoints(playerUUID); + + if (checkpointManager.hasCheckpointAt(playerUUID, blockLocation)) { + MessageUtil.send(player, "&eYou already have a checkpoint at this respawn anchor!"); + event.setCancelled(true); + return; + } + + // Check for charge state and prevent setting checkpoint on uncharged anchor + BlockData blockData = clickedBlock.getBlockData(); + if (blockData instanceof RespawnAnchor anchorData) { + if (anchorData.getCharges() <= 0) { + MessageUtil.send(player, "&cThis respawn anchor is uncharged!" + + " Charge it with glowstone to set a checkpoint."); + event.setCancelled(true); + return; + } + } + + // Remove all other anchor checkpoints, there can be only one per player + checkpointManager.removeAnchorCheckpoints(playerUUID, false); + + Checkpoint newCheckpoint = new Checkpoint(playerUUID, blockLocation); + checkpointManager.addCheckpoint(playerUUID, newCheckpoint); + + String worldName = blockLocation.getWorld() != null ? + blockLocation.getWorld().getName().replace("world_", "") : "unknown"; + + MessageUtil.send(player, "&aCheckpoint set at respawn anchor &f(" + + blockLocation.getBlockX() + ", " + + blockLocation.getBlockY() + ", " + + blockLocation.getBlockZ() + ", " + + worldName + ")&a!"); + + player.playSound(blockLocation, Sound.valueOf("BLOCK_RESPAWN_ANCHOR_SET_SPAWN"), SoundCategory.BLOCKS, 1.0f, 1.0f); + + event.setCancelled(true); + + } + + if (!isRegularCampfire && !isSoulCampfire) { return; } + + switch (env) { + case NORMAL: + if (isRegularCampfire && !configManager.isDimentionEnabledOverworld()) return; + if (isSoulCampfire && !configManager.isDimentionEnabledOverworldSoul()) return; + break; + case NETHER: + if (isRegularCampfire && !configManager.isDimentionEnabledNether()) return; + if (isSoulCampfire && !configManager.isDimentionEnabledNetherSoul()) return; + break; + case THE_END: + if (isRegularCampfire && !configManager.isDimentionEnabledEnd()) return; + if (isSoulCampfire && !configManager.isDimentionEnabledEndSoul()) return; + break; + } + + + Material itemInHand = player.getInventory().getItemInMainHand().getType(); - if (isInteractItem(itemInHand)) { + + // Only allow checkpoint creation if player is sneaking (crouching) or has an empty hand + // This is required to fix food cooking on campfire and extinguishing it with a splash water bottle + if (configManager.isEmptyHandOrSneakRequired()) { + if (!player.isSneaking() && itemInHand != Material.AIR) { + return; + } + } else if (isInteractItem(itemInHand)) { + return; + } + + if (!player.hasPermission("campfirecheckpoints.use")) { return; } @@ -80,9 +214,10 @@ public void onPlayerInteract(@NotNull PlayerInteractEvent event) { UUID playerUUID = player.getUniqueId(); Location blockLocation = clickedBlock.getLocation(); - + CheckpointManager checkpointManager = plugin.getCheckpointManager(); - ConfigManager configManager = plugin.getConfigManager(); + + checkpointManager.validateAllCheckpoints(playerUUID); if (checkpointManager.hasCheckpointAt(playerUUID, blockLocation)) { MessageUtil.send(player, "&eYou already have a checkpoint at this campfire!"); @@ -92,28 +227,41 @@ public void onPlayerInteract(@NotNull PlayerInteractEvent event) { if (checkpointManager.tryExecuteOverride(playerUUID, blockLocation)) { playCheckpointEffects(player, blockLocation); - MessageUtil.send(player, "&aCheckpoint override confirmed! Previous checkpoint removed."); + MessageUtil.send(player, "&aCheckpoint override confirmed!"); event.setCancelled(true); return; } - int radius = configManager.getRadius(); + int min_distance = configManager.getMinDistance(); + + if (type == Material.SOUL_CAMPFIRE) { + min_distance = configManager.getSoulMinDistance(); + }; + Checkpoint existingNearby = checkpointManager.findCheckpointWithinRadius( - playerUUID, blockLocation, radius + playerUUID, blockLocation, min_distance ); if (existingNearby != null) { - checkpointManager.setPendingOverride(playerUUID, existingNearby, blockLocation); + int timeout = configManager.getOverrideConfirmationTimeout(); + + if (timeout > 0) { + checkpointManager.setPendingOverride(playerUUID, blockLocation, min_distance); + } + Location existingLoc = existingNearby.getBlockLocation(); String existingCoords = existingLoc != null ? String.format("(%d, %d, %d)", existingLoc.getBlockX(), existingLoc.getBlockY(), existingLoc.getBlockZ()) : "(unknown)"; - - int timeout = configManager.getOverrideConfirmationTimeout(); - MessageUtil.send(player, "&cWarning: &eYou have a checkpoint at " + existingCoords + - " within " + radius + " blocks!"); - MessageUtil.send(player, "&eRight-click again within " + timeout + " seconds to override it."); + + if (timeout > 0) { + MessageUtil.send(player, "&cWarning: &eYou have a checkpoint at " + existingCoords + + " within " + min_distance + " blocks!"); + MessageUtil.send(player, "&eRight-click again within " + timeout + " seconds to override it."); + } else { + MessageUtil.send(player, "&cYou already have a checkpoint nearby within " + min_distance + " blocks!"); + } event.setCancelled(true); return; } @@ -121,7 +269,12 @@ public void onPlayerInteract(@NotNull PlayerInteractEvent event) { if (!checkpointManager.canCreateCheckpoint(playerUUID)) { int max = configManager.getMaxCheckpointsPerPlayer(); MessageUtil.send(player, "&cYou have reached the maximum number of checkpoints (" + max + ")!"); - MessageUtil.send(player, "&7Use &e/cc delete &7to remove an old checkpoint first."); + + if (configManager.isDeleteCommandAllowed()) { + MessageUtil.send(player, "&7Use &e/cc delete &7to remove an old checkpoint first."); + } else { + MessageUtil.send(player, "&7You need to break an old checkpoint before creating a new one."); + } event.setCancelled(true); return; } @@ -153,7 +306,7 @@ private boolean isInteractItem(@NotNull Material material) { private void playCheckpointEffects(@NotNull Player player, @NotNull Location location) { ConfigManager configManager = plugin.getConfigManager(); World world = location.getWorld(); - + if (world == null) { return; } @@ -188,7 +341,7 @@ private void playCheckpointEffects(@NotNull Player player, @NotNull Location loc public void onPlayerDeath(@NotNull PlayerDeathEvent event) { Player player = event.getEntity(); Location deathLoc = player.getLocation(); - + if (deathLoc.getWorld() != null) { deathLocations.put(player.getUniqueId(), deathLoc.clone()); } @@ -198,7 +351,7 @@ public void onPlayerDeath(@NotNull PlayerDeathEvent event) { public void onPlayerRespawn(@NotNull PlayerRespawnEvent event) { Player player = event.getPlayer(); UUID playerUUID = player.getUniqueId(); - + Location deathLocation = deathLocations.remove(playerUUID); if (deathLocation == null) { return; @@ -206,21 +359,25 @@ public void onPlayerRespawn(@NotNull PlayerRespawnEvent event) { CheckpointManager checkpointManager = plugin.getCheckpointManager(); ConfigManager configManager = plugin.getConfigManager(); - + + checkpointManager.validateAllCheckpoints(playerUUID); + Checkpoint closestCheckpoint = checkpointManager.findClosestCheckpoint(playerUUID, deathLocation); - - Location bedSpawn = player.getRespawnLocation(); - boolean hasBedSpawn = bedSpawn != null && event.isBedSpawn(); - - RespawnResult respawnResult = determineRespawnLocation( - closestCheckpoint, - bedSpawn, - hasBedSpawn, - deathLocation, - configManager.getRespawnPriority(), - configManager.getRadius() + final RespawnOption spawnAtCheckpoint = new RespawnOption(closestCheckpoint, configManager); + + final RespawnOption spawnAtBed = new RespawnOption(player.getRespawnLocation(), + null, false, true, event.isBedSpawn(), configManager); + + // Find anchor checkpoint for this player, if any + Checkpoint anchorCheckpoint = checkpointManager.findAnchorCheckpoint(playerUUID); + final RespawnOption spawnAtAnchor = new RespawnOption(anchorCheckpoint, configManager); + + // Select best option + RespawnOption respawnResult = determineRespawnLocation( + spawnAtCheckpoint, spawnAtAnchor, spawnAtBed, deathLocation, + configManager.getRespawnPriority() ); - + if (respawnResult == null || respawnResult.location == null) { // No valid respawn location, let vanilla handle it return; @@ -234,14 +391,20 @@ public void onPlayerRespawn(@NotNull PlayerRespawnEvent event) { final boolean usedCheckpoint = respawnResult.isCheckpoint; final boolean usedBed = respawnResult.isBed; + final boolean usedAnchor = respawnResult.isAnchor; final boolean extinguished = respawnResult.isCheckpoint && configManager.isExtinguishOnRespawn(); - + Bukkit.getScheduler().runTaskLater(plugin, () -> { if (player.isOnline()) { if (usedCheckpoint) { - MessageUtil.send(player, "&aYou respawned at your campfire checkpoint!"); - if (extinguished) { - MessageUtil.send(player, "&7The campfire has been extinguished. Use Flint & Steel to relight it."); + if (usedAnchor) { + MessageUtil.send(player, "&aYou respawned at your respawn anchor!"); + } else { + MessageUtil.send(player, "&aYou respawned at your campfire checkpoint!"); + if (extinguished) { + MessageUtil.send(player, "&7The campfire has been extinguished." + + " Use Flint & Steel to relight it."); + } } } else if (usedBed) { } @@ -252,141 +415,231 @@ public void onPlayerRespawn(@NotNull PlayerRespawnEvent event) { /** * Determines the respawn location based on priority settings */ - private @Nullable RespawnResult determineRespawnLocation( - @Nullable Checkpoint checkpoint, - @Nullable Location bedSpawn, - boolean hasBedSpawn, + private @Nullable RespawnOption determineRespawnLocation( + final @NotNull RespawnOption checkpointSpawn, + final @NotNull RespawnOption anchorSpawn, + final @NotNull RespawnOption bedSpawn, @NotNull Location deathLocation, - @NotNull RespawnPriority priority, - double radius) { - - // Get checkpoint spawn location if available - Location checkpointSpawn = null; - boolean hasValidCheckpoint = false; - - if (checkpoint != null) { - checkpointSpawn = checkpoint.getSpawnLocation(); - hasValidCheckpoint = checkpointSpawn != null && checkpointSpawn.getWorld() != null; - } - - // If neither is available, return null - if (!hasValidCheckpoint && !hasBedSpawn) { - return null; - } - - // If only checkpoint is available - if (hasValidCheckpoint && !hasBedSpawn) { - return new RespawnResult(checkpointSpawn, true, false, checkpoint); - } - - // If only bed is available - if (!hasValidCheckpoint && hasBedSpawn) { - return new RespawnResult(bedSpawn, false, true, null); - } - - // Both are available - use priority setting + @NotNull RespawnPriority priority) { + + boolean hasBedSpawn = bedSpawn.isValidWithinRadius(deathLocation); + boolean hasAnchorSpawn = anchorSpawn.isValidWithinRadius(deathLocation); + boolean hasValidCheckpoint = checkpointSpawn.isValidWithinRadius(deathLocation); + + boolean hasBedOrAnchorSpawn = hasBedSpawn || hasAnchorSpawn; + RespawnOption bedOrAnchorSpawn = hasAnchorSpawn ? anchorSpawn : bedSpawn; + + if (!hasBedOrAnchorSpawn) { + return hasValidCheckpoint ? checkpointSpawn : null; + } + + if (!hasValidCheckpoint) { + return bedOrAnchorSpawn; + } + + // several options available - use priority setting switch (priority) { case CHECKPOINT: - return new RespawnResult(checkpointSpawn, true, false, checkpoint); - + return checkpointSpawn; + case BED: - return new RespawnResult(bedSpawn, false, true, null); - + return bedOrAnchorSpawn; + case CLOSEST: - return determineClosestRespawn( - checkpoint, checkpointSpawn, - bedSpawn, - deathLocation, - radius - ); - + double checkpointDistSq = checkpointSpawn.distanceSquared(deathLocation); + double bedOrAnchorDistSq = bedOrAnchorSpawn.distanceSquared(deathLocation); + + return bedOrAnchorDistSq < checkpointDistSq ? bedOrAnchorSpawn : checkpointSpawn; + default: - return new RespawnResult(checkpointSpawn, true, false, checkpoint); + return checkpointSpawn; } } - /** - * Determines which respawn point is closer to death location - */ - private @NotNull RespawnResult determineClosestRespawn( - @NotNull Checkpoint checkpoint, - @NotNull Location checkpointSpawn, - @NotNull Location bedSpawn, - @NotNull Location deathLocation, - double radius) { - - double checkpointDistSq = checkpoint.distanceSquared(deathLocation); - double bedDistSq = calculateDistanceSquared(bedSpawn, deathLocation); - - // Check if bed is in a different world - if (bedSpawn.getWorld() == null || !bedSpawn.getWorld().equals(deathLocation.getWorld())) { - // Bed is in different world, use checkpoint - return new RespawnResult(checkpointSpawn, true, false, checkpoint); - } - - // Check if bed is within radius - double radiusSquared = radius * radius; - boolean bedWithinRadius = bedDistSq <= radiusSquared; - - // If bed is not within radius, use checkpoint - if (!bedWithinRadius) { - return new RespawnResult(checkpointSpawn, true, false, checkpoint); - } - - // Both are within radius, use the closer one - if (checkpointDistSq <= bedDistSq) { - return new RespawnResult(checkpointSpawn, true, false, checkpoint); - } else { - return new RespawnResult(bedSpawn, false, true, null); + @EventHandler(priority = EventPriority.MONITOR) + public void onAnchorChargeOrDischarge(org.bukkit.event.block.BlockPhysicsEvent event) { + Block block = event.getBlock(); + + if (block.getType() != Material.RESPAWN_ANCHOR) { + return; } - } - private double calculateDistanceSquared(@NotNull Location a, @NotNull Location b) { - if (a.getWorld() == null || b.getWorld() == null) { - return Double.MAX_VALUE; + Location blockLoc = block.getLocation(); + Checkpoint checkpoint = plugin.getCheckpointManager().getCheckpointAt(blockLoc); + if (checkpoint == null) return; + + BlockData data = block.getBlockData(); + int charge = -1; + if (data instanceof org.bukkit.block.data.type.RespawnAnchor anchorData) { + charge = anchorData.getCharges(); } - if (!a.getWorld().equals(b.getWorld())) { - return Double.MAX_VALUE; + + boolean wasLit = checkpoint.isLit(); + boolean isNowLit = charge > 0; + + if (wasLit != isNowLit) { + checkpoint.setLit(isNowLit); + plugin.getCheckpointManager().setCheckpointLit(checkpoint, isNowLit); + Player owner = Bukkit.getPlayer(checkpoint.getOwnerUUID()); + if (owner != null && owner.isOnline()) { + if (isNowLit) { + MessageUtil.send(owner, "&aYour respawn anchor at &f(" + + blockLoc.getBlockX() + ", " + + blockLoc.getBlockY() + ", " + + blockLoc.getBlockZ() + ") &ahas been charged!"); + } else { + MessageUtil.send(owner, "&eYour respawn anchor at &f(" + + blockLoc.getBlockX() + ", " + + blockLoc.getBlockY() + ", " + + blockLoc.getBlockZ() + ") &ehas no charge left!"); + } + } } - - double dx = a.getX() - b.getX(); - double dy = a.getY() - b.getY(); - double dz = a.getZ() - b.getZ(); - return dx * dx + dy * dy + dz * dz; } private void handleCheckpointRespawn( @NotNull Checkpoint checkpoint, @NotNull CheckpointManager checkpointManager, @NotNull ConfigManager configManager) { - - if (configManager.isExtinguishOnRespawn()) { + + boolean anchor = checkpoint.isAnchor(); + + if (configManager.isExtinguishOnRespawn() || anchor) { Location blockLoc = checkpoint.getBlockLocation(); if (blockLoc != null && blockLoc.getWorld() != null) { Block campfireBlock = blockLoc.getBlock(); BlockData blockData = campfireBlock.getBlockData(); - + if (blockData instanceof Lightable lightable) { lightable.setLit(false); campfireBlock.setBlockData(lightable); checkpointManager.setCheckpointLit(checkpoint, false); + + // Play configured respawn sound + Sound respawnSound = configManager.getSoundOnRespawn(); + if (respawnSound != null) { + blockLoc.getWorld().playSound(blockLoc, Sound.valueOf(respawnSound.name()), + SoundCategory.BLOCKS, 1.0f, 1.0f); + } + + } else if (anchor && blockData instanceof RespawnAnchor anchorData) { + int charges = anchorData.getCharges() - 1; + anchorData.setCharges(charges); + campfireBlock.setBlockData(anchorData); + + blockLoc.getWorld().playSound(blockLoc, Sound.valueOf("BLOCK_RESPAWN_ANCHOR_DEPLETE"), + SoundCategory.BLOCKS, 1.0f, 1.0f); + + if (charges <= 0) { + checkpoint.setLit(false); + checkpointManager.setCheckpointLit(checkpoint, false); + } } } } } - private static final class RespawnResult { + private static final class RespawnOption { final @Nullable Location location; + final @Nullable Checkpoint checkpoint; final boolean isCheckpoint; final boolean isBed; - final @Nullable Checkpoint checkpoint; - - RespawnResult(@Nullable Location location, boolean isCheckpoint, boolean isBed, - @Nullable Checkpoint checkpoint) { + final boolean isAnchor; + final boolean valid; + final double radiusSq; + private final ConfigManager configManager; + + RespawnOption(@Nullable Location location, @Nullable Checkpoint checkpoint, + boolean isCheckpoint, boolean isBed, boolean valid, @NotNull ConfigManager configManager) { this.location = location; + this.checkpoint = checkpoint; this.isCheckpoint = isCheckpoint; this.isBed = isBed; + this.valid = valid; + this.configManager = configManager; + + this.isAnchor = isCheckpoint && checkpoint != null && checkpoint.isAnchor(); + this.radiusSq = calcRadiusSq(); + } + + RespawnOption(@Nullable Checkpoint checkpoint, @NotNull ConfigManager configManager) { + this.configManager = configManager; + if (checkpoint == null || !checkpoint.isLit()) { + this.location = null; + this.checkpoint = checkpoint; + this.isCheckpoint = true; + this.isBed = false; + this.valid = false; + this.isAnchor = false; + this.radiusSq = 0; + return; + } + + this.location = checkpoint.getSpawnLocation(); this.checkpoint = checkpoint; + this.isCheckpoint = true; + this.isBed = false; + this.valid = location != null && location.getWorld() != null; + + this.isAnchor = checkpoint.isAnchor(); + this.radiusSq = calcRadiusSq(); + } + + private double calcRadiusSq() { + if (isBed || isAnchor) { + return Double.MAX_VALUE; + } + if (!isCheckpoint) { + // neither a bed, an anchor nor a checkpoint, return 0 radius just in case + return 0; + } + + double radius; + if (checkpoint != null && checkpoint.isSoul()) { + radius = configManager.getSoulRadius(); + } else { + radius = configManager.getRadius(); + } + return radius * radius; + } + + public boolean isValid() { + if (!valid) { + return false; + } + if (location == null || location.getWorld() == null) { + return false; + } + if (isCheckpoint && checkpoint == null) { + return false; + } + return true; + } + + public boolean isValidWithinRadius(@NotNull Location other) { + if (!this.isValid()) { + return false; + } + if (radiusSq == Double.MAX_VALUE) { + // if it is an anchor or a bed, radiusSq is Double.MAX_VALUE + return true; + } + // if the dimensions differ, distanceSquared returns Double.MAX_VALUE + return distanceSquared(other) <= radiusSq; + } + + public double distanceSquared(@NotNull Location other) { + if (!this.isValid() || other.getWorld() == null) { + return Double.MAX_VALUE; + } + if (!location.getWorld().equals(other.getWorld())) { + return Double.MAX_VALUE; + } + + double dx = location.getX() - other.getX(); + double dy = location.getY() - other.getY(); + double dz = location.getZ() - other.getZ(); + return dx * dx + dy * dy + dz * dz; } } @@ -401,25 +654,27 @@ public void onPlayerQuit(@NotNull PlayerQuitEvent event) { public void onBlockBreak(@NotNull BlockBreakEvent event) { Block block = event.getBlock(); Material type = block.getType(); - - if (type != Material.CAMPFIRE && type != Material.SOUL_CAMPFIRE) { + + // Handle campfire and respawn anchor checkpoints + if (type != Material.CAMPFIRE && type != Material.SOUL_CAMPFIRE && type != Material.RESPAWN_ANCHOR) { return; } Location blockLocation = block.getLocation(); CheckpointManager checkpointManager = plugin.getCheckpointManager(); - + Checkpoint checkpoint = checkpointManager.removeCheckpointAt(blockLocation); - + if (checkpoint != null) { Player owner = Bukkit.getPlayer(checkpoint.getOwnerUUID()); + String checkpointType = checkpoint.isAnchor() ? "respawn anchor" : "campfire checkpoint"; if (owner != null && owner.isOnline()) { - MessageUtil.send(owner, "&cYour campfire checkpoint at &f(" + - blockLocation.getBlockX() + ", " + - blockLocation.getBlockY() + ", " + + MessageUtil.send(owner, "&cYour " + checkpointType + " at &f(" + + blockLocation.getBlockX() + ", " + + blockLocation.getBlockY() + ", " + blockLocation.getBlockZ() + ") &chas been destroyed!"); } - + Player breaker = event.getPlayer(); if (!breaker.getUniqueId().equals(checkpoint.getOwnerUUID())) { MessageUtil.send(breaker, "&7You destroyed a checkpoint belonging to another player."); @@ -432,7 +687,7 @@ public void onCampfireExtinguish(@NotNull PlayerInteractEvent event) { if (event.getAction() != Action.RIGHT_CLICK_BLOCK) { return; } - + Block clickedBlock = event.getClickedBlock(); if (clickedBlock == null) { return; @@ -445,7 +700,7 @@ public void onCampfireExtinguish(@NotNull PlayerInteractEvent event) { Player player = event.getPlayer(); Material itemInHand = player.getInventory().getItemInMainHand().getType(); - + if (!isShovel(itemInHand) && itemInHand != Material.WATER_BUCKET) { return; } @@ -459,13 +714,13 @@ public void onCampfireExtinguish(@NotNull PlayerInteractEvent event) { Bukkit.getScheduler().runTaskLater(plugin, () -> { Block block = blockLoc.getBlock(); BlockData newBlockData = block.getBlockData(); - + if (newBlockData instanceof Lightable newLightable && !newLightable.isLit()) { Checkpoint checkpoint = plugin.getCheckpointManager().getCheckpointAt(blockLoc); if (checkpoint != null && checkpoint.isLit()) { checkpoint.setLit(false); plugin.getCheckpointManager().setCheckpointLit(checkpoint, false); - + Player owner = Bukkit.getPlayer(checkpoint.getOwnerUUID()); if (owner != null && owner.isOnline()) { MessageUtil.send(owner, "&eYour campfire checkpoint at &f(" + @@ -483,20 +738,42 @@ public void onCampfireLight(@NotNull PlayerInteractEvent event) { if (event.getAction() != Action.RIGHT_CLICK_BLOCK) { return; } - + Block clickedBlock = event.getClickedBlock(); if (clickedBlock == null) { return; } Material type = clickedBlock.getType(); - if (type != Material.CAMPFIRE && type != Material.SOUL_CAMPFIRE) { + + boolean isRegularCampfire = (type == Material.CAMPFIRE); + boolean isSoulCampfire = (type == Material.SOUL_CAMPFIRE); + + if (!isRegularCampfire && !isSoulCampfire) { return; } + ConfigManager configManager = plugin.getConfigManager(); + World.Environment env = event.getPlayer().getWorld().getEnvironment(); + + switch (env) { + case NORMAL: + if (isRegularCampfire && !configManager.isDimentionEnabledOverworld()) return; + if (isSoulCampfire && !configManager.isDimentionEnabledOverworldSoul()) return; + break; + case NETHER: + if (isRegularCampfire && !configManager.isDimentionEnabledNether()) return; + if (isSoulCampfire && !configManager.isDimentionEnabledNetherSoul()) return; + break; + case THE_END: + if (isRegularCampfire && !configManager.isDimentionEnabledEnd()) return; + if (isSoulCampfire && !configManager.isDimentionEnabledEndSoul()) return; + break; + } + Player player = event.getPlayer(); Material itemInHand = player.getInventory().getItemInMainHand().getType(); - + if (itemInHand != Material.FLINT_AND_STEEL && itemInHand != Material.FIRE_CHARGE) { return; } @@ -510,13 +787,13 @@ public void onCampfireLight(@NotNull PlayerInteractEvent event) { Bukkit.getScheduler().runTaskLater(plugin, () -> { Block block = blockLoc.getBlock(); BlockData newBlockData = block.getBlockData(); - + if (newBlockData instanceof Lightable newLightable && newLightable.isLit()) { Checkpoint checkpoint = plugin.getCheckpointManager().getCheckpointAt(blockLoc); if (checkpoint != null && !checkpoint.isLit()) { checkpoint.setLit(true); plugin.getCheckpointManager().setCheckpointLit(checkpoint, true); - + Player owner = Bukkit.getPlayer(checkpoint.getOwnerUUID()); if (owner != null && owner.isOnline()) { MessageUtil.send(owner, "&aYour campfire checkpoint at &f(" + diff --git a/src/main/java/com/campfirecheckpoints/manager/CheckpointManager.java b/src/main/java/com/campfirecheckpoints/manager/CheckpointManager.java index 509847b..84ffc28 100644 --- a/src/main/java/com/campfirecheckpoints/manager/CheckpointManager.java +++ b/src/main/java/com/campfirecheckpoints/manager/CheckpointManager.java @@ -11,6 +11,8 @@ import com.google.gson.JsonParser; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.entity.Player; +import com.campfirecheckpoints.util.MessageUtil; import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -65,7 +67,7 @@ public void loadCheckpoints() { try (FileReader reader = new FileReader(dataFile)) { JsonObject root = JsonParser.parseReader(reader).getAsJsonObject(); - + if (root.has("checkpoints")) { JsonArray checkpointsArray = root.getAsJsonArray("checkpoints"); int loaded = 0; @@ -102,7 +104,7 @@ private void markDirty() { if (saveTask != null && !saveTask.isCancelled()) { saveTask.cancel(); } - + saveTask = Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> { if (dirty.compareAndSet(true, false)) { saveCheckpointsInternal(); @@ -115,7 +117,7 @@ public void shutdown() { if (saveTask != null && !saveTask.isCancelled()) { saveTask.cancel(); } - + if (dirty.get()) { saveCheckpointsInternal(); @@ -201,7 +203,7 @@ public boolean removeCheckpoint(@NotNull UUID playerUUID, int index) { if (checkpoints == null) { return false; } - + Checkpoint removed; synchronized (checkpoints) { if (index < 0 || index >= checkpoints.size()) { @@ -212,7 +214,7 @@ public boolean removeCheckpoint(@NotNull UUID playerUUID, int index) { playerCheckpoints.remove(playerUUID); } } - + if (removed != null) { locationIndex.remove(removed.getLocationKey()); } @@ -223,7 +225,7 @@ public boolean removeCheckpoint(@NotNull UUID playerUUID, int index) { public @Nullable Checkpoint removeCheckpointAt(@NotNull Location location) { String locationKey = createLocationKey(location); Checkpoint checkpoint = locationIndex.remove(locationKey); - + if (checkpoint != null) { List checkpoints = playerCheckpoints.get(checkpoint.getOwnerUUID()); if (checkpoints != null) { @@ -236,7 +238,7 @@ public boolean removeCheckpoint(@NotNull UUID playerUUID, int index) { } markDirty(); } - + return checkpoint; } @@ -250,7 +252,7 @@ public boolean removeCheckpoint(@NotNull UUID playerUUID, int index) { synchronized (checkpoints) { for (Checkpoint checkpoint : checkpoints) { - if (checkpoint.isWithinRadius(location, radius)) { + if (checkpoint.isWithinRadius(location, radius, false)) { return checkpoint; } } @@ -268,10 +270,13 @@ public boolean removeCheckpoint(@NotNull UUID playerUUID, int index) { double radius = plugin.getConfigManager().getRadius(); double radiusSquared = radius * radius; + double radiusSoul = plugin.getConfigManager().getSoulRadius(); + double radiusSoulSquared = radiusSoul * radiusSoul; + synchronized (checkpoints) { return checkpoints.stream() .filter(Checkpoint::isLit) - .filter(cp -> cp.distanceSquared(deathLocation) <= radiusSquared) + .filter(cp -> cp.isWithinRespawnRadius(deathLocation, radiusSquared, radiusSoulSquared)) .min(Comparator.comparingDouble(cp -> cp.distanceSquared(deathLocation))) .orElse(null); } @@ -301,12 +306,12 @@ public void setCheckpointLit(@NotNull Checkpoint checkpoint, boolean lit) { } - public void setPendingOverride(@NotNull UUID playerUUID, @NotNull Checkpoint toOverride, - @NotNull Location newLocation) { + public void setPendingOverride(@NotNull UUID playerUUID, @NotNull Location newLocation, double radius) { overrideLock.lock(); try { - pendingOverrides.put(playerUUID, new PendingOverride(toOverride, newLocation, - System.currentTimeMillis())); + pendingOverrides.put(playerUUID, + new PendingOverride(newLocation, radius, + System.currentTimeMillis())); } finally { overrideLock.unlock(); } @@ -346,15 +351,40 @@ private void executeOverrideInternal(@NotNull UUID playerUUID, @NotNull PendingO // Remove old checkpoint List checkpoints = playerCheckpoints.get(playerUUID); if (checkpoints != null) { + List toRemove = new ArrayList<>(); + List keysToRemove = new ArrayList<>(); + + synchronized (checkpoints) { + for (Checkpoint checkpoint : checkpoints) { + if (checkpoint.isWithinRadius(pending.newLocation, pending.radius, false)) { + toRemove.add(checkpoint); + keysToRemove.add(checkpoint.getLocationKey()); + } + } + } + synchronized (checkpoints) { - checkpoints.remove(pending.toOverride); + for (Checkpoint checkpoint : toRemove) { + checkpoints.remove(checkpoint); + } + if (checkpoints.isEmpty()) { playerCheckpoints.remove(playerUUID); } } - locationIndex.remove(pending.toOverride.getLocationKey()); + + Player owner = Bukkit.getPlayer(playerUUID); + + synchronized(locationIndex) { + for (String key : keysToRemove) { + locationIndex.remove(key); + if (owner != null && owner.isOnline()) { + MessageUtil.send(owner, "&fPrevious checkpoint removed: &e(" + key + ")!"); + } + } + } } - + // Add new checkpoint Checkpoint newCheckpoint = new Checkpoint(playerUUID, pending.newLocation); addCheckpoint(playerUUID, newCheckpoint); @@ -367,7 +397,7 @@ public boolean hasPendingOverride(@NotNull UUID playerUUID) { if (pending == null) { return false; } - + long timeout = plugin.getConfigManager().getOverrideConfirmationTimeoutMillis(); if (System.currentTimeMillis() - pending.timestamp > timeout) { pendingOverrides.remove(playerUUID); @@ -386,7 +416,7 @@ public boolean hasPendingOverride(@NotNull UUID playerUUID) { if (pending == null) { return null; } - + long timeout = plugin.getConfigManager().getOverrideConfirmationTimeoutMillis(); if (System.currentTimeMillis() - pending.timestamp > timeout) { pendingOverrides.remove(playerUUID); @@ -415,13 +445,13 @@ private boolean locationEquals(@NotNull Location a, @NotNull Location b) { } public static final class PendingOverride { - public final @NotNull Checkpoint toOverride; + public final @NotNull double radius; public final @NotNull Location newLocation; public final long timestamp; - public PendingOverride(@NotNull Checkpoint toOverride, @NotNull Location newLocation, + public PendingOverride(@NotNull Location newLocation, double radius, long timestamp) { - this.toOverride = toOverride; + this.radius = radius; this.newLocation = newLocation; this.timestamp = timestamp; } @@ -430,4 +460,210 @@ public PendingOverride(@NotNull Checkpoint toOverride, @NotNull Location newLoca public int getTotalCheckpointCount() { return locationIndex.size(); } + + public void validateAllCheckpoints(@Nullable UUID playerUUID) { + List toRemove = new ArrayList<>(); + int extinguished = 0; + int unchargedAnchors = 0; + + if (playerUUID == null) { + for (List checkpoints : playerCheckpoints.values()) { + CheckpointValidationResult result = checkAndCollectInvalid(checkpoints); + toRemove.addAll(result.toRemove); + + extinguished += result.extinguished; + unchargedAnchors += result.unchargedAnchors; + } + } else { + List checkpoints = playerCheckpoints.get(playerUUID); + if (checkpoints != null) { + CheckpointValidationResult result = checkAndCollectInvalid(checkpoints); + toRemove.addAll(result.toRemove); + extinguished += result.extinguished; + unchargedAnchors += result.unchargedAnchors; + } + } + for (Checkpoint checkpoint : toRemove) { + String type = checkpoint.isAnchor() ? "anchor" : "campfire"; + removeCheckpointAt(checkpoint.getBlockLocation()); + // Notify owner if online + Player owner = Bukkit.getPlayer(checkpoint.getOwnerUUID()); + if (owner != null && owner.isOnline()) { + MessageUtil.send(owner, "&eYour " + type + " checkpoint at &f" + checkpoint.getLocationKey() + " &chas been broken."); + } + } + if (!toRemove.isEmpty() || extinguished > 0) { + markDirty(); + String prefix = (playerUUID == null) ? "[All]" : "[Player]"; + plugin.getLogger().info(prefix + " Removed " + toRemove.size() + + " invalid checkpoints, extinguished " + extinguished + + ", uncharged anchors " + unchargedAnchors + "."); + } + } + + private static class CheckpointValidationResult { + List toRemove = new ArrayList<>(); + int extinguished = 0; + int unchargedAnchors = 0; + } + + private CheckpointValidationResult checkAndCollectInvalid(List checkpoints) { + CheckpointValidationResult result = new CheckpointValidationResult(); + synchronized (checkpoints) { + for (Checkpoint checkpoint : checkpoints) { + Location loc = checkpoint.getBlockLocation(); + if (loc == null || loc.getWorld() == null) { + result.toRemove.add(checkpoint); + continue; + } + org.bukkit.block.Block block = loc.getWorld().getBlockAt(loc); + org.bukkit.Material type = block.getType(); + boolean isCampfire = (type == org.bukkit.Material.CAMPFIRE); + boolean isSoulCampfire = (type == org.bukkit.Material.SOUL_CAMPFIRE); + boolean isRespawnAnchor = (type == org.bukkit.Material.RESPAWN_ANCHOR); + + if (!isCampfire && !isSoulCampfire && !isRespawnAnchor) { + result.toRemove.add(checkpoint); + continue; + } + + org.bukkit.block.data.BlockData data = block.getBlockData(); + Player owner = Bukkit.getPlayer(checkpoint.getOwnerUUID()); + + if (data instanceof org.bukkit.block.data.Lightable lightable) { + if (!lightable.isLit() && checkpoint.isLit()) { + setCheckpointLit(checkpoint, false); + result.extinguished++; + + // Notify owner about extinguished checkpoint + if (owner != null && owner.isOnline()) { + MessageUtil.send(owner, "&eYour campfire checkpoint at &f" + checkpoint.getLocationKey() + " &chas been extinguished."); + } + } else if (lightable.isLit() && !checkpoint.isLit()) { + setCheckpointLit(checkpoint, true); + + // Notify owner about relit checkpoint + if (owner != null && owner.isOnline()) { + MessageUtil.send(owner, "&eYour campfire checkpoint at &f" + checkpoint.getLocationKey() + " &ahas been relit."); + } + } + } else if (data instanceof org.bukkit.block.data.type.RespawnAnchor anchorData) { + if (anchorData.getCharges() <= 0 && checkpoint.isLit()) { + setCheckpointLit(checkpoint, false); + result.unchargedAnchors++; + + if (owner != null && owner.isOnline()) { + MessageUtil.send(owner, "&eYour anchor at &f" + checkpoint.getLocationKey() + " &eis not charged."); + } + } else if (anchorData.getCharges() > 0 && !checkpoint.isLit()) { + setCheckpointLit(checkpoint, true); + + if (owner != null && owner.isOnline()) { + MessageUtil.send(owner, "&eYour anchor at &f" + checkpoint.getLocationKey() + " &ahas been recharged."); + } + } + } else { + result.toRemove.add(checkpoint); + } + } + } + return result; + } + + + /* + * Deletes all respawn-anchor checkpoints for the given player. + * This is done in one pass and keeps {@link #locationIndex} in sync. + */ + public void removeAnchorCheckpoints(@NotNull UUID playerUUID, boolean byBed) { + List checkpoints = playerCheckpoints.get(playerUUID); + if (checkpoints == null) { + return; + } + + boolean removedAny = false; + + synchronized (checkpoints) { + for (Iterator it = checkpoints.iterator(); it.hasNext(); ) { + Checkpoint cp = it.next(); + if (cp == null || !cp.isAnchor()) { + continue; + } + + locationIndex.remove(cp.getLocationKey()); + it.remove(); + removedAny = true; + + Player owner = Bukkit.getPlayer(playerUUID); + if (owner != null && owner.isOnline()) { + if (byBed) { + MessageUtil.send(owner, "&eYour respawn anchor at &f" + + cp.getLocationKey() + " &ehas been disabled."); + } else { + MessageUtil.send(owner, "&eYour respawn anchor at &f" + + cp.getLocationKey() + " &chas been removed."); + } + plugin.getLogger().info("[Player] Removed respawn anchor at &f" + + cp.getLocationKey() + " &7for " + playerUUID + "."); + } + } + + if (checkpoints.isEmpty()) { + playerCheckpoints.remove(playerUUID); + } + } + + if (removedAny) { + markDirty(); + } + } + + /* + * Returns the player's respawn-anchor checkpoint (if any). + * If multiple anchor checkpoints somehow exist, this method will keep the newest one + * (by createdAt) and remove the rest. + */ + public @Nullable Checkpoint findAnchorCheckpoint(@NotNull UUID playerUUID) { + List checkpoints = playerCheckpoints.get(playerUUID); + if (checkpoints == null) { + return null; + } + + Checkpoint best = null; + boolean removedAny = false; + + synchronized (checkpoints) { + for (Iterator it = checkpoints.iterator(); it.hasNext(); ) { + Checkpoint cp = it.next(); + if (cp == null || !cp.isAnchor()) { + continue; + } + + if (best == null) { + best = cp; + continue; + } + + // Keep the newest anchor checkpoint + if (cp.getCreatedAt() > best.getCreatedAt()) { + // remove previous best + locationIndex.remove(best.getLocationKey()); + it.remove(); + removedAny = true; + best = cp; + } else { + // remove current cp + locationIndex.remove(cp.getLocationKey()); + it.remove(); + removedAny = true; + } + } + } + + if (removedAny) { + markDirty(); + } + + return best; + } } \ No newline at end of file diff --git a/src/main/java/com/campfirecheckpoints/manager/ConfigManager.java b/src/main/java/com/campfirecheckpoints/manager/ConfigManager.java index c8308d4..f4ad519 100644 --- a/src/main/java/com/campfirecheckpoints/manager/ConfigManager.java +++ b/src/main/java/com/campfirecheckpoints/manager/ConfigManager.java @@ -12,33 +12,100 @@ public final class ConfigManager { private final @NotNull CampfireCheckpoints plugin; - + // Cached config values + + private boolean overworldEnableRegularCampfires; + private boolean netherEnableRegularCampfires; + private boolean endEnableRegularCampfires; + private boolean overworldEnableSoulCampfires; + private boolean netherEnableSoulCampfires; + private boolean endEnableSoulCampfires; + + private boolean respawnAnchorsEnabled; + private boolean overworldRespawnAnchorsEnabled; + private boolean netherRespawnAnchorsEnabled; + private boolean endRespawnAnchorsEnabled; + + private boolean vanillaRespawnAnchorsNether; + private int radius; + private int soulRadius; + private int minDistance; + private int soulMinDistance; private boolean extinguishOnRespawn; private @NotNull Sound soundOnSet; + private @NotNull Sound soundOnRespawn; private int overrideConfirmationTimeout; private int maxCheckpointsPerPlayer; private @NotNull RespawnPriority respawnPriority; + private boolean emptyHandOrSneakRequired; + private boolean deleteCommandAllowed; // Default values + private static final boolean DEFAULT_DIMENTION_OVERWORLD = true; + private static final boolean DEFAULT_DIMENTION_NETHER = false; + private static final boolean DEFAULT_DIMENTION_END = false; + private static final boolean DEFAULT_DIMENTION_OVERWORLD_SOUL = true; + private static final boolean DEFAULT_DIMENTION_NETHER_SOUL = true; + private static final boolean DEFAULT_DIMENTION_END_SOUL = false; + private static final boolean DEFAULT_DIMENTION_OVERWORLD_ANCHOR = false; + private static final boolean DEFAULT_DIMENTION_NETHER_ANCHOR = true; + private static final boolean DEFAULT_DIMENTION_END_ANCHOR = false; + private static final int DEFAULT_RADIUS = 500; + private static final int DEFAULT_SOUL_RADIUS = 1000; + private static final int DEFAULT_MIN_DISTANCE = 250; + private static final int DEFAULT_SOUL_MIN_DISTANCE = 500; private static final boolean DEFAULT_EXTINGUISH = true; private static final Sound DEFAULT_SOUND = Sound.BLOCK_RESPAWN_ANCHOR_SET_SPAWN; + private static final Sound DEFAULT_SOUND_ON_RESPAWN = Sound.BLOCK_RESPAWN_ANCHOR_DEPLETE; private static final int DEFAULT_OVERRIDE_TIMEOUT = 5; private static final int DEFAULT_MAX_CHECKPOINTS = 0; // 0 = unlimited private static final RespawnPriority DEFAULT_RESPAWN_PRIORITY = RespawnPriority.CHECKPOINT; + private static final boolean DEFAULT_EMPTY_HAND_OR_SNEAK_REQUIRED = true; + private static final boolean DEFAULT_DELETE_COMMAND_ALLOWED = true; + private static final boolean DEFAULT_RESPAWN_ANCHORS_ENABLED = false; + private static final boolean DEFAULT_VANILLA_RESPAWN_ANCHORS_NETHER = false; public ConfigManager(@NotNull CampfireCheckpoints plugin) { this.plugin = plugin; this.soundOnSet = DEFAULT_SOUND; + this.soundOnRespawn = DEFAULT_SOUND_ON_RESPAWN; this.respawnPriority = DEFAULT_RESPAWN_PRIORITY; + this.vanillaRespawnAnchorsNether = DEFAULT_VANILLA_RESPAWN_ANCHORS_NETHER; reload(); } public void reload() { FileConfiguration config = plugin.getConfig(); + this.overworldEnableRegularCampfires = config.getBoolean("enable-regular-overworld", DEFAULT_DIMENTION_OVERWORLD); + this.netherEnableRegularCampfires = config.getBoolean("enable-regular-nether", DEFAULT_DIMENTION_NETHER); + this.endEnableRegularCampfires = config.getBoolean("enable-regular-end", DEFAULT_DIMENTION_END); + + this.overworldEnableSoulCampfires = config.getBoolean("enable-soul-overworld", DEFAULT_DIMENTION_OVERWORLD_SOUL); + this.netherEnableSoulCampfires = config.getBoolean("enable-soul-nether", DEFAULT_DIMENTION_NETHER_SOUL); + this.endEnableSoulCampfires = config.getBoolean("enable-soul-end", DEFAULT_DIMENTION_END_SOUL); + + this.respawnAnchorsEnabled = config.getBoolean("enable-respawn-anchors", DEFAULT_RESPAWN_ANCHORS_ENABLED); + + // Vanilla respawn anchor mechanics toggle for Nether (only makes sense if anchors are enabled) + this.vanillaRespawnAnchorsNether = config.getBoolean( + "vanilla-respawn-anchors-in-nether", + DEFAULT_VANILLA_RESPAWN_ANCHORS_NETHER + ); + + if (this.respawnAnchorsEnabled) { + this.overworldRespawnAnchorsEnabled = config.getBoolean("enable-respawn-anchors-overworld", DEFAULT_DIMENTION_OVERWORLD_ANCHOR); + this.netherRespawnAnchorsEnabled = config.getBoolean("enable-respawn-anchors-nether", DEFAULT_DIMENTION_NETHER_ANCHOR); + this.endRespawnAnchorsEnabled = config.getBoolean("enable-respawn-anchors-end", DEFAULT_DIMENTION_END_ANCHOR); + } else { + this.overworldRespawnAnchorsEnabled = false; + this.netherRespawnAnchorsEnabled = false; + this.endRespawnAnchorsEnabled = false; + } + // Load radius this.radius = config.getInt("radius", DEFAULT_RADIUS); if (radius <= 0) { @@ -46,12 +113,30 @@ public void reload() { this.radius = DEFAULT_RADIUS; } + this.soulRadius = config.getInt("soul-campfire-radius", DEFAULT_SOUL_RADIUS); + if (radius <= 0) { + plugin.getLogger().warning("Invalid soul campfire radius in config. Using default: " + DEFAULT_SOUL_RADIUS); + this.soulRadius = DEFAULT_SOUL_RADIUS; + } + + this.minDistance = config.getInt("min-distance", DEFAULT_MIN_DISTANCE); + if (minDistance <= 0) { + plugin.getLogger().warning("Invalid min distance in config. Using default: " + DEFAULT_MIN_DISTANCE); + this.minDistance = DEFAULT_MIN_DISTANCE; + } + + this.soulMinDistance = config.getInt("soul-campfire-min-distance", DEFAULT_SOUL_MIN_DISTANCE); + if (minDistance <= 0) { + plugin.getLogger().warning("Invalid min distance in config. Using default: " + DEFAULT_SOUL_MIN_DISTANCE); + this.soulMinDistance = DEFAULT_SOUL_MIN_DISTANCE; + } + // Load extinguish-on-respawn this.extinguishOnRespawn = config.getBoolean("extinguish-on-respawn", DEFAULT_EXTINGUISH); // Load override confirmation timeout this.overrideConfirmationTimeout = config.getInt("override-confirmation-timeout", DEFAULT_OVERRIDE_TIMEOUT); - if (overrideConfirmationTimeout <= 0) { + if (overrideConfirmationTimeout < 0) { plugin.getLogger().warning("Invalid override-confirmation-timeout in config. Using default: " + DEFAULT_OVERRIDE_TIMEOUT); this.overrideConfirmationTimeout = DEFAULT_OVERRIDE_TIMEOUT; } @@ -72,23 +157,96 @@ public void reload() { try { this.soundOnSet = Sound.valueOf(soundName.toUpperCase()); } catch (IllegalArgumentException e) { - plugin.getLogger().log(Level.WARNING, + plugin.getLogger().log(Level.WARNING, "Invalid sound '" + soundName + "' in config. Using default."); this.soundOnSet = DEFAULT_SOUND; } - plugin.getLogger().info("Configuration loaded - Radius: " + radius + - ", Extinguish: " + extinguishOnRespawn + + // Load respawn sound + String respawnSoundName = config.getString("sound-on-respawn", DEFAULT_SOUND_ON_RESPAWN.name()); + try { + this.soundOnRespawn = Sound.valueOf(respawnSoundName.toUpperCase()); + } catch (IllegalArgumentException e) { + plugin.getLogger().log(Level.WARNING, + "Invalid sound-on-respawn '" + respawnSoundName + "' in config. Using default."); + this.soundOnRespawn = DEFAULT_SOUND_ON_RESPAWN; + } + + // Load empty-hand-or-sneak-required + this.emptyHandOrSneakRequired = config.getBoolean("require-empty-hand-or-sneak", + DEFAULT_EMPTY_HAND_OR_SNEAK_REQUIRED); + + // Load delete-command-allowed + this.deleteCommandAllowed = config.getBoolean("allow-delete-command", DEFAULT_DELETE_COMMAND_ALLOWED); + + plugin.getLogger().info("Configuration loaded " + + "- Regular campfires enabled (overworld): " + overworldEnableRegularCampfires + + ", Regular campfires enabled (nether): " + netherEnableRegularCampfires + + ", Regular campfires enabled (end): " + endEnableRegularCampfires + + ", Soul campfires enabled (overworld): " + overworldEnableSoulCampfires + + ", Soul campfires enabled (nether): " + netherEnableSoulCampfires + + ", Soul campfires enabled (end): " + endEnableSoulCampfires + + ", Respawn anchors enabled (overworld): " + overworldRespawnAnchorsEnabled + + ", Respawn anchors enabled (nether): " + netherRespawnAnchorsEnabled + + ", Respawn anchors enabled (end): " + endRespawnAnchorsEnabled + + ", Vanilla respawn anchors (nether): " + vanillaRespawnAnchorsNether + + ", Radius: " + radius + + ", Radius (soul campfires): " + soulRadius + + ", Min. distance: " + minDistance + + ", Min. distance (soul campfires): " + soulMinDistance + + ", Extinguish: " + extinguishOnRespawn + ", Timeout: " + overrideConfirmationTimeout + "s" + ", MaxCheckpoints: " + (maxCheckpointsPerPlayer == 0 ? "unlimited" : maxCheckpointsPerPlayer) + ", RespawnPriority: " + respawnPriority.getConfigValue() + - ", Sound: " + soundOnSet.name()); + ", Sound: " + soundOnSet.name() + + ", SoundOnRespawn: " + soundOnRespawn.name()); + } + + public boolean isDimentionEnabledOverworld() { + return overworldEnableRegularCampfires; + } + public boolean isDimentionEnabledOverworldSoul() { + return overworldEnableSoulCampfires; + } + public boolean isDimentionEnabledNether() { + return netherEnableRegularCampfires; + } + public boolean isDimentionEnabledNetherSoul() { + return netherEnableSoulCampfires; + } + public boolean isDimentionEnabledEnd() { + return endEnableRegularCampfires; + } + public boolean isDimentionEnabledEndSoul() { + return endEnableSoulCampfires; + } + + public boolean isDimentionEnabledOverworldAnchor() { + return respawnAnchorsEnabled && overworldRespawnAnchorsEnabled; + } + public boolean isDimentionEnabledNetherAnchor() { + return respawnAnchorsEnabled && netherRespawnAnchorsEnabled; + } + public boolean isDimentionEnabledEndAnchor() { + return respawnAnchorsEnabled && endRespawnAnchorsEnabled; } public int getRadius() { return radius; } + public int getSoulRadius() { + return soulRadius; + } + + public int getMinDistance() { + return minDistance; + } + + public int getSoulMinDistance() { + return soulMinDistance; + } + public boolean isExtinguishOnRespawn() { return extinguishOnRespawn; } @@ -97,6 +255,10 @@ public boolean isExtinguishOnRespawn() { return soundOnSet; } + public @NotNull Sound getSoundOnRespawn() { + return soundOnRespawn; + } + public int getOverrideConfirmationTimeout() { return overrideConfirmationTimeout; } @@ -116,4 +278,20 @@ public boolean hasCheckpointLimit() { public @NotNull RespawnPriority getRespawnPriority() { return respawnPriority; } + + public boolean isEmptyHandOrSneakRequired() { + return emptyHandOrSneakRequired; + } + + public boolean isDeleteCommandAllowed() { + return deleteCommandAllowed; + } + + public boolean RespawnAnchorsEnabled() { + return respawnAnchorsEnabled; + } + + public boolean vanillaAnchorsEnabledInNether() { + return vanillaRespawnAnchorsNether; + } } \ No newline at end of file diff --git a/src/main/java/com/campfirecheckpoints/model/Checkpoint.java b/src/main/java/com/campfirecheckpoints/model/Checkpoint.java index 148a93b..2a03d2b 100644 --- a/src/main/java/com/campfirecheckpoints/model/Checkpoint.java +++ b/src/main/java/com/campfirecheckpoints/model/Checkpoint.java @@ -3,6 +3,7 @@ import com.google.gson.JsonObject; import org.bukkit.Bukkit; +import org.bukkit.Material; import org.bukkit.Location; import org.bukkit.World; import org.jetbrains.annotations.NotNull; @@ -20,6 +21,8 @@ public final class Checkpoint { private final int z; private final long createdAt; private volatile boolean lit; + private volatile boolean soul; + private volatile boolean anchor; public Checkpoint(@NotNull UUID ownerUUID, @NotNull Location location) { Objects.requireNonNull(ownerUUID, "Owner UUID cannot be null"); @@ -33,10 +36,15 @@ public Checkpoint(@NotNull UUID ownerUUID, @NotNull Location location) { this.z = location.getBlockZ(); this.createdAt = System.currentTimeMillis(); this.lit = true; + + Material blockType = location.getBlock().getType(); + this.soul = (blockType == Material.SOUL_CAMPFIRE); + this.anchor = (blockType == Material.RESPAWN_ANCHOR); } - private Checkpoint(@NotNull UUID ownerUUID, @NotNull String worldName, - int x, int y, int z, long createdAt, boolean lit) { + private Checkpoint(@NotNull UUID ownerUUID, @NotNull String worldName, + int x, int y, int z, long createdAt, boolean lit, + boolean soul, boolean anchor) { this.ownerUUID = ownerUUID; this.worldName = worldName; this.x = x; @@ -44,6 +52,8 @@ private Checkpoint(@NotNull UUID ownerUUID, @NotNull String worldName, this.z = z; this.createdAt = createdAt; this.lit = lit; + this.soul = soul; + this.anchor = anchor; } public @NotNull UUID getOwnerUUID() { @@ -78,6 +88,21 @@ public void setLit(boolean lit) { this.lit = lit; } + public boolean isSoul() { + return soul; + } + + public void setSoul(boolean soul) { + this.soul = soul; + } + + public boolean isAnchor() { + return anchor; + } + + public void setAnchor(boolean anchor) { + this.anchor = anchor; + } public @Nullable Location getSpawnLocation() { World world = Bukkit.getWorld(worldName); @@ -100,6 +125,7 @@ public double distanceSquared(@Nullable Location location) { if (location == null || location.getWorld() == null) { return Double.MAX_VALUE; } + if (!location.getWorld().getName().equals(worldName)) { return Double.MAX_VALUE; } @@ -111,10 +137,28 @@ public double distanceSquared(@Nullable Location location) { } - public boolean isWithinRadius(@Nullable Location location, double radius) { + public boolean isWithinRespawnRadius(@Nullable Location location, + double radiusSquaredRegular, + double radiusSquaredSoul) { + + if (anchor) { + return true; // Respawn anchors are not affected by radius checks + } + + double radiusSquared = soul ? radiusSquaredSoul : radiusSquaredRegular; + return (distanceSquared(location) <= radiusSquared); + } + + + public boolean isWithinRadius(@Nullable Location location, double radius, boolean includeAnchors) { + if (anchor) { + return includeAnchors; // Respawn anchors are not affected by radius checks + } + if (location == null) { return false; } + return distanceSquared(location) <= radius * radius; } @@ -130,6 +174,8 @@ public boolean isWithinRadius(@Nullable Location location, double radius) { json.addProperty("z", z); json.addProperty("createdAt", createdAt); json.addProperty("lit", lit); + json.addProperty("soul", soul); + json.addProperty("anchor", anchor); return json; } @@ -147,8 +193,10 @@ public boolean isWithinRadius(@Nullable Location location, double radius) { long createdAt = json.has("createdAt") ? json.get("createdAt").getAsLong() : System.currentTimeMillis(); boolean lit = !json.has("lit") || json.get("lit").getAsBoolean(); + boolean soul = json.has("soul") && json.get("soul").getAsBoolean(); + boolean anchor = json.has("anchor") && json.get("anchor").getAsBoolean(); - return new Checkpoint(ownerUUID, world, x, y, z, createdAt, lit); + return new Checkpoint(ownerUUID, world, x, y, z, createdAt, lit, soul, anchor); } catch (Exception e) { return null; } @@ -178,7 +226,7 @@ public int hashCode() { @Override public String toString() { - return String.format("Checkpoint{owner=%s, world=%s, pos=[%d, %d, %d], lit=%s}", - ownerUUID, worldName, x, y, z, lit); + return String.format("Checkpoint{owner=%s, world=%s, pos=[%d, %d, %d], lit=%s, soul=%s, anchor=%s}", + ownerUUID, worldName, x, y, z, lit, soul, anchor); } } \ No newline at end of file diff --git a/src/main/java/com/campfirecheckpoints/model/RespawnPriority.java b/src/main/java/com/campfirecheckpoints/model/RespawnPriority.java index d673897..9acfc6a 100644 --- a/src/main/java/com/campfirecheckpoints/model/RespawnPriority.java +++ b/src/main/java/com/campfirecheckpoints/model/RespawnPriority.java @@ -6,17 +6,17 @@ public enum RespawnPriority { - + /** * Campfire checkpoint always takes priority over bed */ CHECKPOINT("checkpoint"), - + /** * Bed spawn always takes priority over checkpoint */ BED("bed"), - + /** * Whichever is closer to the death location takes priority */ @@ -37,14 +37,14 @@ public enum RespawnPriority { if (value == null) { return CHECKPOINT; } - + String lowercaseValue = value.toLowerCase().trim(); for (RespawnPriority priority : values()) { if (priority.configValue.equals(lowercaseValue)) { return priority; } } - + return CHECKPOINT; } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 1621755..67aa49b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -3,22 +3,49 @@ # Campfire Checkpoints Configuration # ================================ -# The maximum radius (in blocks) for checkpoint operations: -# - When setting a checkpoint, checks for existing ones within this radius -# - When respawning, finds the closest checkpoint within this radius of death +# Configure the campfire types used in the dimentions +enable-regular-overworld: true +enable-regular-nether: false +enable-regular-end: false + +enable-soul-overworld: true +enable-soul-nether: true +enable-soul-end: false + +# The maximum radius (in blocks) for checkpoint respawn (with regular campfires): +# When respawning, finds the closest checkpoint within this radius of death radius: 500 +# The minimal distance (in blocks) between checkpoints (with regular campfires): +# When setting a checkpoint, checks for existing ones within this radius +min-distance: 250 + +# The maximum radius (in blocks) for checkpoint respawn (with soul campfires) +# When respawning, finds the closest checkpoint within this radius of death +soul-campfire-radius: 1000 + +# The minimal distance (in blocks) between checkpoints (with soul campfires): +# When setting a checkpoint, checks for existing ones within this radius +soul-campfire-min-distance: 500 + # If true, the campfire will be extinguished after a player respawns at it. # The player must re-light it with Flint & Steel to use it again. extinguish-on-respawn: true # The sound played when a checkpoint is successfully set. +# Default: BLOCK_RESPAWN_ANCHOR_SET_SPAWN # Valid sounds: https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Sound.html sound-on-set: BLOCK_RESPAWN_ANCHOR_SET_SPAWN +# The sound played when a player respawns using a checkpoint (campfire or respawn anchor). +# Default: BLOCK_RESPAWN_ANCHOR_DEPLETE +sound-on-respawn: BLOCK_RESPAWN_ANCHOR_DEPLETE + # Time in seconds that players have to confirm overriding an existing checkpoint # When a player tries to set a checkpoint within radius of an existing one, # they must right-click again within this time to confirm +# +# Set to 0 to forbid overriding checkpoints override-confirmation-timeout: 5 # Maximum number of checkpoints a player can have (0 = unlimited) @@ -28,9 +55,37 @@ max-checkpoints-per-player: 0 # Priority when both bed spawn and campfire checkpoint are available within radius # Options: # "checkpoint" - Campfire checkpoint always takes priority over bed (default) -# "bed" - Bed spawn always takes priority over checkpoint +# "bed" - Bed spawn or respawn anchor always take priority over checkpoint # "closest" - Whichever is closer to the death location takes priority # # Note: This only applies when BOTH a valid bed spawn AND a lit checkpoint # are within the configured radius of the death location. -respawn-priority: checkpoint \ No newline at end of file +respawn-priority: checkpoint + +# If true, players must be crouching or have an empty hand to set a checkpoint. +# This prevents accidental checkpoint creation when cooking food or +# extinguishing a campfire with a splash water bottle. +require-empty-hand-or-sneak: true + +# If false, the /cc delete command will be disabled and hidden from the list. +allow-delete-command: true + +# Override respawn anchors, making them function as checkpoints to allow +# setting global spawnpoints without losing the spawnpoint set by a bed. +# Respawn anchors don't have radius and minimal distance checks. +# You can only have one respawn anchor checkpoint per player. +# If another respawn anchor checkpoint is set, the old one will be deleted. +enable-respawn-anchors: false + +# Per-dimension overrides for respawn anchors. Only work if enable-respawn-anchors is true. +enable-respawn-anchors-overworld: false +enable-respawn-anchors-nether: true +enable-respawn-anchors-end: false + +# By default, this plugin removes the vanilla respawn anchor mechanics +# Set this to true if you want respawn anchors to function in the Nether +# with vanilla-style (overriding the bed spawn point) +# +# NOTE: This doesn't work if enable-respawn-anchors-nether is true +# NOTE: This doesn't matter if enable-respawn-anchors is false +vanilla-respawn-anchors-in-nether: false diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 3a2b022..9fc5afa 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,10 +1,10 @@ # src/main/resources/plugin.yml name: CampfireCheckpoints -version: '1.1.0' +version: '1.2.0' main: com.campfirecheckpoints.CampfireCheckpoints api-version: '1.21' description: A respawn system using campfires as checkpoints -author: fbi +author: fbi + tweaks by parar020100 commands: cc: