package tc.oc.pgm.renewable;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.logging.Logger;
import com.google.common.collect.ImmutableRangeMap;
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.geometry.Vec3;
import org.bukkit.material.MaterialData;
import org.bukkit.util.BlockVector;
import tc.oc.commons.bukkit.util.BlockFaces;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.bukkit.util.BlockVectorSet;
import tc.oc.commons.bukkit.util.MaterialCounter;
import tc.oc.commons.bukkit.util.NMSHacks;
import tc.oc.commons.core.logging.ClassLogger;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.events.BlockTransformEvent;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.filters.query.BlockQuery;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.Repeatable;
import tc.oc.pgm.snapshot.SnapshotMatchModule;
@ListenerScope(MatchScope.RUNNING)
public class Renewable implements Listener {
private static final int MAX_FAILED_ITERATIONS = 100;
private static final int SHUFFLE_SAMPLE_ITERATIONS = 10;
private static final int SHUFFLE_SAMPLE_RANGE = 5;
private final RenewableDefinition definition;
private final Match match;
private final Logger logger;
// Current inverse distribution of shuffleable materials relative to the initial state (which is unknown).
// This is used to choose a material when renewing shuffleable blocks.
private final MaterialCounter shuffleableMaterialDeficit = new MaterialCounter();
// Set of blocks that are immediately renewable, dynamically updated from block events.
// Maintaining this set avoids nearly all trial and error logic in the renewal tick.
private final BlockVectorSet renewablePool = new BlockVectorSet();
// Number of blocks that currently must to be renewed to keep up with the configured rate.
private long lastTick;
private SnapshotMatchModule snapshotMatchModule;
// Cached queries of the renewable/shuffleable filters, invalidated every tick.
// These are queries of the original blocks, not the current blocks.
// This should cut down on repeated queries.
private Map<BlockVector, Filter.QueryResponse> renewableCache = new HashMap<>();
private Map<BlockVector, Filter.QueryResponse> shuffleableCache = new HashMap<>();
public Renewable(RenewableDefinition definition, Match match, Logger parent) {
this.definition = definition;
this.match = match;
this.logger = new ClassLogger(parent, getClass());
updateLastTick();
}
void invalidateCaches() {
renewableCache.clear();
shuffleableCache.clear();
}
SnapshotMatchModule snapshot() {
if(snapshotMatchModule == null) {
snapshotMatchModule = match.needMatchModule(SnapshotMatchModule.class);
}
return snapshotMatchModule;
}
boolean isOriginalRenewable(BlockVector pos) {
if(!definition.region.contains(pos)) return false;
Filter.QueryResponse response = renewableCache.get(pos);
if(response == null) {
response = definition.renewableBlocks.query(new BlockQuery(snapshot().getOriginalBlock(pos)));
}
return response.isAllowed();
}
boolean isOriginalShuffleable(BlockVector pos) {
if(!definition.region.contains(pos)) return false;
Filter.QueryResponse response = shuffleableCache.get(pos);
if(response == null) {
response = definition.shuffleableBlocks.query(new BlockQuery(snapshot().getOriginalBlock(pos)));
}
return response.isAllowed();
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockChange(BlockTransformEvent event) {
BlockState oldState = event.getOldState(), newState = event.getNewState();
updateRenewablePool(newState);
if(definition.growAdjacent) {
for(BlockFace face : BlockFaces.NEIGHBORS) {
updateRenewablePool(BlockFaces.getRelative(newState, face));
}
}
if(isOriginalShuffleable(BlockUtils.position(newState))) {
if(definition.shuffleableBlocks.query(new BlockQuery(oldState)).isAllowed()) {
shuffleableMaterialDeficit.increment(oldState, 1);
}
if(definition.shuffleableBlocks.query(new BlockQuery(newState)).isAllowed()) {
shuffleableMaterialDeficit.increment(newState, -1);
}
}
}
@Repeatable
public void tick(Match match) {
invalidateCaches();
float interval = updateLastTick(); // should always be 1
float count = interval * definition.renewalsPerSecond / 20f; // calculate renewals per tick
if(definition.rateScaled) count *= renewablePool.size();
for(;count > 0 && !renewablePool.isEmpty(); count--) {
if(match.getRandom().nextFloat() < count) {
for(int i = 0; i < MAX_FAILED_ITERATIONS; i++) {
if(renew(renewablePool.chooseRandom(match.getRandom()))) break;
}
}
}
}
long updateLastTick() {
long delta = match.getClock().now().tick - lastTick;
lastTick = match.getClock().now().tick;
return delta;
}
void updateRenewablePool(BlockState block) {
if(canRenew(block)) {
renewablePool.add(BlockUtils.position(block));
} else {
renewablePool.remove(BlockUtils.position(block));
}
}
boolean isNew(BlockState currentState) {
// If original block does not match renewable rule, block is new
BlockVector pos = BlockUtils.position(currentState);
if(!isOriginalRenewable(pos)) return true;
// If original and current material are both shuffleable, block is new
MaterialData currentMaterial = currentState.getMaterialData();
if(isOriginalShuffleable(pos) && definition.shuffleableBlocks.query(new BlockQuery(currentState)).isAllowed()) return true;
// If current material matches original, block is new
if(currentMaterial.equals(snapshot().getOriginalMaterial(pos))) return true;
// Otherwise, block is not new (can be renewed)
return false;
}
boolean hasNewNeighbor(BlockState block) {
for(BlockFace face : BlockFaces.NEIGHBORS) {
if(isNew(BlockFaces.getRelative(block, face))) return true;
}
return false;
}
boolean canRenew(BlockState currentState) {
// Must not already be new
if(isNew(currentState)) return false;
// Must grow from an adjacent block that is renewed
if(definition.growAdjacent && !hasNewNeighbor(currentState)) return false;
// Current block must be replaceable
if(!definition.replaceableBlocks.query(new BlockQuery(currentState)).isAllowed()) return false;
return true;
}
boolean isClearOfEntities(Vec3 pos) {
if(definition.avoidPlayersRange > 0d) {
double rangeSquared = definition.avoidPlayersRange * definition.avoidPlayersRange;
pos = pos.blockCenter();
for(MatchPlayer player : match.getParticipatingPlayers()) {
Location location = player.getBukkit().getLocation().add(0,1,0);
if(location.toVector().distanceSquared(pos) < rangeSquared) {
return false;
}
}
}
return true;
}
MaterialData sampleShuffledMaterial(BlockVector pos) {
Random random = match.getRandom();
int range = SHUFFLE_SAMPLE_RANGE;
int diameter = range * 2 + 1;
for(int i = 0; i < SHUFFLE_SAMPLE_ITERATIONS; i++) {
BlockState block = snapshot().getOriginalBlock(pos.getBlockX() + random.nextInt(diameter) - range,
pos.getBlockY() + random.nextInt(diameter) - range,
pos.getBlockZ() + random.nextInt(diameter) - range);
if(definition.shuffleableBlocks.query(new BlockQuery(block)).isAllowed()) return block.getMaterialData();
}
return null;
}
MaterialData chooseShuffledMaterial() {
ImmutableRangeMap.Builder<Double, MaterialData> weightsBuilder = ImmutableRangeMap.builder();
double sum = 0d;
for(MaterialData material : shuffleableMaterialDeficit.materials()) {
double weight = shuffleableMaterialDeficit.get(material);
if(weight > 0) {
weightsBuilder.put(Range.closedOpen(sum, sum + weight), material);
sum += weight;
}
}
RangeMap<Double, MaterialData> weights = weightsBuilder.build();
return weights.get(match.getRandom().nextDouble() * sum);
}
boolean renew(BlockVector pos) {
MaterialData material;
if(isOriginalShuffleable(pos)) {
// If position is shuffled, first try to find a nearby shuffleable block to swap with.
// This helps to make shuffling less predictable when the material deficit is small or
// out of proportion to the original distribution of materials.
material = sampleShuffledMaterial(pos);
// If that fails, choose a random material, weighted by the current material deficits.
if(material == null) material = chooseShuffledMaterial();
} else {
material = snapshot().getOriginalMaterial(pos);
}
if(material != null) {
return renew(pos, material);
}
return false;
}
boolean renew(BlockVector pos, MaterialData material) {
// We need to do the entity check here rather than canRenew, because we are not
// notified when entities move in our out of the way.
if(!isClearOfEntities(pos)) return false;
Location location = pos.toLocation(match.getWorld());
Block block = location.getBlock();
BlockState newState = location.getBlock().getState();
newState.setMaterialData(material);
BlockRenewEvent event = new BlockRenewEvent(block, newState, this);
match.callEvent(event); // Our own handler will get this and remove the block from the pool
if(event.isCancelled()) return false;
newState.update(true, true);
if(definition.particles) {
NMSHacks.playBlockBreakEffect(match.getWorld(), pos, material.getItemType());
}
if(definition.sound) {
NMSHacks.playBlockPlaceSound(match.getWorld(), pos, material.getItemType(), 1f);
}
return true;
}
}