package me.badbones69.crazycrates.multisupport.nms.v1_14_R1; import net.minecraft.server.v1_14_R1.*; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.craftbukkit.v1_14_R1.CraftWorld; import java.io.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * All you need to handle the Mojang structure format * @author Michel_0 * @version 2.4 */ public class StructureService { /** * A comfort method for all lazy guys. Automatically switches to structure arrays, when using an area larger than 32x32x32 * @param corners - 2 opposite edges (order doesn't matter) * @param destination - The destination file (for single structure) or the destination folder (for structure array) */ public static void createAndSaveAny(Location[] corners, File destination) throws IOException { StructureService.createAndSaveAny(corners, "?", destination); } /** * A comfort method for all lazy guys. Automatically switches to structure arrays, when using an area larger than 32x32x32 * @param corners - 2 opposite edges (order doesn't matter) * @param author - The listed author * @param destination - The destination file (for single structure) or the destination folder (for structure array) */ public static void createAndSaveAny(Location[] corners, String author, File destination) throws IOException { Location[] normalized = StructureService.normalizeEdges(corners[0], corners[1]); int[] dimensions = StructureService.getDimensions(normalized); if (dimensions[0] > 32 || dimensions[1] > 32 || dimensions[2] > 32) { DefinedStructure[] structures = StructureService.createStructuresArray(normalized, author); StructureService.saveStructuresArray(structures, destination); StructureService.saveAreaDimFile(dimensions, destination); } else { DefinedStructure structure = StructureService.createSingleStructure(normalized, author); StructureService.saveSingleStructure(structure, destination); } } /** * Creates a single structure of maximum 32x32x32 blocks. If you need a larger area, use {@link #createStructuresArray(Location[], String)} * @param corners - The edges of the are (order doesn't matter) * @return DefinedStructure - The new structure instance */ public static DefinedStructure createSingleStructure(Location[] corners) { return StructureService.createSingleStructure(corners, "?"); } /** * Creates a single structure of maximum 32x32x32 blocks. If you need a larger area, use {@link #createStructuresArray(Location[], String)} * @param corners - The edges of the are (order doesn't matter) * @param author - The listed author of the structure * @return DefinedStructure - The new structure instance */ public static DefinedStructure createSingleStructure(Location[] corners, String author) { if (corners.length != 2) throw new IllegalArgumentException("An area needs to be set up by exactly 2 opposite edges!"); Location[] normalized = StructureService.normalizeEdges(corners[0], corners[1]); WorldServer world = ((CraftWorld) normalized[0].getWorld()).getHandle(); int[] dimensions = StructureService.getDimensions(normalized); if (dimensions[0] > 32 || dimensions[1] > 32 || dimensions[2] > 32) throw new IllegalArgumentException("A single structure can only be 32x32x32! If you need more, use #createStructuresArea."); DefinedStructure structure = new DefinedStructure(); structure.a(world, new BlockPosition(normalized[0].getBlockX(), normalized[0].getBlockY(), normalized[0].getBlockZ()), new BlockPosition(dimensions[0], dimensions[1], dimensions[2]), true, Blocks.STRUCTURE_VOID); structure.a(author); return structure; } /** * Saves a structure NBT file to a given destination file * @param structure - The structure to be saved * @param destination - The NBT file to be created */ public static void saveSingleStructure(DefinedStructure structure, File destination) throws IOException { NBTTagCompound fileTag = new NBTTagCompound(); fileTag = structure.a(fileTag); if (structure.b() != null && !structure.b().equals("?")) fileTag.setString("author", structure.b()); NBTCompressedStreamTools.a(fileTag, new FileOutputStream(new File(destination + ".nbt"))); } /** * Splits up an area in 32x32x32 structures, creates those and fills an array with them * @param corners - 2 Edges of the area (order doesn't matter) * @return DefinedStructure[] - The structures in a one dimensional array, sorted by y, z, x (iterates along x, then z, then y) */ public static DefinedStructure[] createStructuresArray(Location[] corners) { return StructureService.createStructuresArray(corners, "?"); } /** * Splits up an area in 32x32x32 structures, creates those and fills an array with them * @param corners - 2 Edges of the area (order doesn't matter) * @param author - The listed author of the structure * @return DefinedStructure[] - The structures in a one dimensional array, sorted by y, z, x (iterates along x, then z, then y) */ public static DefinedStructure[] createStructuresArray(Location[] corners, String author) { if (corners.length != 2) throw new IllegalArgumentException("An area needs to be set up by exactly 2 opposite edges!"); Location[] normalized = StructureService.normalizeEdges(corners[0], corners[1]); WorldServer world = ((CraftWorld) normalized[0].getWorld()).getHandle(); int[] dimensions = StructureService.getDimensions(normalized); int[] areas = StructureService.getAreaSections(dimensions); DefinedStructure[] structures = new DefinedStructure[areas[0] * areas[1] * areas[2]]; for (int x = 0; x < areas[0]; x++) { for (int y = 0; y < areas[1]; y++) { for (int z = 0; z < areas[2]; z++) { DefinedStructure structure = new DefinedStructure(); int width, height, length; if (x == areas[0] - 1 && dimensions[0] % 32 != 0) width = dimensions[0] % 32; else width = 32; if (y == areas[1] - 1 && dimensions[1] % 32 != 0) height = dimensions[1] % 32; else height = 32; if (z == areas[2] - 1 && dimensions[2] % 32 != 0) length = dimensions[2] % 32; else length = 32; structure.a(world, new BlockPosition((x * 32) + normalized[0].getBlockX(), (y * 32) + normalized[0].getBlockY(), (z * 32) + normalized[0].getBlockZ()), new BlockPosition(width, height, length), true, Blocks.STRUCTURE_VOID); structure.a(author); structures[StructureService.getYzxIndex(x, y, z, areas[0], areas[2])] = structure; } } } return structures; } /** * Saves an one dimensional array of structures to files within a given folder. For importing, the file structure is important, don't change it. * @param structures - The structures array * @param folder - The folder, which will directly be filled */ public static void saveStructuresArray(DefinedStructure[] structures, File folder) throws IOException { if (!folder.exists()) folder.mkdirs(); for (int i = 0; i < structures.length; i++) { NBTTagCompound fileTag = new NBTTagCompound(); fileTag = structures[i].a(fileTag); if (structures[i].b() != null && !structures[i].b().equals("?")) fileTag.setString("author", structures[i].b()); NBTCompressedStreamTools.a(fileTag, new FileOutputStream(new File(folder, folder.getName() + "_" + i + ".nbt"))); } } /** * A comfort method for all lazy guys. Automatically switches to structure arrays, when the source is a folder, no file * @param source - The structure array folder or the structure NBT file * @param startEdge - The starting corner for pasting (lowest x, y, z coordinates) * @param rotation - You may rotate the structure by 90 degrees steps */ public static void loadAndInsertAny(File source, Location startEdge, Rotation rotation) throws IOException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (source.isDirectory()) { DefinedStructure[] structures = StructureService.loadLegacyStructuresArray(source, startEdge.getWorld()); StructureService.insertStructuresArray(structures, StructureService.loadAreaDimFile(source), startEdge, rotation.getNMSRot()); } else { DefinedStructure structure = StructureService.loadLegacySingleStructure(source, startEdge.getWorld()); StructureService.insertSingleStructure(structure, startEdge, rotation.getNMSRot()); } } /** * A comfort method for all lazy guys. Automatically switches to structure arrays, when the source is a folder, no file * @param source - The structure array folder or the structure NBT file * @param startEdge - The starting corner for pasting (lowest x, y, z coordinates) */ public static void loadAndInsertAny(File source, Location startEdge) throws IOException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { StructureService.loadAndInsertAny(source, startEdge, Rotation.DEG_0); } /** * Loads a single structure NBT file and creates a new structure object instance. Also converts pre1.13 versions. * @param source - The structure file * @param world - The world (actually ANY world) instance to receive a data fixer (legacy converter) * @return DefinedStructure - The new instance * @deprecated Only for pre1.13, uses the NMS 1.13 DataFixer to convert stuff */ @Deprecated public static DefinedStructure loadLegacySingleStructure(File source, World world) throws IOException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { Method parseAndConvert = DefinedStructureManager.class.getDeclaredMethod("a", InputStream.class); parseAndConvert.setAccessible(true); // 1.13 WorldServer#C, 1.13.1 WorldServer#D return (DefinedStructure) parseAndConvert.invoke(((CraftWorld) world).getHandle().r(), new FileInputStream(source)); } /** * Loads a single structure NBT file and creates a new structure object instance. Works only for structures created with 1.13 or later! * @param source - The structure file * @return DefinedStructure - The new instance */ public static DefinedStructure loadSingleStructure(File source) throws IOException { DefinedStructure structure = new DefinedStructure(); structure.b(NBTCompressedStreamTools.a(new FileInputStream(source))); return structure; } /** * Pastes a single structure into the world * @param structure - The structure to be pasted * @param startEdge - The starting corner with the lowest x, y, z coordinates * @param rotation - You may rotate the structure by 90 degrees steps */ public static void insertSingleStructure(Object structure, Location startEdge, Rotation rotation) { StructureService.insertSingleStructure((DefinedStructure) structure, startEdge, rotation.getNMSRot()); } /** * Pastes a single structure into the world * @param structure - The structure to be pasted * @param startEdge - The starting corner with the lowest x, y, z coordinates * @param rotation - You may rotate the structure by 90 degrees steps */ public static void insertSingleStructure(DefinedStructure structure, Location startEdge, EnumBlockRotation rotation) { WorldServer world = ((CraftWorld) startEdge.getWorld()).getHandle(); DefinedStructureInfo structInfo = new DefinedStructureInfo().a(EnumBlockMirror.NONE).a(rotation).a(false).a((ChunkCoordIntPair) null).c(false).a(new Random()); BlockPosition blockPosition = new BlockPosition(startEdge.getBlockX(), startEdge.getBlockY(), startEdge.getBlockZ()); structure.a(world, blockPosition, structInfo); } public static List<Location> getSingleStructureLocations(File source, Location startEdge) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException { DefinedStructure structure = StructureService.loadLegacySingleStructure(source, startEdge.getWorld()); List<Location> locations = new ArrayList<>(); NBTTagCompound fileTag = new NBTTagCompound(); fileTag = structure.a(fileTag); NBTTagList list = (NBTTagList) fileTag.get("size"); Location endEdge = startEdge.clone().add(Integer.parseInt(list.get(0).toString()) - 1, Integer.parseInt(list.get(1).toString()) - 1, Integer.parseInt(list.get(2).toString()) - 1); int topBlockX = (startEdge.getBlockX() < endEdge.getBlockX() ? endEdge.getBlockX() : startEdge.getBlockX()); int bottomBlockX = (startEdge.getBlockX() > endEdge.getBlockX() ? endEdge.getBlockX() : startEdge.getBlockX()); int topBlockY = (startEdge.getBlockY() < endEdge.getBlockY() ? endEdge.getBlockY() : startEdge.getBlockY()); int bottomBlockY = (startEdge.getBlockY() > endEdge.getBlockY() ? endEdge.getBlockY() : startEdge.getBlockY()); int topBlockZ = (startEdge.getBlockZ() < endEdge.getBlockZ() ? endEdge.getBlockZ() : startEdge.getBlockZ()); int bottomBlockZ = (startEdge.getBlockZ() > endEdge.getBlockZ() ? endEdge.getBlockZ() : startEdge.getBlockZ()); for (int x = bottomBlockX; x <= topBlockX; x++) { for (int z = bottomBlockZ; z <= topBlockZ; z++) { for (int y = bottomBlockY; y <= topBlockY; y++) { locations.add(new Location(startEdge.getWorld(), x, y, z)); } } } return locations; } public static Location getOtherEdge(File source, Location startEdge) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException { DefinedStructure structure = StructureService.loadLegacySingleStructure(source, startEdge.getWorld()); NBTTagCompound fileTag = new NBTTagCompound(); fileTag = structure.a(fileTag); NBTTagList list = (NBTTagList) fileTag.get("size"); return startEdge.clone().add(Integer.parseInt(list.get(0).toString()) - 1, Integer.parseInt(list.get(1).toString()) - 1, Integer.parseInt(list.get(2).toString()) - 1); } /** * Loads all structure segments back into one array. The folder file contents are important, don't change it after saving. Also converts pre1.13 versions. * @param folder - The folder containing an NBT file by the same name with dimensions and NBT files of structures with an counter added * @param world - The world (actually ANY world) instance to receive a data fixer (legacy converter) * @return DefinedStructure[] - A one dimensional array, just like the folder * @deprecated Only for pre 1.13, uses the NMS 1.13 DataFixer to convert stuff */ @Deprecated public static DefinedStructure[] loadLegacyStructuresArray(File folder, World world) throws IOException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!new File(folder, folder.getName() + ".nbt").exists()) throw new IllegalArgumentException("This is not a valid structure area export folder!"); DefinedStructure[] structures = new DefinedStructure[folder.listFiles().length - 1]; for (File file : folder.listFiles()) { if (!file.getName().equals(folder.getName() + ".nbt")) { Method parseAndConvert = DefinedStructureManager.class.getDeclaredMethod("a", InputStream.class); parseAndConvert.setAccessible(true); // 1.13 WorldServer#C, 1.13.1 WorldServer#D DefinedStructure structure = (DefinedStructure) parseAndConvert.invoke(((CraftWorld) world).getHandle().r(), new FileInputStream(file)); String suffix = file.getName().split("_")[file.getName().split("_").length - 1]; suffix = suffix.substring(0, suffix.length() - 4); structures[Integer.parseInt(suffix)] = structure; } } return structures; } /** * Loads all structure segments back into one array. The folder file contents are important, don't change it after saving. Works only for structures created with 1.13 or later! * @param folder - The folder containing an NBT file by the same name with dimensions and NBT files of structures with an counter added * @return DefinedStructure[] - A one dimensional array, just like the folder */ public static DefinedStructure[] loadStructuresArray(File folder) throws IOException { if (!new File(folder, folder.getName() + ".nbt").exists()) throw new IllegalArgumentException("This is not a valid structure area export folder!"); DefinedStructure[] structures = new DefinedStructure[folder.listFiles().length - 1]; for (File file : folder.listFiles()) { if (!file.getName().equals(folder.getName() + ".nbt")) { DefinedStructure structure = new DefinedStructure(); structure.b(NBTCompressedStreamTools.a(new FileInputStream(file))); String suffix = file.getName().split("_")[file.getName().split("_").length - 1]; suffix = suffix.substring(0, suffix.length() - 4); structures[Integer.parseInt(suffix)] = structure; } } return structures; } /** * Pastes a array of structures into the world * @param structures - A one dimensional array of structures, sorted by y, z, x (iterates along x, then z, then y) * @param dimensions - The width, height, length of the complete resulting area * @param startEdge - The starting edge with the lowest x, y, z coordinates * @param rotation - You may rotate the structure by 90 degrees steps */ public static void insertStructuresArray(Object[] structures, int[] dimensions, Location startEdge, Rotation rotation) { StructureService.insertStructuresArray((DefinedStructure[]) structures, dimensions, startEdge, rotation.getNMSRot()); } /** * Pastes a array of structures into the world * @param structures - A one dimensional array of structures, sorted by y, z, x (iterates along x, then z, then y) * @param dimensions - The width, height, length of the complete resulting area * @param startEdge - The starting edge with the lowest x, y, z coordinates * @param rotation - You may rotate the structure by 90 degrees steps */ public static void insertStructuresArray(DefinedStructure[] structures, int[] dimensions, Location startEdge, EnumBlockRotation rotation) { int[] areas = StructureService.getAreaSections(dimensions); WorldServer world = ((CraftWorld) startEdge.getWorld()).getHandle(); for (int x = 0; x < areas[0]; x++) { for (int y = 0; y < areas[1]; y++) { for (int z = 0; z < areas[2]; z++) { DefinedStructureInfo structInfo = new DefinedStructureInfo().a(EnumBlockMirror.NONE).a(rotation).a(false).a((ChunkCoordIntPair) null).c(false).a(new Random()); structures[StructureService.getYzxIndex(x, y, z, areas[0], areas[2])].a(world, new BlockPosition((x * 32) + startEdge.getBlockX(), (y * 32) + startEdge.getBlockY(), (z * 32) + startEdge.getBlockZ()), structInfo); } } } } /** * Saves a simple NBT file (same name as folder name), containing an integer array with the given dimensions * @param dimension - The integer array * @param folder - The parent folder */ public static void saveAreaDimFile(int[] dimension, File folder) throws IOException { NBTTagCompound fileTag = new NBTTagCompound(); fileTag.setIntArray("dimensions", dimension); NBTCompressedStreamTools.a(fileTag, new FileOutputStream(new File(folder, folder.getName() + ".nbt"))); } /** * Loads a simple NBT file (same name as folder name), containing an integer array with dimensions * @param folder - The parent folder * @return int[3] - width, height, length */ public static int[] loadAreaDimFile(File folder) throws IOException { return NBTCompressedStreamTools.a(new FileInputStream(new File(folder, folder.getName() + ".nbt"))).getIntArray("dimensions"); } /** * Get the amount of blocks along x axis (width), y axis (height), z axis (length) * @param corners - The 2 opposite edges, in best case the first has the lowest coordinates in x, y, z * @return int[3] - Width, height, length */ public static int[] getDimensions(Location[] corners) { if (corners.length != 2) throw new IllegalArgumentException("An area needs to be set up by exactly 2 opposite edges!"); return new int[] {corners[1].getBlockX() - corners[0].getBlockX() + 1, corners[1].getBlockY() - corners[0].getBlockY() + 1, corners[1].getBlockZ() - corners[0].getBlockZ() + 1}; } /** * Calculates how many 32x32x32 sections are needed to fill an area * @param dimensions - The area size * @return int[3] - Amount of sections along x, y, z axis */ public static int[] getAreaSections(int[] dimensions) { if (dimensions.length != 3) throw new IllegalArgumentException("An dimension needs to contain width, height & length!"); int width, height, length; width = dimensions[0] / 32; height = dimensions[1] / 32; length = dimensions[2] / 32; if (dimensions[0] % 32 != 0) width = width + 1; if (dimensions[1] % 32 != 0) height = height + 1; if (dimensions[2] % 32 != 0) length = length + 1; return new int[] {width, height, length}; } /** * Swaps the edge corners if necessary, so the first edge will be at the lowest coordinates and the highest will be at the edge with the highest coordinates * @param startBlock - Any corner * @param endBlock - The other corner * @return Location[2] array - [0] = lowest edge, [1] = highest edge */ public static Location[] normalizeEdges(Location startBlock, Location endBlock) { int xMin, xMax, yMin, yMax, zMin, zMax; if (startBlock.getBlockX() <= endBlock.getBlockX()) { xMin = startBlock.getBlockX(); xMax = endBlock.getBlockX(); } else { xMin = endBlock.getBlockX(); xMax = startBlock.getBlockX(); } if (startBlock.getBlockY() <= endBlock.getBlockY()) { yMin = startBlock.getBlockY(); yMax = endBlock.getBlockY(); } else { yMin = endBlock.getBlockY(); yMax = startBlock.getBlockY(); } if (startBlock.getBlockZ() <= endBlock.getBlockZ()) { zMin = startBlock.getBlockZ(); zMax = endBlock.getBlockZ(); } else { zMin = endBlock.getBlockZ(); zMax = startBlock.getBlockZ(); } return new Location[] {new Location(startBlock.getWorld(), xMin, yMin, zMin), new Location(startBlock.getWorld(), xMax, yMax, zMax)}; } /** * Calculates the index within a linear array, interpreting it as 3D area, Y Z X (Sorted by height, then length, then width ) * @param x - The X position within the area * @param y - The Y position within the area * @param z - The Z position within the area * @param width - The width of the area * @param length - The length of the area * @return int - The array index */ public static int getYzxIndex(int x, int y, int z, int width, int length) { return width * length * y + StructureService.getZxIndex(x, z, width); } /** * Calculates the index within a linear array, interpreting it as 2D area, Z X (Sorted by length, then width ) * @param x - The X position within the area * @param z - The Z position within the area * @param width - The width of the area * @return int - The array index */ public static int getZxIndex(int x, int z, int width) { return z * width + x; } /** * Find out, which structure in an array of structure is the one you need at a specific position * @param dimension - The width, height, length of the whole structure array * @param x - The x coordinate * @param y - The y coordinate * @param z - The z coordinate * @return int - The array index for your structure array */ public static int getStructureArrayIndex(int[] dimension, int x, int y, int z) { int[] sections = StructureService.getAreaSections(dimension); return StructureService.getYzxIndex(x / 32, y / 32, z / 32, sections[0], sections[2]); } /** * Used as rotation interface to NMS * @author Michel_0 */ public enum Rotation { DEG_0(EnumBlockRotation.NONE), DEG_90(EnumBlockRotation.CLOCKWISE_90), DEG_180(EnumBlockRotation.CLOCKWISE_180), DEG_270(EnumBlockRotation.COUNTERCLOCKWISE_90); private EnumBlockRotation rotNMS; Rotation(EnumBlockRotation rotNMS) { this.rotNMS = rotNMS; } public EnumBlockRotation getNMSRot() { return this.rotNMS; } } }