package com.wasteofplastic.askyblock; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.UUID; import org.apache.commons.lang.math.NumberUtils; import org.bukkit.ChatColor; import org.bukkit.ChunkSnapshot; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.material.MaterialData; import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.scheduler.BukkitTask; import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; import com.google.common.collect.Multiset.Entry; import com.google.common.collect.Multisets; import com.wasteofplastic.askyblock.events.IslandPostLevelEvent; import com.wasteofplastic.askyblock.events.IslandPreLevelEvent; import com.wasteofplastic.askyblock.util.Pair; import com.wasteofplastic.askyblock.util.Util; public class LevelCalcByChunk { private static final int MAX_CHUNKS = 200; private static final long SPEED = 1; private boolean checking = true; private BukkitTask task; private final ASkyBlock plugin; private Set<Pair<Integer, Integer>> chunksToScan; private final Island island; private final World world; private final CommandSender asker; private final UUID targetPlayer; private final Results result; // Copy the limits hashmap private HashMap<MaterialData, Integer> limitCount; private boolean report; private long oldLevel; public LevelCalcByChunk(final ASkyBlock plugin, final Island island, final UUID targetPlayer, final CommandSender asker, final boolean report) { this.plugin = plugin; this.island = island; this.world = island != null ? island.getCenter().getWorld() : null; this.asker = asker; this.targetPlayer = targetPlayer; this.limitCount = new HashMap<>(Settings.blockLimits); this.report = report; this.oldLevel = plugin.getPlayers().getIslandLevel(targetPlayer); // Results go here result = new Results(); if (island == null) { return; } // Get chunks to scan chunksToScan = getChunksToScan(island); // Start checking checking = true; // Start a recurring task until done or cancelled task = plugin.getServer().getScheduler().runTaskTimer(plugin, ()-> { if (this.island.getOwner() == null) { task.cancel(); return; } Set<ChunkSnapshot> chunkSnapshot = new HashSet<>(); if (checking) { Iterator<Pair<Integer, Integer>> it = chunksToScan.iterator(); if (!it.hasNext()) { // Nothing left tidyUp(); return; } // Add chunk snapshots to the list while (it.hasNext() && chunkSnapshot.size() < MAX_CHUNKS) { Pair<Integer, Integer> pair = it.next(); if (!world.isChunkLoaded(pair.x, pair.z)) { world.loadChunk(pair.x, pair.z); chunkSnapshot.add(world.getChunkAt(pair.x, pair.z).getChunkSnapshot()); world.unloadChunk(pair.x, pair.z); } else { chunkSnapshot.add(world.getChunkAt(pair.x, pair.z).getChunkSnapshot()); } it.remove(); } // Move to next step checking = false; checkChunksAsync(chunkSnapshot); } }, 0L, SPEED); } private void checkChunksAsync(final Set<ChunkSnapshot> chunkSnapshot) { // Run async task to scan chunks plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { for (ChunkSnapshot chunk: chunkSnapshot) { scanChunk(chunk); } // Nothing happened, change state checking = true; }); } @SuppressWarnings("deprecation") private void scanChunk(ChunkSnapshot chunk) { for (int x = 0; x< 16; x++) { // Check if the block coord is inside the protection zone and if not, don't count it if (chunk.getX() * 16 + x < island.getMinProtectedX() || chunk.getX() * 16 + x >= island.getMinProtectedX() + island.getProtectionSize()) { continue; } for (int z = 0; z < 16; z++) { // Check if the block coord is inside the protection zone and if not, don't count it if (chunk.getZ() * 16 + z < island.getMinProtectedZ() || chunk.getZ() * 16 + z >= island.getMinProtectedZ() + island.getProtectionSize()) { continue; } for (int y = 0; y < island.getCenter().getWorld().getMaxHeight(); y++) { Material blockType = Material.getMaterial(chunk.getBlockTypeId(x, y, z)); boolean belowSeaLevel = Settings.seaHeight > 0 && y <= Settings.seaHeight; // Air is free if (!blockType.equals(Material.AIR)) { checkBlock(blockType, chunk.getBlockData(x, y, z), belowSeaLevel); } } } } } private void checkBlock(Material type, int blockData, boolean belowSeaLevel) { // Currently, there is no alternative to using block data (Feb 2018) @SuppressWarnings("deprecation") MaterialData md = new MaterialData(type, (byte) blockData); int count = limitCount(md); if (count != 0) { if (belowSeaLevel) { result.underWaterBlockCount += count; result.uwCount.add(md); } else { result.rawBlockCount += count; result.mdCount.add(md); } } } /** * Checks if a block has been limited or not and whether a block has any value or not * @param md * @return value of the block if can be counted */ private int limitCount(MaterialData md) { MaterialData generic = new MaterialData(md.getItemType()); if (limitCount.containsKey(md) && Settings.blockValues.containsKey(md)) { int count = limitCount.get(md); if (count > 0) { limitCount.put(md, --count); return Settings.blockValues.get(md); } else { result.ofCount.add(md); return 0; } } else if (limitCount.containsKey(generic) && Settings.blockValues.containsKey(generic)) { int count = limitCount.get(generic); if (count > 0) { limitCount.put(generic, --count); return Settings.blockValues.get(generic); } else { result.ofCount.add(md); return 0; } } else if (Settings.blockValues.containsKey(md)) { return Settings.blockValues.get(md); } else if (Settings.blockValues.containsKey(generic)) { return Settings.blockValues.get(generic); } else { result.ncCount.add(md); return 0; } } /** * Get a set of all the chunks in island * @param island * @return */ private Set<Pair<Integer, Integer>> getChunksToScan(Island island) { Set<Pair<Integer, Integer>> chunkSnapshot = new HashSet<>(); for (int x = island.getMinProtectedX(); x < (island.getMinProtectedX() + island.getProtectionSize() + 16); x += 16) { for (int z = island.getMinProtectedZ(); z < (island.getMinProtectedZ() + island.getProtectionSize() + 16); z += 16) { Pair<Integer, Integer> pair = new Pair<>(world.getBlockAt(x, 0, z).getChunk().getX(), world.getBlockAt(x, 0, z).getChunk().getZ()); chunkSnapshot.add(pair); } } return chunkSnapshot; } private void tidyUp() { // Cancel task.cancel(); // Finalize calculations result.rawBlockCount += (long)(result.underWaterBlockCount * Settings.underWaterMultiplier); // Set the death penalty result.deathHandicap = plugin.getPlayers().getDeaths(island.getOwner()); // Set final score result.score = (result.rawBlockCount / Settings.levelCost) - result.deathHandicap - island.getLevelHandicap(); // Run any modifications // Get the permission multiplier if it is available int levelMultiplier = 1; Player player = plugin.getServer().getPlayer(targetPlayer); if (player != null) { // Get permission multiplier for (PermissionAttachmentInfo perms : player.getEffectivePermissions()) { if (perms.getPermission().startsWith(Settings.PERMPREFIX + "island.multiplier.")) { String spl[] = perms.getPermission().split(Settings.PERMPREFIX + "island.multiplier."); if (spl.length > 1) { if (!NumberUtils.isDigits(spl[1])) { plugin.getLogger().severe("Player " + player.getName() + " has permission: " + perms.getPermission() + " <-- the last part MUST be a number! Ignoring..."); } else { // Get the max value should there be more than one levelMultiplier = Math.max(levelMultiplier, Integer.valueOf(spl[1])); } } } // Do some sanity checking if (levelMultiplier < 1) { levelMultiplier = 1; } } } // Calculate how many points are required to get to the next level long pointsToNextLevel = (Settings.levelCost * (result.score + 1 + island.getLevelHandicap())) - ((result.rawBlockCount * levelMultiplier) - (result.deathHandicap * Settings.deathpenalty)); // Sometimes it will return 0, so calculate again to make sure it will display a good value if(pointsToNextLevel == 0) pointsToNextLevel = (Settings.levelCost * (result.score + 2 + island.getLevelHandicap()) - ((result.rawBlockCount * levelMultiplier) - (result.deathHandicap * Settings.deathpenalty))); // All done. informPlayers(saveLevel(island, targetPlayer, pointsToNextLevel)); } private void informPlayers(IslandPreLevelEvent event) { // Fire the island post level calculation event final IslandPostLevelEvent event3 = new IslandPostLevelEvent(targetPlayer, island, event.getLongLevel(), event.getLongPointsToNextLevel()); plugin.getServer().getPluginManager().callEvent(event3); if(!event3.isCancelled()){ // Check that sender still is online if (asker != null) { // Check if console if (!(asker instanceof Player)) { // Console if (!report) { Util.sendMessage(asker, ChatColor.GREEN + plugin.myLocale().islandislandLevelis.replace("[level]", String.valueOf(plugin.getPlayers().getIslandLevel(targetPlayer)))); } else { sendConsoleReport(asker); Util.sendMessage(asker, ChatColor.GREEN + plugin.myLocale().islandislandLevelis.replace("[level]", String.valueOf(plugin.getPlayers().getIslandLevel(targetPlayer)))); } } else { // Player if (!report) { // Tell offline team members the island level changed if (plugin.getPlayers().getIslandLevel(targetPlayer) != oldLevel) { //plugin.getLogger().info("DEBUG: telling offline players"); plugin.getMessages().tellOfflineTeam(targetPlayer, ChatColor.GREEN + plugin.myLocale().islandislandLevelis.replace("[level]", String.valueOf(plugin.getPlayers().getIslandLevel(targetPlayer)))); } if (((Player)asker).isOnline()) { String message = ChatColor.GREEN + plugin.myLocale(((Player)asker).getUniqueId()).islandislandLevelis.replace("[level]", String.valueOf(plugin.getPlayers().getIslandLevel(targetPlayer))); if (Settings.deathpenalty != 0) { message += " " + plugin.myLocale(((Player)asker).getUniqueId()).levelDeaths.replace("[number]", String.valueOf(result.deathHandicap)); } Util.sendMessage(asker, message); //Send player how many points are required to reach next island level if (event.getLongPointsToNextLevel() >= 0) { String toNextLevel = ChatColor.GREEN + plugin.myLocale(((Player)asker).getUniqueId()).islandrequiredPointsToNextLevel.replace("[points]", String.valueOf(event.getLongPointsToNextLevel())); toNextLevel = toNextLevel.replace("[next]", String.valueOf(plugin.getPlayers().getIslandLevel(targetPlayer) + 1)); Util.sendMessage(asker, toNextLevel); } } } else { if (((Player)asker).isOnline()) { sendConsoleReport(asker); } Util.sendMessage(asker, ChatColor.GREEN + plugin.myLocale().islandislandLevelis + " " + ChatColor.WHITE + plugin.getPlayers().getIslandLevel(targetPlayer)); if (event.getLongPointsToNextLevel() >= 0) { String toNextLevel = ChatColor.GREEN + plugin.myLocale().islandrequiredPointsToNextLevel.replace("[points]", String.valueOf(event.getLongPointsToNextLevel())); toNextLevel = toNextLevel.replace("[next]", String.valueOf(plugin.getPlayers().getIslandLevel(targetPlayer) + 1)); Util.sendMessage(asker, toNextLevel); } } } } } } private IslandPreLevelEvent saveLevel(Island island, UUID targetPlayer, long pointsToNextLevel) { // Fire the pre-level event final IslandPreLevelEvent event = new IslandPreLevelEvent(targetPlayer, island, result.score); event.setLongPointsToNextLevel(pointsToNextLevel); plugin.getServer().getPluginManager().callEvent(event); if (!event.isCancelled()) { // Save the value plugin.getPlayers().setIslandLevel(island.getOwner(), event.getLongLevel()); if (plugin.getPlayers().inTeam(targetPlayer)) { //plugin.getLogger().info("DEBUG: player is in team"); for (UUID member : plugin.getPlayers().getMembers(targetPlayer)) { //plugin.getLogger().info("DEBUG: updating team member level too"); if (plugin.getPlayers().getIslandLevel(member) != event.getLongLevel()) { plugin.getPlayers().setIslandLevel(member, event.getLongLevel()); plugin.getPlayers().save(member); } } UUID leader = plugin.getPlayers().getTeamLeader(targetPlayer); if (leader != null) { plugin.getTopTen().topTenAddEntry(leader, event.getLongLevel()); } } else { plugin.getTopTen().topTenAddEntry(targetPlayer, event.getLongLevel()); } } return event; } private void sendConsoleReport(CommandSender asker) { List<String> reportLines = new ArrayList<>(); // provide counts reportLines.add("Level Log for island at " + island.getCenter()); reportLines.add("Island owner UUID = " + island.getOwner()); reportLines.add("Total block value count = " + String.format("%,d",result.rawBlockCount)); reportLines.add("Level cost = " + Settings.levelCost); //reportLines.add("Level multiplier = " + levelMultiplier + " (Player must be online to get a permission multiplier)"); //reportLines.add("Schematic level handicap = " + levelHandicap + " (level is reduced by this amount)"); reportLines.add("Deaths handicap = " + result.deathHandicap); reportLines.add("Level calculated = " + result.score); reportLines.add("=================================="); int total = 0; if (!result.uwCount.isEmpty()) { reportLines.add("Underwater block count (Multiplier = x" + Settings.underWaterMultiplier + ") value"); reportLines.add("Total number of underwater blocks = " + String.format("%,d",result.uwCount.size())); reportLines.addAll(sortedReport(total, result.uwCount)); } reportLines.add("Regular block count"); reportLines.add("Total number of blocks = " + String.format("%,d",result.mdCount.size())); reportLines.addAll(sortedReport(total, result.mdCount)); reportLines.add("Blocks not counted because they exceeded limits: " + String.format("%,d",result.ofCount.size())); //entriesSortedByCount = Multisets.copyHighestCountFirst(ofCount).entrySet(); Iterable<Multiset.Entry<MaterialData>> entriesSortedByCount = result.ofCount.entrySet(); Iterator<Entry<MaterialData>> it = entriesSortedByCount.iterator(); while (it.hasNext()) { Entry<MaterialData> type = it.next(); Integer limit = Settings.blockLimits.get(type.getElement()); String explain = ")"; if (limit == null) { MaterialData generic = new MaterialData(type.getElement().getItemType()); limit = Settings.blockLimits.get(generic); explain = " - All types)"; } reportLines.add(type.getElement().toString() + ": " + String.format("%,d",type.getCount()) + " blocks (max " + limit + explain); } reportLines.add("=================================="); reportLines.add("Blocks on island that are not in config.yml"); reportLines.add("Total number = " + String.format("%,d",result.ncCount.size())); //entriesSortedByCount = Multisets.copyHighestCountFirst(ncCount).entrySet(); entriesSortedByCount = result.ncCount.entrySet(); it = entriesSortedByCount.iterator(); while (it.hasNext()) { Entry<MaterialData> type = it.next(); reportLines.add(type.getElement().toString() + ": " + String.format("%,d",type.getCount()) + " blocks"); } reportLines.add("================================="); for (String line : reportLines) { asker.sendMessage(line); } } private Collection<String> sortedReport(int total, Multiset<MaterialData> materialDataCount) { Collection<String> result = new ArrayList<>(); Iterable<Multiset.Entry<MaterialData>> entriesSortedByCount = Multisets.copyHighestCountFirst(materialDataCount).entrySet(); for (Entry<MaterialData> en : entriesSortedByCount) { MaterialData type = en.getElement(); int value = 0; if (Settings.blockValues.containsKey(type)) { // Specific value = Settings.blockValues.get(type); } else if (Settings.blockValues.containsKey(new MaterialData(type.getItemType()))) { // Generic value = Settings.blockValues.get(new MaterialData(type.getItemType())); } if (value > 0) { result.add(type.toString() + ":" + String.format("%,d", en.getCount()) + " blocks x " + value + " = " + (value * en.getCount())); total += (value * en.getCount()); } } result.add("Subtotal = " + total); result.add("=================================="); return result; } /** * Results class * */ public class Results { Multiset<MaterialData> mdCount = HashMultiset.create(); Multiset<MaterialData> uwCount = HashMultiset.create(); Multiset<MaterialData> ncCount = HashMultiset.create(); Multiset<MaterialData> ofCount = HashMultiset.create(); long rawBlockCount = 0; Island island; long underWaterBlockCount = 0; long score = 0; int deathHandicap = 0; } }