Sketch block change tracking (incomplete, deactivated by default).

Represents the first "simplistic" approach to block change tracking,
only attempting to make vertical push/pull work.

It seems that we need to add on-ground checking accounting for piston
moves as well, otherwise anything with pistons retracting will lead to
survivalfly violations. Pistons extending and retracting may also
randomly move around players, including dragging them into the piston
block with the bounding box (not center of player).

In order to make on-ground work, we might need to check in another
place, possibly check where resetFrom an resetTo are set. Performance
questions might remain, there might also be a slight redesign necessary,
in order to run some sub-routines more side-effect free, to check
several branches, including after-failure checking.
This commit is contained in:
asofold 2015-12-07 07:44:00 +01:00
parent dfcc30aed9
commit eb3a86857f
9 changed files with 685 additions and 7 deletions

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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<LinkedList<BlockChangeEntry>> blocks = new LinkedCoordHashMap<LinkedList<BlockChangeEntry>>();
// 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<Block> 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<Block>(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<UUID, WorldNode> worldMap = new LinkedHashMap<UUID, BlockChangeTracker.WorldNode>();
/** Use to avoid duplicate entries with pistons. Always empty after processing. */
private final Set<Block> processBlocks = new HashSet<Block>();
// 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<Block> 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<BlockChangeEntry> entries = worldNode.blocks.get(x, y, z, MoveOrder.END);
if (entries == null) {
entries = new LinkedList<BlockChangeTracker.BlockChangeEntry>();
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<BlockChangeEntry> entries) {
int removed = 0;
final Iterator<BlockChangeEntry> 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<Entry<UUID, WorldNode>> 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<fr.neatmonster.nocheatplus.utilities.ds.map.CoordMap.Entry<LinkedList<BlockChangeEntry>>> blockIt = worldNode.blocks.iterator();
while (blockIt.hasNext()) {
final LinkedList<BlockChangeEntry> 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<BlockChangeEntry> 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<BlockChangeEntry> 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;
}
}

View File

@ -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<Object>, ComponentRegi
*/
public LogManager getLogManager();
/**
* Get the block change tracker (pistons, other).
* @return
*/
public BlockChangeTracker getBlockChangeTracker();
}

View File

@ -545,7 +545,7 @@ public class DefaultConfig extends ConfigFile {
));
set(ConfPaths.COMPATIBILITY_BLOCKS + ConfPaths.SUB_ALLOWINSTANTBREAK, new LinkedList<String>());
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.

View File

@ -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.<br>
* Minimal optimizations: take block flags directly, on-ground max/min bounds, only set stairs if not on ground and not reset-condition.

View File

@ -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;
}
}

View File

@ -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) {