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: