diff --git a/src/simpleWarBackup/BackupIO.java b/src/simpleWarBackup/BackupIO.java index 29b8310..be48a5b 100644 --- a/src/simpleWarBackup/BackupIO.java +++ b/src/simpleWarBackup/BackupIO.java @@ -1,113 +1,180 @@ package simpleWarBackup; +import java.io.DataInputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; -import java.util.HashMap; +import java.io.RandomAccessFile; import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.World; import org.bukkit.craftbukkit.v1_12_R1.CraftChunk; import org.bukkit.craftbukkit.v1_12_R1.CraftWorld; +import org.bukkit.plugin.java.JavaPlugin; +import net.minecraft.server.v1_12_R1.Chunk; import net.minecraft.server.v1_12_R1.NBTCompressedStreamTools; import net.minecraft.server.v1_12_R1.NBTTagCompound; import net.minecraft.server.v1_12_R1.RegionFile; +import net.minecraft.server.v1_12_R1.World; public class BackupIO { - private static File dataFolder, backupsFolder; + private static File pluginDir, backupsDir; - public static final HashMap> backups - = new HashMap>(); - - /** - * TODO - * - * @param dataFolder - * @param backupsFolder - */ - public static void initialize(File dataFolder, File backupsFolder) + protected static void initialize(JavaPlugin plugin) { - BackupIO.dataFolder = dataFolder; - BackupIO.backupsFolder = backupsFolder; - BackupIO.dataFolder.mkdir(); - BackupIO.backupsFolder.mkdir(); - - initializeBackupsMap(); - } - - /** - * TODO - */ - public static void initializeBackupsMap() - { - HashMap map; - File worldFolder; - File backupFolder; - - /* Directories that do not exist will be made when - * the plugin attempts to save a backup for the - * given world. - */ - for (World world : Bukkit.getWorlds()) - { - backups.put(world, map = new HashMap()); - worldFolder = new File(backupsFolder, world.getName()); - if (worldFolder.exists() && worldFolder.list() != null) - { - for (String backupName : worldFolder.list()) - { - backupFolder = new File(worldFolder, backupName); - map.put(backupName, backupFolder); - } - } - } + pluginDir = plugin.getDataFolder(); + backupsDir = new File(pluginDir, "Backup Files"); } /** * TODO * - * @param backupFolder - * @param chunkX - * @param chunkZ + * @param world + * @param backup * @return */ - public static DataOutputStream chunkDataOutputStream(File backupFolder, int chunkX, int chunkZ) + private static File getBackupDir(CraftWorld world, String backup) { - return RegionFileCache.get(backupFolder, chunkX, chunkZ) // get(...) returns a RegionFile - .b(chunkX & 31, chunkZ & 31); // b(...) returns a DataOutputStream + return new File(new File(backupsDir, world.getName()), backup); } /** * TODO * - * @param backup * @param world + * @param backup * @param chunks * @throws IOException */ - public static void backupChunks(String backup, World world, Chunk... chunks) throws IOException + public static void backup( + CraftWorld world, String backup, CraftChunk... chunks) + throws IOException { - System.out.println("backupChunks has been called");//TODO - final File folder = backups.get(world).get(backup); - final net.minecraft.server.v1_12_R1.World worldNMS - = ((CraftWorld) world).getHandle(); + final World worldNMS = world.getHandle(); + Chunk chunkNMS; + NBTTagCompound chunkNBT; // to be serialized - NBTTagCompound chunkNBT; - DataOutputStream output; + final File backupDir = getBackupDir(world, backup); + int x, z; + RegionFile regionFile; + DataOutputStream chunkPipe; // serialized to this - for (Chunk chunk : chunks) + /* chunkPipe is a series of data outputs pointing to a ChunkBuffer. + * The internal class ChunkBuffer is defined within RegionFile, and + * extends ByteArrayOutputStream. It holds a byte[] buffer and two + * int values representing the chunk's coordinates. + */ + + for (CraftChunk chunk : chunks) { - chunkNBT = ChunkNBTWriter.write(((CraftChunk) chunk).getHandle(), worldNMS); - output = BackupIO.chunkDataOutputStream(folder, chunk.getX(), chunk.getZ()); + chunkNMS = chunk.getHandle(); + chunkNBT = ChunkNBTTools.save(chunkNMS, worldNMS); - NBTCompressedStreamTools.a(chunkNBT, (DataOutput) output); - output.close(); + x = chunk.getX(); + z = chunk.getZ(); + regionFile = RegionFileCache.get(backupDir, x, z); + chunkPipe = regionFile.b(x & 31, z & 31); + + /* below serializes chunkNBT and writes the bytes to chunkPipe, + * which then, on close(), writes those bytes into the RegionFile's + * internal RandomAccessFile, which points to the actual .mca file. + */ + NBTCompressedStreamTools.a(chunkNBT, (DataOutput) chunkPipe); + chunkPipe.close(); } } + + + /** + * TODO + * + * @param world + * @param backup + * @param chunks + */ + private static void restoreWhenSafe(CraftWorld world, String backup, long[] chunks) + { + + } + + + /** + * TODO + * + * @param world + * @param backup + */ + private static void restoreWhenSafe(CraftWorld world, String backup) + { + /* + File worldDir = new File(backupsDir, world.getName()); + File backupDir = new File(worldDir, backup); + File regionDir = new File(backupDir, "region"); + + if (!regionDir.exists() || + !regionDir.isDirectory()) + return; + + String[] regionFileNames = regionDir.list(); + if (regionFileNames == null) + return; + + for (String regionFileName : regionFileNames) + { + //TODO BLAHAHAHABLHABLHBALJHBFLJDHBFKWYBFKUHEBF + } + */ + } + + + + public static boolean restoreTest(int x, int z) throws IOException + { + CraftWorld world = (CraftWorld) Bukkit.getWorld("world"); + + File worldDir = new File(backupsDir, "world"); + File backupDir = new File(worldDir, "test"); + File regionDir = new File(backupDir, "region"); + File mcaFile = new File(regionDir, "r.1.1.mca"); + + RegionFile regionFileSource = RegionFileCache.get(backupDir, x, z); + RegionFile regionFileTarget = RegionFileCache.getFromMinecraft(world, x, z); + + DataInputStream dataInput = regionFileSource.a(x & 31, z & 31); + DataOutputStream dataOutput = regionFileTarget.b(x & 31, z & 31); + + /* Look in RegionFile to find how to get length. + * Length is recorded in the first 4 bytes of each + * chunk's data, in the body of the region file. + * + * Read header for chunk data location, seek to + * that location and read first 4 bytes. This is + * the length for byte[] buf below. + */ + /*int length; + { + RandomAccessFile raf = RegionFileCache.getRAF(regionFileSource); + if (raf == null) return false; + ... + } + + byte[] buf = new byte[length]; + + dataInput.readFully(buf); + dataOutput.write(buf); + dataOutput.close();*/ + + if (dataInput == null) + return false; + + NBTCompressedStreamTools.a( + NBTCompressedStreamTools.a(dataInput), + (java.io.DataOutput) dataOutput); + + return true; + } } diff --git a/src/simpleWarBackup/ChunkNBTWriter.java b/src/simpleWarBackup/ChunkNBTTools.java similarity index 96% rename from src/simpleWarBackup/ChunkNBTWriter.java rename to src/simpleWarBackup/ChunkNBTTools.java index cc97ee2..56a5384 100644 --- a/src/simpleWarBackup/ChunkNBTWriter.java +++ b/src/simpleWarBackup/ChunkNBTTools.java @@ -20,7 +20,7 @@ import net.minecraft.server.v1_12_R1.World; * methods in ChunkRegionLoader, an NMS class. Changes have been commented, so that these * methods may be more easily updated as Spigot releases newer versions. */ -public final class ChunkNBTWriter +public final class ChunkNBTTools { /** * To find the new data version after an update, @@ -38,7 +38,7 @@ public final class ChunkNBTWriter * @param DATA_VERSION * @return */ - public static NBTTagCompound write(Chunk chunk, World world) + public static NBTTagCompound save(Chunk chunk, World world) { System.out.println("ChunkNBTWriter.write has been called");//TODO NBTTagCompound chunkNBT = new NBTTagCompound(); @@ -47,8 +47,8 @@ public final class ChunkNBTWriter chunkNBT.set("Level", levelNBT); chunkNBT.setInt("DataVersion", DATA_VERSION); - ChunkNBTWriter.saveEntities(levelNBT, chunk, world); - ChunkNBTWriter.saveBody(levelNBT, chunk, world); + ChunkNBTTools.saveEntities(levelNBT, chunk, world); + ChunkNBTTools.saveBody(levelNBT, chunk, world); return chunkNBT; } diff --git a/src/simpleWarBackup/Main.java b/src/simpleWarBackup/Main.java index 1ac3c4e..176752e 100644 --- a/src/simpleWarBackup/Main.java +++ b/src/simpleWarBackup/Main.java @@ -23,37 +23,56 @@ public class Main extends JavaPlugin implements Listener getCommand("testBackupChunk").setExecutor(this); getCommand("testRestoreChunk").setExecutor(this); - File dataFolder = this.getDataFolder(); - BackupIO.initialize(dataFolder, - new File(dataFolder, - "Backup Files")); + BackupIO.initialize(this); } public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!(sender instanceof Player) || !sender.getName().equals("iie")) return false; - System.out.println("player is iie");//TODO + if (!(sender instanceof Player)) return false; + if (!sender.getName().equals("iie")) return false; + sender.sendMessage("some command received"); Location loc = ((Player) sender).getLocation(); + World world = loc.getWorld(); + Chunk chunk = loc.getChunk(); - World world = loc.getWorld(); - Chunk chunk = loc.getChunk(); + //test-chunks to be test-restored + int[][] xz = {{187, 187}, + {40 , 40 }}; if (command.getName().equals("testBackupChunk")) { - System.out.println("command label = testBackupChunk");//TODO - try { - BackupIO.backupChunks("test", world, chunk); - System.out.println("tried BackupIO.backupChunks");//TODO - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + try { BackupIO.backup((CraftWorld) world, "test", (CraftChunk) chunk); } + catch (IOException e) { e.printStackTrace(); } } - - else if (label.equals("testRestoreChunk")) + else if (command.getName().equals("testRestoreChunk")) { - + sender.sendMessage("testRestoreChunk command received"); + for (int[] coords : xz) + { + if (world.isChunkLoaded(coords[0], coords[1])) + { + sender.sendMessage("chunk " + coords[0] + ", " + coords[1] + " is still loaded"); + return false; + } + } + { + sender.sendMessage("chunk is not loaded"); + try + { + for (int[] coords : xz) + { + sender.sendMessage( + BackupIO.restoreTest(coords[0], coords[1]) ? "restore worked" : + "restore failed"); + } + } + catch (IOException e) + { + e.printStackTrace(); + sender.sendMessage("IOException, restore failed"); + } + } } return true; } diff --git a/src/simpleWarBackup/RegionFileCache.java b/src/simpleWarBackup/RegionFileCache.java index 405eb18..38ab098 100644 --- a/src/simpleWarBackup/RegionFileCache.java +++ b/src/simpleWarBackup/RegionFileCache.java @@ -1,49 +1,71 @@ package simpleWarBackup; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.RandomAccessFile; import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.v1_12_R1.CraftWorld; + import net.minecraft.server.v1_12_R1.RegionFile; +import net.minecraft.server.v1_12_R1.World; /** * TODO write this */ -public class RegionFileCache +public final class RegionFileCache { - private static final Map regionFiles = new HashMap(); + private static final int cap = 16; //capacity - how many regionFiles to keep cached + private static final Map cache = new HashMap(); /** - * Copied from net.minecraft.server.RefionFileCache.a(File, int, int). Returns any - * cached RegionFile mapped to the given File. Not finding one, creates and caches, - * then returns, a new RegionFile. Before caching a new RegionFile, checks that the - * cache contains fewer than 16 RegionFiles, and, finding 16 or more, calls - * {@link #clearCache() RegionFileCache.clearCace()}. + * Returns name of the region file that would contain the given chunk coordinates. + * Name follows the standard minecraft format: "r.[region x].[region z].mca". + * Region coordinate = chunk coordinate >> 5. * - * @param backupFolder folder containing "region" folder - * @param x chunk X - * @param z chunk Z + * @param x global chunk coordinate x + * @param z global chunk coordinate z * @return */ - public static synchronized RegionFile get(File backupFolder, int x, int z) + public static String path(int x, int z) { - System.out.println("RegionFileCache.get has been called");//TODO - x >>= 5; z >>= 5; - File regionFolder = new File(backupFolder, "region"); - File regionFileFile = new File(regionFolder, "r." + x + "." + z + ".mca"); - RegionFile regionFile = regionFiles.get(regionFileFile); + return "r." + (x >> 5) + "." + (z >> 5) + ".mca"; + } + + /** + * Copied from net.minecraft.server.RefionFileCache.a(File, int, int).

+ * + * Returns any cached RegionFile mapped to the given File. Not finding one, creates + * and caches, then returns, a new RegionFile. Before caching a new RegionFile, checks + * that the cache contains fewer than 16 RegionFiles, and, finding 16 or more, calls + * {@link #clearCache() RegionFileCache.clearCache()}. + * + * @param backupDir where to look for a "region" folder + * @param x global chunk coordinate x + * @param z global chunk coordinate z + * @return + */ + public static synchronized RegionFile get(File backupDir, int x, int z) + { + File regionDir = new File(backupDir, "region"); + File mcaFile = new File(regionDir, path(x,z)); + + RegionFile regionFile = cache.get(mcaFile); if (regionFile != null) return regionFile; - if (!regionFolder.exists()) - regionFolder.mkdirs(); - if (regionFiles.size() >= 16) + if (!regionDir.exists()) + regionDir.mkdirs(); + if (cache.size() >= cap) clearCache(); - regionFile = new RegionFile(regionFileFile); - RegionFileCache.regionFiles.put(regionFileFile, regionFile); + regionFile = new RegionFile(mcaFile); + RegionFileCache.cache.put(mcaFile, regionFile); + RegionFileCache.cacheRAF(mcaFile, regionFile); return regionFile; } @@ -54,7 +76,7 @@ public class RegionFileCache */ public static synchronized void clearCache() { - Iterator iterator = RegionFileCache.regionFiles.values().iterator(); + Iterator iterator = RegionFileCache.cache.values().iterator(); /* regionFile.c() closes the private RandomAccessFile * inside the regionFile object, and that's all it does. @@ -66,6 +88,97 @@ public class RegionFileCache try { if (regionFile != null) regionFile.c(); } catch (IOException e) { e.printStackTrace(); } } - RegionFileCache.regionFiles.clear(); + RegionFileCache.cache.clear(); + } + + + /** + * TODO + * + * @param world + * @param x + * @param z + * @return + */ + public static RegionFile getFromMinecraft(CraftWorld world, int x, int z) + { + /* root directory of the world, the directory + * sharing the world's name and holding its + * data. 'region' folder is found in here. + */ + File dir = world.getHandle().getDataManager().getDirectory(); + + return net.minecraft.server.v1_12_R1.RegionFileCache.a(dir, x, z); + } + + + /** + * TODO + * + * @param world + * @param x + * @param z + * @return + */ + public static RegionFile getFromMinecraft(World world, int x, int z) + { + /* root directory of the world, the directory + * sharing the world's name and holding its + * data. 'region' folder is found in here. + */ + File dir = world.getDataManager().getDirectory(); + + return net.minecraft.server.v1_12_R1.RegionFileCache.a(dir, x, z); + } + + + + /*-------------------------------RandomAccessFiles-------------------------------*/ + + /* RegionFiles keep their actual .mca files private, but those bytes can be useful + * to read. The auxiliary cache below stores, for each cached RegionFile, a read-only + * RandomAccessFile pointing to the .mca file. + */ + + private static Map cacheRAF = new HashMap(); + + /** + * TODO + * + * @param file + * @param regionFile + */ + private static void cacheRAF(File file, RegionFile regionFile) + { + try { cacheRAF.put(regionFile, new RandomAccessFile(file, "r")); } + catch (FileNotFoundException e) { e.printStackTrace(); } + } + + + /** + * TODO + * + * @param regionFile + * @return + */ + public static RandomAccessFile getRAF(RegionFile regionFile) + { + return cacheRAF.get(regionFile); + } + + + /** + * TODO + * + * @param regionFile + * @param x + * @param z + * @return + */ + public static int getChunkByteLength(RegionFile regionFile, int x, int z) + { + return 0;//TODO } }