/* * This file is part of Libelula Minecraft Edition Project. * * Libelula Minecraft Edition is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Libelula Minecraft Edition is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Libelula Minecraft Edition. * If not, see <http://www.gnu.org/licenses/>. * */ package me.libelula.meode; import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.Material; import static org.bukkit.Material.BED_BLOCK; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.Sign; import org.bukkit.entity.Player; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.plugin.Plugin; /** * Class MEODE of the plugin. * * @author Diego Lucio D'Onofrio <ddonofrio@member.fsf.org> * @version 1.0 */ public class MEODE extends Store { private final int maxDbSizeMB; private final int maxEventInRAM; private final Plugin plugin; private final HDStore hds; private final RAMStore rams; public boolean debug; public SyncTask sync; List<Player> adminQuering; private final Lock _adminQuery_mutex; public static int dbEngineVersion = 1; /** * * @param dbPath Path to DB directory. * @param maxDbSizeMB Maximum amount of MB to store in disk before rotating * storage information. * @param maxEventInRAM Maximum amount of events in RAM before saving to * disk. * @param sendStats If it is true, anonymous statistic usage will be sent to * developer. * @param plugin main plugin. */ public MEODE(String dbPath, int maxDbSizeMB, int maxEventInRAM, Plugin plugin, boolean debug) throws Exception { this.maxDbSizeMB = maxDbSizeMB; this.maxEventInRAM = maxEventInRAM; this.plugin = plugin; this.debug = debug; hds = new HDStore(dbPath, maxDbSizeMB, plugin); hds.debug = debug; rams = new RAMStore(plugin, hds); sync = new SyncTask(null, null, plugin); this.adminQuering = new ArrayList<>(); _adminQuery_mutex = new ReentrantLock(true); } public boolean hasItBeenChanged(Block block) { return rams.filterContains(block.getLocation()) || hds.filterContains(block.getLocation()); } public void addEvent(BlockBreakEvent e) { rams.storeEvent(e); if (rams.getEventsInMemory() >= maxEventInRAM) { hds.peristRamAsynchronously(rams); } } public void addEvent(BlockPlaceEvent e) { rams.storeEvent(e); if (rams.getEventsInMemory() >= maxEventInRAM) { hds.peristRamAsynchronously(rams); } } public void persistRamSynchronously() { int eventsToSave = rams.getEventsInMemory(); try { hds.peristRamSynchronously(rams); if (hds.debug) { plugin.getLogger().log(Level.INFO, "{0} events were stored in disk.", eventsToSave); } } catch (Exception ex) { plugin.getLogger().severe(ex.toString()); plugin.getLogger().log(Level.SEVERE, "{0} events were lost.", eventsToSave); } } public void asyncTellBlockInfo(Block block, Player admin, Boolean placed) { if (!hasItBeenChanged(block)) { admin.sendMessage(ChatColor.RED + "There is no records about this block."); } else { Object objects[] = {this, rams, hds, block.getLocation(), admin.getName(), placed}; new AsyncTask(AsyncTask.TaskType.QUERY_EVENT, objects, plugin).runTaskAsynchronously(plugin); } } public static void syncTellBlockInfo(MEODE meode, RAMStore rams, HDStore hds, Location loc, String adminName, boolean placed) { String result; result = rams.query(loc, placed); if (result != null) { meode.sync.sendSyncMessageToPlayer(adminName, ChatColor.GREEN + result); } else { try { result = hds.query(loc, placed); } catch (IOException | ClassNotFoundException | ParseException ex) { meode.sync.sendSyncMessageToPlayer(adminName, ChatColor.RED + "Internal server error while performing query."); meode.plugin.getLogger().severe(ex.toString()); return; } if (result != null) { meode.sync.sendSyncMessageToPlayer(adminName, ChatColor.GREEN + result); } else { meode.sync.sendSyncMessageToPlayer(adminName, ChatColor.RED + "Information about this block is no longer available."); } } } public void asyncRestoreBlock(Block block, Player admin) { if (hasItBeenChanged(block)) { String adminName; if (admin != null) { adminName = admin.getName(); } else { adminName = null; } Object objects[] = {this, block.getLocation(), adminName}; new AsyncTask(AsyncTask.TaskType.RESTORE_BLOCK, objects, plugin).runTaskAsynchronously(plugin); } } public static void syncRestoreBlock(Plugin plugin, MEODE meode, Location loc, String adminName) throws IOException, ClassNotFoundException, ParseException { BlockEvent be = meode.rams.getLastBlockEvent(loc, false); if (be == null) { be = meode.hds.getLastBlockEvent(loc, false); } if (be != null) { Object[] objects = {adminName, be}; new SyncTask(SyncTask.TaskType.RESTORE_BLOCK, objects, plugin).runTask(plugin); } } public void asyncQuerySellection(Location position, int radius, Player admin, String playerName) { Location locMin = new Location(position.getWorld(), position.getBlockX(), position.getBlockY(), position.getBlockZ()); Location locMax = new Location(position.getWorld(), position.getBlockX(), position.getBlockY(), position.getBlockZ()); locMin.subtract(radius, radius, radius); locMax.add(radius, radius, radius); asyncQuerySellection(locMin, locMax, admin, playerName); } public void asyncQuerySellection(Location locMin, Location locMax, Player admin, String playerName) { Object[] objects = {this, locMin, locMax, admin, playerName}; new AsyncTask(AsyncTask.TaskType.QUERY_SELECTION, objects, plugin).runTaskAsynchronously(plugin); } public TreeSet<BlockEvent> querySellection(MEODE meode, Location locMin, Location locMax, String playerName) { TreeSet<BlockEvent> blockEventSet = rams.getBlockEvents(locMin, locMax, playerName); TreeSet<BlockEvent> hdBlockEventSet = null; try { hdBlockEventSet = hds.getBlockEvents(locMin, locMax, blockEventSet, playerName); // } catch (IOException | ClassNotFoundException | ParseException ex) { } catch (Exception ex) { plugin.getLogger().warning("MEODE failure on querySellection():".concat(ex.toString())); } if (hdBlockEventSet != null) { blockEventSet.addAll(hdBlockEventSet); } return blockEventSet; } public void syncQuerySellection(Location locMin, Location locMax, Player admin, String playerName) { _adminQuery_mutex.lock(); boolean isAdminQuerying = adminQuering.contains(admin); _adminQuery_mutex.unlock(); if (isAdminQuerying) { sync.sendSyncMessageToPlayer(admin.getName(), ChatColor.RED + "You must wait current query finished before launching a new one."); return; } else { _adminQuery_mutex.lock(); adminQuering.add(admin); _adminQuery_mutex.unlock(); } TreeSet<BlockEvent> blockEventSet = querySellection(this, locMin, locMax, playerName); TreeMap<String, Long> playerChange = new TreeMap<>(); for (Iterator<BlockEvent> it = blockEventSet.iterator(); it.hasNext();) { BlockEvent be = it.next(); playerChange.put(be.playerName, be.eventTime); } for (Map.Entry<String, Long> entry : playerChange.entrySet()) { sync.sendSyncMessageToPlayer(admin.getName(), ChatColor.GREEN + entry.getKey() + ": " + new SimpleDateFormat("yyyy-MM-dd HH:mm").format(entry.getValue())); } if (playerChange.isEmpty()) { sync.sendSyncMessageToPlayer(admin.getName(), ChatColor.RED + "No records where found in this area"); } _adminQuery_mutex.lock(); adminQuering.remove(admin); _adminQuery_mutex.unlock(); } public void asyncEditSellection(Location position, int radius, Player admin, String playerName, boolean undo) { Location locMin = new Location(position.getWorld(), position.getBlockX(), position.getBlockY(), position.getBlockZ()); Location locMax = new Location(position.getWorld(), position.getBlockX(), position.getBlockY(), position.getBlockZ()); locMin.subtract(radius, radius, radius); locMax.add(radius, radius, radius); asyncEditSellection(locMin, locMax, admin, playerName, undo); } public void asyncEditSellection(Location locMin, Location locMax, Player admin, String playerName, boolean undo) { Object[] objects = {this, locMin, locMax, admin, playerName, undo}; new AsyncTask(AsyncTask.TaskType.EDIT_SELECTION, objects, plugin).runTaskAsynchronously(plugin); } public void syncEditSellection(Location locMin, Location locMax, Player admin, String playerName, boolean undo) { _adminQuery_mutex.lock(); boolean isAdminQuerying = adminQuering.contains(admin); _adminQuery_mutex.unlock(); if (isAdminQuerying) { sync.sendSyncMessageToPlayer(admin.getName(), ChatColor.RED + "You must wait current query finished before launching a new one."); return; } else { _adminQuery_mutex.lock(); adminQuering.add(admin); _adminQuery_mutex.unlock(); } TreeSet<BlockEvent> blockEventSet = querySellection(this, locMin, locMax, playerName); TreeSet<BlockEvent> chunk = new TreeSet<>(new BlockEventsTimeComparator()); int schedule = 0; for (Iterator<BlockEvent> it = blockEventSet.iterator(); it.hasNext();) { BlockEvent be = it.next(); chunk.add(be); if (chunk.size() >= 50) { schedule++; sync.callChunkRestore(chunk, undo, schedule); chunk = new TreeSet<>(new BlockEventsTimeComparator()); } } sync.callChunkRestore(chunk, undo, schedule + 1); sync.sendSyncMessageToPlayer(admin.getName(), ChatColor.AQUA + " " + blockEventSet.size() + (undo ? " blocks changes has been undone." : " blocks changes has been redone")); _adminQuery_mutex.lock(); adminQuering.remove(admin); _adminQuery_mutex.unlock(); } public static void restoreChunk(Plugin plugin, TreeSet<BlockEvent> chunk, boolean undo) { if (chunk.isEmpty()) { return; } World w = chunk.first().location.getWorld(); if (undo) { for (Iterator<BlockEvent> it = chunk.iterator(); it.hasNext();) { BlockEvent be = it.next(); Location loc = be.location; if (be.placed) { loc.getBlock().setType(Material.AIR); } else { setBlockBEValue(null, be); } } } else { for (Iterator<BlockEvent> it = chunk.descendingIterator(); it.hasNext();) { BlockEvent be = it.next(); Location loc = be.location; if (be.placed) { setBlockBEValue(null, be); } else { loc.getBlock().setType(Material.AIR); } } } } private static void updateAditionalData(Block block, String aditionalData) { if (block.getType() == Material.WALL_SIGN || block.getType() == Material.SIGN_POST) { if (aditionalData != null) { Sign sign = (Sign) block.getState(); sign.setLine(0, aditionalData.substring(0, 14)); sign.setLine(1, aditionalData.substring(15, 29)); sign.setLine(2, aditionalData.substring(30, 44)); sign.setLine(3, aditionalData.substring(45, 59)); sign.update(true); } } } public static void setBlockBEValue(Player admin, BlockEvent be) { Block block = be.location.getBlock(); block.setTypeIdAndData(be.blockTypeID, be.blockData, true); updateAditionalData(block, be.aditionalData); switch (block.getType()) { case WOODEN_DOOR: case IRON_DOOR_BLOCK: Auxiliary.getOtherDoorBlock(block).setTypeIdAndData(block.getTypeId(), Auxiliary.getOtherDoorBlockData(block), true); break; case BED_BLOCK: Auxiliary.getOtherBedBlock(block).setTypeIdAndData(block.getTypeId(), Auxiliary.getOtherBedBlockData(block), true); break; case WALL_SIGN: case SIGN_POST: updateAditionalData(block, be.aditionalData); } if (admin != null) { admin.sendMessage(ChatColor.DARK_GRAY + "" + ChatColor.ITALIC + new SimpleDateFormat("yyyy-MM-dd HH:mm").format(be.eventTime) .concat(" ").concat(be.playerName) .concat(be.placed ? " placed " : " removed ") .concat(Material.getMaterial(be.blockTypeID).toString())); } } }