Block change tracking: add flags for use and redstone. (+)

Add block flags:
* F_VARIABLE_USE
* F_VARIABLE_REDSTONE

Add a test to ensure block flags are unique and not 0L.

(+) Use individual MiniListener instances.
(+) Track right click blocks (use block) and apply, aiming at trap door
issues.
This commit is contained in:
asofold 2018-04-02 15:17:37 +02:00
parent 4c30b3570e
commit 0a373fb28b
4 changed files with 242 additions and 87 deletions

View File

@ -15,26 +15,30 @@
package fr.neatmonster.nocheatplus.compat.blocks.changetracker;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.event.Event.Result;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockPistonExtendEvent;
import org.bukkit.event.block.BlockPistonRetractEvent;
import org.bukkit.event.block.BlockRedstoneEvent;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.material.Directional;
import org.bukkit.material.Door;
import org.bukkit.material.MaterialData;
import fr.neatmonster.nocheatplus.NCPAPIProvider;
import fr.neatmonster.nocheatplus.components.NoCheatPlusAPI;
import fr.neatmonster.nocheatplus.components.registry.order.RegistrationOrder.RegisterMethodWithOrder;
import fr.neatmonster.nocheatplus.event.mini.MiniListener;
import fr.neatmonster.nocheatplus.logging.Streams;
import fr.neatmonster.nocheatplus.utilities.ReflectionUtil;
import fr.neatmonster.nocheatplus.utilities.map.BlockProperties;
@ -52,7 +56,66 @@ public class BlockChangeListener implements Listener {
private final BlockChangeTracker tracker;
private final boolean retractHasBlocks;
private boolean enabled = true;
private final Set<Material> redstoneMaterials = new HashSet<Material>();
/** Default tag for listeners. */
private final String defaultTag = "system.nocheatplus.blockchangetracker";
/**
* NOTE: Using MiniListenerWithOrder (and @Override before @EventHandler)
* would make the registry attempt to register with Bukkit for 'Object'.
*/
private final MiniListener<?>[] miniListeners = new MiniListener<?>[] {
new MiniListener<BlockRedstoneEvent>() {
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
@RegisterMethodWithOrder(tag = defaultTag)
@Override
public void onEvent(BlockRedstoneEvent event) {
if (enabled) {
onBlockRedstone(event);
}
}
},
new MiniListener<EntityChangeBlockEvent>() {
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
@RegisterMethodWithOrder(tag = defaultTag)
@Override
public void onEvent(EntityChangeBlockEvent event) {
if (enabled) {
onEntityChangeBlock(event);
}
}
},
new MiniListener<BlockPistonExtendEvent>() {
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
@RegisterMethodWithOrder(tag = defaultTag)
@Override
public void onEvent(BlockPistonExtendEvent event) {
if (enabled) {
onPistonExtend(event);
}
}
},
new MiniListener<BlockPistonRetractEvent>() {
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
@RegisterMethodWithOrder(tag = defaultTag)
@Override
public void onEvent(BlockPistonRetractEvent event) {
if (enabled) {
onPistonRetract(event);
}
}
},
new MiniListener<PlayerInteractEvent>() {
@EventHandler(ignoreCancelled = false, priority = EventPriority.MONITOR)
@RegisterMethodWithOrder(tag = defaultTag)
@Override
public void onEvent(PlayerInteractEvent event) {
if (enabled) {
onPlayerInteract(event);
}
}
}
};
public BlockChangeListener(final BlockChangeTracker tracker) {
this.tracker = tracker;
@ -63,14 +126,16 @@ public class BlockChangeListener implements Listener {
else {
retractHasBlocks = true;
}
// TODO: Make an access method to test this/such in BlockProperties!
for (Material material : Material.values()) {
if (material.isBlock()) {
final String name = material.name().toLowerCase();
if (name.indexOf("door") >= 0 || name.indexOf("gate") >= 0) {
redstoneMaterials.add(material);
}
}
}
/**
* Register actual listener(s).
*/
public void register() {
// TODO: Replace 'if (enabled)' by actually unregistering the listeners.
final NoCheatPlusAPI api = NCPAPIProvider.getNoCheatPlusAPI();
for (final MiniListener<?> listener : miniListeners) {
api.addComponent(listener);
}
}
@ -111,21 +176,13 @@ public class BlockChangeListener implements Listener {
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPistonExtend(final BlockPistonExtendEvent event) {
if (!enabled) {
return;
}
private void onPistonExtend(final BlockPistonExtendEvent event) {
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;
}
private void onPistonRetract(final BlockPistonRetractEvent event) {
final List<Block> blocks;
if (retractHasBlocks) {
blocks = event.getBlocks();
@ -170,11 +227,7 @@ public class BlockChangeListener implements Listener {
// DebugUtil.debug("BlockPhysics: " + block); // TODO: REMOVE
// }
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onBlockRedstone(final BlockRedstoneEvent event) {
if (!enabled) {
return;
}
private void onBlockRedstone(final BlockRedstoneEvent event) {
final int oldCurrent = event.getOldCurrent();
final int newCurrent = event.getNewCurrent();
if (oldCurrent == newCurrent || oldCurrent > 0 && newCurrent > 0) {
@ -183,31 +236,15 @@ public class BlockChangeListener implements Listener {
// TODO: Fine grained enabling state (pistons, doors, other).
final Block block = event.getBlock();
// TODO: Abstract method for a block and a set of materials (redstone, interact, ...).
if (block == null || !redstoneMaterials.contains(block.getType())) {
if (block == null
|| (BlockProperties.getBlockFlags(block.getType()) | BlockProperties.F_VARIABLE_REDSTONE) == 0) {
return;
}
addRedstoneBlock(block);
}
private void addRedstoneBlock(final Block block) {
final MaterialData materialData = block.getState().getData();
if (materialData instanceof Door) {
final Door door = (Door) materialData;
final Block otherBlock = block.getRelative(door.isTopHalf() ? BlockFace.DOWN : BlockFace.UP);
/*
* TODO: Double doors... detect those too? Is it still more
* efficient than using BlockPhysics with lazy delayed updating
* (TickListener...). Hinge corner... possibilities?
*/
if (redstoneMaterials.contains(otherBlock.getType())) {
tracker.addBlocks(block, otherBlock);
// DebugUtil.debug("BlockRedstone door: " + block + " / " + otherBlock); // TODO: REMOVE
return;
}
}
// Only the single block remains.
tracker.addBlocks(block);
// DebugUtil.debug("BlockRedstone: " + block); // TODO: REMOVE
addSimpleBlock(block, BlockProperties.F_VARIABLE_REDSTONE);
}
// @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
@ -223,11 +260,7 @@ public class BlockChangeListener implements Listener {
// }
// }
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onEntityChangeBlock(final EntityChangeBlockEvent event) {
if (!enabled) {
return;
}
private void onEntityChangeBlock(final EntityChangeBlockEvent event) {
final Block block = event.getBlock();
if (block != null) {
// TODO: Filters?
@ -236,4 +269,48 @@ public class BlockChangeListener implements Listener {
}
}
private void onPlayerInteract(final PlayerInteractEvent event) {
// Check preconditions.
final Result result = event.useInteractedBlock();
if (event.getAction() == Action.RIGHT_CLICK_BLOCK
&& (result == Result.ALLOW
|| !event.isCancelled() && result == Result.DEFAULT)) {
final Block block = event.getClickedBlock();
if (block != null) {
final Material type = block.getType();
if ((BlockProperties.getBlockFlags(type) | BlockProperties.F_VARIABLE_USE) != 0L) {
addSimpleBlock(block, BlockProperties.F_VARIABLE_USE);
}
}
}
}
/**
* Add a past state for this block, extending for the other block in case of
* doors.
*
* @param block
* @param relevantFlags
*/
private void addSimpleBlock(final Block block, final long relevantFlags) {
final MaterialData materialData = block.getState().getData();
if (materialData instanceof Door) {
final Door door = (Door) materialData;
final Block otherBlock = block.getRelative(door.isTopHalf() ? BlockFace.DOWN : BlockFace.UP);
/*
* TODO: In case of redstone: Double doors... detect those too? Is it still more
* efficient than using BlockPhysics with lazy delayed updating
* (TickListener...). Hinge corner... possibilities?
*/
if (otherBlock != null // Top of the map / special case.
&& (BlockProperties.getBlockFlags(otherBlock.getType())
| relevantFlags) == 0) {
tracker.addBlocks(block, otherBlock);
return;
}
}
// Only the single block remains.
tracker.addBlocks(block);
}
}

View File

@ -658,39 +658,39 @@ public class BlockProperties {
protected static final Map<Material, Long> blockFlags = new HashMap<Material, Long>();
/** Flag position for stairs. */
public static final long F_STAIRS = 0x1;
public static final long F_STAIRS = 0x1L;
/** The Constant F_LIQUID. */
public static final long F_LIQUID = 0x2;
public static final long F_LIQUID = 0x2L;
// TODO: maybe remove F_SOLID use (unless for setting F_GROUND on init).
/** Minecraft isSolid result. Used for setting ground flag - Subject to change / rename.*/
public static final long F_SOLID = 0x4;
public static final long F_SOLID = 0x4L;
/** Compatibility flag: regard this block as passable always. */
public static final long F_IGN_PASSABLE = 0x8;
public static final long F_IGN_PASSABLE = 0x8L;
/** The Constant F_WATER. */
public static final long F_WATER = 0x10;
public static final long F_WATER = 0x10L;
/** The Constant F_LAVA. */
public static final long F_LAVA = 0x20;
public static final long F_LAVA = 0x20L;
/** Override bounding box: 1.5 blocks high, like fences.<br>
* NOTE: This might have relevance for passable later.
*/
public static final long F_HEIGHT150 = 0x40;
public static final long F_HEIGHT150 = 0x40L;
/** The player can stand on these, sneaking or not. */
public static final long F_GROUND = 0x80; // TODO:
public static final long F_GROUND = 0x80L; // TODO:
/** Override bounding box: 1 block height.<br>
* NOTE: This should later be ignored by passable, rather.
*/
public static final long F_HEIGHT100 = 0x100;
public static final long F_HEIGHT100 = 0x100L;
/** Climbable like ladder and vine (allow to land on without taking damage). */
public static final long F_CLIMBABLE = 0x200;
public static final long F_CLIMBABLE = 0x200L;
/** The block can change shape. This is most likely not 100% accurate... */
public static final long F_VARIABLE = 0x400;
public static final long F_VARIABLE = 0x400L;
// /** The block has full bounds (0..1), inaccurate! */
// public static final int F_FULL = 0x800;
/** Block has full xz-bounds. */
public static final long F_XZ100 = 0x800;
public static final long F_XZ100 = 0x800L;
/**
* This flag indicates that everything between the minimum ground height and
@ -701,105 +701,117 @@ public class BlockProperties {
* otherwise colliding blocks
* ({@link #isPassableWorkaround(BlockCache, int, int, int, double, double, double, IBlockCacheNode, double, double, double, double)}).
*/
public static final long F_GROUND_HEIGHT = 0x1000;
public static final long F_GROUND_HEIGHT = 0x1000L;
/**
* The height is assumed to decrease from 1.0 with increasing data value from 0 to 0x7, with 0x7 being the lowest.
* (repeating till 0x15)). 0x8 means falling/full block. This is meant to model flowing water/lava. <br>
* However the hit-box for collision checks will be set to 0.5 height or 1.0 height only.
*/
public static final long F_HEIGHT_8SIM_DEC = 0x2000;
public static final long F_HEIGHT_8SIM_DEC = 0x2000L;
/**
* The height is assumed to increase with data value up to 0x7, repeating up to 0x15.<br>
* However the hit-box for collision checks will be set to 0.5 height or 1.0 height only,<br>
* as with the 1.4.x snow levels.
*/
public static final long F_HEIGHT_8SIM_INC = 0x4000;
public static final long F_HEIGHT_8SIM_INC = 0x4000L;
/**
* The height increases with data value (8 heights).<br>
* This is for MC 1.5 snow levels.
*/
public static final long F_HEIGHT_8_INC = 0x8000;
public static final long F_HEIGHT_8_INC = 0x8000L;
/** All rail types a minecart can move on. */
public static final long F_RAILS = 0x10000;
public static final long F_RAILS = 0x10000L;
/** ICE. */
public static final long F_ICE = 0x20000;
public static final long F_ICE = 0x20000L;
/** LEAVES. */
public static final long F_LEAVES = 0x40000;
public static final long F_LEAVES = 0x40000L;
/** THIN FENCE (glass panes, iron fence). */
public static final long F_THIN_FENCE = 0x80000;
public static final long F_THIN_FENCE = 0x80000L;
/** Meta-flag to indicate that the (max.-) edges should mean a collision, can be passed to collidesBlock. */
public static final long F_COLLIDE_EDGES = 0x100000;
public static final long F_COLLIDE_EDGES = 0x100000L;
/** Thick fence (default wooden fence). */
public static final long F_THICK_FENCE = 0x200000;
public static final long F_THICK_FENCE = 0x200000L;
/** Fence gate style with 0x04 being fully passable. */
public static final long F_PASSABLE_X4 = 0x400000;
public static final long F_PASSABLE_X4 = 0x400000L;
// TODO: Separate no fall damage flag ? [-> on ground could return "dominating" flags, or extra flags]
/** Like slime block: bounce back 25% of fall height without taking fall damage [TODO: Check/adjust]. */
public static final long F_BOUNCE25 = 0x800000;
public static final long F_BOUNCE25 = 0x800000L;
/**
* The facing direction is described by the lower 3 data bits in order of
* NSWE, starting at and defaulting to 2, which includes invalid states.
* Main purpose is ladders, no guarantees on defaults for other blocks yet.
*/
public static final long F_FACING_LOW3D2_NSWE = 0x1000000;
public static final long F_FACING_LOW3D2_NSWE = 0x1000000L;
/**
* The direction the block is attached to is described by the lower 2 bits
* in order of SNEW.
*/
public static final long F_ATTACHED_LOW2_SNEW = 0x2000000;
public static final long F_ATTACHED_LOW2_SNEW = 0x2000000L;
/**
* The hacky way to force sfNoLowJump when the block at from has this flag.
*/
public static final long F_ALLOW_LOWJUMP = 0x4000000;
public static final long F_ALLOW_LOWJUMP = 0x4000000L;
/** One eighth block height (0.125). */
public static final long F_HEIGHT8_1 = 0x8000000;
public static final long F_HEIGHT8_1 = 0x8000000L;
/**
* Fall distance is divided by 2, if a move goes through this medium
* (currently only supports liquid).
*/
public static final long F_FALLDIST_HALF = 0x10000000;
public static final long F_FALLDIST_HALF = 0x10000000L;
/**
* Fall distance is set to zero, if a move goes through this medium
* (currently only supports liquid).
*/
public static final long F_FALLDIST_ZERO = 0x20000000;
public static final long F_FALLDIST_ZERO = 0x20000000L;
/**
* Minimum height 15/16 (0.9375 = 1 - 0.0625). <br>
* Only applies with F_GROUND_HEIGHT set.
*/
public static final long F_MIN_HEIGHT16_15 = 0x40000000;
public static final long F_MIN_HEIGHT16_15 = 0x40000000L;
/**
* Minimum height 1/16 (0.0625). <br>
* Only applies with F_GROUND_HEIGHT set.
*/
public static final long F_MIN_HEIGHT16_1 = 0x80000000; // TODO: Lily pad min height of MC versions?
// TODO: Lily pad min height of MC versions?
public static final long F_MIN_HEIGHT16_1 = 0x80000000L;
/** CARPET. **/
public static final long F_CARPET = 0x100000000L;
/** Cobweb like blocks (adhesive). */
public static final long F_COBWEB = 0x200000000L;
/**
* Block change tracking: ordinary right click interaction (use) can change
* the shape.
*/
public static final long F_VARIABLE_USE = 0x400000000L;
/**
* Block change tracking: block redstone events can change the shape.
*/
public static final long F_VARIABLE_REDSTONE = 0x800000000L;
// TODO: Convenience constants combining all height / minheight flags.
// TODO: When flags are out, switch to per-block classes :p.
@ -1084,11 +1096,27 @@ public class BlockProperties {
}
// F_PASSABLE_X4
for (final Material mat : new Material[]{
for (final Material mat : new Material[] {
Material.FENCE_GATE,
Material.TRAP_DOOR, // TODO: Players can stand on - still passable past 1.9?
}) {
setFlag(mat, F_PASSABLE_X4); // TODO: Flag is abused for other checks, need another one.
// TODO: PASSABLE_X4 is abused for other checks, need another one?
setFlag(mat, F_PASSABLE_X4);
}
// F_VARIABLE_REDSTONE, F_VARIABLE_USE
for (Material material : Material.values()) {
if (material.isBlock()) {
final String name = material.name().toLowerCase();
if (name.endsWith("_door")
|| name.endsWith("_trapdoor")
|| name.endsWith("fence_gate")) {
setFlag(material, F_VARIABLE_REDSTONE);
if (!name.contains("iron")) {
setFlag(material, F_VARIABLE_USE);
}
}
}
}
// F_FACING_LOW3D2_NSWE
@ -1408,6 +1436,21 @@ public class BlockProperties {
return tags;
}
/**
* Get all flag names. Results don't start with the 'F_' prefix.
*
* @return
*/
public static Collection<String> getAllFlagNames() {
final Set<String> res = new LinkedHashSet<String>();
for (final String name : nameFlagMap.keySet()) {
if (!name.startsWith("F_")) {
res.add(name);
}
}
return res;
}
/**
* Convenience method to parse a flag.
*
@ -1953,7 +1996,11 @@ public class BlockProperties {
if (blockProps.hardness <= 2
&& (blockProps.tool.toolType == ToolType.AXE
|| blockProps.tool.toolType == ToolType.SPADE
|| (blockProps.hardness < 0.8 && (blockId != Material.NETHERRACK && blockId != Material.SNOW && blockId != Material.SNOW_BLOCK && blockId != Material.STONE_PLATE)))) {
|| (blockProps.hardness < 0.8
&& (blockId != Material.NETHERRACK
&& blockId != Material.SNOW
&& blockId != Material.SNOW_BLOCK
&& blockId != Material.STONE_PLATE)))) {
// Also roughly.
return true;
}

View File

@ -0,0 +1,31 @@
package fr.neatmonster.nocheatplus.test;
import static org.junit.Assert.fail;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
import fr.neatmonster.nocheatplus.utilities.map.BlockProperties;
public class TestBlockFlags {
@Test
public void testIfFlagsAreUnique() {
final Collection<String> flags = BlockProperties.getAllFlagNames();
final Set<Long> occupied = new HashSet<Long>();
for (final String name : flags) {
final long flag = BlockProperties.parseFlag(name);
if (flag == 0L) {
fail("Flag '" + name + "' must not be 0L.");
}
if (occupied.contains(flag)) {
fail("Flag '" + flag + "' already is occupied.");
}
occupied.add(flag);
}
}
}

View File

@ -1185,7 +1185,7 @@ public class NoCheatPlus extends JavaPlugin implements NoCheatPlusAPI {
&& config.getBoolean(ConfPaths.COMPATIBILITY_BLOCKS_CHANGETRACKER_PISTONS)) {
if (blockChangeListener == null) {
blockChangeListener = new BlockChangeListener(blockChangeTracker);
this.addComponent(blockChangeListener);
blockChangeListener.register();
}
blockChangeListener.setEnabled(true);
}