package com.leontg77.uhc.worlds; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayDeque; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.TreeSet; import java.util.UUID; import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.command.CommandSender; import org.bukkit.scheduler.BukkitRunnable; import org.bukkit.scheduler.BukkitTask; import com.leontg77.uhc.Main; @SuppressWarnings("deprecation") public class AntiStripmine { private static AntiStripmine manager = new AntiStripmine(); private EnumSet<Material> defaultOres = EnumSet.of(Material.DIAMOND_ORE, Material.GOLD_ORE, Material.LAPIS_ORE, Material.QUARTZ_ORE); private ArrayDeque<ChunkOreRemover> queue = new ArrayDeque<ChunkOreRemover>(); private HashSet<ChunkOreRemover> checked = new HashSet<ChunkOreRemover>(); private HashMap<UUID, WorldData> worlds = new HashMap<UUID, WorldData>(); private BukkitTask tick = null; private Material oreReplacer; private int removalFactor; private int maxHeight; /** * Gets the instance of this class * * @return The instance. */ public static AntiStripmine getInstance() { return manager; } public void setup() { this.oreReplacer = Material.STONE; this.removalFactor = 100; this.maxHeight = 64; } public void queue(ChunkOreRemover remover) { this.queue.add(remover); if (this.tick == null) { this.tick = Bukkit.getScheduler().runTask(Main.plugin, new Runnable() { public void run() { long started = System.nanoTime(); while ((!queue.isEmpty()) && (System.nanoTime() - started < 45000000L)) { queue.pop().run(); } if (queue.isEmpty()) { tick = null; } else { tick = Bukkit.getScheduler().runTask(Main.plugin, this); } } }); } this.checked.add(remover); } public int getDefaultMaxHeight() { return this.maxHeight; } public int getDefaultRemovalFactor() { return this.removalFactor; } public Material getDefaultOreReplacer() { return this.oreReplacer; } public EnumSet<Material> getDefaultOres() { return this.defaultOres; } public void displayStats(CommandSender sender) { for (WorldData worldData : this.worlds.values()) { worldData.displayStats(sender); } } public WorldData getWorldData(World world) { WorldData worldData = (WorldData) this.worlds.get(world.getUID()); if (worldData == null) { worldData = registerWorld(world); } return worldData; } public boolean wasChecked(ChunkOreRemover remover) { return this.checked.contains(remover); } public WorldData registerWorld(World world) { WorldData worldData = new WorldData(world); this.worlds.put(world.getUID(), worldData); worldData.load(); return worldData; } public class WorldData { private EnumMap<Material, Integer> remaining = new EnumMap<Material, Integer>(Material.class); private long total; private int chunks; private UUID worldId; private boolean enabled; private EnumSet<Material> excluded = EnumSet.noneOf(Material.class); private EnumSet<Material> ores = EnumSet.noneOf(Material.class); private int maxHeight; private int removalFactor; private Material oreReplacer; private final File queuedFile; private final File completedFile; private Set<ChunkOreRemover> noOresFound = new HashSet<ChunkOreRemover>(); public WorldData(World world) { this.worldId = world.getUID(); File worldFolder = new File(Main.plugin.getDataFolder(), "antistripmine" + File.separator + world.getName() + File.separator); if (!worldFolder.exists()) { worldFolder.mkdirs(); } this.queuedFile = new File(worldFolder, "queued.log"); this.completedFile = new File(worldFolder, "completed.log"); if (world.getEnvironment() != World.Environment.NORMAL) { this.enabled = false; } else { this.enabled = true; } this.ores.addAll(getDefaultOres()); this.maxHeight = getDefaultMaxHeight(); this.removalFactor = getDefaultRemovalFactor(); this.oreReplacer = getDefaultOreReplacer(); } public EnumSet<Material> getExcluded() { return this.excluded; } public int getMaxHeight() { return this.maxHeight; } public boolean isEnabled() { return this.enabled; } public int getRemovalFactor() { return this.removalFactor; } public Material getOreReplacer() { return this.oreReplacer; } public EnumSet<Material> getOres() { return this.ores; } public World getWorld() { return Bukkit.getServer().getWorld(this.worldId); } public UUID getWorldId() { return this.worldId; } public void displayStats(CommandSender sender) { if (this.chunks == 0) { sender.sendMessage("AntiBranchMining has not run yet on " + getWorld().getName()); return; } sender.sendMessage(getWorld().getName() + ": Average " + this.total / this.chunks + " ns per chunk (" + this.total / this.chunks / 1.0E9D + "s)"); for (Material material : this.remaining.keySet()) { int amount = ((Integer) this.remaining.get(material)).intValue(); sender.sendMessage(material.name() + ": " + amount + " remaining in " + this.chunks + " chunks (average " + amount / this.chunks + " per chunk)"); } } public void load() { final WorldData worldData = this; new BukkitRunnable() { public void run() { Set<String> toQueue = new TreeSet<String>(); Throwable localThrowable3; try { BufferedReader in = new BufferedReader(new FileReader(WorldData.this.queuedFile)); localThrowable3 = null; try { String line; while ((line = in.readLine()) != null) { toQueue.add(line); } } catch (Throwable localThrowable1) { localThrowable3 = localThrowable1; throw localThrowable1; } finally { if (in != null) { if (localThrowable3 != null) { try { in.close(); } catch (Throwable x2) { localThrowable3.addSuppressed(x2); } } else { in.close(); } } } } catch (FileNotFoundException ex) { } catch (IOException ex) { Main.plugin.getLogger().log(Level.SEVERE, "Cannot read " + WorldData.this.queuedFile.getPath(), ex); } try { BufferedReader in = new BufferedReader(new FileReader(WorldData.this.completedFile)); localThrowable3 = null; try { String line; while ((line = in.readLine()) != null) { String[] parts = line.split(" "); toQueue.remove(parts[0]); for (int i = 2; i < parts.length; i++) { String[] kvp = parts[i].split(":"); int materialId = Integer.parseInt(kvp[0]); int amount = Integer.parseInt(kvp[1]); Material material = Material.getMaterial(materialId); if (WorldData.this.remaining.containsKey(material)) { WorldData.this.remaining.put(material, Integer.valueOf(((Integer) WorldData.this.remaining.get(material)).intValue() + amount)); } else { WorldData.this.remaining.put(material, Integer.valueOf(amount)); } } } } catch (Throwable localThrowable2) { localThrowable3 = localThrowable2; throw localThrowable2; } finally { if (in != null) { if (localThrowable3 != null) { try { in.close(); } catch (Throwable x2) { localThrowable3.addSuppressed(x2); } } else { in.close(); } } } } catch (FileNotFoundException ex) { } catch (IOException ex) { Main.plugin.getLogger().log(Level.SEVERE, "Cannot read " + WorldData.this.completedFile.getPath(), ex); } int i = 1; for (String record : toQueue) { String[] parts = record.split(","); final int x = Integer.parseInt(parts[0]); final int z = Integer.parseInt(parts[1]); new BukkitRunnable() { public void run() { queue(new ChunkOreRemover(worldData, getWorld().getChunkAt(x, z))); } }.runTaskLater(Main.plugin, i++); } final int queued = toQueue.size(); new BukkitRunnable() { public void run() { Main.plugin.getLogger().info("Loaded data for " + WorldData.this.getWorld().getName() + ", checking " + queued + " chunks"); } }.runTask(Main.plugin); } }.runTaskAsynchronously(Main.plugin); } public void logQueued(ChunkOreRemover remover) { final String record = remover.toString() + "\n"; new BukkitRunnable() { public void run() { synchronized (WorldData.this.queuedFile) { try { BufferedWriter out = new BufferedWriter(new FileWriter(WorldData.this.queuedFile, true)); Throwable localThrowable2 = null; try { out.write(record); } catch (Throwable localThrowable1) { localThrowable2 = localThrowable1; throw localThrowable1; } finally { if (out != null) { if (localThrowable2 != null) { try { out.close(); } catch (Throwable x2) { localThrowable2.addSuppressed(x2); } } else { out.close(); } } } } catch (IOException ex) { Main.plugin.getLogger().log(Level.SEVERE, "Failed to save to " + WorldData.this.queuedFile.getPath() + ": " + record, ex); } } } }.runTaskAsynchronously(Main.plugin); } public void logCompleted(ChunkOreRemover remover, long duration, EnumMap<Material, Integer> remaining) { this.chunks += 1; this.total += duration; StringBuilder sb = new StringBuilder(); sb.append(remover.toString()); sb.append(" ").append(Long.toString(duration)); for (Map.Entry<Material, Integer> entry : remaining.entrySet()) { Material material = (Material) entry.getKey(); int amount = ((Integer) entry.getValue()).intValue(); sb.append(" ").append(material.getId()).append(":").append(amount); if (this.remaining.containsKey(material)) { this.remaining.put(material, Integer.valueOf(((Integer) this.remaining.get(material)).intValue() + amount)); } else { this.remaining.put(material, Integer.valueOf(amount)); } } sb.append("\n"); final String record = sb.toString(); new BukkitRunnable() { public void run() { synchronized (WorldData.this.completedFile) { try { BufferedWriter out = new BufferedWriter(new FileWriter(WorldData.this.completedFile, true)); Throwable localThrowable2 = null; try { out.write(record); } catch (Throwable localThrowable1) { localThrowable2 = localThrowable1; throw localThrowable1; } finally { if (out != null) { if (localThrowable2 != null) { try { out.close(); } catch (Throwable x2) { localThrowable2.addSuppressed(x2); } } else { out.close(); } } } } catch (IOException ex) { Main.plugin.getLogger().log(Level.SEVERE, "Failed to save to " + WorldData.this.completedFile.getPath() + ": " + record, ex); } } } }.runTaskAsynchronously(Main.plugin); } public boolean hasNoOres(ChunkOreRemover remover) { if (this.noOresFound.contains(remover)) { Main.plugin.getLogger().info("Confirmed that " + getWorld().getName() + " " + remover.toString() + " still has no ores on the second pass"); return true; } return false; } public void notifyNoOres(final ChunkOreRemover remover) { this.noOresFound.add(remover); Main.plugin.getLogger().info("No ores were found in " + getWorld().getName() + " " + remover.toString() + ": Scheduling for a second check in 5 seconds"); new BukkitRunnable() { public void run() { queue(remover); } }.runTaskLater(Main.plugin, 100L); } public boolean equals(Object obj) { if ((obj instanceof WorldData)) { WorldData other = (WorldData) obj; return other.worldId.equals(this.worldId); } return false; } public int hashCode() { return Objects.hashCode(this.worldId); } } public static class ChunkOreRemover implements Runnable { private static final Random random = new Random(); private WorldData worldData; private int chunkX; private int chunkZ; public ChunkOreRemover(WorldData worldData, Chunk chunk) { this.worldData = worldData; this.chunkX = chunk.getX(); this.chunkZ = chunk.getZ(); } private Block getBlock(Chunk chunk, int dx, int y, int dz) { if ((y < 0) || (y > 255)) { return null; } if ((dx >= 0) && (dx <= 15) && (dz >= 0) && (dz <= 15)) { return chunk.getBlock(dx, y, dz); } int x = chunk.getX() * 16 + dx; int z = chunk.getZ() * 16 + dz; if (chunk.getWorld().isChunkLoaded(x >> 4, z >> 4)) { return chunk.getWorld().getBlockAt(x, y, z); } return null; } private void allowLinked(Block block, HashMap<Block, HashSet<Block>> toRemove, HashSet<Block> allowed) { if (toRemove.containsKey(block)) { HashSet<Block> linked = toRemove.get(block); toRemove.remove(block); allowed.add(block); for (Block link : linked) { allowLinked(link, toRemove, allowed); } } } private void addLinked(Block block, HashMap<Block, HashSet<Block>> toRemove, HashSet<Block> vein) { if ((toRemove.containsKey(block)) && (!vein.contains(block))) { vein.add(block); HashSet<Block> linked = toRemove.get(block); for (Block link : linked) { if (link.getType().equals(block.getType())) { addLinked(link, toRemove, vein); } } } } private void addRemainingOres(EnumMap<Material, Integer> remaining, Material type, int size) { if (!remaining.containsKey(type)) { remaining.put(type, Integer.valueOf(size)); } else { remaining.put(type, Integer.valueOf(((Integer) remaining.get(type)).intValue() + size)); } } public void run() { long started = System.nanoTime(); HashMap<Block, HashSet<Block>> toRemove = new HashMap<Block, HashSet<Block>>(); HashSet<Block> allowed = new HashSet<Block>(); World world = this.worldData.getWorld(); Chunk chunk = world.getChunkAt(this.chunkX, this.chunkZ); int maxHeight = this.worldData.getMaxHeight(); int removalFactor = this.worldData.getRemovalFactor(); EnumSet<Material> excludedOres = this.worldData.getExcluded(); Material oreReplacer = this.worldData.getOreReplacer(); EnumSet<Material> ores = this.worldData.getOres(); boolean hasNoOres = true; for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { for (int y = 0; y <= maxHeight; y++) { Block block = getBlock(chunk, x, y, z); if (block != null) { if (ores.contains(block.getType())) { hasNoOres = false; HashSet<Block> nearOres = new HashSet<Block>(); for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { for (int dz = -1; dz <= 1; dz++) { if ((dx != 0) || (dy != 0) || (dz != 0)) { Block near = getBlock(chunk, x + dx, y + dy, z + dz); if (near != null) { if (ores.contains(near.getType())) { nearOres.add(near); } if ((allowed.contains(near)) || (near.isEmpty()) || (near.isLiquid())) { allowed.add(block); } } } } } } if (allowed.contains(block)) { for (Block near : nearOres) { allowLinked(near, toRemove, allowed); } toRemove.remove(block); } else { toRemove.put(block, nearOres); } } } } } } EnumMap<Material, Integer> remaining = new EnumMap<Material, Integer>(Material.class); while (!toRemove.isEmpty()) { HashSet<Block> vein = new HashSet<Block>(); Block next = (Block) toRemove.keySet().iterator().next(); addLinked(next, toRemove, vein); Material type = vein.iterator().next().getType(); if ((excludedOres.contains(type)) || ((removalFactor < 100) && (random.nextInt(100) >= removalFactor))) { addRemainingOres(remaining, type, vein.size()); } else { for (Block block : vein) { block.setType(oreReplacer); } } toRemove.keySet().removeAll(vein); } for (Block block : allowed) { addRemainingOres(remaining, block.getType(), 1); } long duration = System.nanoTime() - started; if ((hasNoOres) && (!this.worldData.hasNoOres(this))) { worldData.notifyNoOres(this); } else { worldData.logCompleted(this, duration, remaining); } } public UUID getWorldName() { return this.worldData.getWorldId(); } public int getChunkX() { return this.chunkX; } public int getChunkZ() { return this.chunkZ; } public boolean equals(Object obj) { if ((obj instanceof ChunkOreRemover)) { ChunkOreRemover other = (ChunkOreRemover) obj; return (other.chunkX == this.chunkX) && (other.chunkZ == this.chunkZ) && (other.worldData.equals(this.worldData)); } return false; } public int hashCode() { int hash = 5; hash = 13 * hash + Objects.hashCode(this.worldData); hash = 13 * hash + this.chunkX; hash = 13 * hash + this.chunkZ; return hash; } public String toString() { return this.chunkX + "," + this.chunkZ; } } }