ProjectAres/PGM/src/main/java/tc/oc/pgm/fallingblocks/FallingBlocksMatchModule.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);
}
}
}