diff --git a/src/simpleWarBackup/BackupIO.java b/src/simpleWarBackup/BackupIO.java index 1c9c8a8..5343a0d 100644 --- a/src/simpleWarBackup/BackupIO.java +++ b/src/simpleWarBackup/BackupIO.java @@ -22,7 +22,14 @@ public class BackupIO { private static File pluginDir, backupsDir; - protected static void initialize(JavaPlugin plugin) + /** + * Defines two File variables: for the plugin's data folder, and for the "Backup Files" + * folder. Does not create either directory. The directories themselves are created + * as-needed, elsewhere in the plugin, during the process of saving a backup. + * + * @param plugin + */ + static void initialize(JavaPlugin plugin) //called from Main onEnable() { pluginDir = plugin.getDataFolder(); backupsDir = new File(pluginDir, "Backup Files"); @@ -30,15 +37,25 @@ public class BackupIO /** - * TODO + * Returns the parent directory containing the "region" destination directory, where + * the .mca files are stored. The path proceeds: backups > backup > town > world. + * This method returns the world directory.

* + * Note: this method does not create any of these directories. All directories are + * created as-needed, elsewhere in the plugin, during the process of saving a backup. + * + * @param backup TODO + * @param town * @param world - * @param backup * @return */ - private static File getBackupDir(CraftWorld world, String backup) + private static File getDir(String backup, String town, CraftWorld world) { - return new File(new File(backupsDir, world.getName()), backup); + return new File( + new File( + new File(backupsDir, backup ) + , town ) + , world.getName() ); } @@ -51,20 +68,20 @@ public class BackupIO * @throws IOException */ public static void backup( - CraftWorld world, String backup, CraftChunk... chunks) + String backup, String town, CraftWorld world, CraftChunk... chunks) throws IOException { final World worldNMS = world.getHandle(); Chunk chunkNMS; NBTTagCompound chunkNBT; // to be serialized - final File backupDir = getBackupDir(world, backup); + final File worldDir = getDir(backup, town, world); int x, z; RegionFile regionFile; DataOutputStream chunkPipe; // serialized to this /* chunkPipe is a series of data outputs pointing to a ChunkBuffer. - * The internal class ChunkBuffer is defined within RegionFile, and + * ChunkBuffer is an internal class defined within RegionFile that * extends ByteArrayOutputStream. It holds a byte[] buffer and two * int values representing the chunk's coordinates. */ @@ -76,11 +93,11 @@ public class BackupIO x = chunk.getX(); z = chunk.getZ(); - regionFile = RegionFileCache.get(backupDir, x, z); + regionFile = RegionFileCache.get(worldDir, 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 + /* below serializes chunkNBT, and writes those bytes to chunkPipe, + * which then, on close(), writes the bytes into this RegionFile's * internal RandomAccessFile, which points to the actual .mca file. */ NBTCompressedStreamTools.a(chunkNBT, (DataOutput) chunkPipe); @@ -94,10 +111,77 @@ public class BackupIO * * @param world * @param backup - * @param chunks + * @param chunks (chunk coords, larger half is z) + * + * @throws IOException */ - private static void restoreWhenSafe(CraftWorld world, String backup, long[] chunks) + private static void restore( + String backup, String town, CraftWorld world, long... chunks) + throws IOException { + final File worldDir = getDir(backup, town, world); + + int x, z; + + RegionFile source; + RegionFile target; + DataInputStream dataInput; + DataOutputStream dataOutput; + + int length; + byte[] buf; + + /* dataInput is the end of a series of inputs, which proceed in + * the following order: ByteArrayInputStream > GZIPInputStream > + * BufferedInputStream > DataInputStream. + * + * dataOutput is a series of outputs pointing to a ChunkBuffer. + * ChunkBuffer is an internal class defined within RegionFile that + * extends ByteArrayOutputStream. It holds a byte[] buffer and two + * int values representing the chunk's coordinates. + */ + for (long chunk : chunks) + { + x = (int) chunk; + z = (int) ( chunk >> 32 ); + + source = RegionFileCache.get(worldDir, x, z); + target = RegionFileCache.getFromMinecraft(world, x, z); + dataInput = source.a(x & 31, z & 31); + dataOutput = target.b(x & 31, z & 31); + + length = RegionFileCache.getChunkLength(source, x, z); + + /* ------------------------------------------------------------- + * getChunkLength(...) attempts to read from RegionFile source's + * private .mca file. It will return length 0 if there is an error, + * otherwise it will return the length value recorded in the file. + * + * Below are two approaches to writing bytes from the source to + * the target. The first is faster, but we need to know the exact + * length of the chunk's data to use this approach. + */ + if (length > 0) + { + buf = new byte[length]; + dataInput.readFully(buf); + dataOutput.write(buf); + } + /* In the case of an error, i.e. length 0, plan B is to convert the + * bytes into an NBTTagCompound, then serialize the NBTTagCompound + * back into bytes at the target location. This approach lets the + * native game handle the data as it normally would, using tools + * built for the purpose, at the cost of some extra effort. + */ + else + { + NBTCompressedStreamTools.a( + NBTCompressedStreamTools.a(dataInput), + (java.io.DataOutput) dataOutput); + } + dataOutput.close(); //writes to the target file + } + } @@ -108,26 +192,9 @@ public class BackupIO * @param world * @param backup */ - private static void restoreWhenSafe(CraftWorld world, String backup) + private static void restoreAll(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 - } - */ } diff --git a/src/simpleWarBackup/ChunkCoordsListOLD.java b/src/simpleWarBackup/ChunkCoordsListOLD.java new file mode 100644 index 0000000..64e965a --- /dev/null +++ b/src/simpleWarBackup/ChunkCoordsListOLD.java @@ -0,0 +1,98 @@ +package simpleWarBackup; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +public class ChunkCoordsListOLD +{ + private static final YamlConfiguration yml = new YamlConfiguration(); + private static final char ps = yml.options().pathSeparator(); + private static File file; + + /** + * TODO + * + * @param plugin + */ + private static void initialize(JavaPlugin plugin) + { + file = new File(plugin.getDataFolder(), "Chunks.yml"); + + /* "Garbage Bin" contains the names of backups that have already been fully + * written back into the game world, and are presumably no longer needed. + * Each entry includes a timestamp, and is deleted after 30 days. + * + * "Storage" lists by coordinates the chunks stored in each of the backups. + * Coordinates are stored as longs, with the smaller half representing x and + * the larger half representing z. + * + * "Queue" lists by coordinates the chunks in each of the backups that are + * slated to be written back into the game world. Coordinates are stored + * in the same manner as in "Storage." + */ + try + { + if (file.exists()) + { + yml.load(file); + + if (!yml.contains("Garbage Bin")) yml.createSection("Garbage Bin"); + if (!yml.contains("Storage")) yml.createSection("Storage"); + if (!yml.contains("Queue")) yml.createSection("Queue"); + } + else + { + yml.createSection("Garbage Bin"); + yml.createSection("Storage"); + yml.createSection("Queue"); + } + yml.save(file); + } + catch(IOException | InvalidConfigurationException e) { e.printStackTrace(); } + } + + + + private static void write(String path, int chunkX) + { + + } + + + /** + * TODO + * + * @param backup + * @param town + * @param world + * @param chunks (chunk coords) + */ + static void backup(String backup, String town, String world, long... chunks) + { + String path = "Storage" + ps + backup + ps + town + ps + world; + + int x, z; + int[] rows; + + for (long chunk : chunks) + { + x = (int) chunk; + z = (int) ( chunk >> 32 ); + + //write(path + ps + (x >> 5) + ", " + (z >> 5) + ps + (z & 31)); + + + } + } + + + static void queue(String backup, String town, String world, long... chunks) + { + yml.set("Queue" + ps + backup + ps + town + ps + world, chunks); + } +} diff --git a/src/simpleWarBackup/RegionChunkList.java b/src/simpleWarBackup/RegionChunkList.java new file mode 100644 index 0000000..b469ea8 --- /dev/null +++ b/src/simpleWarBackup/RegionChunkList.java @@ -0,0 +1,6 @@ +package simpleWarBackup; + +public class RegionChunkList +{ + +} diff --git a/src/simpleWarBackup/RegionFileCache.java b/src/simpleWarBackup/RegionFileCache.java index 29ae231..d2cd43b 100644 --- a/src/simpleWarBackup/RegionFileCache.java +++ b/src/simpleWarBackup/RegionFileCache.java @@ -20,7 +20,7 @@ import net.minecraft.server.v1_12_R1.World; */ public final class RegionFileCache { - private static final int cap = 16; //capacity - how many regionFiles to keep cached + private static final int cap = 32; //capacity - how many regionFiles to keep cached private static final Map cache = new HashMap(); /** @@ -45,7 +45,7 @@ public final class RegionFileCache * 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 backupDir parent folder containing the "region" folder * @param x global chunk coordinate x * @param z global chunk coordinate z * @return @@ -66,7 +66,7 @@ public final class RegionFileCache regionFile = new RegionFile(mcaFile); RegionFileCache.cache.put(mcaFile, regionFile); - RegionFileCache.cacheRAF(mcaFile, regionFile); + RegionFileCache.cacheChunkAccess(mcaFile, regionFile); return regionFile; } @@ -90,7 +90,7 @@ public final class RegionFileCache catch (IOException e) { e.printStackTrace(); } } RegionFileCache.cache.clear(); - RegionFileCache.cacheRAF.clear(); + RegionFileCache.cacheChunkAccess.clear(); } @@ -114,55 +114,16 @@ public final class RegionFileCache } - /** - * 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); - } + /*=================================Chunk Access=================================*/ - - /*---------------------------------Chunk Access---------------------------------*/ - - /* 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, and an int[] storing the offset values - * for each chunk's bytes within the file. + /* Secondary cache, for holding references to the private fields "c" and "d" in each + * RegionFile cached above. TODO write more */ private static Map cacheRAF = new HashMap(); - - /** - * TODO - */ - public static class ChunkAccess - { - //private static Field offsetField = RegionFile.class.getDeclaredField("d"); - - final RandomAccessFile bytes; - final int[] offsets; - - ChunkAccess(RandomAccessFile bytes, int[] chunkOffsets) - { - this.bytes = bytes; - this.offsets = chunkOffsets; - } - } + ChunkAccess> cacheChunkAccess = new HashMap(); /** * TODO @@ -170,21 +131,18 @@ public final class RegionFileCache * @param file * @param regionFile */ - private static void cacheRAF(File file, RegionFile regionFile) + private static void cacheChunkAccess(File file, RegionFile regionFile) { - - } - - - /** - * TODO - * - * @param regionFile - * @return - */ - public static RandomAccessFile getRAF(RegionFile regionFile) - { - return null;//TODO + try + { + cacheChunkAccess.put(regionFile, new ChunkAccess(regionFile)); + } + catch (FileNotFoundException | + IllegalAccessException | + IllegalArgumentException e) + { + e.printStackTrace();//TODO + } } @@ -196,8 +154,98 @@ public final class RegionFileCache * @param z * @return */ - public static int getChunkByteLength(RegionFile regionFile, int x, int z) + public static int getChunkLength(RegionFile regionFile, int x, int z) { - return 0;//TODO + ChunkAccess access = cacheChunkAccess.get(regionFile); + + /* "offset" refers to the location of the chunk's bytes + * within the .mca file - or how many bytes into the file + * to seek when reading - divided by 4096. + */ + int offset; + + /* The first four bytes at that location, the first four + * bytes of the chunk's data, are an int recording the + * chunk's length in bytes, not including those four. + * + * The next byte represents compression scheme. This byte, + * like the four previous bytes, is omitted when the game + * reads a chunk's data. + * + * This method returns the length of data actually read. + */ + if (access != null && (offset = access.offset[x + z * 32]) > 0) + { + try + { + access.bytes.seek(offset * 4096); + return access.bytes.readInt() - 1; + } + catch (IOException e) { e.printStackTrace(); } + } + return 0; } + + + + /** + * Gives access to the private fields {@code int[] d}, and {@code RandomAccessFile c}, + * within a RegionFile object. The names of these fields may change in new releases of + * Spigot. TODO explain what both of those fields are + */ + private static class ChunkAccess + { + static final Field offsetField; + final int[] offset; + + static final Field bytesField; + final RandomAccessFile bytes; + + static + { + Field tmp1 = null; + Field tmp2 = null; + try + { + tmp1 = RegionFile.class.getDeclaredField("d"); + tmp1.setAccessible(true); + + tmp2 = RegionFile.class.getDeclaredField("c"); + tmp2.setAccessible(true); + } + catch (NoSuchFieldException | SecurityException e) + { + /* TODO take more drastic action. Either + * Field missing would mean that Minecraft + * has refactored, breaking the whole plugin + */ + e.printStackTrace(); + } + offsetField = tmp1; + bytesField = tmp2; + } + + + /** + * Creates a new DataAccess object, which holds references to the private variables + * {@code int[] d} and {@code RandomAccessFile c} within the given RegionFile. Variables + * are accessed by reflection. TODO write more + * + * @param regionFile the RegionFile whose chunks are to be accessed + * + * @throws FileNotFoundException + * @throws IllegalAccessException + * @throws IllegalArgumentException + */ + ChunkAccess(RegionFile regionFile) + throws FileNotFoundException, + IllegalAccessException, + IllegalArgumentException + { + this.offset = (int[]) offsetField.get(regionFile); + this.bytes = (RandomAccessFile) bytesField .get(regionFile); + } + } + + }