Claim protection from a local index: revision-polled panel endpoint, no per-block network checks

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
Claude committed 2026-09-30 17:51:05 +00:00
1 parent 78da10364f
commit 5defda1128
10 files changed
+442 -127

No files matched your search

@@ -36,6 +36,8 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
});
});
Bukkit.getScheduler().runTaskTimer(this, integration::tick, 1, 1);
// Claim protection runs from a local copy of the panel's claims, refreshed in the background.
integration.startClaimSync();
// Central Live Map: mirror chunks and player positions to the panel.
if (getConfig().getBoolean("livemap.enabled", true)) {
@@ -171,22 +173,12 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
public void join(PlayerJoinEvent event) {
if (integration != null) {
integration.activity.join(event.getPlayer().getUniqueId(), event.getPlayer().getName());
Chunk chunk = event.getPlayer().getLocation().getChunk();
integration.prefetchClaims(dimension(chunk.getWorld()), chunk.getX(), chunk.getZ(), event.getPlayer().getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void move(PlayerMoveEvent event) {
if (integration == null || event.getTo() == null) return;
org.bukkit.Location from = event.getFrom(), to = event.getTo();
if (from.getWorld() == to.getWorld() && (from.getBlockX() >> 4) == (to.getBlockX() >> 4)
&& (from.getBlockZ() >> 4) == (to.getBlockZ() >> 4)) return;
integration.prefetchClaims(dimension(to.getWorld()), to.getBlockX() >> 4, to.getBlockZ() >> 4, event.getPlayer().getUniqueId());
}
@EventHandler(priority = EventPriority.MONITOR)
public void leave(PlayerQuitEvent event) {
deniedAt.remove(event.getPlayer().getUniqueId());
if (integration != null) integration.activity.leave(event.getPlayer().getUniqueId(), event.getPlayer().getName());
}
@@ -200,33 +192,45 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
return integration != null && integration.settings().guildsEnabled() && integration.settings().landClaimingEnabled();
}
private final java.util.Map<UUID, Long> deniedAt = new java.util.concurrent.ConcurrentHashMap<>();
/**
* Would {@code player} be stopped from changing {@code block}? Answers from the
* local claim index (no network, and block.getX() >> 4 rather than getChunk()
* so a check never loads a chunk). Staff with scopenet.claims.bypass are exempt.
*/
private ChunkCheckResult denial(Player player, Block block) {
if (!claimsEnabled() || player.hasPermission("scopenet.claims.bypass")) return null;
ChunkCheckResult check = integration.checkChunk(dimension(block.getWorld()), block.getX() >> 4, block.getZ() >> 4, player.getUniqueId());
return check.claimed() && !check.allowed() ? check : null;
}
/** One "claimed" notice per second per player, however many events fire. */
private void tell(Player player, ChunkCheckResult check) {
long now = System.currentTimeMillis();
Long last = deniedAt.get(player.getUniqueId());
if (last != null && now - last < 1000) return;
deniedAt.put(player.getUniqueId(), now);
player.sendMessage(ChatColor.RED + "This chunk is claimed by [" + check.guildTag() + "] " + check.guildName() + "!");
}
private boolean protectedFrom(Player player, Block block) {
if (!claimsEnabled()) return false;
Chunk chunk = block.getChunk();
ChunkCheckResult check = integration.checkChunk(dimension(block.getWorld()), chunk.getX(), chunk.getZ(), player.getUniqueId());
if (check.allowed()) return false;
player.sendMessage(ChatColor.RED + "This chunk is protected by a guild or the claim check is unavailable.");
ChunkCheckResult check = denial(player, block);
if (check == null) return false;
tell(player, check);
return true;
}
/** Environmental changes (fire, fluids, pistons, explosions) never touch claimed land. */
private boolean claimed(Block block) {
if (!claimsEnabled()) return false;
Chunk chunk = block.getChunk();
// A non-member identity protects environmental changes, including
// while the panel is unavailable and the lookup is pending.
return integration.checkChunk(dimension(block.getWorld()), chunk.getX(), chunk.getZ(), new UUID(0, 0)).claimed();
return claimsEnabled() && integration.isClaimed(dimension(block.getWorld()), block.getX() >> 4, block.getZ() >> 4);
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void broken(BlockBreakEvent event) {
if (integration != null && integration.settings().guildsEnabled() && integration.settings().landClaimingEnabled()) {
Chunk chunk = event.getBlock().getChunk();
ChunkCheckResult check = integration.checkChunk(dimension(event.getBlock().getWorld()), chunk.getX(), chunk.getZ(), event.getPlayer().getUniqueId());
if (check.claimed() && !check.allowed()) {
event.setCancelled(true);
event.getPlayer().sendMessage(ChatColor.RED + "This chunk is claimed by [" + check.guildTag() + "] " + check.guildName() + "!");
return;
}
if (protectedFrom(event.getPlayer(), event.getBlock())) {
event.setCancelled(true);
return;
}
add(event.getPlayer(), "blocks_broken");
if (integration != null) integration.activity.action(event.getPlayer().getUniqueId(), event.getPlayer().getName(),
@@ -235,14 +239,9 @@ public final class ScopenetPlugin extends JavaPlugin implements Listener {
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void placed(BlockPlaceEvent event) {
if (integration != null && integration.settings().guildsEnabled() && integration.settings().landClaimingEnabled()) {
Chunk chunk = event.getBlock().getChunk();
ChunkCheckResult check = integration.checkChunk(dimension(event.getBlock().getWorld()), chunk.getX(), chunk.getZ(), event.getPlayer().getUniqueId());
if (check.claimed() && !check.allowed()) {
event.setCancelled(true);
event.getPlayer().sendMessage(ChatColor.RED + "This chunk is claimed by [" + check.guildTag() + "] " + check.guildName() + "!");
return;
}
if (protectedFrom(event.getPlayer(), event.getBlock())) {
event.setCancelled(true);
return;
}
add(event.getPlayer(), "blocks_placed");
if (integration != null) integration.activity.action(event.getPlayer().getUniqueId(), event.getPlayer().getName(),