Next-generation sand/gravel logging (fixes #762)

- Sand/gravel that breaks (e.g. landing on a torch) no longer logs a phantom placement.
- Falls now log at the actual landing coord, even when blocks like torches or grass are in the column.
- Naturally-triggered falls (mobs, world gen, plugins, explosions) are now logged.
- Falls caused by a player breaking the support are now attributed to the breaker at the real landing coord.
- Falls without a player cause are now attributed to a new #gravity tag.
- Rollback and restore of sand/gravel falls are now symmetric, leaving no phantom blocks in either direction.
This commit is contained in:
Intelli committed 2026-04-24 16:08:45 -06:00
1 parent 0d47338038
commit 4ae0affe4b
8 files changed
+181 -130

No files matched your search

@@ -248,20 +248,6 @@ public class Queue {
}, blockLocation.getLocation(), ticks);
}
protected static void queueBlockGravityValidate(final String user, final Location location, final Block block, final Material blockType, int ticks) {
Scheduler.scheduleSyncDelayedTask(CoreProtect.getInstance(), () -> {
try {
Block placementBlock = BlockUtil.gravityScan(location, blockType, user);
if (!block.equals(placementBlock)) {
queueBlockPlace(user, placementBlock.getState(), blockType, null, blockType, -1, 0, null);
}
}
catch (Exception e) {
e.printStackTrace();
}
}, location, ticks);
}
protected static void queueContainerBreak(String user, Location location, Material type, ItemStack[] oldInventory) {
int currentConsumer = Consumer.currentConsumer;
int consumerId = Consumer.newConsumerId(currentConsumer);
@@ -29,6 +29,7 @@ import net.coreprotect.listener.entity.EntityDeathListener;
import net.coreprotect.listener.entity.EntityExplodeListener;
import net.coreprotect.listener.entity.EntityInteractListener;
import net.coreprotect.listener.entity.EntityPickupItemListener;
import net.coreprotect.listener.entity.EntitySpawnListener;
import net.coreprotect.listener.entity.HangingBreakByEntityListener;
import net.coreprotect.listener.entity.HangingBreakListener;
import net.coreprotect.listener.entity.HangingPlaceListener;
@@ -131,6 +132,7 @@ public final class ListenerHandler {
pluginManager.registerEvents(new EntityExplodeListener(), plugin);
pluginManager.registerEvents(new EntityInteractListener(), plugin);
pluginManager.registerEvents(new EntityPickupItemListener(), plugin);
pluginManager.registerEvents(new EntitySpawnListener(), plugin);
pluginManager.registerEvents(new HangingPlaceListener(), plugin);
pluginManager.registerEvents(new HangingBreakListener(), plugin);
pluginManager.registerEvents(new HangingBreakByEntityListener(), plugin);
@@ -1,7 +1,5 @@
package net.coreprotect.listener.block;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.bukkit.GameMode;
@@ -38,7 +36,9 @@ import net.coreprotect.consumer.Queue;
import net.coreprotect.database.Database;
import net.coreprotect.model.BlockGroup;
import net.coreprotect.paper.PaperAdapter;
import net.coreprotect.thread.CacheHandler;
import net.coreprotect.utility.BlockUtils;
import net.coreprotect.utility.WorldUtils;
public final class BlockBreakListener extends Queue implements Listener {
@@ -86,7 +86,6 @@ public final class BlockBreakListener extends Queue implements Listener {
}
protected static void processBlockBreak(Player player, String user, Block block, boolean logBreak, int skipScan) {
List<Block> placementMap = new ArrayList<>();
Material type = block.getType();
World world = block.getWorld();
int x = block.getX();
@@ -124,24 +123,9 @@ public final class BlockBreakListener extends Queue implements Listener {
Location scanLocation = locationMap[scanMin - 1];
Block scanBlock = world.getBlockAt(scanLocation);
Material scanType = scanBlock.getType();
if (scanMin == 5) {
if (scanType.hasGravity() || BukkitAdapter.ADAPTER.isSuspiciousBlock(scanType)) {
if (Config.getConfig(world).BLOCK_MOVEMENT) {
// log the top-most sand/gravel block as being removed
int scanY = y + 2;
boolean topFound = false;
while (!topFound) {
Block topBlock = world.getBlockAt(x, scanY, z);
Material topMaterial = topBlock.getType();
if (!topMaterial.hasGravity() && !BukkitAdapter.ADAPTER.isSuspiciousBlock(topMaterial)) {
scanLocation = new Location(world, x, (scanY - 1), z);
topFound = true;
}
scanY++;
}
placementMap.add(scanBlock);
}
}
if (scanMin == 5 && (scanType.hasGravity() || BukkitAdapter.ADAPTER.isSuspiciousBlock(scanType)) && Config.getConfig(world).BLOCK_MOVEMENT) {
// FallingBlock listeners read lookupCache to attribute the chain reaction; overwrite the cache up the gravity stack so falls record under whoever broke this block instead of #gravity or the original placer.
seedGravityFallAttribution(user, world, x, z, y + 1, scanType);
}
if (!BlockGroup.TRACK_ANY.contains(scanType)) {
if (scanMin != 5 && scanMin != 6 && !scanDown) { // side block
@@ -208,11 +192,6 @@ public final class BlockBreakListener extends Queue implements Listener {
log = true;
}
}
else if (scanMin == 5) {
if (scanType.hasGravity() || BukkitAdapter.ADAPTER.isSuspiciousBlock(scanType)) {
log = true;
}
}
}
}
else {
@@ -323,13 +302,6 @@ public final class BlockBreakListener extends Queue implements Listener {
scanMin++;
}
for (Block placementBlock : placementMap) {
Material placementType = placementBlock.getType();
if (placementType.hasGravity()) {
queueBlockPlace(user, block.getState(), placementType, null, null, -1, 0, placementBlock.getBlockData().getAsString());
}
}
}
@EventHandler(priority = EventPriority.MONITOR)
@@ -337,7 +309,27 @@ public final class BlockBreakListener extends Queue implements Listener {
if (!event.isCancelled()) {
String user = event.getPlayer().getName();
Block block = event.getBlock();
processBlockBreak(event.getPlayer(), user, event.getBlock(), Config.getConfig(block.getWorld()).BLOCK_BREAK, BlockUtil.NONE);
processBlockBreak(event.getPlayer(), user, block, Config.getConfig(block.getWorld()).BLOCK_BREAK, BlockUtil.NONE);
}
}
private static void seedGravityFallAttribution(String user, World world, int x, int z, int startY, Material startType) {
int worldId = WorldUtils.getWorldId(world.getName());
int timestamp = (int) (System.currentTimeMillis() / 1000L);
int worldMax = world.getMaxHeight();
Material currentType = startType;
int scanY = startY;
while (scanY < worldMax) {
String coordKey = x + "." + scanY + "." + z + "." + worldId;
CacheHandler.lookupCache.put(coordKey, new Object[] { timestamp, user, currentType });
scanY++;
if (scanY >= worldMax) {
break;
}
currentType = world.getBlockAt(x, scanY, z).getType();
if (!currentType.hasGravity() && !BukkitAdapter.ADAPTER.isSuspiciousBlock(currentType)) {
break;
}
}
}
@@ -79,29 +79,6 @@ public final class BlockExplodeListener extends Queue implements Listener {
}
}
}
else if (scanType.hasGravity() && Config.getConfig(world).BLOCK_MOVEMENT) {
// log the top-most sand/gravel block as being removed
int scanY = location.getBlockY() + 1;
boolean topFound = false;
while (!topFound) {
Block topBlock = world.getBlockAt(location.getBlockX(), scanY, location.getBlockZ());
Material topMaterial = topBlock.getType();
if (!topMaterial.hasGravity()) {
location = new Location(world, location.getBlockX(), (scanY - 1), location.getBlockZ());
topFound = true;
// log block attached to top as being removed
if (BlockGroup.TRACK_ANY.contains(topMaterial) || BlockGroup.TRACK_TOP.contains(topMaterial) || BlockGroup.TRACK_TOP_BOTTOM.contains(topMaterial)) {
blockMap.put(topBlock.getLocation(), topBlock);
}
}
scanY++;
}
Block gravityBlock = location.getBlock();
blockMap.put(location, gravityBlock);
Queue.queueBlockGravityValidate(user, location, gravityBlock, scanType, 0);
}
}
scanMin++;
}
@@ -27,8 +27,10 @@ import net.coreprotect.config.Config;
import net.coreprotect.consumer.Queue;
import net.coreprotect.model.BlockGroup;
import net.coreprotect.paper.PaperAdapter;
import net.coreprotect.thread.CacheHandler;
import net.coreprotect.utility.BlockUtils;
import net.coreprotect.utility.MaterialUtils;
import net.coreprotect.utility.WorldUtils;
public final class BlockPlaceListener extends Queue implements Listener {
@@ -38,12 +40,9 @@ public final class BlockPlaceListener extends Queue implements Listener {
if (!event.isCancelled() && Config.getConfig(world).BLOCK_PLACE) {
Player player = event.getPlayer();
Block blockPlaced = event.getBlockPlaced();
Block blockLogged = blockPlaced;
String bBlockData = null;
BlockState blockReplaced = event.getBlockReplacedState();
Material blockType = blockPlaced.getType();
Material forceType = null;
int forceData = -1;
boolean abort = false;
if (blockType == Material.LECTERN && blockReplaced.getType() == Material.LECTERN) {
@@ -57,7 +56,7 @@ public final class BlockPlaceListener extends Queue implements Listener {
bBlockData = waterlogged.getAsString();
blockReplaced = null;
}
Queue.queueBlockPlaceDelayed(player.getName(), blockLogged.getLocation(), blockLogged.getType(), bBlockData, blockReplaced, 0);
Queue.queueBlockPlaceDelayed(player.getName(), blockPlaced.getLocation(), blockPlaced.getType(), bBlockData, blockReplaced, 0);
abort = true;
}
else if (BlockGroup.FIRE.contains(blockType)) {
@@ -78,15 +77,7 @@ public final class BlockPlaceListener extends Queue implements Listener {
}
if (!abort) {
if (Config.getConfig(world).BLOCK_MOVEMENT) {
blockLogged = BlockUtil.gravityScan(blockLogged.getLocation(), blockLogged.getType(), player.getName());
if (!blockLogged.equals(blockPlaced)) {
forceType = blockType;
blockReplaced = blockLogged.getState();
}
}
BlockData blockData = blockLogged.getBlockData();
BlockData blockData = blockPlaced.getBlockData();
Waterlogged waterlogged = BlockUtils.checkWaterlogged(blockData, blockReplaced);
if (waterlogged != null) {
bBlockData = waterlogged.getAsString();
@@ -94,7 +85,7 @@ public final class BlockPlaceListener extends Queue implements Listener {
}
// fix for placed bisected blocks randomly returning the top or bottom of the block in this event
BlockState blockState = blockLogged.getState();
BlockState blockState = blockPlaced.getState();
if (blockState.getBlockData() instanceof Bisected && !(blockState.getBlockData() instanceof Stairs || blockState.getBlockData() instanceof TrapDoor)) {
if (((Bisected) blockState.getBlockData()).getHalf().equals(Half.TOP)) {
if (blockPlaced.getY() > BukkitAdapter.ADAPTER.getMinHeight(world)) {
@@ -103,7 +94,15 @@ public final class BlockPlaceListener extends Queue implements Listener {
}
}
Queue.queueBlockPlace(player.getName(), blockState, blockPlaced.getType(), blockReplaced, forceType, forceData, 0, bBlockData);
if (blockType.hasGravity()) {
// FallingBlock listeners read lookupCache before the async consumer can populate it; record the placer synchronously so the fall and landing get attributed to the player instead of #gravity.
int worldId = WorldUtils.getWorldId(world.getName());
int timestamp = (int) (System.currentTimeMillis() / 1000L);
String coordKey = blockPlaced.getX() + "." + blockPlaced.getY() + "." + blockPlaced.getZ() + "." + worldId;
CacheHandler.lookupCache.put(coordKey, new Object[] { timestamp, player.getName(), blockType });
}
Queue.queueBlockPlace(player.getName(), blockState, blockPlaced.getType(), blockReplaced, null, -1, 0, bBlockData);
if (BukkitAdapter.ADAPTER.isSign(blockType)) {
if (Config.getConfig(world).SIGN_TEXT) {
@@ -6,6 +6,7 @@ import org.bukkit.block.Block;
import org.bukkit.entity.EnderDragon;
import org.bukkit.entity.Enderman;
import org.bukkit.entity.Entity;
import org.bukkit.entity.FallingBlock;
import org.bukkit.entity.Fox;
import org.bukkit.entity.Ravager;
import org.bukkit.entity.Silverfish;
@@ -19,59 +20,115 @@ import org.bukkit.event.entity.EntityChangeBlockEvent;
import net.coreprotect.config.Config;
import net.coreprotect.consumer.Queue;
import net.coreprotect.thread.CacheHandler;
import net.coreprotect.utility.WorldUtils;
public final class EntityChangeBlockListener extends Queue implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
protected void onEntityChangeBlock(EntityChangeBlockEvent event) {
World world = event.getBlock().getWorld();
if (!event.isCancelled() && Config.getConfig(world).ENTITY_CHANGE) {
Entity entity = event.getEntity(); // Can be sand/gravel
Block block = event.getBlock();
Material newtype = event.getTo();
Material type = event.getBlock().getType();
String e = "";
if (entity instanceof Enderman) {
e = "#enderman";
if (event.isCancelled() || !Config.getConfig(world).ENTITY_CHANGE) {
return;
}
Entity entity = event.getEntity();
Block block = event.getBlock();
Material newtype = event.getTo();
Material type = event.getBlock().getType();
if (entity instanceof FallingBlock) {
handleFallingBlock(event, (FallingBlock) entity, block, newtype, type, world);
return;
}
String e = "";
if (entity instanceof Enderman) {
e = "#enderman";
}
else if (entity instanceof EnderDragon) {
e = "#enderdragon";
}
else if (entity instanceof Fox) {
e = "#fox";
}
else if (entity instanceof Wither) {
e = "#wither";
}
else if (entity instanceof Turtle) {
e = "#turtle";
}
else if (entity instanceof Ravager) {
e = "#ravager";
}
else if (entity instanceof Zombie) {
e = "#zombie";
}
else if (entity instanceof Silverfish) {
if (newtype.equals(Material.AIR) || newtype.equals(Material.CAVE_AIR)) {
e = "#silverfish";
}
else if (entity instanceof EnderDragon) {
e = "#enderdragon";
}
else if (entity.getType().name().equals("WIND_CHARGE")) {
e = "#windcharge";
}
else if (entity.getType().name().equals("BREEZE_WIND_CHARGE")) {
e = "#breezewindcharge";
}
if (e.length() > 0) {
if (newtype.equals(Material.AIR) || newtype.equals(Material.CAVE_AIR)) {
Queue.queueBlockBreak(e, block.getState(), type, block.getBlockData().getAsString(), 0);
}
else if (entity instanceof Fox) {
e = "#fox";
}
else if (entity instanceof Wither) {
e = "#wither";
}
else if (entity instanceof Turtle) {
e = "#turtle";
}
else if (entity instanceof Ravager) {
e = "#ravager";
}
else if (entity instanceof Zombie) {
e = "#zombie";
}
else if (entity instanceof Silverfish) {
if (newtype.equals(Material.AIR) || newtype.equals(Material.CAVE_AIR)) {
e = "#silverfish";
}
}
else if (entity.getType().name().equals("WIND_CHARGE")) {
e = "#windcharge";
}
else if (entity.getType().name().equals("BREEZE_WIND_CHARGE")) {
e = "#breezewindcharge";
}
if (e.length() > 0) {
if (newtype.equals(Material.AIR) || newtype.equals(Material.CAVE_AIR)) {
Queue.queueBlockBreak(e, block.getState(), type, block.getBlockData().getAsString(), 0);
}
else {
queueBlockPlace(e, block.getState(), type, block.getState(), newtype, -1, 0, event.getBlockData().getAsString());
}
else {
queueBlockPlace(e, block.getState(), type, block.getState(), newtype, -1, 0, event.getBlockData().getAsString());
}
}
}
private void handleFallingBlock(EntityChangeBlockEvent event, FallingBlock fallingBlock, Block block, Material newtype, Material type, World world) {
int worldId = WorldUtils.getWorldId(world.getName());
if (newtype.equals(Material.AIR) || newtype.equals(Material.CAVE_AIR)) {
if (!Config.getConfig(world).BLOCK_BREAK) {
return;
}
String coordKey = block.getX() + "." + block.getY() + "." + block.getZ() + "." + worldId;
String user = lookupCachedUser(coordKey, type);
if (user == null) {
user = "#gravity";
}
Queue.queueBlockBreak(user, block.getState(), type, block.getBlockData().getAsString(), 0);
return;
}
if (!Config.getConfig(world).BLOCK_PLACE) {
return;
}
Object[] originData = CacheHandler.fallingBlockSpawnCache.remove(fallingBlock.getUniqueId().toString());
String user = "#gravity";
if (originData != null) {
String originKey = (String) originData[1];
String lookupUser = lookupCachedUser(originKey, fallingBlock.getBlockData().getMaterial());
if (lookupUser != null) {
user = lookupUser;
}
}
queueBlockPlace(user, block.getState(), type, block.getState(), newtype, -1, 0, event.getBlockData().getAsString());
}
private static String lookupCachedUser(String coordKey, Material expectedType) {
Object[] data = CacheHandler.lookupCache.get(coordKey);
if (data == null) {
return null;
}
if (expectedType != null && data[2] instanceof Material && !data[2].equals(expectedType)) {
return null;
}
return (String) data[1];
}
}
@@ -0,0 +1,33 @@
package net.coreprotect.listener.entity;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntitySpawnEvent;
import net.coreprotect.thread.CacheHandler;
import net.coreprotect.utility.WorldUtils;
public final class EntitySpawnListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
protected void onEntitySpawn(EntitySpawnEvent event) {
if (event.getEntityType() != EntityType.FALLING_BLOCK) {
return;
}
Location location = event.getLocation();
if (location.getWorld() == null) {
return;
}
int worldId = WorldUtils.getWorldId(location.getWorld().getName());
String originKey = location.getBlockX() + "." + location.getBlockY() + "." + location.getBlockZ() + "." + worldId;
int timestamp = (int) (System.currentTimeMillis() / 1000L);
CacheHandler.fallingBlockSpawnCache.put(event.getEntity().getUniqueId().toString(), new Object[] { timestamp, originKey });
}
}
@@ -24,6 +24,7 @@ public class CacheHandler implements Runnable {
public static ConcurrentHashMap<String, Object[]> bonemealDuplicateCache = new ConcurrentHashMap<>(16, 0.75f, 2);
public static ConcurrentHashMap<String, Object[]> entityKillDuplicateCache = new ConcurrentHashMap<>(16, 0.75f, 2);
public static ConcurrentHashMap<String, Object[]> redstoneCache = new ConcurrentHashMap<>(16, 0.75f, 2);
public static ConcurrentHashMap<String, Object[]> fallingBlockSpawnCache = new ConcurrentHashMap<>(16, 0.75f, 2);
public static String locationKey(Location location) {
if (location == null || location.getWorld() == null) {
@@ -63,7 +64,7 @@ public class CacheHandler implements Runnable {
public void run() {
while (ConfigHandler.serverRunning) {
try {
for (int id = 0; id < 13; id++) {
for (int id = 0; id < 14; id++) {
Thread.sleep(1000);
int scanTime = 30;
Map cache = CacheHandler.lookupCache;
@@ -116,6 +117,10 @@ public class CacheHandler implements Runnable {
cache = CacheHandler.entityKillDuplicateCache;
scanTime = 900; // 15 minutes
break;
case 13:
cache = CacheHandler.fallingBlockSpawnCache;
scanTime = 30; // 30 seconds
break;
}
int timestamp = (int) (System.currentTimeMillis() / 1000L) - scanTime;