313 lines
12 KiB
Java
313 lines
12 KiB
Java
package tc.oc.pgm.fallingblocks;
|
|
|
|
import gnu.trove.iterator.TLongObjectIterator;
|
|
import gnu.trove.map.TLongObjectMap;
|
|
import gnu.trove.map.hash.TLongObjectHashMap;
|
|
import gnu.trove.set.TLongSet;
|
|
import gnu.trove.set.hash.TLongHashSet;
|
|
import org.bukkit.Material;
|
|
import org.bukkit.World;
|
|
import org.bukkit.block.Block;
|
|
import org.bukkit.block.BlockFace;
|
|
import org.bukkit.block.BlockState;
|
|
import org.bukkit.entity.FallingBlock;
|
|
import org.bukkit.event.EventHandler;
|
|
import org.bukkit.event.EventPriority;
|
|
import org.bukkit.event.Listener;
|
|
import org.bukkit.event.block.BlockFallEvent;
|
|
import org.bukkit.material.MaterialData;
|
|
import org.bukkit.scheduler.BukkitTask;
|
|
import tc.oc.commons.bukkit.util.LongDeque;
|
|
import tc.oc.pgm.events.ListenerScope;
|
|
import tc.oc.pgm.match.Match;
|
|
import tc.oc.pgm.match.MatchScope;
|
|
import tc.oc.pgm.match.ParticipantState;
|
|
import tc.oc.pgm.events.BlockTransformEvent;
|
|
import tc.oc.pgm.events.ParticipantBlockTransformEvent;
|
|
import tc.oc.pgm.match.MatchModule;
|
|
import tc.oc.commons.bukkit.util.Materials;
|
|
|
|
import javax.annotation.Nullable;
|
|
import java.util.List;
|
|
import java.util.logging.Level;
|
|
|
|
import static tc.oc.commons.bukkit.util.BlockUtils.encodePos;
|
|
import static tc.oc.commons.bukkit.util.BlockUtils.neighborPos;
|
|
import static tc.oc.commons.bukkit.util.BlockUtils.blockAt;
|
|
|
|
@ListenerScope(MatchScope.RUNNING)
|
|
public class FallingBlocksMatchModule extends MatchModule implements Listener {
|
|
private static final BlockFace[] NEIGHBORS = { BlockFace.WEST, BlockFace.EAST, BlockFace.DOWN, BlockFace.UP, BlockFace.NORTH, BlockFace.SOUTH };
|
|
|
|
// Maximum total blocks to search through over a single tick
|
|
private static final int MAX_SEARCH_VISITS_PER_TICK = 4096;
|
|
private static class MaxSearchVisitsExceeded extends Exception {}
|
|
private int visitsThisTick, visitsWorstTick;
|
|
|
|
private final List<FallingBlocksRule> rules;
|
|
private final TLongObjectMap<TLongObjectMap<ParticipantState>> blockDisturbersByTick = new TLongObjectHashMap<>();
|
|
private BukkitTask task;
|
|
|
|
public FallingBlocksMatchModule(Match match, List<FallingBlocksRule> rules) {
|
|
super(match);
|
|
this.rules = rules;
|
|
}
|
|
|
|
private @Nullable FallingBlocksRule ruleWithShortestDelay(BlockState block) {
|
|
FallingBlocksRule shortest = null;
|
|
for(FallingBlocksRule rule : this.rules) {
|
|
if(rule.canFall(block) && (shortest == null || shortest.delay > rule.delay)) {
|
|
shortest = rule;
|
|
}
|
|
}
|
|
return shortest;
|
|
}
|
|
|
|
@Override
|
|
public void enable() {
|
|
super.enable();
|
|
this.task = this.getMatch().getServer().getScheduler().runTaskTimer(this.getMatch().getPlugin(), new Runnable() {
|
|
@Override public void run() {
|
|
FallingBlocksMatchModule.this.fallCheck();
|
|
}
|
|
}, 0, 1);
|
|
}
|
|
|
|
@Override
|
|
public void disable() {
|
|
if(this.task != null) {
|
|
this.task.cancel();
|
|
this.task = null;
|
|
}
|
|
|
|
logger.info("Longest search for this match: " + this.visitsWorstTick);
|
|
super.disable();
|
|
}
|
|
|
|
private void logError(MaxSearchVisitsExceeded ex) {
|
|
getMatch().getMap().getLogger().log(Level.SEVERE, "Exceeded max search visits (" + MAX_SEARCH_VISITS_PER_TICK + ") for this tick", ex);
|
|
}
|
|
|
|
/**
|
|
* Test if the given block is either self-supporting (doesn't match any falling rules) or is adjacent to a supported block.
|
|
* The supported and unsupported arguments may contain the results of previous completed searches. The blocks visited by
|
|
* this search will be added to one or the other set depending on the final result.
|
|
*
|
|
* @param pos position of the block to test
|
|
* @param supported set of blocks already known to be supported
|
|
* @param unsupported set of blocks already known to be unsupported
|
|
*
|
|
* @return true iff the given block is definitely supported
|
|
*/
|
|
private boolean isSupported(long pos, TLongSet supported, TLongSet unsupported) throws MaxSearchVisitsExceeded {
|
|
World world = this.getMatch().getWorld();
|
|
|
|
LongDeque queue = new LongDeque();
|
|
TLongSet visited = new TLongHashSet();
|
|
queue.add(pos);
|
|
visited.add(pos);
|
|
|
|
while(!queue.isEmpty()) {
|
|
pos = queue.remove();
|
|
|
|
if(supported.contains(pos)) {
|
|
// If we find a block already known to be supported, it supports all blocks visited in the search.
|
|
supported.addAll(visited);
|
|
return true;
|
|
}
|
|
|
|
if(++this.visitsThisTick > MAX_SEARCH_VISITS_PER_TICK) {
|
|
throw new MaxSearchVisitsExceeded();
|
|
}
|
|
|
|
if(unsupported.contains(pos)) {
|
|
// Don't continue the search through blocks known to be unsupported
|
|
continue;
|
|
}
|
|
|
|
Block block = blockAt(world, pos);
|
|
if(block == null) continue;
|
|
|
|
boolean selfSupporting = true;
|
|
for(FallingBlocksRule rule : this.rules) {
|
|
if(rule.canFall(block)) {
|
|
// If a rule matches, this block is not self-supporting,
|
|
// and its status depends on the final result of the search.
|
|
selfSupporting = false;
|
|
|
|
// Continue the search through any neighbors that are capable of
|
|
// supporting this block, and have not yet been visited.
|
|
for(BlockFace face : NEIGHBORS) {
|
|
long neighborPos = neighborPos(pos, face);
|
|
if(!visited.contains(neighborPos)) {
|
|
Block neighbor = blockAt(world, neighborPos);
|
|
if(rule.canSupport(neighbor, face)) {
|
|
queue.add(neighborPos);
|
|
visited.add(neighborPos);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(selfSupporting) {
|
|
// If no rules match this block, then it is self-supporting,
|
|
// and it can support all the other visited blocks.
|
|
supported.addAll(visited);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// If the entire block network has been searched without finding a
|
|
// supported block, then we know the entire network is unsupported.
|
|
unsupported.addAll(visited);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the number of unsupported blocks connected to any blocks neighboring the given location,
|
|
* which is assumed to contain an air block. The search may bail out early when the count is greater
|
|
* or equal to the given limit, though this cannot be guaranteed.
|
|
*/
|
|
private int countUnsupportedNeighbors(long pos, int limit) {
|
|
TLongSet supported = new TLongHashSet();
|
|
TLongSet unsupported = new TLongHashSet();
|
|
|
|
try {
|
|
for(BlockFace face : NEIGHBORS) {
|
|
if(!this.isSupported(neighborPos(pos, face), supported, unsupported)) {
|
|
if(unsupported.size() >= limit) break;
|
|
}
|
|
}
|
|
}
|
|
catch(MaxSearchVisitsExceeded ex) {
|
|
this.logError(ex);
|
|
}
|
|
|
|
return unsupported.size();
|
|
}
|
|
|
|
/**
|
|
* Return the number of unsupported blocks connected to any blocks neighboring the given location.
|
|
* An air block is placed there temporarily if it is not already air. The search may bail out early
|
|
* when the count is >= the given limit, though this cannot be guaranteed.
|
|
*/
|
|
public int countUnsupportedNeighbors(Block block, int limit) {
|
|
BlockState state = null;
|
|
if(block.getType() != Material.AIR) {
|
|
state = block.getState();
|
|
block.setTypeIdAndData(0, (byte) 0, false);
|
|
}
|
|
|
|
int count = countUnsupportedNeighbors(encodePos(block), limit);
|
|
|
|
if(state != null) {
|
|
block.setTypeIdAndData(state.getTypeId(), state.getRawData(), false);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Make any unsupported blocks fall that are disturbed for the current tick
|
|
*/
|
|
private void fallCheck() {
|
|
this.visitsWorstTick = Math.max(this.visitsWorstTick, this.visitsThisTick);
|
|
this.visitsThisTick = 0;
|
|
|
|
World world = this.getMatch().getWorld();
|
|
TLongObjectMap<ParticipantState> blockDisturbers = this.blockDisturbersByTick.remove(this.getMatch().getClock().now().tick);
|
|
if(blockDisturbers == null) return;
|
|
|
|
TLongSet supported = new TLongHashSet();
|
|
TLongSet unsupported = new TLongHashSet();
|
|
TLongObjectMap<ParticipantState> fallsByBreaker = new TLongObjectHashMap<>();
|
|
|
|
try {
|
|
while(!blockDisturbers.isEmpty()) {
|
|
long pos = blockDisturbers.keySet().iterator().next();
|
|
ParticipantState breaker = blockDisturbers.remove(pos);
|
|
|
|
// Search down for the first block that can actually fall
|
|
for(;;) {
|
|
long below = neighborPos(pos, BlockFace.DOWN);
|
|
if(!Materials.isColliding(blockAt(world, below).getType())) break;
|
|
blockDisturbers.remove(pos); // Remove all the blocks we find along the way
|
|
pos = below;
|
|
}
|
|
|
|
// Check if the block needs to fall, if it isn't already falling
|
|
if(!fallsByBreaker.containsKey(pos) && !this.isSupported(pos, supported, unsupported)) {
|
|
fallsByBreaker.put(pos, breaker);
|
|
}
|
|
}
|
|
} catch(MaxSearchVisitsExceeded ex) {
|
|
this.logError(ex);
|
|
}
|
|
|
|
for(TLongObjectIterator<ParticipantState> iter = fallsByBreaker.iterator(); iter.hasNext();) {
|
|
iter.advance();
|
|
this.fall(iter.key(), iter.value());
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("deprecation")
|
|
private void fall(long pos, @Nullable ParticipantState breaker) {
|
|
// Block must be removed BEFORE spawning the FallingBlock, or it will not appear on the client
|
|
// https://bugs.mojang.com/browse/MC-72248
|
|
Block block = blockAt(this.getMatch().getWorld(), pos);
|
|
BlockState oldState = block.getState();
|
|
block.setType(Material.AIR, false);
|
|
FallingBlock fallingBlock = oldState.spawnFallingBlock();
|
|
|
|
BlockFallEvent event = new BlockFallEvent(block, fallingBlock);
|
|
getMatch().callEvent(breaker == null ? new BlockTransformEvent(event, block, Material.AIR)
|
|
: new ParticipantBlockTransformEvent(event, block, Material.AIR, breaker));
|
|
|
|
if(event.isCancelled()) {
|
|
fallingBlock.remove();
|
|
oldState.update(true, false); // Restore the old block if the fall is cancelled
|
|
} else {
|
|
// This is already air, but physics have not been applied yet, so do that now
|
|
block.simulateChangeForNeighbors(oldState.getMaterialData(), new MaterialData(Material.AIR));
|
|
}
|
|
}
|
|
|
|
private void disturb(long pos, BlockState blockState, @Nullable ParticipantState disturber) {
|
|
FallingBlocksRule rule = this.ruleWithShortestDelay(blockState);
|
|
if(rule != null) {
|
|
long tick = this.getMatch().getClock().now().tick + rule.delay;
|
|
TLongObjectMap<ParticipantState> blockDisturbers = this.blockDisturbersByTick.get(tick);
|
|
|
|
if(blockDisturbers == null) {
|
|
blockDisturbers = new TLongObjectHashMap<>();
|
|
this.blockDisturbersByTick.put(tick, blockDisturbers);
|
|
}
|
|
|
|
Block block = blockState.getBlock();
|
|
if(!blockDisturbers.containsKey(pos)) {
|
|
blockDisturbers.put(pos, disturber);
|
|
}
|
|
}
|
|
}
|
|
|
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
|
public void onBlockChange(BlockTransformEvent event) {
|
|
BlockState newState = event.getNewState();
|
|
Block block = newState.getBlock();
|
|
long pos = encodePos(block);
|
|
|
|
// Only breaks are credited. Making a bridge fall by updating a block
|
|
// does not credit you with breaking the bridge.
|
|
ParticipantState breaker = event.isBreak() ? ParticipantBlockTransformEvent.getPlayerState(event) : null;
|
|
|
|
if(!(event.getCause() instanceof BlockFallEvent)) {
|
|
this.disturb(pos, newState, breaker);
|
|
}
|
|
|
|
for(BlockFace face : NEIGHBORS) {
|
|
this.disturb(neighborPos(pos, face), block.getRelative(face).getState(), breaker);
|
|
}
|
|
}
|
|
}
|