diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/MovingConfig.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/MovingConfig.java index 7abb22ff..5d37a4f2 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/MovingConfig.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/MovingConfig.java @@ -173,7 +173,7 @@ public class MovingConfig extends ACheckConfig { public final boolean assumeSprint; public final int speedGrace; public final boolean enforceLocation; -// public final boolean blockChangeTrackerPush; + public final boolean blockChangeTrackerPush; // Vehicles public final boolean vehicleEnforceLocation; @@ -288,7 +288,7 @@ public class MovingConfig extends ACheckConfig { } else { enforceLocation = ref.decide(); } -// blockChangeTrackerPush = config.getBoolean(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_ACTIVE) && config.getBoolean(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_PISTONS); + blockChangeTrackerPush = config.getBoolean(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_ACTIVE) && config.getBoolean(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_PISTONS); ref = config.getAlmostBoolean(ConfPaths.MOVING_VEHICLES_ENFORCELOCATION, AlmostBoolean.MAYBE); vehicleEnforceLocation = ref.decideOptimistically(); // Currently rather enabled. diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/MovingData.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/MovingData.java index 5329c011..932195ee 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/MovingData.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/MovingData.java @@ -138,6 +138,8 @@ public class MovingData extends ACheckData { public SimpleEntry verVelUsed = null; /** Compatibility entry for bouncing of slime blocks and the like. */ public SimpleEntry verticalBounce = null; + /** Last used block change id (BlockChangeTracker). */ + public long blockChangeId = 0; // TODO: Need split into several? /** Tick at which walk/fly speeds got changed last time. */ public int speedTick = 0; @@ -863,8 +865,8 @@ public class MovingData extends ACheckData { } /** - * Check the verVelUsed field and return that if appropriate. Otherwise - * call useVerticalVelocity(amount). + * Use the verVelUsed field, if it matches. Otherwise call + * useVerticalVelocity(amount). * * @param amount * @return diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/SurvivalFly.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/SurvivalFly.java index d5e302c3..e3c4ea35 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/SurvivalFly.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/checks/moving/SurvivalFly.java @@ -18,6 +18,8 @@ import fr.neatmonster.nocheatplus.checks.ViolationData; import fr.neatmonster.nocheatplus.checks.moving.model.LiftOffEnvelope; import fr.neatmonster.nocheatplus.checks.moving.model.MoveData; import fr.neatmonster.nocheatplus.compat.BridgeEnchant; +import fr.neatmonster.nocheatplus.compat.blocks.BlockChangeTracker; +import fr.neatmonster.nocheatplus.compat.blocks.BlockChangeTracker.Direction; import fr.neatmonster.nocheatplus.logging.Streams; import fr.neatmonster.nocheatplus.permissions.Permissions; import fr.neatmonster.nocheatplus.utilities.BlockCache; @@ -97,11 +99,14 @@ public class SurvivalFly extends Check { /** For temporary use: LocUtil.clone before passing deeply, call setWorld(null) after use. */ private final Location useLoc = new Location(null, 0, 0, 0); + private final BlockChangeTracker blockChangeTracker; + /** * Instantiates a new survival fly check. */ public SurvivalFly() { super(CheckType.MOVING_SURVIVALFLY); + blockChangeTracker = NCPAPIProvider.getNoCheatPlusAPI().getBlockChangeTracker(); } /** @@ -370,7 +375,20 @@ public class SurvivalFly extends Check { vDistanceAboveLimit = res[1]; } - // TODO: on ground -> on ground improvements + // Post-check recovery. + if (vDistanceAboveLimit > 0.0 && Math.abs(yDistance) <= 1.0 && cc.blockChangeTrackerPush) { + // TODO: Better place for checking for push [redesign for intermediate result objects?]. + // Vertical push/pull. + double[] pushResult = getPushResultVertical(yDistance, from, to, data); + if (pushResult != null) { + vAllowedDistance = pushResult[0]; + vDistanceAboveLimit = pushResult[1]; + } + } + // Push/pull sideways. + // TODO: Slightly itchy: regard x and z separately (Better in another spot). + + // TODO: on ground -> on ground improvements. // Debug output. final int tagsLength; @@ -536,6 +554,44 @@ public class SurvivalFly extends Check { return null; } + /** + * Check for push/pull by pistons, alter data appropriately (blockChangeId). + * + * @param yDistance + * @param from + * @param to + * @param data + * @return + */ + private double[] getPushResultVertical(final double yDistance, final PlayerLocation from, final PlayerLocation to, final MovingData data) { + final long oldChangeId = data.blockChangeId; + // TODO: Allow push up to 1.0 (or 0.65 something) even beyond block borders, IF COVERED [adapt PlayerLocation]. + // Push (/pull) up. + if (yDistance > 0.0) { + // TODO: Other conditions? [some will be in passable later]. + double maxDistYPos = 1.0 - (from.getY() - from.getBlockY()); // TODO: Margin ? + final long changeIdYPos = from.getBlockChangeIdPush(blockChangeTracker, oldChangeId, Direction.Y_POS, yDistance); + if (changeIdYPos != -1) { + data.blockChangeId = Math.max(data.blockChangeId, changeIdYPos); + tags.add("push_y_pos"); + return new double[]{maxDistYPos, 0.0}; + } + } + // Push (/pull) down. + else if (yDistance < 0.0) { + // TODO: Other conditions? [some will be in passable later]. + double maxDistYPos = from.getY() - from.getBlockY(); // TODO: Margin ? + final long changeIdYPos = from.getBlockChangeIdPush(blockChangeTracker, oldChangeId, Direction.Y_NEG, -yDistance); + if (changeIdYPos != -1) { + data.blockChangeId = Math.max(data.blockChangeId, changeIdYPos); + tags.add("push_y_neg"); + return new double[]{maxDistYPos, 0.0}; + } + } + // Nothing found. + return null; + } + /** * Set data.nextFriction according to media. * @param from diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/compat/blocks/BlockChangeTracker.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/compat/blocks/BlockChangeTracker.java new file mode 100644 index 00000000..f0b8dd31 --- /dev/null +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/compat/blocks/BlockChangeTracker.java @@ -0,0 +1,475 @@ +package fr.neatmonster.nocheatplus.compat.blocks; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; + +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPistonExtendEvent; +import org.bukkit.event.block.BlockPistonRetractEvent; +import org.bukkit.material.Directional; +import org.bukkit.material.MaterialData; + +import fr.neatmonster.nocheatplus.NCPAPIProvider; +import fr.neatmonster.nocheatplus.logging.Streams; +import fr.neatmonster.nocheatplus.utilities.BlockProperties; +import fr.neatmonster.nocheatplus.utilities.ReflectionUtil; +import fr.neatmonster.nocheatplus.utilities.TickTask; +import fr.neatmonster.nocheatplus.utilities.ds.map.LinkedCoordHashMap; +import fr.neatmonster.nocheatplus.utilities.ds.map.LinkedCoordHashMap.MoveOrder; + + +public class BlockChangeTracker { + /** These blocks certainly can't be pushed nor pulled. */ + public static long F_MOVABLE_IGNORE = BlockProperties.F_LIQUID; + /** These blocks might be pushed or pulled. */ + public static long F_MOVABLE = BlockProperties.F_GROUND | BlockProperties.F_SOLID; + + public static enum Direction { + NONE, + X_POS, + X_NEG, + Y_POS, + Y_NEG, + Z_POS, + Z_NEG; + + public static Direction getDirection(final BlockFace blockFace) { + final int x = blockFace.getModX(); + if (x == 1) { + return X_POS; + } + else if (x == -1) { + return X_NEG; + } + final int y = blockFace.getModY(); + if (y == 1) { + return Y_POS; + } + else if (y == -1) { + return Y_NEG; + } + final int z = blockFace.getModZ(); + if (z == 1) { + return Z_POS; + } + else if (z == -1) { + return Z_NEG; + } + return NONE; + } + + } + + public static class WorldNode { + public final LinkedCoordHashMap> blocks = new LinkedCoordHashMap>(); + // TODO: Filter mechanism for player activity by chunks or chunk sections (some margin, only add if activity, let expire by tick). + /** Tick of last change. */ + public int lastChangeTick = 0; + + /** Total number of BlockChangeEntry instances. */ + public int size = 0; + + public final UUID worldId; + + public WorldNode(UUID worldId) { + this.worldId = worldId; + } + + public void clear() { + blocks.clear(); + size = 0; + } + } + + /** + * Record a state of a block. + * + * @author asofold + * + */ + public static class BlockChangeEntry { + public final long id; + public final int tick, x, y, z; + public final Direction direction; + + /** + * A push entry. + * @param id + * @param tick + * @param x + * @param y + * @param z + * @param direction + */ + public BlockChangeEntry(long id, int tick, int x, int y, int z, Direction direction) { + this.id = id; + this.tick = tick; + this.x = x; + this.y = y; + this.z = z; + this.direction = direction; + } + + // Might follow: Id, data, block shape. Convenience methods for testing. + } + + public static class BlockChangeListener implements Listener { + private final BlockChangeTracker tracker; + private final boolean retractHasBlocks; + private boolean enabled = true; + public BlockChangeListener(final BlockChangeTracker tracker) { + this.tracker = tracker; + if (ReflectionUtil.getMethodNoArgs(BlockPistonRetractEvent.class, "getBlocks") == null) { + retractHasBlocks = false; + NCPAPIProvider.getNoCheatPlusAPI().getLogManager().info(Streams.STATUS, "Assume legacy piston behavior."); + } else { + retractHasBlocks = true; + } + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + private BlockFace getDirection(final Block pistonBlock) { + final MaterialData data = pistonBlock.getState().getData(); + if (data instanceof Directional) { + Directional directional = (Directional) data; + return directional.getFacing(); + } + return null; + } + + /** + * Get the direction, in which blocks are or would be moved (towards the piston). + * + * @param pistonBlock + * @param eventDirection + * @return + */ + private BlockFace getRetractDirection(final Block pistonBlock, final BlockFace eventDirection) { + // Tested for pistons directed upwards. + // TODO: Test for pistons directed downwards, N, W, S, E. + // TODO: distinguish sticky vs. not sticky. + final BlockFace pistonDirection = getDirection(pistonBlock); + if (pistonDirection == null) { + return eventDirection; + } + else { + return eventDirection.getOppositeFace(); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) + public void onPistonExtend(final BlockPistonExtendEvent event) { + if (!enabled) { + return; + } + final BlockFace direction = event.getDirection(); + //DebugUtil.debug("EXTEND event=" + event.getDirection() + " piston=" + getDirection(event.getBlock())); + tracker.addPistonBlocks(event.getBlock().getRelative(direction), direction, event.getBlocks()); + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) + public void onPistonRetract(final BlockPistonRetractEvent event) { + if (!enabled) { + return; + } + final List blocks; + if (retractHasBlocks) { + // TODO: Legacy: Set flag in constructor (getRetractLocation). + blocks = event.getBlocks(); + } + else { + // TODO: Use getRetractLocation. + @SuppressWarnings("deprecation") + final Location retLoc = event.getRetractLocation(); + if (retLoc == null) { + blocks = null; + } else { + final Block retBlock = retLoc.getBlock(); + final long flags = BlockProperties.getBlockFlags(retBlock.getType()); + if ((flags & F_MOVABLE_IGNORE) == 0L && (flags & F_MOVABLE) != 0L) { + blocks = new ArrayList(1); + blocks.add(retBlock); + } else { + blocks = null; + } + } + } + // TODO: Special cases (don't push upwards on retract, with the resulting location being a solid block). + final Block pistonBlock = event.getBlock(); + final BlockFace direction = getRetractDirection(pistonBlock, event.getDirection()); + //DebugUtil.debug("RETRACT event=" + event.getDirection() + " piston=" + getDirection(event.getBlock()) + " decide=" + getRetractDirection(event.getBlock(), event.getDirection())); + tracker.addPistonBlocks(pistonBlock.getRelative(direction.getOppositeFace()), direction, blocks); + } + } + + /** Change id/count, increasing with each entry added internally. */ + private long maxChangeId = 0; + + private int expirationAgeTicks = 80; // TODO: Configurable. + private int worldNodeSkipSize = 500; // TODO: Configurable. + + /** + * Store the WorldNode instances by UUID, containing the block change + * entries (and filters). Latest entries must be sorted to the end. + */ + private final Map worldMap = new LinkedHashMap(); + + /** Use to avoid duplicate entries with pistons. Always empty after processing. */ + private final Set processBlocks = new HashSet(); + + // TODO: Consider tracking regions of player activity (chunk sections, with a margin around the player) and filter. + + /** + * Process the data, as given by a BlockPistonExtendEvent or + * BlockPistonRetractEvent. + * + * @param pistonBlock + * This block is added directly, unless null. + * @param blockFace + * @param movedBlocks + * Unless null, each block and the relative block in the given + * direction (!) are added. + */ + public void addPistonBlocks(final Block pistonBlock, final BlockFace blockFace, final List movedBlocks) { + final int tick = TickTask.getTick(); + final UUID worldId = pistonBlock.getWorld().getUID(); + WorldNode worldNode = worldMap.get(worldId); + if (worldNode == null) { + // TODO: With activity tracking this should be a return. + worldNode = new WorldNode(worldId); + worldMap.put(worldId, worldNode); + } + // TODO: (else) With activity tracking still check if lastActivityTick is too old (lazily expire entire worlds). + final long changeId = ++maxChangeId; + // Avoid duplicates by adding to a set. + if (pistonBlock != null) { + processBlocks.add(pistonBlock); + } + if (movedBlocks != null) { + for (final Block movedBlock : movedBlocks) { + processBlocks.add(movedBlock); + processBlocks.add(movedBlock.getRelative(blockFace)); + } + } + // Process queued blocks. + for (final Block block : processBlocks) { + addPistonBlock(changeId, tick, worldNode, block, blockFace); + } + processBlocks.clear(); + } + + /** + * Add a block moved by a piston (or the piston itself). + * + * @param changeId + * @param tick + * @param worldId + * @param block + * @param blockFace + */ + private void addPistonBlock(final long changeId, final int tick, final WorldNode worldNode, final Block targetBlock, final BlockFace blockFace) { + // TODO: A filter for regions of player activity. + // TODO: Test which ones can actually push a player (and what type of push). + // Add this block. + addBlockChange(changeId, tick, worldNode, targetBlock.getX(), targetBlock.getY(), targetBlock.getZ(), Direction.getDirection(blockFace)); + } + + /** + * Add a block change. Simplistic version (no actual block states/shapes are + * stored). + * + * @param x + * @param y + * @param z + * @param direction + * If not NONE, pushing into that direction is assumed. + */ + private void addBlockChange(final long changeId, final int tick, final WorldNode worldNode, final int x, final int y, final int z, final Direction direction) { + worldNode.lastChangeTick = tick; + final BlockChangeEntry entry = new BlockChangeEntry(changeId, tick, x, y, z, direction); + LinkedList entries = worldNode.blocks.get(x, y, z, MoveOrder.END); + if (entries == null) { + entries = new LinkedList(); + worldNode.blocks.put(x, y, z, entries, MoveOrder.END); // Add to end. + } else { + // Lazy expiration check for this block. + if (!entries.isEmpty() && entries.getFirst().tick < tick - expirationAgeTicks) { + worldNode.size -= expireEntries(tick - expirationAgeTicks, entries); + } + } + // With tracking actual block states/shapes, an entry for the previous state must be present (update last time or replace last or create first). + entries.add(entry); // Add latest to the end always. + worldNode.size ++; + //DebugUtil.debug("Add block change: " + x + "," + y + "," + z + " " + direction + " " + changeId); // TODO: REMOVE + } + + private int expireEntries(final int olderThanTick, final LinkedList entries) { + int removed = 0; + final Iterator it = entries.iterator(); + while (it.hasNext()) { + if (it.next().tick < olderThanTick) { + it.remove(); + removed ++; + } else { + return removed; + } + } + return removed; + } + + /** + * Check expiration on tick. + * + * @param currentTick + */ + public void checkExpiration(final int currentTick) { + final int olderThanTick = currentTick - expirationAgeTicks; + final Iterator> it = worldMap.entrySet().iterator(); + while (it.hasNext()) { + final WorldNode worldNode = it.next().getValue(); + if (worldNode.lastChangeTick < olderThanTick) { + worldNode.clear(); + it.remove(); + } else { + // Check for expiration of individual blocks. + if (worldNode.size < worldNodeSkipSize) { + continue; + } + final Iterator>> blockIt = worldNode.blocks.iterator(); + while (blockIt.hasNext()) { + final LinkedList entries = blockIt.next().getValue(); + if (!entries.isEmpty()) { + if (entries.getFirst().tick < olderThanTick) { + worldNode.size -= expireEntries(olderThanTick, entries); + } + } + if (entries.isEmpty()) { + blockIt.remove(); + } + } + if (worldNode.size == 0) { + // TODO: With activity tracking, nodes get removed based on last activity only. + it.remove(); + } + } + } + } + + /** + * Query if there is a push available into the indicated direction. + * + * @param gtChangeId + * A matching entry must have a greater id than the given one + * (all ids are greater than 0). + * @param tick + * The current tick. Used for lazy expiration. + * @param worldId + * @param x + * Block Coordinates where a push might have happened. + * @param y + * @param z + * @param direction + * Desired direction of the push. + * @return The id of a matching entry, or -1 if there is no matching entry. + */ + public long getChangeIdPush(final long gtChangeId, final long tick, final UUID worldId, final int x, final int y, final int z, final Direction direction) { + final WorldNode worldNode = worldMap.get(worldId); + if (worldNode == null) { + return -1; + } + return getChangeIdPush(gtChangeId, tick, worldNode, x, y, z, direction); + } + + /** + * Query if there is a push available into the indicated direction. + * + * @param gtChangeId + * A matching entry must have a greater id than the given one + * (all ids are greater than 0). + * @param tick + * The current tick. Used for lazy expiration. + * @param worldNode + * @param x + * Block Coordinates where a push might have happened. + * @param y + * @param z + * @param direction + * Desired direction of the push. Pass null to ignore direction. + * @return The id of the oldest matching entry, or -1 if there is no + * matching entry. + */ + private long getChangeIdPush(final long gtChangeId, final long tick, final WorldNode worldNode, final int x, final int y, final int z, final Direction direction) { + // TODO: Might add some policy (start at age, oldest first, newest first). + final long olderThanTick = tick - expirationAgeTicks; + // Lazy expiration of entire world nodes. + if (worldNode.lastChangeTick < olderThanTick) { + worldNode.clear(); + worldMap.remove(worldNode.worldId); + //DebugUtil.debug("EXPIRE WORLD"); // TODO: REMOVE + return -1; + } + // Check individual entries. + final LinkedList entries = worldNode.blocks.get(x, y, z); + if (entries == null) { + //DebugUtil.debug("NO ENTRIES: " + x + "," + y + "," + z); + return -1; + } + //DebugUtil.debug("Entries at: " + x + "," + y + "," + z); + final Iterator it = entries.iterator(); + while (it.hasNext()) { + final BlockChangeEntry entry = it.next(); + if (entry.tick < olderThanTick) { + //DebugUtil.debug("Lazy expire: " + x + "," + y + "," + z + " " + entry.id); + it.remove(); + } else { + if (entry.id > gtChangeId && (direction == null || entry.direction == direction)) { + return entry.id; + } + } + } + // Remove entries from map + remove world if empty. + if (entries.isEmpty()) { + worldNode.blocks.remove(x, y, z); + if (worldNode.size == 0) { + worldMap.remove(worldNode.worldId); + } + } + return -1; + } + + public void clear() { + for (final WorldNode worldNode : worldMap.values()) { + worldNode.clear(); + } + worldMap.clear(); + } + + public int size() { + int size = 0; + for (final WorldNode worldNode : worldMap.values()) { + size += worldNode.size; + } + return size; + } + +} diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/components/NoCheatPlusAPI.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/components/NoCheatPlusAPI.java index 97093ede..51bed436 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/components/NoCheatPlusAPI.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/components/NoCheatPlusAPI.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.Map; import java.util.Set; +import fr.neatmonster.nocheatplus.compat.blocks.BlockChangeTracker; import fr.neatmonster.nocheatplus.logging.LogManager; @@ -130,4 +131,10 @@ public interface NoCheatPlusAPI extends ComponentRegistry, ComponentRegi */ public LogManager getLogManager(); + /** + * Get the block change tracker (pistons, other). + * @return + */ + public BlockChangeTracker getBlockChangeTracker(); + } diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java index e741887f..28857c93 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java @@ -545,7 +545,7 @@ public class DefaultConfig extends ConfigFile { )); set(ConfPaths.COMPATIBILITY_BLOCKS + ConfPaths.SUB_ALLOWINSTANTBREAK, new LinkedList()); set(ConfPaths.COMPATIBILITY_BLOCKS + ConfPaths.SUB_OVERRIDEFLAGS + ".snow", "default"); - set(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_ACTIVE, true); + set(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_ACTIVE, false); // TODO: Activate once it really works? set(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_PISTONS, true); // // Update internal factory based on all the new entries to the "actions" section. diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/utilities/PlayerLocation.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/utilities/PlayerLocation.java index 11ca9f15..e3a6565e 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/utilities/PlayerLocation.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/utilities/PlayerLocation.java @@ -1,5 +1,7 @@ package fr.neatmonster.nocheatplus.utilities; +import java.util.UUID; + import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; @@ -7,6 +9,8 @@ import org.bukkit.entity.Player; import org.bukkit.util.Vector; import fr.neatmonster.nocheatplus.compat.MCAccess; +import fr.neatmonster.nocheatplus.compat.blocks.BlockChangeTracker; +import fr.neatmonster.nocheatplus.compat.blocks.BlockChangeTracker.Direction; /** * An utility class used to know a lot of things for a player and a location @@ -942,6 +946,79 @@ public class PlayerLocation { return blockCache.getTypeId(blockX, blockY + 1, blockZ); } + /** + * Check for push using the full bounding box (pistons). + * + * @param blockChangeTracker + * @param oldChangeId + * @param direction + * @param coverDistance + * The (always positive) distance to cover. + * @return The lowest id greater than oldChangeId, or -1 if nothing found.. + */ + public long getBlockChangeIdPush(final BlockChangeTracker blockChangeTracker, final long oldChangeId, final Direction direction, final double coverDistance) { + final int tick = TickTask.getTick(); + final UUID worldId = world.getUID(); + final int iMinX = Location.locToBlock(minX); + final int iMaxX = Location.locToBlock(maxX); + final int iMinY = Location.locToBlock(minY); + final int iMaxY = Location.locToBlock(maxY); + final int iMinZ = Location.locToBlock(minZ); + final int iMaxZ = Location.locToBlock(maxZ); + long minId = Long.MAX_VALUE; + for (int x = iMinX; x <= iMaxX; x++) { + for (int z = iMinZ; z <= iMaxZ; z++) { + for (int y = iMinY; y <= iMaxY; y++) { + final long tempId = blockChangeTracker.getChangeIdPush(oldChangeId, tick, worldId, x, y, z, direction); + if (tempId != -1 && tempId < minId) { + // Check vs. coverDistance, exclude cases where the piston can't push that far. + if (coverDistance > 0.0 && coversDistance(x, y, z, direction, coverDistance)) { + minId = tempId; + } + } + } + } + } + return minId == Long.MAX_VALUE ? -1 : minId; + } + + /** + * Test if a block fully pushed into that direction can push the player by coverDistance. + * + * @param x Block coordinates. + * @param y + * @param z + * @param direction + * @param coverDistance + * @return + */ + private boolean coversDistance(final int x, final int y, final int z, final Direction direction, final double coverDistance) { + switch (direction) { + case Y_POS: { + return y + 1.0 - Math.max(minY, (double) y) >= coverDistance; + } + case Y_NEG: { + return Math.min(maxY, (double) y + 1) - y >= coverDistance; + } + case X_POS: { + return x + 1.0 - Math.max(minX, (double) x) >= coverDistance; + } + case X_NEG: { + return Math.min(maxX, (double) x + 1) - x >= coverDistance; + } + case Z_POS: { + return z + 1.0 - Math.max(minZ, (double) z) >= coverDistance; + } + case Z_NEG: { + return Math.min(maxZ, (double) z + 1) - z >= coverDistance; + } + default: { + // Assume anything does (desired direction is NONE, read as ALL, thus accept all). + return true; + } + } + } + /** * Set cached info according to other.
* Minimal optimizations: take block flags directly, on-ground max/min bounds, only set stairs if not on ground and not reset-condition. diff --git a/NCPPlugin/src/main/java/fr/neatmonster/nocheatplus/NoCheatPlus.java b/NCPPlugin/src/main/java/fr/neatmonster/nocheatplus/NoCheatPlus.java index 456698ad..7038753d 100644 --- a/NCPPlugin/src/main/java/fr/neatmonster/nocheatplus/NoCheatPlus.java +++ b/NCPPlugin/src/main/java/fr/neatmonster/nocheatplus/NoCheatPlus.java @@ -51,6 +51,8 @@ import fr.neatmonster.nocheatplus.compat.DefaultComponentFactory; import fr.neatmonster.nocheatplus.compat.MCAccess; import fr.neatmonster.nocheatplus.compat.MCAccessConfig; import fr.neatmonster.nocheatplus.compat.MCAccessFactory; +import fr.neatmonster.nocheatplus.compat.blocks.BlockChangeTracker; +import fr.neatmonster.nocheatplus.compat.blocks.BlockChangeTracker.BlockChangeListener; import fr.neatmonster.nocheatplus.compat.versions.BukkitVersion; import fr.neatmonster.nocheatplus.compat.versions.GenericVersion; import fr.neatmonster.nocheatplus.compat.versions.ServerVersion; @@ -197,6 +199,11 @@ public class NoCheatPlus extends JavaPlugin implements NoCheatPlusAPI { /** Hook for logging all violations. */ protected final AllViolationsHook allViolationsHook = new AllViolationsHook(); + /** Block change tracking (pistons, other). */ + private final BlockChangeTracker blockChangeTracker = new BlockChangeTracker(); + /** Listener for the BlockChangeTracker (register once, lazy). */ + private BlockChangeListener blockChangeListener = null; + /** Tick listener that is only needed sometimes (component registration). */ protected final OnDemandTickListener onDemandTickListener = new OnDemandTickListener() { @Override @@ -683,6 +690,12 @@ public class NoCheatPlus extends JavaPlugin implements NoCheatPlusAPI { genericInstances.clear(); // Feature tags. featureTags.clear(); + // BlockChangeTracker. + blockChangeTracker.clear(); + if (blockChangeListener != null) { + blockChangeListener.setEnabled(false); + blockChangeListener = null; // Only on disable. + } // Clear command changes list (compatibility issues with NPCs, leads to recalculation of perms). if (changedCommands != null){ @@ -862,6 +875,7 @@ public class NoCheatPlus extends JavaPlugin implements NoCheatPlusAPI { // Register sub-components (allow later added to use registries, if any). processQueuedSubComponentHolders(); } + updateBlockChangeTracker(config); // Register "higher level" components (check listeners). for (final Object obj : new Object[]{ @@ -934,6 +948,14 @@ public class NoCheatPlus extends JavaPlugin implements NoCheatPlusAPI { // TODO: Prepare check data for players [problem: permissions]? Bukkit.getScheduler().scheduleSyncDelayedTask(this, new PostEnableTask(commandHandler, onlinePlayers)); + // Mid-term cleanup (seconds range). + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, new Runnable() { + @Override + public void run() { + midTermCleanup(); + } + }, 83, 83); + // Set StaticLog to more efficient output. StaticLog.setStreamID(Streams.STATUS); // Tell the server administrator that we finished loading NoCheatPlus now. @@ -1010,6 +1032,8 @@ public class NoCheatPlus extends JavaPlugin implements NoCheatPlusAPI { // Cache some things. TODO: Where is this comment from !? // Re-setup allViolationsHook. allViolationsHook.setConfig(new AllViolationsConfig(config)); + // Set block change tracker. + updateBlockChangeTracker(config); } /** @@ -1025,6 +1049,21 @@ public class NoCheatPlus extends JavaPlugin implements NoCheatPlusAPI { clearExemptionsOnLeave = config.getBoolean(ConfPaths.COMPATIBILITY_EXEMPTIONS_REMOVE_LEAVE); } + private void updateBlockChangeTracker(final ConfigFile config) { + if (config.getBoolean(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_ACTIVE) + && config.getBoolean(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_PISTONS)) { + if (blockChangeListener == null) { + blockChangeListener = new BlockChangeListener(blockChangeTracker); + this.addComponent(blockChangeListener); + } + blockChangeListener.setEnabled(true); + } + else if (blockChangeListener != null) { + blockChangeListener.setEnabled(false); + blockChangeTracker.clear(); + } + } + @Override public LogManager getLogManager() { return logManager; @@ -1248,7 +1287,9 @@ public class NoCheatPlus extends JavaPlugin implements NoCheatPlusAPI { sched.cancelTask(consistencyCheckerTaskId); } ConfigFile config = ConfigManager.getConfigFile(); - if (!config.getBoolean(ConfPaths.DATA_CONSISTENCYCHECKS_CHECK, true)) return; + if (!config.getBoolean(ConfPaths.DATA_CONSISTENCYCHECKS_CHECK, true)) { + return; + } // Schedule task in seconds. final long delay = 20L * config.getInt(ConfPaths.DATA_CONSISTENCYCHECKS_INTERVAL, 1, 3600, 10); consistencyCheckerTaskId = sched.scheduleSyncRepeatingTask(this, new Runnable() { @@ -1259,6 +1300,15 @@ public class NoCheatPlus extends JavaPlugin implements NoCheatPlusAPI { }, delay, delay ); } + /** + * Several seconds, repeating. + */ + protected void midTermCleanup() { + if (blockChangeListener.isEnabled()) { + blockChangeTracker.checkExpiration(TickTask.getTick()); + } + } + /** * Run consistency checks for at most the configured duration. If not finished, a task will be scheduled to continue. */ @@ -1367,4 +1417,9 @@ public class NoCheatPlus extends JavaPlugin implements NoCheatPlusAPI { return Collections.unmodifiableMap(allTags); } + @Override + public BlockChangeTracker getBlockChangeTracker() { + return blockChangeTracker; + } + } diff --git a/NCPPlugin/src/test/java/fr/neatmonster/nocheatplus/PluginTests.java b/NCPPlugin/src/test/java/fr/neatmonster/nocheatplus/PluginTests.java index fb9cc70b..3e810800 100644 --- a/NCPPlugin/src/test/java/fr/neatmonster/nocheatplus/PluginTests.java +++ b/NCPPlugin/src/test/java/fr/neatmonster/nocheatplus/PluginTests.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.Set; import fr.neatmonster.nocheatplus.compat.MCAccess; +import fr.neatmonster.nocheatplus.compat.blocks.BlockChangeTracker; import fr.neatmonster.nocheatplus.compat.bukkit.MCAccessBukkit; import fr.neatmonster.nocheatplus.components.ComponentRegistry; import fr.neatmonster.nocheatplus.components.NoCheatPlusAPI; @@ -139,6 +140,11 @@ public class PluginTests { throw new UnsupportedOperationException(); } + @Override + public BlockChangeTracker getBlockChangeTracker() { + throw new UnsupportedOperationException(); + } + } public static void setDummNoCheatPlusAPI(boolean force) {